对于Fragment的处理,上一节中介绍了使用Navigation导航Fragment页面,当使用ViewPager + Fragment架构页面时,就会出现缓存问题,当然这不是因为Fragment有缓存功能。

真正让Fragment产生预加载的原因就是ViewPager的缓存机制,ViewPager默认情况下会有1页的缓存,但是这个1页的含义就是会缓存当前页面的左右1页的数据,也就是说,当前页面的左右1页有数据加载的操作,那么就会执行预加载。虽然我们没有切换到该页面,但是该页面已经进行了预加载,这就会造成不必要的资源浪费。

当Fragment界面进行预加载的时候,带来的性能问题就是UI卡顿。默认情况下,我们认为就是当前页面在网络请求加载数据,但是不知道的是,其他页面也在网络请求加载数据,这种情况下就导致当前页面加载很慢,页面渲染的时候拉长,就造成了UI的卡顿。

1、ViewPager的适配器模式

在Android中,提供了2种适配器,分别为FragmentPagerAdapterFragmentStatePagerAdapter,这两种适配器适配对象都是Fragment。

ViewPager滑动的过程中,所经历的生命周期主要分为以下4步,就是populate方法:

void populate(int newCurrentItem) {
        ItemInfo oldCurInfo = null;
        if (mCurItem != newCurrentItem) {
            oldCurInfo = infoForPosition(mCurItem);
            mCurItem = newCurrentItem;
        }

        if (mAdapter == null) {
            sortChildDrawingOrder();
            return;
        }

        // Bail now if we are waiting to populate.  This is to hold off
        // on creating views from the time the user releases their finger to
        // fling to a new position until we have finished the scroll to
        // that position, avoiding glitches from happening at that point.
        if (mPopulatePending) {
            if (DEBUG) Log.i(TAG, "populate is pending, skipping for now...");
            sortChildDrawingOrder();
            return;
        }

        // Also, don't populate until we are attached to a window.  This is to
        // avoid trying to populate before we have restored our view hierarchy
        // state and conflicting with what is restored.
        if (getWindowToken() == null) {
            return;
        }
		**#第一步:开始更新**
        mAdapter.startUpdate(this);
		#获取缓存的大小
        final int pageLimit = mOffscreenPageLimit;
        #起始位置
        final int startPos = Math.max(0, mCurItem - pageLimit);
        final int N = mAdapter.getCount();
        #终点位置
        final int endPos = Math.min(N - 1, mCurItem + pageLimit);

        if (N != mExpectedAdapterCount) {
            String resName;
            try {
                resName = getResources().getResourceName(getId());
            } catch (Resources.NotFoundException e) {
                resName = Integer.toHexString(getId());
            }
            throw new IllegalStateException("The application's PagerAdapter changed the adapter's"
                    + " contents without calling PagerAdapter#notifyDataSetChanged!"
                    + " Expected adapter item count: " + mExpectedAdapterCount + ", found: " + N
                    + " Pager id: " + resName
                    + " Pager class: " + getClass()
                    + " Problematic adapter: " + mAdapter.getClass());
        }

        // Locate the currently focused item or add it if needed.
        int curIndex = -1;
        ItemInfo curItem = null;
        for (curIndex = 0; curIndex < mItems.size(); curIndex++) {
            final ItemInfo ii = mItems.get(curIndex);
            if (ii.position >= mCurItem) {
                if (ii.position == mCurItem) curItem = ii;
                break;
            }
        }

        if (curItem == null && N > 0) {
            curItem = addNewItem(mCurItem, curIndex);
        }

        // Fill 3x the available width or up to the number of offscreen
        // pages requested to either side, whichever is larger.
        // If we have no current item we have no work to do.
        #第二步:开始适配数据
        if (curItem != null) {
            float extraWidthLeft = 0.f;
            int itemIndex = curIndex - 1;
            ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
            final int clientWidth = getClientWidth();
            final float leftWidthNeeded = clientWidth <= 0 ? 0 :
                    2.f - curItem.widthFactor + (float) getPaddingLeft() / (float) clientWidth;
            for (int pos = mCurItem - 1; pos >= 0; pos--) {
                if (extraWidthLeft >= leftWidthNeeded && pos < startPos) {
                    if (ii == null) {
                        break;
                    }
                    #for循环清除起始startPos之外的缓存数据
                    if (pos == ii.position && !ii.scrolling) {
                        mItems.remove(itemIndex);
                        mAdapter.destroyItem(this, pos, ii.object);
                        if (DEBUG) {
                            Log.i(TAG, "populate() - destroyItem() with pos: " + pos
                                    + " view: " + ((View) ii.object));
                        }
                        itemIndex--;
                        curIndex--;
                        ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
                    }
                } else if (ii != null && pos == ii.position) {
                    extraWidthLeft += ii.widthFactor;
                    itemIndex--;
                    ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
                } else {
                	#添加Item
                    ii = addNewItem(pos, itemIndex + 1);
                    extraWidthLeft += ii.widthFactor;
                    curIndex++;
                    ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
                }
            }

            float extraWidthRight = curItem.widthFactor;
            itemIndex = curIndex + 1;
            if (extraWidthRight < 2.f) {
                ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
                final float rightWidthNeeded = clientWidth <= 0 ? 0 :
                        (float) getPaddingRight() / (float) clientWidth + 2.f;
                for (int pos = mCurItem + 1; pos < N; pos++) {
                    if (extraWidthRight >= rightWidthNeeded && pos > endPos) {
                        if (ii == null) {
                            break;
                        }
                        #for循环清除endPos之外的缓存数据
                        if (pos == ii.position && !ii.scrolling) {
                            mItems.remove(itemIndex);
                            mAdapter.destroyItem(this, pos, ii.object);
                            if (DEBUG) {
                                Log.i(TAG, "populate() - destroyItem() with pos: " + pos
                                        + " view: " + ((View) ii.object));
                            }
                            ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
                        }
                    } else if (ii != null && pos == ii.position) {
                        extraWidthRight += ii.widthFactor;
                        itemIndex++;
                        ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
                    } else {
                        #添加新的数据
                        ii = addNewItem(pos, itemIndex);
                        itemIndex++;
                        extraWidthRight += ii.widthFactor;
                        ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
                    }
                }
            }

            calculatePageOffsets(curItem, curIndex, oldCurInfo);
			#第3步:设置当前页面
            mAdapter.setPrimaryItem(this, mCurItem, curItem.object);
        }

        if (DEBUG) {
            Log.i(TAG, "Current page list:");
            for (int i = 0; i < mItems.size(); i++) {
                Log.i(TAG, "#" + i + ": page " + mItems.get(i).position);
            }
        }
		# 结束更新
        mAdapter.finishUpdate(this);

        // Check width measurement of current pages and drawing sort order.
        // Update LayoutParams as needed.
        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = getChildAt(i);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            lp.childIndex = i;
            if (!lp.isDecor && lp.widthFactor == 0.f) {
                // 0 means requery the adapter for this, it doesn't have a valid width.
                final ItemInfo ii = infoForChild(child);
                if (ii != null) {
                    lp.widthFactor = ii.widthFactor;
                    lp.position = ii.position;
                }
            }
        }
        sortChildDrawingOrder();

        if (hasFocus()) {
            View currentFocused = findFocus();
            ItemInfo ii = currentFocused != null ? infoForAnyChild(currentFocused) : null;
            if (ii == null || ii.position != mCurItem) {
                for (int i = 0; i < getChildCount(); i++) {
                    View child = getChildAt(i);
                    ii = infoForChild(child);
                    if (ii != null && ii.position == mCurItem) {
                        if (child.requestFocus(View.FOCUS_FORWARD)) {
                            break;
                        }
                    }
                }
            }
        }
    }

