viewPager+Fragment从零开始实现懒加载

相信很多小伙伴在面试的时候,都会被问到性能优化这一块,当然对于一个成熟的app,这一块是必不可少的。我们在开发app的时候,经常会有这样的需求:底部或者顶部几个栏目,中间内容跟着栏目的切换而变化,需要用到的控件就是TabLayout+ViewPager。做出来这样的效果其实并不难,跟着官方文档也能做出来,但是你真正了解过ViewPager的缓存机制吗?你知道在viewPager中Fragment的生命周期是怎样的? 这就是懒加载的由来。AndroidX版本,系统自己也实现了懒加载机制,使用的是LifeCycle进行管理,所以,本篇文章将带大家从原理到实现,从v4版本到Androidx版本拿下懒加载。

一、懒加载

首先大家需要了解什么是懒加载?所谓的懒加载就是需要的时候(界面即将展示给用户)在加载,不需要的(界面即将销毁)暂停页面的一切操作。比如我页面中有很多fragment,并且都需要在初始化的时候执行网络等耗时操作,加载一个fragment的耗时是2s,那么完全加载所有的fragment就需要消耗大量的cpu资源,导致页面卡顿。以以下demo为例:
在这里插入图片描述

优化前:

在这里插入图片描述
现象说明:

  • 前两行日志是刚进入activity就打印了,说明默认加载了2个fragment
  • 后面几行日志就是我不断的来回切换fragment。发现有时候销毁的并不是上一个fragment(比如:fragmentThree->fragmentTwo,销毁的是fragmentFour)。并且这种不确定因素很强。

优化后:
在这里插入图片描述
现象说明:

  • 第一行日志是刚进入activity就打印了,只会加载第一个fragment
  • 后面无论fragment如何切换,销毁的永远是上一个fragment,创建的永远是现在的fragment。

下面让我们根据不同版本的ViewPager实现这种懒加载优化

二、v4版本

1、原理

首先需要了解,为什么会出现上面默认加载两个fragment的问题???我们知道可以通过设置Viewpager.setOffscreenPageLimit(num) 来设置默认缓存多少个fragment。跟进源码:
在这里插入图片描述
确定limit的值最少为1,并且默认为1,所以系统会默认缓存1个fragment,加上第一页显示的fragment,总共需要初始化2个fragment。当我们设置limit的数量大于1时,会进入populate()方法,这里就是真正实现fragment缓存的方法。

void populate(int newCurrentItem) {
       ...

		//1、开始
        mAdapter.startUpdate(this);

        ...

        // 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);
        }

       
        if (curItem != null) {
			//2、计算左边需要加入缓存的item
            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;
                    }
                    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 {
					//2、1创建新的item
                    ii = addNewItem(pos, itemIndex + 1);
                    extraWidthLeft += ii.widthFactor;
                    curIndex++;
                    ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
                }
            }
			//3、计算右边需要加入缓存的item
            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;
                        }
                        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 {
                   	 	//3、1创建新的item
                        ii = addNewItem(pos, itemIndex);
                        itemIndex++;
                        extraWidthRight += ii.widthFactor;
                        ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
                    }
                }
            }

            calculatePageOffsets(curItem, curIndex, oldCurInfo);

			//4、设置当前需要显示的item
            mAdapter.setPrimaryItem(this, mCurItem, curItem.object);
        }

		//5、结束
        mAdapter.finishUpdate(this);

        ...
    }

这个方法大致分为5个步骤:

  • Adapter.startUpdate()
  • 计算左边需要移到缓存、销毁的界面,以及创建新的item(调用addNewItem),保存起来
  • 计算右边需要移到缓存、销毁的界面,以及创建新的item(调用addNewItem),保存起来
  • addNewItem()
  • Adapter.setPrimaryItem()
  • Adapter.finishUpdate()

这里采用了适配器模式,第二步和第三步过程中会新建新的item,并保存在ArrayList中,代码中变量为mItems。其实从这里也可以看到PagerAdapter的调用生命周期。

private final ArrayList<ItemInfo> mItems = new ArrayList<ItemInfo>();
 ItemInfo addNewItem(int position, int index) {
        ItemInfo ii = new ItemInfo();
        ii.position = position;
        ii.object = mAdapter.instantiateItem(this, position);
        ii.widthFactor = mAdapter.getPageWidth(position);
        if (index < 0 || index >= mItems.size()) {
            mItems.add(ii);
        } else {
            mItems.add(index, ii);
        }
        return ii;
    }

