Android學(xué)習(xí)小計:ViewPager2中的Fragment懶加載實現(xiàn)方式

前言
ViewPager2是官方推出的新控件,從名稱上也能看出是用于替代ViewPager的,它是基于RecyclerView實現(xiàn)的溉委,因此可以實現(xiàn)一些ViewPager沒有的功能,最實用的一點就是支持豎直方向滾動了奏路。
雖然很早就聽說過,但是從一些文章中也多少了解到ViewPager2使用的一些坑臊诊,也就一直沒有正式使用過鸽粉。前不久ViewPager2發(fā)布了1.0.0正式版,心想是時候嘗試一下了抓艳。哈哈触机,可能是因為此前寫過兩篇懶加載相關(guān)的文章吧,我第一時間想到的不是ViewPager新功能的使用玷或,而是在配合Fragment時如何實現(xiàn)懶加載儡首。本文就來具體探究一下ViewPager2中的懶加載問題,關(guān)于ViewPager2的使用已經(jīng)有很多詳細的文章了偏友,不是本文的研究重點蔬胯,因此就不會具體介紹了。

在進入正文之前要強調(diào)一下位他,本文的分析基于ViewPager2的1.0.0版本笔宿,是在androidx包下的,因此在使用ViewPager2之前需要做好androidx的適配工作棱诱。

利用ViewPager2加載多個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時的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)過以上幾步我們就實現(xiàn)了利用ViewPager2加載多個Fragment,當(dāng)然我這里是為了簡單演示厦凤,具體的Fragment類我就不展示了鼻吮。

Fragment切換時的生命周期方法執(zhí)行情況

接下來我們具體來看一下Fragment切換時生命周期方法的執(zhí)行情況。我在測試用例中添加了6個Fragment较鼓,在Fragment的生命周期回調(diào)方法中打印執(zhí)行情況椎木,具體執(zhí)行結(jié)果如下:

  • 初始情況顯示第一個Fragment

可以看出此時只創(chuàng)建出了第一個Fragment,生命周期方法執(zhí)行到了onResume()博烂,其他的幾個Fragment并沒有創(chuàng)建香椎。

  • 切換到第二個Fragment

此時創(chuàng)建出了第二個Fragment,生命周期方法同樣執(zhí)行到onResume()禽篱,同時畜伐,第一個Fragment執(zhí)行onPause()方法。

  • 切換到第三個Fragment

和上一種情況相同躺率,創(chuàng)建出第三個Fragment玛界,執(zhí)行到onResume()方法万矾,同時第二個Fragment執(zhí)行onPause()方法。

  • 切換到第四個Fragment

和前兩種情況相同慎框,同樣是創(chuàng)建出當(dāng)前Fragment勤众,生命周期方法執(zhí)行到onResume(),并且上一個Fragment執(zhí)行onPause()方法鲤脏。不同的是们颜,此時會銷毀第一個Fragment,依次執(zhí)行onStop()猎醇、onDestroyView()窥突、onDestroy()onDetach()方法。

  • 切換到第五個Fragment

和上一種情況相同硫嘶,創(chuàng)建出第五個Fragment阻问,生命周期方法執(zhí)行到onResume(),第四個Fragment執(zhí)行onPause()方法沦疾,同時銷毀第二個Fragment称近。

  • 切換到第六個(最后一個)Fragment

可以看出此時創(chuàng)建出了第六個Fragment,生命周期方法執(zhí)行到onResume()哮塞,第五個Fragment執(zhí)行onPause()方法刨秆,如果按照上面兩種情況的執(zhí)行結(jié)果來看,此時應(yīng)該會銷毀第三個Fragment忆畅,但實際上并沒有衡未。
從以上幾種情況下Fragment生命周期方法的執(zhí)行情況來看,不難看出ViewPager2默認情況下不會預(yù)先創(chuàng)建出下一個Fragment家凯。但與此同時缓醋,F(xiàn)ragment的銷毀情況就令我有些不解了,如果不看切換到最后一個Fragment的情況绊诲,我們可以猜測是由于ViewPager2內(nèi)部RecyclerView的緩存機制導(dǎo)致最多可以存在三個Fragment送粱,但是切換到最后一個Fragment的情況就違背了我們的猜測,很明顯此時并沒有銷毀前面的Fragment掂之。接下來我們就根據(jù)上述結(jié)果來分析一下ViewPager2加載Fragment的幾個問題抗俄。