(1)调用适配器的startUpdate开始滑动更新;

(2)一开始根据PageLimit获取缓存量级,得到缓存的起始位置坐标和终点位置坐标,当滑动的时候,一部分会从缓存区移除,一部分会进入缓存区,因此左右两边都开始对缓存区以外的数据destoryItem,对于新添加进来的addItem

(3)当滑动完成之后,设置一下当前页面setPrimaryItem

(4)调用适配器的finishUpdate结束更新。

2、setUserVisiableHint的生命周期

假设当前页面有4个Tab界面,pageLimit = 1

swift懒加载 fragment懒加载原理_android


如果当页面从Tab1跳转到Tab3,因为Tab2已经被初始化缓存过了,所以跳转到Tab4首先要缓存Tab4。

(1)缓存Tab4,setUserVisiableHint(false)

(2)Tab1从缓存区移除,setUserVisiableHint(false)

(3)Tab3设置为当前页面,setUserVisiableHint(true)

(4)最后才开始Fragment的生命周期。

@Override
    public void finishUpdate(@NonNull ViewGroup container) {
        if (mCurTransaction != null) {
            // We drop any transactions that attempt to be committed
            // from a re-entrant call to finishUpdate(). We need to
            // do this as a workaround for Robolectric running measure/layout
            // calls inline rather than allowing them to be posted
            // as they would on a real device.
            if (!mExecutingFinishUpdate) {
                try {
                    mExecutingFinishUpdate = true;
                    #这个时候才commit提交,开始Fragment的生命周期
                    mCurTransaction.commitNowAllowingStateLoss();
                } finally {
                    mExecutingFinishUpdate = false;
                }
            }
            mCurTransaction = null;
        }
    }