每一个item信息被存放在ItemInfo 中:

static class ItemInfo {
        Object object;
        int position;
        boolean scrolling;
        float widthFactor;
        float offset;
    }

这个类包含当前item在视图中的位置position,object就是fragment对象本身。

那么这个object是如何被创建出来的呢???回到**addNewItem()**方法,注意下面一句代码

 ii.object = mAdapter.instantiateItem(this, position);

这个instantiateItem方法在源码中是个空方法,需要使用者在子类中自己实现具体。这就不得不引出FragmentPagerAdapter,我们看看他是如何实现的???

--------FragmentPagerAdapter.java--------

@Override
    public Object instantiateItem(@NonNull ViewGroup container, int position) {
	
		//mFragmentManager管理事务
		if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        }

        final long itemId = getItemId(position);

        // 通过tag找到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);
            
            //将fragment添加到事务中
            mCurTransaction.add(container.getId(), fragment,
                    makeFragmentName(container.getId(), itemId));
        }
		//新增fragment不为当前的fragment
        if (fragment != mCurrentPrimaryItem) {
        	//调用setUserVisibleHint方法
            fragment.setMenuVisibility(false);
            fragment.setUserVisibleHint(false);
        }
		//返回fragment对象
        return fragment;
    }

从上面的代码可以看到,创建fragment的过程中首先会调用它的setUserVisibleHint(false) 方法,并且是默认不可见的。根据上面分析ViewPager的生命周期,接下来会进入**setPrimaryItem()**方法

 @Override
    public void setPrimaryItem(ViewGroup container, int position, Object object) {
        Fragment fragment = (Fragment)object;
        if (fragment != mCurrentPrimaryItem) {
            if (mCurrentPrimaryItem != null) {
                mCurrentPrimaryItem.setMenuVisibility(false);
                mCurrentPrimaryItem.setUserVisibleHint(false);
            }
            if (fragment != null) {
                fragment.setMenuVisibility(true);
                fragment.setUserVisibleHint(true);
            }
            mCurrentPrimaryItem = fragment;
        }
    }

将上一界面设置为不可见,将当前界面设置为可见,再次调用 setUserVisibleHint(true)

总结:

  • PagerAdapter生命周期:startUpdate->instantiateItem()->setPrimaryItem()->finishUpdate;
  • viewPager中的缓存是保存在mItems的ArrayList中,每个item信息被封装成ItemInfo对象;
  • 所有fragment创建的时候都会调用setUserVisibleHint(false),在fragment即将可见的时候会调用setUserVisibleHint(true)。

ok,了解了上面的原理,就可以进行懒加载处理了。就是处理setUserVisibleHint() 关键方法

2、代码
public abstract class LazyFragment extends Fragment {

    protected View root;
    //当前view是否创建
    private boolean isViewCreated = false;
    //当前view状态变化
    private boolean beforeVisibleState  = false;
    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
        if(root == null){
            root = getLayoutInflater().inflate(getLayoutResource(),container,false);
        }
        initView(root);

        isViewCreated = true;
        if(getUserVisibleHint()){
            setUserVisibleHint(true);
        }

        return root;
    }

    @Override
    public void onResume() {
        super.onResume();
        if (getUserVisibleHint()) {
            dispatchHintState(true);
        }


    }
    @Override
    public void onPause() {
        super.onPause();
        if (getUserVisibleHint()) {
            dispatchHintState(false);
        }


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

    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        if(isViewCreated){
            if(!beforeVisibleState && isVisibleToUser){ //从不可见到可见
                dispatchHintState(true);
            }else if(beforeVisibleState && !isVisibleToUser){ //从可见到不可见
                dispatchHintState(false);
            }
        }
    }

    private void dispatchHintState(boolean state){

        //避免走两次,因为onResume会再次调用
        if(beforeVisibleState == state){
            return;
        }
        
        beforeVisibleState = state;
        if(state){
            startLoadData();
        }else{
            stopLoadData();
        }
    }
    /**
     * 停止加载数据
     */
    protected void stopLoadData() {
    }

    /**
     * 开始加载数据
     */
    protected void startLoadData() {
    }

    protected abstract void initView(View root);

    protected abstract int getLayoutResource();
}

所有的fragment继承这个LazyFragment,重写需要的方法就行了。