ViewPager2中的setOffscreenPageLimit()方法

通過示例中的執(zhí)行結(jié)果我們可以發(fā)現(xiàn)ViewPager2默認情況下不會像ViewPager那樣預(yù)先加載出兩側(cè)的Fragment,這是為什么呢板惑,我們可能會想到ViewPager中預(yù)加載相關(guān)的一個方法: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();
    }
}

方法傳入一個整型數(shù)值,表示當(dāng)前Fragment兩側(cè)的預(yù)加載數(shù)量晒夹,很多人可能都知道裆馒,ViewPager默認的預(yù)加載數(shù)量為1姊氓,也就是會預(yù)先創(chuàng)建出當(dāng)前Fragment左右兩側(cè)的一個Fragment。從代碼中我們可以看出喷好,如果我們傳入的數(shù)值小于1翔横,依然會將預(yù)加載數(shù)量設(shè)置為1,這也導(dǎo)致了ViewPager無法取消預(yù)加載梗搅,也因此才會需要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中默認的預(yù)加載數(shù)量mOffscreenPageLimit為OFFSCREEN_PAGE_LIMIT_DEFAULT也就是-1,我們可以通過傳入該默認值或者大于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中掘托,用于計算LinearLayoutManager布局的額外空間,也就是RecyclerView顯示范圍之外的空間籍嘹,計算結(jié)果在保存參數(shù)extraLayoutSpace中闪盔,它是一個長度為2的整型數(shù)組,extraLayoutSpace[0]表示頂部/左側(cè)的額外空間辱士,extraLayoutSpace[1]表示底部/右側(cè)的額外空間(取決于方向)泪掀。LinearLayoutManagerImpl重寫了該方法,方法內(nèi)部首先判斷了mOffscreenPageLimit的值颂碘,如果等于默認值OFFSCREEN_PAGE_LIMIT_DEFAULT族淮,則直接調(diào)用父類方法,不設(shè)置額外的布局空間凭涂;如果mOffscreenPageLimit的值大于1祝辣,則設(shè)置左右(或上下)兩邊的額外空間為getPageSize() * pageLimit,相當(dāng)于預(yù)加載出了兩邊的Fragment切油。
看到這里我們就清楚了為什么默認情況下ViewPager2不會預(yù)加載出兩側(cè)的Fragment蝙斜,就是因為默認的預(yù)加載數(shù)量為-1。和ViewPager一樣澎胡,我們可以通過調(diào)用setOffscreenPageLimit()方法孕荠,傳入大于1的值來設(shè)置預(yù)加載數(shù)量。
在此前的示例中攻谁,我們添加下面的代碼:

mViewPager2.setOffscreenPageLimit(1);

首次顯示第一個Fragment時打印的結(jié)果如下:

可以看出此時ViewPager2就會預(yù)先創(chuàng)建出下一個Fragment稚伍,和ViewPager默認的情況相同。

RecyclerView中的緩存和預(yù)取機制