也就是说在setUserVisiableHint之后,才进行Fragment的布局加载和数据加载,这是懒加载的关键之处。

3、懒加载框架搭建

public abstract class LazyFragment extends Fragment {

    private static final String LAZY_LOAD_TAG = "lazy_load";
    private View rootView;
    //View是否初始化完成
    private boolean isViewCreated;
    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        Log.e(LAZY_LOAD_TAG,"onCreateView");

        if(rootView == null) {
            rootView = LayoutInflater.from(getContext()).inflate(getLayout(), container, false);
        }
        isViewCreated = true;
        initView(rootView);
        if(getUserVisibleHint()){
            //如果当前页面是可见的,那么就分发事件加载
            dispatchUserVisibleHint(true);
        }
        return rootView;
    }

    protected abstract void initView(View rootView);

    protected abstract int getLayout();

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
    }
    //判断当前Fragment是否可见
    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        Log.e(LAZY_LOAD_TAG,"setUserVisibleHint");

        //当view初始化完成之后,才可以加载
        if(isViewCreated) {
            if (isVisibleToUser == true) {
                //当前Fragment可见,分发事件加载数据
                dispatchUserVisibleHint(true);
            } else {
                dispatchUserVisibleHint(false);
            }
        }
    }
    //分发事件:加载数据
    private void dispatchUserVisibleHint(boolean isVisibleToUser) {
        Log.e(LAZY_LOAD_TAG,"dispatchUserVisibleHint");

        if(isVisibleToUser){
            //数据加载
            onFragmentLoad();
        }else{
            //停止数据加载
            onFragmentStop();
        }
    }

    protected void onFragmentStop() {

    }

    protected void onFragmentLoad() {

    }

    @Override
    public void onResume() {
        super.onResume();
    }

    @Override
    public void onPause() {
        super.onPause();
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
    }


}

在进行懒加载的时候,需要注意的是,因为setUserVisibleHint是在View初始化之前就执行的,所以懒加载的时候,必须要等待View初始化完成之后,才可以进行事件分发,因此设置一个标志位isViewCreated,判断当前页面是否加载完成。

当默认加载首页的时候,因为只有在setUserVisibleHint为可见的时候,才会分发事件,但是这个时候,主界面还没有初始化完成,也就是说主界面会一直卡在setUserVisibleHint位置,因为isViewCreated为false,所以就不会进行数据加载。

if(getUserVisibleHint()){
            //如果当前页面是可见的,那么就分发事件加载
            dispatchUserVisibleHint(true);
        }

因此在当前界面需要判断该页面是否可见,如果可见,那么就可以分发事件加载数据。

目前来说,在相邻页面之间切换时,是没有问题的,但是在跨页面跳转的时候,比如从Tab4跳转到Tab2,那么会出现以下的场景。