对于这个类需要注意几个点

  • viewPager中fragment生命周期:setUserVisibleHint->onCreateView->onResume->onPause->onDestroyView
  • 当我们第一个fragment即将可见的时候,会调用startLoadData()方法,如果子类重写了它,并且进行了ui操作,就会报错,这是因为此次还没有调用onCreateView()方法,拿不到控件的。所以这里要是用 isViewCreated 这个变量进行控制
  • 当我们从上一个fragmentTwo->fragementOne的时候,fragmenTwo即将消失,并且是从可见到不可见的;fragmentOne即将显示,并且是从不可见到可见的。所以这里要用 beforeVisibleState 这个变量进行严格控制,防止其他fragment乱入,仅仅处理这两个fragment的切换。
  • 当我们从fragment中跳转到其他的activity,需要停止当前fragment的数据加载,并且重新回到该fragemnt需要重新加载数据,这样就需要重写onResume和onPause方法了,设置数据加载状态。

ok,以上就完成了旧版本的Viewpager嵌套fragment的懒加载机制。试试吧!!!

二、androidX版本

对于androidX,抛弃了setUserVisibleHint这个方法,底层使用LifeCycle来绑定fragment生命周期,让我们来看看原理吧。

1、原理

同样进入到PagerAdapter的实现类FragmentPagerAdapter中,看他是如何创建一个Item对象的。

@Override
    public Object instantiateItem(@NonNull ViewGroup container, int position) {
    
       ... 和之前代码一样
       
        if (fragment != mCurrentPrimaryItem) {
            fragment.setMenuVisibility(false);
			//当满足条件时,为事务添加最大走到的生命周期为onStart
            if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
                mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.STARTED);
            } else {
            //和v4的分析一致,做了兼容
                fragment.setUserVisibleHint(false);
            }
        }

        return fragment;
    }

所以合理重点分析

 mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.STARTED);
public FragmentTransaction setMaxLifecycle(@NonNull Fragment fragment,
            @NonNull Lifecycle.State state) {
        addOp(new Op(OP_SET_MAX_LIFECYCLE, fragment, state));
        return this;
    }

这里开始创建一个Op对象,走一下构造方法

        Op(int cmd, Fragment fragment) {
            this.mCmd = cmd;
            this.mFragment = fragment;
            this.mOldMaxState = Lifecycle.State.RESUMED;
            this.mCurrentMaxState = Lifecycle.State.RESUMED;
        }
        Op(int cmd, @NonNull Fragment fragment, Lifecycle.State state) {
            this.mCmd = cmd;
            this.mFragment = fragment;
            this.mOldMaxState = fragment.mMaxState;
            this.mCurrentMaxState = state;
        }

看到这里再对比之前,我们知道,v4版本的fragment在初始化的时候会走到onResume生命周期,而androidX则只会走到onStart()方法,所以我们可以判断是否执行onResume()方法来判断当前fragment是否可见。

2、代码
public abstract class LazyFragment extends Fragment {

    protected View root;
    //当前view状态变化
    private boolean beforeVisibleState  = false;
    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
        if(root == null){
            root = getLayoutInflater().inflate(getLayoutResource(),container,false);
        }
        initView(root);

        return root;
    }

    @Override
    public void onResume() {
        super.onResume();
        if (!beforeVisibleState && isResumed()) {
            dispatchHintState(true);
        }
        beforeVisibleState = true;

    }
    @Override
    public void onPause() {
        super.onPause();
        if (beforeVisibleState && !isResumed()) {
            dispatchHintState(false);
        }
        beforeVisibleState = false;
    }

    private void dispatchHintState(boolean state){

        //避免走两次,因为onResume会再次调用
        if(beforeVisibleState == state){
            return;
        }

        beforeVisibleState = state;
        if(state){
            startLoadData();
        }else{
            stopLoadData();
        }
    }
    /**
     * 停止加载数据
     */
    protected void stopLoadData() {
    }

    /**
     * 开始加载数据
     */
    protected void startLoadData() {
    }

    protected abstract void initView(View root);

    protected abstract int getLayoutResource();
}

只需要在onResume和onPause方法中控制逻辑即可。在activity中需要这样使用FragmentPagerAdapter

FragmentPagerAdapter adapter = new FragmentPagerAdapter(getSupportFragmentManager(),BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
            @NonNull
            @Override
            public Fragment getItem(int position) {
                return fragments.get(position);
            }

            @Override
            public int getCount() {
                return fragments.size();
            }
        };

构造方法传入BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT。这样就可以实现懒加载了。

源码地址

懒加载源码

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值