接下來我們來看一下Fragment的銷毀情況戚宦,探究一下為什么在上面的示例中ViewPager2切換到最后一個Fragment時沒有銷毀前面的Fragment个曙。在此之前,我們先要了解一下RecyclerView的緩存機制和預(yù)取機制受楼。
RecyclerView的緩存機制算是老生常談的問題了垦搬,核心在它的一個內(nèi)部類Recycler中呼寸,Item的回收和復(fù)用相關(guān)工作都是Recycler來進行的,RecyclerView的緩存可以分為多級猴贰,由于我了解得非常淺顯对雪,這里就不詳細介紹了,大家可以自行查看相關(guān)文章米绕。我們直接來看和ViewPager2中Fragment回收相關(guān)的緩存——mCachedViews瑟捣,它的類型是ArrayList<ViewHolder>,移出屏幕的Item對應(yīng)的ViewHolder都會被優(yōu)先緩存到該容器中栅干。Recycler類中有一個成員變量mViewCacheMax迈套,表示mCachedViews最大的緩存數(shù)量,默認值為2非驮,我們可以通過調(diào)用RecyclerView的setItemViewCacheSize()方法來設(shè)置緩存大小交汤。
回到我們的具體場景中,通過查看FragmentStateAdapter類的源碼劫笙,我們可以看到芙扎,此時mCachedViews中保存的ViewHolder類型為FragmentViewHolder,它的視圖根布局是一個FrameLayout填大,F(xiàn)ragment會被添加到對應(yīng)的FrameLayout中戒洼,因此緩存ViewHolder其實就相當(dāng)于緩存了Fragment,為了簡明允华,我后面就都說成緩存Fragment了圈浇,大家清楚這樣說是不準(zhǔn)確的就好了。在上面的示例中靴寂,我們使用ViewPager2加載了6個Fragment磷蜀,當(dāng)切換到第四個Fragment時,由于最多只能緩存兩個Fragment百炬,此時mCachedViews中緩存的是第二個Fragment和第三個Fragment褐隆,因此第一個Fragment就要被銷毀,之后切換到第五個Fragment的情況同理剖踊,此時會緩存第三個和第四個Fragment庶弃,因此第二個Fragment被銷毀。接下來問題就來了德澈,如果按照這樣的解釋歇攻,當(dāng)切換到第六個Fragment時應(yīng)該銷毀第三個Fragment,上面的示例中很明顯沒有啊梆造,這又是為什么呢缴守?
這就涉及到RecyclerView的預(yù)取(Prefetch)機制了,它是官方在support v25版本包中引入的功能斧散,具體表現(xiàn)為在RecyclerView滑動時會預(yù)先加載出下一個Item供常,準(zhǔn)確地說是預(yù)先創(chuàng)建出下一個Item對應(yīng)的ViewHolder摊聋。默認情況下預(yù)取功能是開啟的鸡捐,我們可以調(diào)用下面的代碼來關(guān)閉:

mRecyclerView.getLayoutManager().setItemPrefetchEnabled(false);

那么預(yù)取機制會對ViewPager2中Fragment的銷毀產(chǎ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滑動時會調(diào)用到mGapWorker的postFromTraversal()方法箍镜,將水平和豎直方向上的位移通過參數(shù)傳入,用于后面計算預(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)部會調(diào)用RecyclerView的post()方法手销,參數(shù)傳入了this歇僧,也就是當(dāng)前GapWorker對象,通過查看GapWorker類的定義可以看到它實現(xiàn)了Runnable锋拖,因此這里就是提交一個任務(wù)到主線程的消息隊列中诈悍。接下來我們來看GapWorker實現(xiàn)的run()方法:

@Override
public void run() {
    // ...
    long nextFrameNs = TimeUnit.MILLISECONDS.toNanos(latestFrameVsyncMs) + mFrameIntervalNs;
    prefetch(nextFrameNs);
    // ...
}

方法內(nèi)部會調(diào)用prefetch()方法,看到方法名大概可以推測出接下來就要進行預(yù)取相關(guān)邏輯了兽埃,我們接著來看侥钳。

void prefetch(long deadlineNs) {
    // 構(gòu)建預(yù)取任務(wù)
    buildTaskList();
    // 開始執(zhí)行預(yù)取任務(wù)
    flushTasksWithDeadline(deadlineNs);
}

prefetch()方法中首先會調(diào)用buildTaskList()方法來構(gòu)建預(yù)取任務(wù),主要是通過此前傳過來的水平和豎直方向位移確定出預(yù)取的位置柄错,接下來會調(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;
        }
    }
    // ...
}

接下來又會調(diào)用RecyclerView中mPrefetchRegistry的collectPrefetchPositionsFromView()方法售貌,mPrefetchRegistry的類型為LayoutPrefetchRegistryImpl给猾,它是GapWorker中的一個內(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)部首先會將LayoutPrefetchRegistryImpl中的成員變量mCount置為0敢伸,接著通過isItemPrefetchEnabled()方法判斷RecyclerView是否開啟了預(yù)取,默認是開啟的毫捣,接下來會執(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)部又會調(diào)用collectPrefetchPositionsForLayoutState()方法,接著調(diào)用layoutPrefetchRegistry的addPosition()方法斑粱,這里的layoutPrefetchRegistry是從上面的collectPrefetchPositionsFromView()方法中傳過來的弃揽,可以看到參數(shù)傳的是this,也就是LayoutPrefetchRegistryImpl對象。我們接著來看LayoutPrefetchRegistryImpl的addPosition()方法:
LayoutPrefetchRegistryImpl的addPosition()方法

@Override
public void addPosition(int layoutPosition, int pixelDistance) {
    // ...
    mCount++;
}

