Androidx Fragment 懶加載機制實現

在到Androidx之前我們使用support提供的Fragment的懶加載機制,基本上使用的是在setUserVisible + onHiddenChanged 這兩個函數。但是在Androidx下setUserVisible已經被Google官方棄用了,推薦我們使用Fragment.setMaxLifecyCle()的方式來處理Fragment的懶加載。

一、Androidx增加FragmentTransaction.setMaxLifecycle方法控制最大生命週期

Google在Androidx的FragmentTransaction中增加了setMaxLifecycle方法來控制Fragment所能調用的最大的聲明週期函數。如下圖所示:

   /**
     * Set a ceiling for the state of an active fragment in this FragmentManager. If fragment is
     * already above the received state, it will be forced down to the correct state.
     *
     * <p>The fragment provided must currently be added to the FragmentManager to have it’s
     * Lifecycle state capped, or previously added as part of this transaction. The
     * {@link Lifecycle.State} passed in must at least be {@link Lifecycle.State#CREATED}, otherwise
     * an {@link IllegalArgumentException} will be thrown.</p>
     *
     * @param fragment the fragment to have it's state capped.
     * @param state the ceiling state for the fragment.
     * @return the same FragmentTransaction instance
     */
    @NonNull
    public FragmentTransaction setMaxLifecycle(@NonNull Fragment fragment, @NonNull Lifecycle.State state) {
        addOp(new Op(OP_SET_MAX_LIFECYCLE, fragment, state));
        return this;
    }

 

可以看到,該方法可以設置活躍狀態下Fragment的最大狀態,如果該Fragment超過了設置的最大狀態,那麼會強制將Fragment降級到正確的狀態。

二、Fragment 在 add+show+hide 模式下的懶加載實現

將需要顯示的 Fragment ,在調用 add 或 show 方法後,setMaxLifecycle(showFragment, Lifecycle.State.RESUMED).

將需要隱藏的 Fragment ,在調用 hide 方法後,setMaxLifecycle(fragment, Lifecycle.State.STARTED).

切換時的代碼如下:

fragmentManager.beginTransaction().apply {
    for (index in fragments.indices) {
        val fragment = fragments[index]
        add(containerViewId, fragment, fragment.javaClass.name)
        if (showPosition == index) {
            setMaxLifecycle(fragment, Lifecycle.State.RESUMED)
        } else {
            hide(fragment)
            setMaxLifecycle(fragment, Lifecycle.State.STARTED)
        }
    }

}.commit()

其中Fragment的代碼如下:

abstract class LazyFragment : Fragment() {

    private var isLoaded = false

    override fun onResume() {
        super.onResume()
        //增加了Fragment是否可見的判斷
        if (!isLoaded && !isHidden) {
            lazyInit()
            Log.d(TAG, "lazyInit:!!!!!!!”)
            isLoaded = true
        }
    }

    override fun onDestroyView() {
        super.onDestroyView()
        isLoaded = false
    }

    abstract fun lazyInit()
}

此實現方案,在較複雜的Fragment嵌套模式下,也能保證正常的懶加載的實現。

三、ViewPager + Fragment 模式下的懶加載實現

在Androidx下,FragmentPagerAdapter、FragmentStatePagerAdapter 類新增了含有behavior的字段的構造函數,並捨棄了FragmentPagerAdapter(@NonNull FragmentManager fm)方法。

  @Deprecated
  public FragmentPagerAdapter(@NonNull FragmentManager fm) {
        this(fm, BEHAVIOR_SET_USER_VISIBLE_HINT);
  }

  public FragmentPagerAdapter(@NonNull FragmentManager fm,  @Behavior int behavior) {
        mFragmentManager = fm;
        mBehavior = behavior;
  }

  public FragmentStatePagerAdapter(@NonNull FragmentManager fm, @Behavior int behavior) {
        mFragmentManager = fm;
        mBehavior = behavior;
  }
其中 Behavior 的類型如下:
    @Retention(RetentionPolicy.SOURCE)
    @IntDef({BEHAVIOR_SET_USER_VISIBLE_HINT, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT})
    private @interface Behavior { }

    @Deprecated
    public static final int BEHAVIOR_SET_USER_VISIBLE_HINT = 0;
   
    public static final int BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT = 1;

類型說明如下:

  • BEHAVIOR_SET_USER_VISIBLE_HINT:當 Fragment 對用戶的可見狀態發生改變時,setUserVisibleHint 方法會被調用。
  • BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT:那麼當前選中的 Fragment 在 Lifecycle.State#RESUMED 狀態 ,其他不可見的 Fragment 會被限制在 Lifecycle.State#STARTED 狀態。

使用 BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT 後,然後Fragment 繼承使用 LazyFragment 後,即可實現Androidx下的ViewPager+Fragment的懶加載機制。

這裏我們探究一下設置了 BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT 能實現懶加載的原因。通過看源碼可以發現,FragmentPagerAdapter 在 setPrimaryItem方法中調用了setMaxLifecycle方法。代碼如下:

