ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

ViewPager缓存

2021-08-13 04:00:06  阅读:310  来源: 互联网

标签:缓存 Fragment ViewPager void isVisibleToUser Override public 加载


引言

本文不再介绍ViewPager1 or ViewPager2的使用方式,而是直接描述其原理,介绍其预加载、缓存、懒加载等相关。给出相关示例,最后给出多层Fragment懒加载的最终代码。

原理

缓存和预加载

ViewPager至少会缓存两针数据,尽管你通过setOffscreenPageLimit(0)来希望不缓存任何数据,但发现起不到任何作用,从ViewPager1的源码中我们可以发现:

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

当然,你也可以通过方法覆盖的方式重写该函数,但还是不建议这种做法,这样会破话ViewPager原有的特性和能力。
值得注意的是,在函数中,我们发现它调用了populate()方法,该方法在onMeasure时也会被调用,它用来预加载和缓存ViewPager的Fragment。

当前Current的左右会各设置一个缓存项,同时由于ViewPager预加载的存在,导致缓存页会也走到onCreateView等生命周期,若你刚好是在onCreateView的时候加载页面,那么就会白白浪费两个页面的内存。
因此,我们希望只在显示页面的时候加载页面,此时就需要用到懒加载。

ViewPager1懒加载

ViewPager + Fragment(注意,这个Fragment不在androidx包),由于预加载的存在,一开始,ViewPager就会将currentItem,以及left cache item和right cache item都加载,就会执行Fragment的生命周期,就会直接来加载页面。
通过抽象源码来看主要预加载流程:

public void populate(mCurItem) {
  // ...
  mAdapter.startUpdate(this);
  // ...
  curItem = addNewItem(mCurItem, curIndex);
  // ...
  for (pos = mCurItem-1; pos>=0; pos--) {
     if (不在预加载范围) {
        mItems.remove(itemIndex);
        mAdapter.destroyItem(this, pos, ii.object);
        // ...
      } else if (在范围,但被加载过) {
          // 忽略,只进行计数等...
      } else {  // 在范围,没有被加载过
          addNewItem(pos, itemIndex + 1);
          // ...
      }
  }
  // 同上
  for (post = mCurItem+1; post<N;post ++) {
     if (不在预加载范围) {
        mItems.remove(itemIndex);
        mAdapter.destroyItem(this, pos,...);
        // ...
      } else if (在范围,但被加载过) {
          // 忽略,只进行计数等...
      } else {  // 在范围,没有被加载过
          addNewItem(pos, itemIndex + 1);
          // ...
      }
  }
  // ...
  mAdapter.setPrimaryItem(this, mCurItem, curItem.object);
  // ...
  mAdapter.finishUpdate(this);
}

主要流程:当onMeasure当前ViewPager的时候会调用populate,在populate中执行一系列缓存和预加载。
1、调用mAdapter.startUpDate(this),表示开始加载
2、调用addNewItem->mApdater.instantiateItem()创建Item(Fragment),同时获取了FragmentManager,调用了beginTransaction()
3、循环遍历当前Item左边的item,如果不在预加载范围内,就调用mAdapter.destroyItem()销毁Item,否则如果在预加载范围内,如果已经被加载,就忽略,否则就调用addNewItem加载新的Item
4、循环遍历当前item右边的item,同上
5、调用setPrimaryItem,调用离开的fragment.setUserVisibleHint(false),调用当前的fragment,setUservisibleHint(true)
6、调用mAdapter.finishUpdate,即调用了transaction.commitNowAllowingStateLoss();

从上述流程不难发现,Fragment生命周期在最后finishiUpdate时通过transaction才开始执行,因此,setUserVisibleHint()函数的调用在Fragment生命周期执行之前。

老方案:ViewPager1+Fragment懒加载