可以看到方法最后會將mCount加1矿微,此時mCount的值變?yōu)?痕慢。接下來我們回到collectPrefetchPositionsFromView()方法,來看方法最后執(zhí)行的一個判斷涌矢。

if (mCount > layout.mPrefetchMaxCountObserved) {
    layout.mPrefetchMaxCountObserved = mCount;
    layout.mPrefetchMaxObservedInInitialPrefetch = nested;
    view.mRecycler.updateViewCacheSize();
}

這里判斷了mCount和mPrefetchMaxCountObserved的大小關(guān)系掖举,mPrefetchMaxCountObserved是LayoutManager中定義的一個整型變量,初始值為0娜庇,因此這里會進入到if判斷中塔次。接著會將mCount賦值給mPrefetchMaxCountObserved吓肋,此時mPrefetchMaxCountObserved的值變?yōu)?掂名,最后會調(diào)用Recycler的updateViewCacheSize()方法,我們來看一下這個方法棚壁。
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)部首先定義了一個整型變量extraCache匕得,字面上看就是額外的緩存继榆,它的值就是上一步中的mPrefetchMaxCountObserved,也就是1汁掠。接下來這一步就重要了略吨,將mRequestedCacheMax + extraCache賦值給mViewCacheMax,我們前面在介紹RecyclerView緩存的時候提到過mViewCacheMax表示mCachedViews的最大緩存數(shù)量调塌,mRequestedCacheMax就是我們設(shè)置的mCachedViews緩存數(shù)量晋南,默認值為2,因此此時mViewCacheMax的值被設(shè)置為3羔砾,也就是說mCachedViews最多可以保存3個ViewHolder(對于我們的場景來說就是Fragment)负间。
看到這里我們就大致清楚了示例中Fragment銷毀情況產(chǎn)生的原因,當(dāng)從第一個Fragment切換到第二個Fragment時會執(zhí)行我們上面分析的預(yù)取邏輯姜凄,將mCachedViews的最大緩存數(shù)量由默認的2置為3政溃。對于切換到第三、第四和第五個Fragment的情況态秧,由于預(yù)取的Fragment占據(jù)了mCachedViews中的一個位置董虱,因此還是表現(xiàn)為最多緩存2個Fragment。當(dāng)切換到第六個也就是最后一個Fragment時申鱼,不需要再預(yù)取下一個Fragment了愤诱,但是此時mCachedViews的最大緩存數(shù)量依然為3,所以第三個Fragment也可以被添加到緩存中捐友,不會被銷毀淫半。
為了驗證得出的結(jié)論,我們首先通過代碼取消ViewPager2內(nèi)部RecyclerView的預(yù)取機制:

((RecyclerView) mViewPager2.getChildAt(0)).getLayoutManager().setItemPrefetchEnabled(false);

然后再來運行一下此前的示例程序匣砖,直接來看切換到最后一個Fragment的情況科吭。

可以看出當(dāng)切換到最后一個Fragment時會銷毀掉第三個Fragment昏滴,此時緩存的Fragment為第四和第五個,這是由于我們關(guān)閉了預(yù)取機制对人,在執(zhí)行LayoutPrefetchRegistryImpl中的collectPrefetchPositionsFromView()方法時不滿足layout.isItemPrefetchEnabled()為true的條件谣殊,不會執(zhí)行后面的邏輯,因此mCachedViews的最大緩存數(shù)量始終為2牺弄,這就驗證了我們的結(jié)論是沒錯的姻几。

ViewPager2中的懶加載方案

由于ViewPager2默認情況下不會預(yù)加載出兩邊的Fragment,相當(dāng)于默認就是懶加載的猖闪,因此如果我們?nèi)绻麤]有通過setOffscreenPageLimit()方法設(shè)置預(yù)加載數(shù)量鲜棠,完全可以不做任何額外處理肌厨。但是對于Fragment很多的情況培慌,由于ViewPager2中的RecyclerView可以緩存Fragment的數(shù)量是有限的,因此會造成Fragment的多次銷毀和創(chuàng)建柑爸,如何解決這個問題呢吵护?下面就介紹一下我的解決方案。
首先設(shè)置ViewPager2的預(yù)加載數(shù)量表鳍,讓ViewPager2預(yù)先創(chuàng)建出所有的Fragment馅而,防止切換造成的頻繁銷毀和創(chuàng)建。

mViewPager2.setOffscreenPageLimit(mFragments.size());