2020-05-07 20:02:39.601 20974-20974/com.example.lazyload E/lazy_load: setUserVisibleHint
2020-05-07 20:02:39.601 20974-20974/com.example.lazyload E/lazy_load: dispatchUserVisibleHint
2020-05-07 20:02:39.601 20974-20974/com.example.lazyload E/TAG: hisFragment数据停止加载
2020-05-07 20:02:39.602 20974-20974/com.example.lazyload E/lazy_load: setUserVisibleHint
2020-05-07 20:02:39.602 20974-20974/com.example.lazyload E/lazy_load: dispatchUserVisibleHint
2020-05-07 20:02:39.602 20974-20974/com.example.lazyload E/TAG: shopFragment数据停止加载
2020-05-07 20:02:39.602 20974-20974/com.example.lazyload E/lazy_load: setUserVisibleHint
2020-05-07 20:02:39.602 20974-20974/com.example.lazyload E/lazy_load: dispatchUserVisibleHint
2020-05-07 20:02:39.602 20974-20974/com.example.lazyload E/lazy_load: setUserVisibleHint
2020-05-07 20:02:39.602 20974-20974/com.example.lazyload E/lazy_load: dispatchUserVisibleHint
2020-05-07 20:02:39.603 20974-20974/com.example.lazyload E/TAG: hisFragment数据加载
2020-05-07 20:02:39.606 20974-20974/com.example.lazyload E/lazy_load: onCreateView
2020-05-07 20:02:39.607 20974-20974/com.example.lazyload E/lazy_load: dispatchUserVisibleHint
2020-05-07 20:02:39.607 20974-20974/com.example.lazyload E/TAG: hisFragment数据加载
2020-05-07 20:02:39.614 20974-20974/com.example.lazyload E/lazy_load: onCreateView

当跳转到Tab2的时候,首先Tab2会销毁一次数据,然后又加载了2次数据,这是不允许的,这样就导致资源的浪费,所以这就涉及到了懒加载时机的问题。

懒加载的时机:从不可见到可见的那一刻;
停止加载数据:从可见到不可见的那一刻。

因此还要设置一个标志位,判断当前界面是不是可见的currentVisiableState = false

//判断当前Fragment是否可见
    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        Log.e(LAZY_LOAD_TAG,"setUserVisibleHint");

        //当view初始化完成之后,才可以加载
        if(isViewCreated) {
            //当前可见状态从不可见----可见
            if (isVisibleToUser == true && !currentVisiableState) {
                //当前Fragment可见,分发事件加载数据
                dispatchUserVisibleHint(true);
            } else if(isVisibleToUser = false && currentVisiableState){ //可见变为不可见
                dispatchUserVisibleHint(false);
            }
        }
    }
    //分发事件:加载数据
    private void dispatchUserVisibleHint(boolean isVisibleToUser) {
        Log.e(LAZY_LOAD_TAG,"dispatchUserVisibleHint");

        //当前可见状态
        currentVisiableState = isVisibleToUser;
        if(isVisibleToUser){
            //数据加载
            onFragmentLoad();
        }else{
            //停止数据加载
            onFragmentStop();
        }
    }
2020-05-07 20:31:10.164 22590-22590/com.example.lazyload E/lazy_load: setUserVisibleHint
2020-05-07 20:31:10.170 22590-22590/com.example.lazyload E/lazy_load: onCreateView
2020-05-07 20:31:10.171 22590-22590/com.example.lazyload E/lazy_load: dispatchUserVisibleHint
2020-05-07 20:31:10.171 22590-22590/com.example.lazyload E/TAG: AboutFragment数据加载
2020-05-07 20:31:10.178 22590-22590/com.example.lazyload E/lazy_load: onCreateView

此时还有一个问题就是,当从一个Fragment界面跳转到一个Activity界面的时候,要执行Fragment的onPause方法,当回到宿主Activity的时候,需要执行Fragment的onResume方法。

当离开宿主Activity的时候,需要停止加载当前页面的数据。

@Override
    public void onResume() {
        super.onResume();
        //当
        if(!currentVisiableState && getUserVisibleHint()){
            dispatchUserVisibleHint(true);
        }
    }

    @Override
    public void onPause() {
        super.onPause();
        //当前页面可见的时候,发送停止加载指令
        if(currentVisiableState && getUserVisibleHint()){
            dispatchUserVisibleHint(false);
        }
    }

当DestoryView的时候,需要把所有的状态值复位。

@Override
    public void onDestroyView() {
        super.onDestroyView();
        isViewCreated = false;
        currentVisiableState = false;
    }