    public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
        Fragment fragment = (Fragment)object;
        //如果當前的fragment不是當前選中並可見的Fragment,那麼就會調用setMaxLifecycle 設置其最大生命週期爲 Lifecycle.State.STARTED
        if (fragment != mCurrentPrimaryItem) {
            if (mCurrentPrimaryItem != null) {
                mCurrentPrimaryItem.setMenuVisibility(false);
                if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
                    if (mCurTransaction == null) {
                        mCurTransaction = mFragmentManager.beginTransaction();
                    }
                    mCurTransaction.setMaxLifecycle(mCurrentPrimaryItem, Lifecycle.State.STARTED);
                } else {
                    mCurrentPrimaryItem.setUserVisibleHint(false);
                }
            }
            //對於其他非可見的Fragment,則設置其最大生命週期爲Lifecycle.State.RESUMED
            fragment.setMenuVisibility(true);
            if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
                if (mCurTransaction == null) {
                    mCurTransaction = mFragmentManager.beginTransaction();
                }
                mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.RESUMED);
            } else {
                fragment.setUserVisibleHint(true);
            }

            mCurrentPrimaryItem = fragment;
        }
    }

四、FragmentPagerAdapter 和 FragmentStatePagerAdapter 區別

1. FragmentPagerAdapter

這兩個Adapter都能配合ViewPager實現Fragment的加載。下面我們來比較一下這兩個類在實現及使用機制上有什麼區別,主要從加載和銷燬兩方面來進行分析。

FragmentPagerAdapter加載Fragment的方法爲:instantiateItem,源碼如下:

    @NonNull
    @Override
    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;
    }
可以看出來,在instantiateItem方法中,主要是將Fragment添加到FragmentManager中。未添加到FragmentManager中的執行add操作,已添加到FragmentManager中的只進行attach操作。
FragmentPagerAdapter 銷燬 Fragment的方法爲:instantiateItem。
    @Override
    public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
        Fragment fragment = (Fragment) object;
        if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        }
        mCurTransaction.detach(fragment);
        if (fragment == mCurrentPrimaryItem) {
            mCurrentPrimaryItem = null;
        }
    }

在destroyItem方法中,只是進行detach操作。detach操作並不會將Fragment銷燬,Fragment依舊是由FragmentManager進行管理。

2. FragmentStatePagerAdapter

FragmentStatePagerAdapter的加載和銷燬的方法名同FragmentPagerAdapter。下面我們先將代碼放出來:

    @NonNull
    @Override
    public Object instantiateItem(@NonNull ViewGroup container, int position) {
        // If we already have this item instantiated, there is nothing
        // to do.  This can happen when we are restoring the entire pager
        // from its saved state, where the fragment manager has already
        // taken care of restoring the fragments we previously had instantiated.
        if (mFragments.size() > position) {
            Fragment f = mFragments.get(position);
            if (f != null) {
                return f;
            }
        }

        if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        }

        Fragment fragment = getItem(position);
        if (DEBUG) Log.v(TAG, "Adding item #" + position + ": f=" + fragment);
        if (mSavedState.size() > position) {
            Fragment.SavedState fss = mSavedState.get(position);
            if (fss != null) {
                fragment.setInitialSavedState(fss);
            }
        }
        while (mFragments.size() <= position) {
            mFragments.add(null);
        }
        fragment.setMenuVisibility(false);
        if (mBehavior == BEHAVIOR_SET_USER_VISIBLE_HINT) {
            fragment.setUserVisibleHint(false);
        }

        mFragments.set(position, fragment);
        mCurTransaction.add(container.getId(), fragment);

        if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
            mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.STARTED);
        }

        return fragment;
    }
    @Override
    public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
        Fragment fragment = (Fragment) object;

        if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        }
        if (DEBUG) Log.v(TAG, "Removing item #" + position + ": f=" + object
                + " v=" + ((Fragment)object).getView());
        while (mSavedState.size() <= position) {
            mSavedState.add(null);
        }
        mSavedState.set(position, fragment.isAdded()
                ? mFragmentManager.saveFragmentInstanceState(fragment) : null);
        mFragments.set(position, null);

        mCurTransaction.remove(fragment);
        if (fragment == mCurrentPrimaryItem) {
            mCurrentPrimaryItem = null;
        }
    }
可以看出來:FragmentStatePagerAdapter是通過一個mFragments數組來存儲fragment的,通過mSavedState數組來存儲fragment銷燬時的狀態,通過position獲取到的fragment可能爲空(被回收),如果爲空,則會再次調用getItem方法重新創建新的fragment,然後將mSavedState中存儲的狀態重新賦予這個新的fragment, 達到fragment恢復的效果。當item在頁面中不可見時,該fragment的狀態會先被保存到mSavedState中,而fragment實例則會被銷燬。

3. 異同比較

相同點:

a). 兩者都會保持當前item(即fragment)和前後的item的狀態。

b). 顯示當前item的同時,Adapter會提前初始化後一個item,並把當前item的前一個item保存在內存中。

不同點:

fragment 存儲、恢復、銷燬 的方式不同,FragmentStatePagerAdapter會完全銷燬滑動過去的item,當需要初始化的時候,會重新初始化頁面。FragmentPagerAdapter 則會保留頁面的狀態,並不會完全銷燬掉。

4. 如何選擇

當Viewpager中fragment數量多的時候,爲保證性能推薦使用FragmentStatePagerAdapter,反之則推薦使用FragmentPagerAdapter。

五、參考資料

1. https://www.jianshu.com/p/2201a107d5b5

2. https://www.jianshu.com/p/a778649c254d

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章