通過此前示例中Fragment切換時生命周期方法的執(zhí)行情況我們不難發(fā)現(xiàn)不管Fragment是否會被預(yù)先創(chuàng)建譬圣,只有可見時才會執(zhí)行到onResume()方法瓮恭,我們正好可以利用這一規(guī)律來實現(xiàn)懶加載,具體實現(xiàn)方式和我此前介紹過的androidx中的Fragment懶加載方案相同厘熟,這里我再簡單說一下屯蹦。

  • 將Fragment加載數(shù)據(jù)的邏輯放到onResume()方法中,這樣就保證了Fragment可見時才會加載數(shù)據(jù)绳姨。
  • 聲明一個變量標(biāo)記是否是首次執(zhí)行onResume()方法登澜,因為每次Fragment由不可見變?yōu)榭梢姸紩?zhí)行onResume()方法,需要防止數(shù)據(jù)的重復(fù)加載飘庄。
    按照以上兩點就可以封裝我們的懶加載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)然這只是我認為比較好的一種方案,如果有什么地方考慮得有問題或是大家有自己的見解都歡迎提出跪削。

總結(jié)

本文探究了利用ViewPager2加載Fragment時生命周期方法的執(zhí)行情況谴仙,進而得出ViewPager2懶加載的實現(xiàn)方式:
簡單來說完全可以不做任何處理,ViewPager2默認就實現(xiàn)了懶加載碾盐。但是如果想避免Fragment頻繁銷毀和創(chuàng)建造成的開銷晃跺,可以通過setOffscreenPageLimit()方法設(shè)置預(yù)加載數(shù)量,將數(shù)據(jù)加載邏輯放到Fragment的onResume()方法中廓旬。
雖說本文的研究對象是ViewPager2哼审,但是文章大部分篇幅都是在分析RecyclerView谐腰,不得不感嘆RecyclerView確實是一個很重要的控件,如何使用大家基本都已經(jīng)爛熟于心了涩盾,但是涉及到原理上的東西就不一樣了十气,我對RecyclerView的了解也是甚淺,有時間的話還是有必要深入學(xué)習(xí)一下的春霍。

參考文章

ViewPager2重大更新砸西,支持offscreenPageLimit
學(xué)不動也要學(xué)!深入了解ViewPager2
RecyclerView預(yù)加載機制源碼分析

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末址儒,一起剝皮案震驚了整個濱河市芹枷,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌莲趣,老刑警劉巖鸳慈,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異喧伞,居然都是意外死亡走芋,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進店門潘鲫,熙熙樓的掌柜王于貴愁眉苦臉地迎上來翁逞,“玉大人,你說我怎么就攤上這事溉仑⊥诤” “怎么了?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵浊竟,是天一觀的道長怨喘。 經(jīng)常有香客問我,道長逐沙,這世上最難降的妖魔是什么哲思? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮吩案,結(jié)果婚禮上棚赔,老公的妹妹穿的比我還像新娘。我一直安慰自己徘郭,他們只是感情好靠益,可當(dāng)我...
    茶點故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著残揉,像睡著了一般胧后。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上抱环,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天壳快,我揣著相機與錄音纸巷,去河邊找鬼。 笑死眶痰,一個胖子當(dāng)著我的面吹牛瘤旨,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播竖伯,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼存哲,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了七婴?” 一聲冷哼從身側(cè)響起祟偷,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎打厘,沒想到半個月后修肠,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡婚惫,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年氛赐,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片先舷。...
    茶點故事閱讀 40,096評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖滓侍,靈堂內(nèi)的尸體忽然破棺而出蒋川,到底是詐尸還是另有隱情,我是刑警寧澤撩笆,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布捺球,位于F島的核電站,受9級特大地震影響夕冲,放射性物質(zhì)發(fā)生泄漏氮兵。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一歹鱼、第九天 我趴在偏房一處隱蔽的房頂上張望泣栈。 院中可真熱鬧,春花似錦弥姻、人聲如沸南片。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽疼进。三九已至,卻和暖如春秧廉,著一層夾襖步出監(jiān)牢的瞬間伞广,已是汗流浹背拣帽。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留嚼锄,地道東北人诞外。 一個月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓,卻偏偏與公主長得像灾票,于是被迫代替她去往敵國和親峡谊。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,037評論 2 355

推薦閱讀更多精彩內(nèi)容