作者:快樂丸
鏈接:https://blog.csdn.net/qq_36486247/article/details/103959356
前言
ViewPager2是官方推出的新控件订讼,從名稱上也能看出是用于替代ViewPager的,它是基于RecyclerView實(shí)現(xiàn)的扇苞,因此可以實(shí)現(xiàn)一些ViewPager沒有的功能欺殿,最實(shí)用的一點(diǎn)就是支持豎直方向滾動(dòng)了寄纵。
雖然很早就聽說過,但是從一些文章中也多少了解到ViewPager2使用的一些坑脖苏,也就一直沒有正式使用過程拭。前不久ViewPager2發(fā)布了1.0.0正式版,心想是時(shí)候嘗試一下了棍潘。哈哈恃鞋,可能是因?yàn)榇饲皩戇^兩篇懶加載相關(guān)的文章吧,我第一時(shí)間想到的不是ViewPager新功能的使用蜒谤,而是在配合Fragment時(shí)如何實(shí)現(xiàn)懶加載山宾。本文就來具體探究一下ViewPager2中的懶加載問題至扰,關(guān)于ViewPager2的使用已經(jīng)有很多詳細(xì)的文章了鳍徽,不是本文的研究重點(diǎn),因此就不會(huì)具體介紹了敢课。
在進(jìn)入正文之前要強(qiáng)調(diào)一下阶祭,本文的分析基于ViewPager2的1.0.0版本,是在androidx包下的直秆,因此在使用ViewPager2之前需要做好androidx的適配工作濒募。
利用ViewPager2加載多個(gè)Fragment
第一步、首先需要在build.gradle文件中添加ViewPager2的依賴
implementation 'androidx.viewpager2:viewpager2:1.0.0'
第二步圾结、在布局文件中添加ViewPager2
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/view_pager2"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
第三步瑰剃、編寫Adapter
需要注意,ViewPager2中加載Fragment時(shí)的Adapter類需要繼承自FragmentStateAdapter筝野,而不是ViewPager中的FragmentStatePagerAdapter晌姚。
public class MyFragmentPagerAdapter extends FragmentStateAdapter {
private List<Fragment> mFragments;
public MyFragmentPagerAdapter(@NonNull FragmentActivity fragmentActivity, List<Fragment> fragments) {
super(fragmentActivity);
this.mFragments = fragments;
}
@NonNull
@Override
public Fragment createFragment(int position) {
return mFragments.get(position);
}
@Override
public int getItemCount() {
return mFragments.size();
}
}
第四步、為ViewPager2設(shè)置Adapter
ViewPager2 mViewPager2 = findViewById(R.id.view_pager2);
List<Fragment> mFragments = new ArrayList<>();
mFragments .add(new FirstFragment());
mFragments .add(new SecondFragment());
mFragments .add(new ThirdFragment());
MyFragmentPagerAdapter mAdapter = new MyFragmentPagerAdapter(this, mFragments);
mViewPager2.setAdapter(mAdapter);
經(jīng)過以上幾步我們就實(shí)現(xiàn)了利用ViewPager2加載多個(gè)Fragment歇竟,當(dāng)然我這里是為了簡(jiǎn)單演示挥唠,具體的Fragment類我就不展示了。
Fragment切換時(shí)的生命周期方法執(zhí)行情況
接下來我們具體來看一下Fragment切換時(shí)生命周期方法的執(zhí)行情況焕议。我在測(cè)試用例中添加了6個(gè)Fragment宝磨,在Fragment的生命周期回調(diào)方法中打印執(zhí)行情況,具體執(zhí)行結(jié)果如下:
-
初始情況顯示第一個(gè)Fragment
可以看出此時(shí)只創(chuàng)建出了第一個(gè)Fragment盅安,生命周期方法執(zhí)行到了onResume()
唤锉,其他的幾個(gè)Fragment并沒有創(chuàng)建。
-
切換到第二個(gè)Fragment
此時(shí)創(chuàng)建出了第二個(gè)Fragment别瞭,生命周期方法同樣執(zhí)行到onResume()
腌紧,同時(shí),第一個(gè)Fragment執(zhí)行onPause()
方法畜隶。
-
切換到第三個(gè)Fragment
和上一種情況相同壁肋,創(chuàng)建出第三個(gè)Fragment号胚,執(zhí)行到onResume()
方法,同時(shí)第二個(gè)Fragment執(zhí)行onPause()
方法浸遗。
-
切換到第四個(gè)Fragment
和前兩種情況相同猫胁,同樣是創(chuàng)建出當(dāng)前Fragment,生命周期方法執(zhí)行到onResume()
跛锌,并且上一個(gè)Fragment執(zhí)行onPause()
方法弃秆。不同的是,此時(shí)會(huì)銷毀第一個(gè)Fragment髓帽,依次執(zhí)行onStop()
菠赚、onDestroyView()
、onDestroy()
和onDetach()
方法郑藏。
-
切換到第五個(gè)Fragment
和上一種情況相同衡查,創(chuàng)建出第五個(gè)Fragment,生命周期方法執(zhí)行到onResume()
必盖,第四個(gè)Fragment執(zhí)行onPause()
方法拌牲,同時(shí)銷毀第二個(gè)Fragment。
-
切換到第六個(gè)(最后一個(gè))Fragment
可以看出此時(shí)創(chuàng)建出了第六個(gè)Fragment歌粥,生命周期方法執(zhí)行到onResume()
塌忽,第五個(gè)Fragment執(zhí)行onPause()
方法,如果按照上面兩種情況的執(zhí)行結(jié)果來看失驶,此時(shí)應(yīng)該會(huì)銷毀第三個(gè)Fragment土居,但實(shí)際上并沒有。
從以上幾種情況下Fragment生命周期方法的執(zhí)行情況來看嬉探,不難看出ViewPager2默認(rèn)情況下不會(huì)預(yù)先創(chuàng)建出下一個(gè)Fragment擦耀。
但與此同時(shí),F(xiàn)ragment的銷毀情況就令我有些不解了甲馋,如果不看切換到最后一個(gè)Fragment的情況埂奈,我們可以猜測(cè)是由于ViewPager2內(nèi)部RecyclerView的緩存機(jī)制導(dǎo)致最多可以存在三個(gè)Fragment,但是切換到最后一個(gè)Fragment的情況就違背了我們的猜測(cè)定躏,很明顯此時(shí)并沒有銷毀前面的Fragment账磺。
接下來我們就根據(jù)上述結(jié)果來分析一下ViewPager2加載Fragment的幾個(gè)問題。
ViewPager2中的setOffscreenPageLimit()
方法
通過示例中的執(zhí)行結(jié)果我們可以發(fā)現(xiàn)ViewPager2默認(rèn)情況下不會(huì)像ViewPager那樣預(yù)先加載出兩側(cè)的Fragment痊远,這是為什么呢垮抗,我們可能會(huì)想到ViewPager中預(yù)加載相關(guān)的一個(gè)方法:setOffscreenPageLimit(),ViewPager2中也定義了該方法碧聪,我們來看一下它們的區(qū)別冒版。
首先來看ViewPager中的setOffscreenPageLimit()
方法:
private static final int DEFAULT_OFFSCREEN_PAGES = 1;
private int mOffscreenPageLimit = DEFAULT_OFFSCREEN_PAGES;
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();
}
}
方法傳入一個(gè)整型數(shù)值,表示當(dāng)前Fragment兩側(cè)的預(yù)加載數(shù)量逞姿,很多人可能都知道辞嗡,ViewPager默認(rèn)的預(yù)加載數(shù)量為1捆等,也就是會(huì)預(yù)先創(chuàng)建出當(dāng)前Fragment左右兩側(cè)的一個(gè)Fragment。
從代碼中我們可以看出续室,如果我們傳入的數(shù)值小于1栋烤,依然會(huì)將預(yù)加載數(shù)量設(shè)置為1,這也導(dǎo)致了ViewPager無法取消預(yù)加載挺狰,也因此才會(huì)需要Fragment的懶加載方案明郭。
接下來我們來看ViewPager2中的setOffscreenPageLimit()
方法:
public static final int OFFSCREEN_PAGE_LIMIT_DEFAULT = -1;
private int mOffscreenPageLimit = OFFSCREEN_PAGE_LIMIT_DEFAULT;
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();
}
我們可以看出ViewPager2中默認(rèn)的預(yù)加載數(shù)量mOffscreenPageLimit為OFFSCREEN_PAGE_LIMIT_DEFAULT也就是-1,我們可以通過傳入該默認(rèn)值或者大于1的整數(shù)來設(shè)置預(yù)加載數(shù)量丰泊。
接下我們來看一下哪里用到了mOffscreenPageLimit薯定,通過全局搜索,我們可以發(fā)現(xiàn)在ViewPager2的內(nèi)部類LinearLayoutManagerImpl中的calculateExtraLayoutSpace()
方法中通過getOffscreenPageLimit()
方法獲取了mOffscreenPageLimit瞳购。
@Override
protected void calculateExtraLayoutSpace(@NonNull RecyclerView.State state,
@NonNull int[] extraLayoutSpace) {
int pageLimit = getOffscreenPageLimit();
if (pageLimit == OFFSCREEN_PAGE_LIMIT_DEFAULT) {
// Only do custom prefetching of offscreen pages if requested
super.calculateExtraLayoutSpace(state, extraLayoutSpace);
return;
}
final int offscreenSpace = getPageSize() * pageLimit;
extraLayoutSpace[0] = offscreenSpace;
extraLayoutSpace[1] = offscreenSpace;
}
calculateExtraLayoutSpace()
方法定義在LinearLayoutManager中话侄,用于計(jì)算LinearLayoutManager布局的額外空間,也就是RecyclerView顯示范圍之外的空間苛败,計(jì)算結(jié)果在保存參數(shù)extraLayoutSpace中满葛,它是一個(gè)長(zhǎng)度為2的整型數(shù)組径簿,extraLayoutSpace[0]表示頂部/左側(cè)的額外空間罢屈,extraLayoutSpace[1]表示底部/右側(cè)的額外空間(取決于方向)。
LinearLayoutManagerImpl重寫了該方法篇亭,方法內(nèi)部首先判斷了mOffscreenPageLimit的值缠捌,如果等于默認(rèn)值OFFSCREEN_PAGE_LIMIT_DEFAULT,則直接調(diào)用父類方法译蒂,不設(shè)置額外的布局空間曼月;
如果mOffscreenPageLimit的值大于1,則設(shè)置左右(或上下)兩邊的額外空間為getPageSize() * pageLimit
柔昼,相當(dāng)于預(yù)加載出了兩邊的Fragment哑芹。
看到這里我們就清楚了為什么默認(rèn)情況下ViewPager2不會(huì)預(yù)加載出兩側(cè)的Fragment,就是因?yàn)槟J(rèn)的預(yù)加載數(shù)量為-1捕透。和ViewPager一樣聪姿,我們可以通過調(diào)用setOffscreenPageLimit()
方法,傳入大于1的值來設(shè)置預(yù)加載數(shù)量乙嘀。
在此前的示例中末购,我們添加下面的代碼:
mViewPager2.setOffscreenPageLimit(1);
首次顯示第一個(gè)Fragment時(shí)打印的結(jié)果如下:
可以看出此時(shí)ViewPager2就會(huì)預(yù)先創(chuàng)建出下一個(gè)Fragment,和ViewPager默認(rèn)的情況相同虎谢。
RecyclerView中的緩存和預(yù)取機(jī)制
接下來我們來看一下Fragment的銷毀情況盟榴,探究一下為什么在上面的示例中ViewPager2切換到最后一個(gè)Fragment時(shí)沒有銷毀前面的Fragment。
在此之前婴噩,我們先要了解一下RecyclerView的緩存機(jī)制和預(yù)取機(jī)制擎场。
RecyclerView的緩存機(jī)制算是老生常談的問題了羽德,核心在它的一個(gè)內(nèi)部類Recycler中,Item的回收和復(fù)用相關(guān)工作都是Recycler來進(jìn)行的迅办,RecyclerView的緩存可以分為多級(jí)玩般,由于我了解得非常淺顯,這里就不詳細(xì)介紹了礼饱,大家可以自行查看相關(guān)文章坏为。
我們直接來看和ViewPager2中Fragment回收相關(guān)的緩存——mCachedViews,它的類型是ArrayList镊绪,移出屏幕的Item對(duì)應(yīng)的ViewHolder都會(huì)被優(yōu)先緩存到該容器中匀伏。
Recycler類中有一個(gè)成員變量mViewCacheMax,表示mCachedViews最大的緩存數(shù)量蝴韭,默認(rèn)值為2够颠,我們可以通過調(diào)用RecyclerView的setItemViewCacheSize()
方法來設(shè)置緩存大小。
回到我們的具體場(chǎng)景中榄鉴,通過查看FragmentStateAdapter類的源碼履磨,我們可以看到,此時(shí)mCachedViews中保存的ViewHolder類型為FragmentViewHolder庆尘,它的視圖根布局是一個(gè)FrameLayout剃诅,F(xiàn)ragment會(huì)被添加到對(duì)應(yīng)的FrameLayout中,因此緩存ViewHolder其實(shí)就相當(dāng)于緩存了Fragment驶忌,為了簡(jiǎn)明矛辕,我后面就都說成緩存Fragment了,大家清楚這樣說是不準(zhǔn)確的就好了付魔。
在上面的示例中聊品,我們使用ViewPager2加載了6個(gè)Fragment,當(dāng)切換到第四個(gè)Fragment時(shí)几苍,由于最多只能緩存兩個(gè)Fragment翻屈,此時(shí)mCachedViews中緩存的是第二個(gè)Fragment和第三個(gè)Fragment,因此第一個(gè)Fragment就要被銷毀妻坝,之后切換到第五個(gè)Fragment的情況同理伸眶,此時(shí)會(huì)緩存第三個(gè)和第四個(gè)Fragment,因此第二個(gè)Fragment被銷毀惠勒。
接下來問題就來了赚抡,如果按照這樣的解釋,當(dāng)切換到第六個(gè)Fragment時(shí)應(yīng)該銷毀第三個(gè)Fragment纠屋,上面的示例中很明顯沒有啊涂臣,這又是為什么呢?
這就涉及到RecyclerView的預(yù)取(Prefetch)機(jī)制了赁遗,它是官方在support v25版本包中引入的功能署辉,具體表現(xiàn)為在RecyclerView滑動(dòng)時(shí)會(huì)預(yù)先加載出下一個(gè)Item,準(zhǔn)確地說是預(yù)先創(chuàng)建出下一個(gè)Item對(duì)應(yīng)的ViewHolder岩四。默認(rèn)情況下預(yù)取功能是開啟的哭尝,我們可以調(diào)用下面的代碼來關(guān)閉:
mRecyclerView.getLayoutManager().setItemPrefetchEnabled(false);
那么預(yù)取機(jī)制會(huì)對(duì)ViewPager2中Fragment的銷毀產(chǎn)生什么影響呢,我們從源碼角度來簡(jiǎn)單分析一下剖煌。首先來看RecyclerView的onTouchEvent()
方法:
RecyclerView的onTouchEvent()方法
@Override
public boolean onTouchEvent(MotionEvent e) {
// ...
switch (action) {
// ...
case MotionEvent.ACTION_MOVE: {
// ...
if (mGapWorker != null && (dx != 0 || dy != 0)) {
mGapWorker.postFromTraversal(this, dx, dy);
}
}
break;
// ...
}
// ...
return true;
}
可以看到在RecyclerView滑動(dòng)時(shí)會(huì)調(diào)用到mGapWorker的postFromTraversal()
方法材鹦,將水平和豎直方向上的位移通過參數(shù)傳入,用于后面計(jì)算預(yù)取的Item位置耕姊。mGapWorker類型為GapWorker桶唐,我們來看它的postFromTraversal()
方法:
GapWorker的postFromTraversal()方法
/**
* Schedule a prefetch immediately after the current traversal.
*/
void postFromTraversal(RecyclerView recyclerView, int prefetchDx, int prefetchDy) {
// ...
recyclerView.post(this);
// ...
}
從方法的注釋上我們也能看出它和RecyclerView的預(yù)取有關(guān),方法內(nèi)部會(huì)調(diào)用RecyclerView的post()
方法茉兰,參數(shù)傳入了this尤泽,也就是當(dāng)前GapWorker對(duì)象,通過查看GapWorker類的定義可以看到它實(shí)現(xiàn)了Runnable规脸,因此這里就是提交一個(gè)任務(wù)到主線程的消息隊(duì)列中坯约。接下來我們來看GapWorker實(shí)現(xiàn)的run()
方法:
@Override
public void run() {
// ...
long nextFrameNs = TimeUnit.MILLISECONDS.toNanos(latestFrameVsyncMs) + mFrameIntervalNs;
prefetch(nextFrameNs);
// ...
}
方法內(nèi)部會(huì)調(diào)用prefetch()
方法,看到方法名大概可以推測(cè)出接下來就要進(jìn)行預(yù)取相關(guān)邏輯了莫鸭,我們接著來看闹丐。
void prefetch(long deadlineNs) {
// 構(gòu)建預(yù)取任務(wù)
buildTaskList();
// 開始執(zhí)行預(yù)取任務(wù)
flushTasksWithDeadline(deadlineNs);
}
prefetch()
方法中首先會(huì)調(diào)用buildTaskList()
方法來構(gòu)建預(yù)取任務(wù),主要是通過此前傳過來的水平和豎直方向位移確定出預(yù)取的位置黔龟,接下來會(huì)調(diào)用flushTasksWithDeadline()
方法來執(zhí)行預(yù)取任務(wù)妇智,我們這里只看buildTaskList()
方法就好滥玷。
private void buildTaskList() {
final int viewCount = mRecyclerViews.size();
int totalTaskCount = 0;
for (int i = 0; i < viewCount; i++) {
RecyclerView view = mRecyclerViews.get(i);
if (view.getWindowVisibility() == View.VISIBLE) {
// 關(guān)鍵代碼
view.mPrefetchRegistry.collectPrefetchPositionsFromView(view, false);
totalTaskCount += view.mPrefetchRegistry.mCount;
}
}
// ...
}
接下來又會(huì)調(diào)用RecyclerView中mPrefetchRegistry的collectPrefetchPositionsFromView()
方法氏身,mPrefetchRegistry的類型為LayoutPrefetchRegistryImpl,它是GapWorker中的一個(gè)內(nèi)部類惑畴,我們接著來看它的collectPrefetchPositionsFromView()
方法蛋欣。
void collectPrefetchPositionsFromView(RecyclerView view, boolean nested) {
mCount = 0;
// ...
final RecyclerView.LayoutManager layout = view.mLayout;
if (view.mAdapter != null
&& layout != null
&& layout.isItemPrefetchEnabled()) {
// ...
// momentum based prefetch, only if we trust current child/adapter state
if (!view.hasPendingAdapterUpdates()) {
layout.collectAdjacentPrefetchPositions(mPrefetchDx, mPrefetchDy,
view.mState, this);
}
if (mCount > layout.mPrefetchMaxCountObserved) {
layout.mPrefetchMaxCountObserved = mCount;
layout.mPrefetchMaxObservedInInitialPrefetch = nested;
view.mRecycler.updateViewCacheSize();
}
}
}
方法內(nèi)部首先會(huì)將LayoutPrefetchRegistryImpl中的成員變量mCount置為0,接著通過isItemPrefetchEnabled()
方法判斷RecyclerView是否開啟了預(yù)取如贷,默認(rèn)是開啟的陷虎,接下來會(huì)執(zhí)行l(wèi)ayout的collectAdjacentPrefetchPositions()
方法,這里的layout是RecyclerView設(shè)置的LayoutManager杠袱,我們以LinearLayoutManager為例尚猿,看一下它的collectAdjacentPrefetchPositions()
方法。
LinearLayoutManager的collectAdjacentPrefetchPositions()方法
@Override
public void collectAdjacentPrefetchPositions(int dx, int dy, RecyclerView.State state,
LayoutPrefetchRegistry layoutPrefetchRegistry) {
// ...
collectPrefetchPositionsForLayoutState(state, mLayoutState, layoutPrefetchRegistry);
}
void collectPrefetchPositionsForLayoutState(RecyclerView.State state, LayoutState layoutState,
LayoutPrefetchRegistry layoutPrefetchRegistry) {
// ...
layoutPrefetchRegistry.addPosition(pos, Math.max(0, layoutState.mScrollingOffset));
}
方法內(nèi)部又會(huì)調(diào)用collectPrefetchPositionsForLayoutState()
方法楣富,接著調(diào)用layoutPrefetchRegistry的addPosition()
方法凿掂,這里的layoutPrefetchRegistry是從上面的collectPrefetchPositionsFromView()
方法中傳過來的,可以看到參數(shù)傳的是this,也就是LayoutPrefetchRegistryImpl對(duì)象庄萎。我們接著來看LayoutPrefetchRegistryImpl的addPosition()
方法:
LayoutPrefetchRegistryImpl的addPosition()方法
@Override
public void addPosition(int layoutPosition, int pixelDistance) {
// ...
mCount++;
}
可以看到方法最后會(huì)將mCount加1踪少,此時(shí)mCount的值變?yōu)?。接下來我們回到collectPrefetchPositionsFromView()
方法糠涛,來看方法最后執(zhí)行的一個(gè)判斷援奢。
if (mCount > layout.mPrefetchMaxCountObserved) {
layout.mPrefetchMaxCountObserved = mCount;
layout.mPrefetchMaxObservedInInitialPrefetch = nested;
view.mRecycler.updateViewCacheSize();
}
這里判斷了mCount和mPrefetchMaxCountObserved的大小關(guān)系,mPrefetchMaxCountObserved是LayoutManager中定義的一個(gè)整型變量忍捡,初始值為0集漾,因此這里會(huì)進(jìn)入到if判斷中。
接著會(huì)將mCount賦值給mPrefetchMaxCountObserved砸脊,此時(shí)mPrefetchMaxCountObserved的值變?yōu)?帆竹,最后會(huì)調(diào)用Recycler的updateViewCacheSize()
方法,我們來看一下這個(gè)方法脓规。
Recycler的updateViewCacheSize()方法
void updateViewCacheSize() {
int extraCache = mLayout != null ? mLayout.mPrefetchMaxCountObserved : 0;
mViewCacheMax = mRequestedCacheMax + extraCache;
// first, try the views that can be recycled
for (int i = mCachedViews.size() - 1;
i >= 0 && mCachedViews.size() > mViewCacheMax; i--) {
recycleCachedViewAt(i);
}
}
方法內(nèi)部首先定義了一個(gè)整型變量extraCache栽连,字面上看就是額外的緩存,它的值就是上一步中的mPrefetchMaxCountObserved侨舆,也就是1秒紧。
接下來這一步就重要了,將mRequestedCacheMax + extraCache
賦值給mViewCacheMax挨下,我們前面在介紹RecyclerView緩存的時(shí)候提到過mViewCacheMax表示mCachedViews的最大緩存數(shù)量熔恢,mRequestedCacheMax就是我們?cè)O(shè)置的mCachedViews緩存數(shù)量,默認(rèn)值為2臭笆,因此此時(shí)mViewCacheMax的值被設(shè)置為3叙淌,也就是說mCachedViews最多可以保存3個(gè)ViewHolder(對(duì)于我們的場(chǎng)景來說就是Fragment)。
看到這里我們就大致清楚了示例中Fragment銷毀情況產(chǎn)生的原因愁铺,當(dāng)從第一個(gè)Fragment切換到第二個(gè)Fragment時(shí)會(huì)執(zhí)行我們上面分析的預(yù)取邏輯鹰霍,將mCachedViews的最大緩存數(shù)量由默認(rèn)的2置為3。
對(duì)于切換到第三茵乱、第四和第五個(gè)Fragment的情況茂洒,由于預(yù)取的Fragment占據(jù)了mCachedViews中的一個(gè)位置,因此還是表現(xiàn)為最多緩存2個(gè)Fragment瓶竭。
當(dāng)切換到第六個(gè)也就是最后一個(gè)Fragment時(shí)督勺,不需要再預(yù)取下一個(gè)Fragment了,但是此時(shí)mCachedViews的最大緩存數(shù)量依然為3斤贰,所以第三個(gè)Fragment也可以被添加到緩存中智哀,不會(huì)被銷毀。
為了驗(yàn)證得出的結(jié)論荧恍,我們首先通過代碼取消ViewPager2內(nèi)部RecyclerView的預(yù)取機(jī)制:
((RecyclerView) mViewPager2.getChildAt(0)).getLayoutManager().setItemPrefetchEnabled(false);
然后再來運(yùn)行一下此前的示例程序瓷叫,直接來看切換到最后一個(gè)Fragment的情況。
可以看出當(dāng)切換到最后一個(gè)Fragment時(shí)會(huì)銷毀掉第三個(gè)Fragment,此時(shí)緩存的Fragment為第四和第五個(gè)赞辩,這是由于我們關(guān)閉了預(yù)取機(jī)制雌芽,在執(zhí)行LayoutPrefetchRegistryImpl中的collectPrefetchPositionsFromView()
方法時(shí)不滿足layout.isItemPrefetchEnabled()
為true的條件,不會(huì)執(zhí)行后面的邏輯辨嗽,因此mCachedViews的最大緩存數(shù)量始終為2世落,這就驗(yàn)證了我們的結(jié)論是沒錯(cuò)的。
ViewPager2中的懶加載方案
由于ViewPager2默認(rèn)情況下不會(huì)預(yù)加載出兩邊的Fragment糟需,相當(dāng)于默認(rèn)就是懶加載的屉佳,因此如果我們?nèi)绻麤]有通過setOffscreenPageLimit()
方法設(shè)置預(yù)加載數(shù)量,完全可以不做任何額外處理洲押。但是對(duì)于Fragment很多的情況武花,由于ViewPager2中的RecyclerView可以緩存Fragment的數(shù)量是有限的,因此會(huì)造成Fragment的多次銷毀和創(chuàng)建杈帐,如何解決這個(gè)問題呢体箕?下面就介紹一下我的解決方案。
首先設(shè)置ViewPager2的預(yù)加載數(shù)量挑童,讓ViewPager2預(yù)先創(chuàng)建出所有的Fragment累铅,防止切換造成的頻繁銷毀和創(chuàng)建。
mViewPager2.setOffscreenPageLimit(mFragments.size());
通過此前示例中Fragment切換時(shí)生命周期方法的執(zhí)行情況我們不難發(fā)現(xiàn)不管Fragment是否會(huì)被預(yù)先創(chuàng)建站叼,只有可見時(shí)才會(huì)執(zhí)行到onResume()
方法娃兽,我們正好可以利用這一規(guī)律來實(shí)現(xiàn)懶加載,具體實(shí)現(xiàn)方式和我此前介紹過的androidx中的Fragment懶加載方案相同尽楔,這里我再簡(jiǎn)單說一下投储。
- 將Fragment加載數(shù)據(jù)的邏輯放到
onResume()
方法中,這樣就保證了Fragment可見時(shí)才會(huì)加載數(shù)據(jù)阔馋。 - 聲明一個(gè)變量標(biāo)記是否是首次執(zhí)行
onResume()
方法玛荞,因?yàn)槊看蜦ragment由不可見變?yōu)榭梢姸紩?huì)執(zhí)行onResume()
方法,需要防止數(shù)據(jù)的重復(fù)加載垦缅。
按照以上兩點(diǎn)就可以封裝我們的懶加載Fragment了冲泥,完整代碼如下:
public abstract class LazyFragment extends Fragment {
private Context mContext;
private boolean isFirstLoad = true; // 是否第一次加載
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mContext = getActivity();
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = LayoutInflater.from(mContext).inflate(getContentViewId(), null);
initView(view);
return view;
}
@Override
public void onResume() {
super.onResume();
if (isFirstLoad) {
// 將數(shù)據(jù)加載邏輯放到onResume()方法中
initData();
initEvent();
isFirstLoad = false;
}
}
/**
* 設(shè)置布局資源id
*
* @return
*/
protected abstract int getContentViewId();
/**
* 初始化視圖
*
* @param view
*/
protected void initView(View view) {
}
/**
* 初始化數(shù)據(jù)
*/
protected void initData() {
}
/**
* 初始化事件
*/
protected void initEvent() {
}
}
當(dāng)然這只是我認(rèn)為比較好的一種方案,如果有什么地方考慮得有問題或是大家有自己的見解都?xì)g迎提出壁涎。
總結(jié)
本文探究了利用ViewPager2加載Fragment時(shí)生命周期方法的執(zhí)行情況,進(jìn)而得出ViewPager2懶加載的實(shí)現(xiàn)方式:
簡(jiǎn)單來說完全可以不做任何處理志秃,ViewPager2默認(rèn)就實(shí)現(xiàn)了懶加載怔球。但是如果想避免Fragment頻繁銷毀和創(chuàng)建造成的開銷,可以通過setOffscreenPageLimit()
方法設(shè)置預(yù)加載數(shù)量浮还,將數(shù)據(jù)加載邏輯放到Fragment的onResume()
方法中竟坛。
雖說本文的研究對(duì)象是ViewPager2,但是文章大部分篇幅都是在分析RecyclerView,不得不感嘆RecyclerView確實(shí)是一個(gè)很重要的控件担汤,如何使用大家基本都已經(jīng)爛熟于心了涎跨,但是涉及到原理上的東西就不一樣了,我對(duì)RecyclerView的了解也是甚淺崭歧,有時(shí)間的話還是有必要深入學(xué)習(xí)一下的隅很。
點(diǎn)關(guān)注,獲得更多Android開發(fā)技能~~