值得注意的是,这里的Fragment,我们使用的是android.support.v4.app.Fragment,值得注意的是,在高版本的SDK中,该类已被弃用。
具体ViewPager1+Fragment的使用在本文中不做赘述。接下来,本文将一步步推理出懒加载的实现。
测试1:由于最终需要通过setUserVisibleHint()来设置当前Fragment的可见状态,因此只需在该函数状态改变时,调用onFragmentLoad();或者onFragmentLoadStop();

package com.hc.viewPager_fragment;

import android.app.Fragment;
import android.os.Bundle;
import android.view.View;
import androidx.annotation.Nullable;

public class LazyFragment01 extends Fragment {

    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        if (isVisibleToUser) {
            onFragmentLoad();
        } else {
            onFragmentLoadStop();
        }
    }

    @Override
    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
    }

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

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

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

    protected void onFragmentLoad() {}

    protected void onFragmentLoadStop() {}
}

上述方案的缺陷是,无法在onFragmentLoad和onFragmentLoadStop中获取UI,因为setUserVisibleHint()函数在Fragment生命周期之前调用,否则会出现奔溃。

测试2:在测试1的基础上,让load和loadStop在onCreateView之后调用。

package com.hc.viewPager_fragment;

import android.app.Fragment;
import android.os.Bundle;
import android.view.View;
import androidx.annotation.Nullable;

public class LazyFragment02 extends Fragment {

    private boolean isCreated;

    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        if (!isCreated) {
            return;
        }
        dispatchVisibleState(isVisibleToUser);
    }

    @Override
    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        isCreated = true;
        if (getUserVisibleHint()) {
            dispatchVisibleState(true);
        }
    }

    private void dispatchVisibleState(boolean isVisibleToUser) {
        if (isVisibleToUser) {
            onFragmentLoad();
        } else {
            onFragmentLoadStop();
        }
    }

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

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

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

    protected void onFragmentLoad() {}

    protected void onFragmentLoadStop() {}
}

我们通过变量,isCreated来决定是否分发,并在onCreateView的时候补充一个load或loadStop分发,因为在setUserVisibleHint的时候会过滤调View还没创建的部分。
但又引发了一个从未显示过的页面也将停止加载的问题。通过该方式,的确可以实现对界面懒加载,但不应该未展示过的界面出现停止加载,导致性能损耗。
出现该问题的原因如下:

从上图可知,由1跳到4,则3、4、5加载,最终显示4,其中3、5也将被ViewPager调用setUserVisibleHint(false),使得也会调用onFragmentLoadStop(),导致一些错误的操作。
因此,测试3:我们还需要一个额外的变量来控制。

package com.hc.viewPager_fragment;

import android.app.Fragment;
import android.os.Bundle;
import android.view.View;
import androidx.annotation.Nullable;

public class LazyFragment03 extends Fragment {

    private boolean isCreated;

    private boolean isPreVisible = false;   // 之前是否可见

    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        if (!isCreated) {
            return;
        }
        if (!isPreVisible && isVisibleToUser) {
            dispatchVisibleState(true);
        } else if(isPreVisible && !isVisibleToUser){
            dispatchVisibleState(false);
        }
    }

    @Override
    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        isCreated = true;
        if (getUserVisibleHint()) {
            dispatchVisibleState(true);
        }
    }

    private void dispatchVisibleState(boolean isVisibleToUser) {
        isPreVisible = isVisibleToUser;
        if (isVisibleToUser) {
            onFragmentLoad();
        } else {
            onFragmentLoadStop();
        }
    }

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

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

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

    protected void onFragmentLoad() {}

    protected void onFragmentLoadStop() {}
}

通过判断之前的状态,和后序的状态来决定事件的分发。

