<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
大家看標題,可能會有點兒懵,什麼是ViewPagers,因為在很久之前,我們使用的都是ViewPager,但是現在更多的是在用ViewPager2,因此用ViewPagers(ViewPager、ViewPager2)來代替兩者,主要介紹兩者的區別。
ViewPagers巢狀Fragment架構,在我們常用的App中隨處可見,抖音的首頁、各大電商app首頁(淘寶、京東、拼多多)等,通過左右滑動切換Tab;但因為ViewPager的預載入機制存在,
我們先看下ViewPager的原始碼:
public void setOffscreenPageLimit(int limit) { if (limit < DEFAULT_OFFSCREEN_PAGES) { Log.w(TAG, "Requested offscreen page limit " + limit + " too small; defaulting to " + DEFAULT_OFFSCREEN_PAGES); limit = DEFAULT_OFFSCREEN_PAGES; } if (limit != mOffscreenPageLimit) { mOffscreenPageLimit = limit; populate(); } }
當我們設定offscreenPageLimit(離屏載入)的數值時,我們可以看到limit的值是有限制,不能小於DEFAULT_OFFSCREEN_PAGES
private static final int DEFAULT_OFFSCREEN_PAGES = 1;
那麼就意味著ViewPager預設支援預載入,我們看下面這張圖
如果紅色區域預設為首頁,根據ViewPager預設預載入的閾值,那麼左右兩邊的頁面同樣也會被載入,如果有網路請求,也就是說,我們沒有開啟左邊的頁面,它已經預設進行了網路請求,這種體驗是非常差的,因為會在暗地裡消耗流量。
理想情況下,我們需要的是開啟某個頁面的時候才去載入,這裡就需要通過懶載入的方式優化。
很多時候,我們在使用Fragment的時候,發現開啟過的頁面再回來,頁面沒有重建重新整理,很多人覺得是Fragment是有快取的,其實並不是Fragment有快取,而是ViewPager具備快取能力;
如果有小夥伴使用過單Activity + 多Fragment架構的時候就會發現,開啟過的頁面再次返回的時候,Fragment會被重建,所以兩種架構都有利弊,關鍵看我們怎麼選擇,下面我們看下ViewPager的快取機制。
public void setAdapter(@Nullable PagerAdapter adapter) { if (mAdapter != null) { ① mAdapter.setViewPagerObserver(null); mAdapter.startUpdate(this); for (int i = 0; i < mItems.size(); i++) { final ItemInfo ii = mItems.get(i); mAdapter.destroyItem(this, ii.position, ii.object); } mAdapter.finishUpdate(this); mItems.clear(); removeNonDecorViews(); mCurItem = 0; scrollTo(0, 0); } ② final PagerAdapter oldAdapter = mAdapter; mAdapter = adapter; mExpectedAdapterCount = 0; ③ if (mAdapter != null) { if (mObserver == null) { mObserver = new PagerObserver(); } mAdapter.setViewPagerObserver(mObserver); mPopulatePending = false; final boolean wasFirstLayout = mFirstLayout; mFirstLayout = true; mExpectedAdapterCount = mAdapter.getCount(); if (mRestoredCurItem >= 0) { mAdapter.restoreState(mRestoredAdapterState, mRestoredClassLoader); setCurrentItemInternal(mRestoredCurItem, false, true); mRestoredCurItem = -1; mRestoredAdapterState = null; mRestoredClassLoader = null; } else if (!wasFirstLayout) { ④ populate(); } else { ⑤ requestLayout(); } } // Dispatch the change to any listeners if (mAdapterChangeListeners != null && !mAdapterChangeListeners.isEmpty()) { for (int i = 0, count = mAdapterChangeListeners.size(); i < count; i++) { mAdapterChangeListeners.get(i).onAdapterChanged(this, oldAdapter, adapter); } } }
核心方法就是setAdapter,像RecyclerView一樣,因為會有快取,所以當頁面滑動的時候,如果快取中存在頁面,那麼就會從快取中取,如果沒有,就需要去建立新的頁面,所以我們先來關注一下PagerAdapter
public abstract class PagerAdapter { private final DataSetObservable mObservable = new DataSetObservable(); private DataSetObserver mViewPagerObserver; public static final int POSITION_UNCHANGED = -1; public static final int POSITION_NONE = -2; public abstract int getCount(); //開始更新 public void startUpdate(@NonNull ViewGroup container) { startUpdate((View) container); } //初始化頁面 @NonNull public Object instantiateItem(@NonNull ViewGroup container, int position) { return instantiateItem((View) container, position); } //銷燬頁面 public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) { destroyItem((View) container, position, object); } //結束重新整理 public void finishUpdate(@NonNull ViewGroup container) { finishUpdate((View) container); } }
PagerAdapter是一個抽象類,那麼這些方法肯定是具體實現類實現,如果我們在使用ViewPager巢狀Fragment的時候,使用的是FragmentPageAdapter
接著回到setAdapter方法中:
wasFirstLayout = true mRestoredCurItem = -1
所以這裡直接走到⑤,呼叫requestLayout方法,會執行到onMeasure,在這個方法中,會執行populate方法(這個大家自己去爬樓)
populate幹了什麼呢?程式碼太多了就不貼出來了,直接上圖:
如果是預設快取(mOffscreenPageLimit = 1),那麼在mItems就會快取3個Fragment
private final ArrayList<ItemInfo> mItems = new ArrayList<ItemInfo>();
當頁面滑動時,page2成為了當前頁,那麼ViewPager的populate做了什麼操作呢?
因此populate乾的主要工作就是,隨著頁面的滑動,將Page從快取中移除銷燬,或者將新頁面新建加入快取中。
如上所述,ViewPager預設就是開啟預載入的,而且預設最多能夠快取3個Fragment頁面,那麼為了避免流量的消耗,需要我們針對預載入這種情況進行頁面懶載入,只有當頁面可見的時候,才能載入資料。
class MainLazyLoadAdapter( fragmentManager: FragmentManager, val fragments:MutableList<Fragment> ) : FragmentPagerAdapter(fragmentManager) { override fun getCount(): Int { return fragments.size } override fun getItem(position: Int): Fragment { return fragments[position] } }
class LazyFragment(val index:Int) : Fragment() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Log.e("TAG","LazyFragment $index onCreate") } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { Log.e("TAG","LazyFragment $index onCreateView") return inflater.inflate(R.layout.fragment_lazy, container, false) } }
val fragments = mutableListOf<Fragment>() for (index in 0..5) { fragments.add(LazyFragment(index)) } vp_lazy_load.adapter = MainLazyLoadAdapter(supportFragmentManager, fragments)
首先我們先看預設預載入狀態,驗證之前原始碼中的原理:
//第一次進來 2022-08-28 13:41:15.759 12677-12677/com.lay.image_process E/TAG: LazyFragment 0 onCreate 2022-08-28 13:41:15.760 12677-12677/com.lay.image_process E/TAG: LazyFragment 0 onCreateView 2022-08-28 13:41:15.783 12677-12677/com.lay.image_process E/TAG: LazyFragment 1 onCreate 2022-08-28 13:41:15.784 12677-12677/com.lay.image_process E/TAG: LazyFragment 1 onCreateView
我們看到第一次進來,第二個Fragment被載入進來,然後右滑,第三個Fragment被載入
2022-08-28 13:41:15.759 12677-12677/com.lay.image_process E/TAG: LazyFragment 0 onCreate 2022-08-28 13:41:15.760 12677-12677/com.lay.image_process E/TAG: LazyFragment 0 onCreateView 2022-08-28 13:41:15.783 12677-12677/com.lay.image_process E/TAG: LazyFragment 1 onCreate 2022-08-28 13:41:15.784 12677-12677/com.lay.image_process E/TAG: LazyFragment 1 onCreateView 2022-08-28 13:48:45.248 12677-12677/com.lay.image_process E/TAG: LazyFragment 2 onCreate 2022-08-28 13:48:45.250 12677-12677/com.lay.image_process E/TAG: LazyFragment 2 onCreateView
當我們滑到第4個Fragment的時候,左滑回到第3個Fragment,發現並沒有重建是因為快取的原因,因為滑到第4個Fragment的時候,第2個Fragment已經被銷燬了,再回到第3個Fragment的時候,第2個Fragment被重建,走了onCreateView方法
2022-08-28 13:41:15.759 12677-12677/com.lay.image_process E/TAG: LazyFragment 0 onCreate 2022-08-28 13:41:15.760 12677-12677/com.lay.image_process E/TAG: LazyFragment 0 onCreateView 2022-08-28 13:41:15.783 12677-12677/com.lay.image_process E/TAG: LazyFragment 1 onCreate 2022-08-28 13:41:15.784 12677-12677/com.lay.image_process E/TAG: LazyFragment 1 onCreateView 2022-08-28 13:48:45.248 12677-12677/com.lay.image_process E/TAG: LazyFragment 2 onCreate 2022-08-28 13:48:45.250 12677-12677/com.lay.image_process E/TAG: LazyFragment 2 onCreateView 2022-08-28 13:50:00.439 12677-12677/com.lay.image_process E/TAG: LazyFragment 3 onCreate 2022-08-28 13:50:00.440 12677-12677/com.lay.image_process E/TAG: LazyFragment 3 onCreateView 2022-08-28 13:50:01.344 12677-12677/com.lay.image_process E/TAG: LazyFragment 4 onCreate 2022-08-28 13:50:01.345 12677-12677/com.lay.image_process E/TAG: LazyFragment 4 onCreateView 2022-08-28 13:50:03.315 12677-12677/com.lay.image_process E/TAG: LazyFragment 1 onCreateView
首先我們先看下,Adapter重建Fragment的時候的核心程式碼
public Object instantiateItem(@NonNull ViewGroup container, int position) { if (mCurTransaction == null) { mCurTransaction = mFragmentManager.beginTransaction(); } final long itemId = getItemId(position); // Do we already have this fragment? String name = makeFragmentName(container.getId(), itemId); Fragment fragment = mFragmentManager.findFragmentByTag(name); if (fragment != null) { if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment); mCurTransaction.attach(fragment); } else { fragment = getItem(position); if (DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment); mCurTransaction.add(container.getId(), fragment, makeFragmentName(container.getId(), itemId)); } if (fragment != mCurrentPrimaryItem) { fragment.setMenuVisibility(false); if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.STARTED); } else { //關鍵程式碼 fragment.setUserVisibleHint(false); } } return fragment; }
我們可以看到,當前Fragment如果被建立但是沒有在當前頁面展示的時候,呼叫了fragment.setUserVisibleHint(false),也就是說setUserVisibleHint能夠監聽當前Fragment是否可見
所以我們對Fragment進行改造:
class LazyFragment(val index:Int) : Fragment() { //判斷當前頁面是否可見 private var isShow = false //判斷頁面是否建立成功 private var isViewCreated = false override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Log.e("TAG","LazyFragment $index onCreate") } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { Log.e("TAG","LazyFragment $index onCreateView") return inflater.inflate(R.layout.fragment_lazy, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) isViewCreated = true lazyLoad() } override fun setUserVisibleHint(isVisibleToUser: Boolean) { super.setUserVisibleHint(isVisibleToUser) Log.e("TAG","LazyFragment $index isVisibleToUser $isVisibleToUser") if(isVisibleToUser){ isShow = true //才有資格去懶載入 lazyLoad() }else{ isShow = false } } private fun lazyLoad() { if(isViewCreated && isShow){ Log.e("TAG","Fragment $index loadData") } } override fun onDestroy() { super.onDestroy() isViewCreated = false isShow = false } }
如果按照之前的方式,當呼叫onViewCreated方法的時候,我們就會載入資料;做了懶載入處理之後,重寫了setUserVisibleHint方法,當前頁面可見的時候,才有資格去載入資料,這樣即便建立了Fragment,但是如果不可見就不會載入資料
2022-08-28 14:06:29.776 25904-25904/com.lay.image_process E/TAG: LazyFragment 0 isVisibleToUser false 2022-08-28 14:06:29.776 25904-25904/com.lay.image_process E/TAG: LazyFragment 1 isVisibleToUser false 2022-08-28 14:06:29.776 25904-25904/com.lay.image_process E/TAG: LazyFragment 0 isVisibleToUser true 2022-08-28 14:06:29.782 25904-25904/com.lay.image_process E/TAG: LazyFragment 0 onCreate 2022-08-28 14:06:29.783 25904-25904/com.lay.image_process E/TAG: LazyFragment 0 onCreateView 2022-08-28 14:06:29.796 25904-25904/com.lay.image_process E/TAG: Fragment 0 loadData 2022-08-28 14:06:29.805 25904-25904/com.lay.image_process E/TAG: LazyFragment 1 onCreate 2022-08-28 14:06:29.805 25904-25904/com.lay.image_process E/TAG: LazyFragment 1 onCreateView 2022-08-28 14:06:59.395 25904-25904/com.lay.image_process E/TAG: LazyFragment 2 isVisibleToUser false 2022-08-28 14:06:59.396 25904-25904/com.lay.image_process E/TAG: LazyFragment 0 isVisibleToUser false 2022-08-28 14:06:59.396 25904-25904/com.lay.image_process E/TAG: LazyFragment 1 isVisibleToUser true 2022-08-28 14:06:59.396 25904-25904/com.lay.image_process E/TAG: Fragment 1 loadData 2022-08-28 14:06:59.399 25904-25904/com.lay.image_process E/TAG: LazyFragment 2 onCreate 2022-08-28 14:06:59.400 25904-25904/com.lay.image_process E/TAG: LazyFragment 2 onCreateView
通過紀錄檔我們可以看到,當首次進入的時候,雖然Fragment 1 被建立了,但是並沒有載入資料。
這裡有個問題,既然可見之後就能載入資料,那麼我只在setUserVisibleHint的時候,判斷是否可見來去載入資料?
其實是不可以的,通過紀錄檔我們能夠發現,setUserVisibleHint是早於onCreate方法呼叫的,也就是說在頁面還沒有建立時,去載入資料有可能導致頁面元素找不到發生空指標異常。
上一小節,我們介紹了ViewPager的載入機制和快取機制,那麼我們把整套頁面搬過來,唯一發生變化的就是將ViewPager轉換為ViewPager2
class MainLazyLoadAdapter2( activity: FragmentActivity, val fragments: MutableList<Fragment> ) : FragmentStateAdapter(activity) { override fun getItemCount(): Int { return fragments.size } override fun createFragment(position: Int): Fragment { return fragments[position] } }
ViewPager2的介面卡使用的是FragmentStateAdapter,因為FragmentStateAdapter繼承了RecyclerView.Adapter,因此支援了橫向滑動和豎向滑動
val fragments = mutableListOf<Fragment>() for (index in 0..5) { fragments.add(LazyFragment(index)) } vp_lazy_load = findViewById(R.id.vp_lazy_load) vp_lazy_load.adapter = MainLazyLoadAdapter2(this, fragments)
用同樣的方式設定了介面卡,我們看下紀錄檔輸出,就會發現,咦?怎麼跟ViewPager不一樣了
2022-08-28 14:47:11.790 15514-15514/com.lay.image_process E/TAG: LazyFragment 0 onCreate 2022-08-28 14:47:11.792 15514-15514/com.lay.image_process E/TAG: LazyFragment 0 onCreateView
剛進來的時候,只有Fragment 1 載入了頁面,並沒有新建快取頁面,當我滑動到下一頁的時候,也只有下一頁的頁面進行了重建
2022-08-28 14:47:11.790 15514-15514/com.lay.image_process E/TAG: LazyFragment 0 onCreate 2022-08-28 14:47:11.792 15514-15514/com.lay.image_process E/TAG: LazyFragment 0 onCreateView 2022-08-28 14:47:13.948 15514-15514/com.lay.image_process E/TAG: LazyFragment 1 onCreate 2022-08-28 14:47:13.948 15514-15514/com.lay.image_process E/TAG: LazyFragment 1 onCreateView
ViewPager2沒有預載入機制嗎?這裡我們就需要看原始碼了,直接奔向setOffscreenPageLimit方法,我們看到跟ViewPager的setOffscreenPageLimit方法是不一樣的
public void setOffscreenPageLimit(@OffscreenPageLimit int limit) { if (limit < 1 && limit != OFFSCREEN_PAGE_LIMIT_DEFAULT) { throw new IllegalArgumentException( "Offscreen page limit must be OFFSCREEN_PAGE_LIMIT_DEFAULT or a number > 0"); } mOffscreenPageLimit = limit; // Trigger layout so prefetch happens through getExtraLayoutSize() mRecyclerView.requestLayout(); }
public static final int OFFSCREEN_PAGE_LIMIT_DEFAULT = -1;
這裡的判斷條件 limit < 1 && limit != OFFSCREEN_PAGE_LIMIT_DEFAULT,有一個數值能夠通過,就是-1,這就意味著,ViewPager2預設是不支援預載入的
但是ViewPager2的快取策略還是存在,因為繼承了RecyclerView的Adapter,所以快取複用機制是跟RecyclerView一致的,預設mViewCaches快取池的大小是3
2022-08-28 15:30:00.579 15514-15514/com.lay.image_process E/TAG: LazyFragment 0 onCreate 2022-08-28 15:30:00.579 15514-15514/com.lay.image_process E/TAG: LazyFragment 0 onCreateView 2022-08-28 15:30:03.883 15514-15514/com.lay.image_process E/TAG: LazyFragment 1 onCreate 2022-08-28 15:30:03.884 15514-15514/com.lay.image_process E/TAG: LazyFragment 1 onCreateView 2022-08-28 15:30:05.064 15514-15514/com.lay.image_process E/TAG: LazyFragment 2 onCreate 2022-08-28 15:30:05.064 15514-15514/com.lay.image_process E/TAG: LazyFragment 2 onCreateView 2022-08-28 15:30:08.997 15514-15514/com.lay.image_process E/TAG: LazyFragment 3 onCreate 2022-08-28 15:30:08.997 15514-15514/com.lay.image_process E/TAG: LazyFragment 3 onCreateView 2022-08-28 15:30:20.005 15514-15514/com.lay.image_process E/TAG: LazyFragment 0 onCreate 2022-08-28 15:30:20.005 15514-15514/com.lay.image_process E/TAG: LazyFragment 0 onCreateView
當我們滑動到第4個Fragment的時候,注意這裡跟ViewPager不一樣的是,ViewPager的快取是快取當前頁的左右兩邊,但是ViewPager2就是RecyclerView的快取機制,順序快取;
當滑動到第4個Fragment的時候,因為快取池大小為3,因此LazyFragment 0 就會從快取池中移除,當再次滑動到LazyFragment 0的時候,就會重建!
所以當我們還在思考如何針對ViewPager的預載入機制做懶載入操作時,請將專案中的ViewPager遷移至ViewPager2
附錄:
當你的專案中還在使用ViewPager時,建議使用當前這個懶載入框架
abstract class BaseLazyFragment<VM : ViewModel, VB : ViewBinding> : Fragment() { private lateinit var viewModel: VM private lateinit var binding: VB private var isShow = false private var isViewCreated = false override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) viewModel = getViewModelInstance() binding = getLayoutInflate(layoutInflater) } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) initView() isViewCreated = true lazyLoad() } override fun setUserVisibleHint(isVisibleToUser: Boolean) { super.setUserVisibleHint(isVisibleToUser) if (isVisibleToUser) { isShow = true lazyLoad() } else { isShow = false } } override fun onDestroy() { super.onDestroy() isShow = false isViewCreated = false } private fun lazyLoad() { if (isShow && isViewCreated) { initData() } } open fun initData() {} open fun initView() {} abstract fun getViewModelInstance(): VM abstract fun getLayoutInflate(layoutInflater: LayoutInflater): VB }
使用方式:
class LazyFragment(val index:Int) : BaseLazyFragment<MainVM,FragmentLazy2Binding>() { override fun initData() { super.initData() Log.e("TAG","LazyFragment $index initData -- ") } override fun getViewModelInstance(): MainVM { return MainVM() } override fun getLayoutInflate(layoutInflater: LayoutInflater): FragmentLazy2Binding { return FragmentLazy2Binding.inflate(layoutInflater) } }
到此這篇關於Android效能優化之ViewPagers + Fragment快取優化的文章就介紹到這了,更多相關Android 快取優化內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45