测试4:当上述还存在一个问题是,Fragment可能是其他Activity跳过来的,那么此时就不会走setUserVisibleHint(),因此需要在onResume和onPause中进行事件分发。
在测试3的基础上增加如下代码:

    @Override
    public void onResume() {
        super.onResume();
        if (!isPreVisible && getUserVisibleHint()) {
            dispatchVisibleState(true);
        }
    }

    @Override
    public void onPause() {
        super.onPause();
        if (isPreVisible && !getUserVisibleHint()) {
            dispatchVisibleState(false);
        }
    }

测试5:当LazyFragment嵌套其他Fragment时,当在onCreateView中加载子Fragment时,会导致还不可见就会被加载,因此需要进行过滤,只有当父可见时才进一步加载。
同时,由于Fragment嵌套,切过去无感知,需要手动分发一下

    private void dispatchVisibleState(boolean isVisibleToUser) {
        if (isPreVisible == isVisibleToUser) {
            return;
        }
        isPreVisible = isVisibleToUser;

        // 解决在initView中嵌套子Fragment的情况,导致还不可见就被加载的情况
        // 有以下情形,在该Fragment的隔壁的LazyFragment中嵌套了ViewPager,在initView的时候初始化了ViewPager,进而使得子Fragment被提前加载,
        // 需要增加如下判断
        // 只有parentFragment可见时才加载
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) {
            Fragment parentFragment = getParentFragment();
            if (parentFragment instanceof LazyFragment01 && !parentIsVisible()) {
                return;
            }
        }

        if (isVisibleToUser) {
            onFragmentLoad();
            dispatchChildVisibleState(true);    // Fragment嵌套时,切过去子Fragment无感知,需要手动分发一下
        } else {
            onFragmentLoadStop();
            dispatchChildVisibleState(false);
        }
    }

    private void dispatchChildVisibleState(boolean state) {
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
            FragmentManager fragmentManager = getChildFragmentManager();
            List<Fragment> fragments = fragmentManager.getFragments();
            if (fragments != null) {
                for (Fragment fragment : fragments) {
                    if (fragment instanceof LazyFragment01 && !fragment.isHidden() && fragment.getUserVisibleHint()) {
                        ((LazyFragment01)fragment).dispatchVisibleState(state);
                    }
                }
            }
        }
    }

还还需注意的是要将可见状态分发给子Fragment。

ViewPager2

ViewPager2中的坑

ViewPager2的用法和ViewPager非常类似,FragmentStatePageAdapter换成了FragmentPageAdapter。但这里仍然有一些坑,如有时候会销毁前面的Fragment,有时候又不会。
具体表现为:当ViewPager2存在多个Fragment时,当访问前几个Fragment的时候,我们发现并不会调用前面的Fragment的销毁。当访问超过3个Fragment的时,开始陆续销毁前面的Fragment。特别的,当访问最后一个Fragment的时候,又不会销毁前面的Fragment了。
原因:RecyclerView的回收复用机制导致的。
1、由于RecyclerView不可见的item使用mCacheView缓存2个。因此,若访问超过3个item时,就会销毁之前的item。
2、由于RecyclerView预取的机制存在,使得在访问第i个item时,会向后预取第i+1个ViewHolder,同时缓存大小也会+1。因此在访问最后一个item时,由于缓存大小为3了,因此相当于前面可以缓存3个item,因此倒数第4个item不会被销毁。

ViewPager2懒加载的用法

1、使用。ViewPager2默认就实现了懒加载。由于ViewPager2的Fragment只有可见时才调用onResume方法,因此我们可以在onResume方法中进行数据加载,这就可以实现懒加载。
2、优化。同时,由于ViewPager2的缓存大小为2+1,因此最多只能缓存3个item,会频繁创建和销毁Fragment,因此我们可以通过调用mViewPager2.setOffscreenPageLimit(mFragments.size());,之后再在onResume中实现加载数据的逻辑。

参考:https://blog.csdn.net/qq_36486247/article/details/103959356

标签:缓存,Fragment,ViewPager,void,isVisibleToUser,Override,public,加载
来源: https://www.cnblogs.com/csluoyao/p/15077838.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有