阅读 1439

Android在多种设计下实现懒加载机制

前言


前段时间在自己的练习项目中想用到懒加载机制,查看了大多数资料只介绍了在View Pager+Fragment组合的情况下实现的懒加载,但是现在大多数App更多的是Fragmentmanager去管理主页面多个Fragment的显示与隐藏,然后主界面的某个或多个Fragment里又嵌套了多个Fragment+ViewPager(详细见下图),对于这种情况,适用于第一种的方式是不能直接解决第二种的情况的,所以写下这篇文章,记录一下踩的几个坑,希望对同像我一样的初学者提供一种思考方式作为参考(如果有错误或者不合适的地方,希望各位前辈能在评论区指出,非常感谢!)。

关于懒加载


1. 什么是懒加载?

懒加载也叫延迟加载,在APP中指的是每次只加载当前页面,是一种很好的优化APP性能的一种方式。

2.为什么要用懒加载?

  • 优化APP性能,提升用户体验:如果用户打开某页面,就会去预加载其它的页面时,数据集较小或者网络性能较优时还好,但是如果数据集过大或者网络性能不佳时,就会造成用户等待的时间较长,APP界面产生明显的滞顿感的情况,严重影响到用户的体验。
  • 减少无效资源的加载,减少服务器的压力,节省用户流量:如果用户只想浏览或者经常浏览某个特定的页面,如果使用预加载的方式,就会造成资源浪费,增加服务器的压力等。

实现懒加载


1.ViewPager+Fragment情况

1.1遇到的问题

在我们平时开发中,经常使用ViewPager+Fragment的组合来实现左右滑动的页面设计(如上图),但是ViewPger有个预加载机制,默认会把ViewPager当前位置的左右相邻页面预先初始化(俗称预加载),即使设置setOffscreenPageLimit(0)也无效果,也会预加载。通过点进源码中发现,如果不主动设置setOffscreenPageLimit()方法,mOffscreenPageLimit默认值为1,即使设置了0(小于1)的值了,但是还会按照mOffscreenPageLimit=limit=1处理。

private int mOffscreenPageLimit = 1;//即使不设置,默认值就为1

public int getOffscreenPageLimit() {
        return this.mOffscreenPageLimit;
    }
    
public void setOffscreenPageLimit(int limit) {
        if (limit < 1) {//设置为0,还是会默认为1
            Log.w("ViewPager", "Requested offscreen page limit " + limit + " too small; defaulting to " + 1);
            limit = 1;
        }
        if (limit != this.mOffscreenPageLimit) {
            this.mOffscreenPageLimit = limit;
            this.populate();
        }
复制代码

1.2 解决思路

Fragment 有一个非生命周期的setUserVisibleHint(boolean isVisibleToUser)回调方法,ViewPager 嵌套 Fragment 时会起作用,如果切换 ViewPager 则该方法也会被调用,参数isVisibleToUsertrue代表当前 Fragment 对用户可见,否则不可见。所以最简单的思路:Fragment可见时才去加载数据,不可见时就不让它加载数据。据我们创建抽象BaseFragment,对其进行封装。首先我们引入isVisibleToUser变量,负责保存当前Fragment对用户的可见状态。同时还有几个值得注意的地方:

  • setUserVisibleHint(boolean isVisibleToUser)方法的回调时机并没有与Fragment的生命周期有确切的关联,比如说,回调时机有可能在onCreateView()方法之后,也可能在onCreateView()方法之前。因此,必须引入一个标志位isPrepareView判断view是否创建完成,不然,很容易会造成空指针异常。我们初始化该变量为false,在onViewCreated()中,也就是view创建完成后,将其赋值为true

  • 数据初始化只应该加载一次,因此,引入第二个标志位,isInitData,初始为false,在数据加载完成之后,将其赋值为true,下次返回此页面时不会再自动加载。至此,我们的懒加载方法考虑了所有条件。也就是当isVisibleToUsertrueisInitDatafalseisPrepareViewtrue时,进行数据加载,并且加载后为了防止重复调用,将isInitData赋值为true

  • 将懒加载数据提取成一个方法,那么这个方法该何时调用呢?首先 setUserVisibleHint(boolean isVisibleToUser)方法中是必须调用的,即当Fragment由可见变为不可见和不可见变为可见时回调。 其次,很容易忽略的一点。对于第一个Fragment,如果setUserVisibleHint(boolean isVisibleToUser )方法在onCreateView()之前调用的话,如果懒加载方法只在setUserVisibleHint(boolean isVisibleToUser )中调用,那么该Fragment将只能在被主动切换一次之后才能加载数据,这肯定是不可能的,因此,我们需要在view创建完成之后,也进行一次调用。思来想去,在onActivityCreated()方法中是最合适的。我们在继承的时候,在onViewCreated()方法中进行一些初始化就行了,这样不会引起冲突。

1.3 BaseFragment代码实现

public abstract class BaseFragment extends Fragment {

    private Boolean isInitData = false; //标志位,判断数据是否初始化
    private Boolean isVisibleToUser = false; //标志位,判断fragment是否可见
    private Boolean isPrepareView = false; //标志位,判断view已经加载完成 避免空指针操作

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        return inflater.inflate(getLayoutId(),container,false);
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        isPrepareView=true;//此时view已经加载完成,设置其为true
    }
    /**
     * 懒加载方法
     */
    public void lazyInitData(){
        if(!isInitData && isVisibleToUser && isPrepareView){//如果数据还没有被加载过,并且fragment已经可见,view已经加载完成
            initData();//加载数据
            isInitData=true;//是否已经加载数据标志重新赋值为true
        }
    }

    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        this.isVisibleToUser=isVisibleToUser;//将fragment是否可见值赋给标志isVisibleToUser
        lazyInitData();//懒加载
    }

    /**
     * fragment生命周期中onViewCreated之后的方法 在这里调用一次懒加载 避免第一次可见不加载数据
     * @param savedInstanceState
     */
    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        lazyInitData();//懒加载
    }

    /**
     * 由子类实现
     * @return 返回子类的布局id
      */
    abstract int getLayoutId();

    /**
     * 加载数据的方法,由子类实现
     */
    abstract void initData();
}
复制代码

2.Fragment+ViewPager+Fragment情况

2.1 遇到的问题

如图2,对于这种由Fragmentmanager管理主页面的多个Fragment的显示与隐藏,在其中的某个Fragment中又嵌套了多个Fragment的情况(如上图),上面的方案是无法解决的,如果主页面的Fragment直接继承上面的BaseFragment,就会出现主页的几个Fragment都不会加载的现象,为什么会这样呢,按道理说Fragment应该可见了,加载数据的判断逻辑应该没问题啊,而且上面那个demo也跑成功了。最终我发现,问题出在setUserVisibleHint()这个方法上,点进去它的源码发现注释中有这么一句话:

This may be used by the system to prioritize operations such as fragment lifecycle updates or loader ordering behavior.
复制代码

也就是说这个可能被用来在一组有序的Fragment里 ,例如 Fragment生命周期的更新。告诉我们这个方法被调用希望在一个pager里,因此 FragmentPagerAdapter 所以可以使用这个,而主页面的几个Fragment我们是通过Fragmentmanager管理的,所以setUserVisibleHint()是不会被调用,而我们设置的isVisibleToUser=false默认值一直不会变,那么lazyInitData()方法也就一直不会执行。

 /**
     * 懒加载方法
     */
    public void lazyInitData(){
        if(!isInitData && isVisibleToUser && isPrepareView){//因为isVisibleToUser一直都是false,所以iniData()是不会被执行的
            initData();//加载数据
            isInitData=true;
        }
    }
    
    
复制代码

2.2 解决思路

这里我的处理方式是,在lazyInitData()中多加了一段处理逻辑,如下:

/**
     * 懒加载方法
     */
    public void lazyInitData(){
        if(!isInitData  && isVisibleToUser && isPrepareView){//如果数据还没有被加载过,并且fragment已经可见,view已经加载完成
            initData();//加载数据
            isInitData=true;//是否已经加载数据标志重新赋值为true
        }else if (!isInitData && getParentFragment()==null && isPrepareView){
            initData();
            isInitData=true;
        }
    }
    
    /**
     * Fragment显示隐藏监听
     * @param hidden
     */
    @Override
    public void onHiddenChanged(boolean hidden) {
        super.onHiddenChanged(hidden);
        if (!hidden) {
        lazyInitData(); 
        }
    }
复制代码

对于主页面的多个Fragment只会在第二个判断逻辑处理(因为它的isVisibleToUser值一直等于false),对于嵌套的Fragment只会经过第一个处理逻辑(因为它的getParentFragment()!=null),然后通过onHiddenChanged()方法去加载lazyInitData()方法,这样以来就能处理这种情况了。

但是这时候又会出现一个问题,如果一个APP里第一种,第二种情况并存的话,这段代码又不适合第一种情况了,因为对于第一种的情况当判定isVisibleToUserfalse时,虽然不走第一个处理逻辑,但是它的getParentFragment()一直是等于null的,那么它就会走第二个判断逻辑,这样又会预加载了。

对于这种情况,我的处理方式: 给每个Fragment设置一个标志值,当是第一种情况时,设为true,第二种情况时,设置false,然后再分别处理相应的判断逻辑。代码如下:

 /**
     * 懒加载方法
     */
    public void lazyInitData(){
        if(setFragmentTarget()){
            if(!isInitData && isVisibleToUser && isPrepareView){//如果数据还没有被加载过,并且fragment已经可见,view已经加载完成
                initData();//加载数据
                isInitData=true;//是否已经加载数据标志重新赋值为true
            }
        }else {
            if(!isInitData && isVisibleToUser && isPrepareView){//如果数据还没有被加载过,并且fragment已经可见,view已经加载完成
                initData();//加载数据
                isInitData=true;//是否已经加载数据标志重新赋值为true
            }else if (!isInitData && getParentFragment()==null && isPrepareView ){
                initData();
                isInitData=true;
            }
        }
    }
    
     /**
     * 设置Fragment target,由子类实现
     */
    abstract boolean setFragmentTarget();
复制代码

经过这样的处理之后,第一种情况和第二种情况,或两者并存的情况下都能保证在继承一个base下,实现懒加载。

2.3 BaseFragmentTwo最终代码实现


public abstract class BaseFragmentTwo extends Fragment {
    private Boolean isInitData = false; //标志位,判断数据是否初始化
    private Boolean isVisibleToUser = false; //标志位,判断fragment是否可见
    private Boolean isPrepareView = false; //标志位,判断view已经加载完成 避免空指针操作

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        return inflater.inflate(getLayoutId(),container,false);
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        isPrepareView=true;//此时view已经加载完成,设置其为true
    }
    /**
     * 懒加载方法
     */
    public void lazyInitData(){
        if(setFragmentTarget()){
            if(!isInitData && isVisibleToUser && isPrepareView){//如果数据还没有被加载过,并且fragment已经可见,view已经加载完成
                initData();//加载数据
                isInitData=true;//是否已经加载数据标志重新赋值为true
            }
        }else {
            if(!isInitData && isVisibleToUser && isPrepareView){//如果数据还没有被加载过,并且fragment已经可见,view已经加载完成
                initData();//加载数据
                isInitData=true;//是否已经加载数据标志重新赋值为true
            }else if (!isInitData && getParentFragment()==null && isPrepareView ){
                initData();
                isInitData=true;
            }
        }
    }



    @Override
    public void onHiddenChanged(boolean hidden) {
        super.onHiddenChanged(hidden);
        if (!hidden) { lazyInitData(); }
    }

    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        this.isVisibleToUser=isVisibleToUser;//将fragment是否可见值赋给标志isVisibleToUser
        lazyInitData();//加载懒加载
    }

    /**
     * fragment生命周期中onViewCreated之后的方法 在这里调用一次懒加载 避免第一次可见不加载数据
     * @param savedInstanceState
     */
    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        lazyInitData();
    }

    /**
     * 由子类实现
     * @return 返回子类的布局id
      */
    abstract int getLayoutId();

    /**
     * 加载数据的方法,由子类实现
     */
    abstract void initData();

    /**
     * 设置Fragment target,由子类实现
     */
    abstract boolean setFragmentTarget();

}


复制代码

其它需要注意:

①给viewpager设置adapter时,一定要传入getChildFragmentManager(),否则getParentFragment()将会一直等于null,这会影响lazyInitData()的判断,导致懒加载出现混乱甚至无效的情况。

②demo中我使用的是ViewPager+Tablayout的组合方式,在使用Tablayout时一定要保证styles.xml中的主题应该使用Theme.AppCompat.Light.NoActionBar或者Theme.AppCompat.LightTheme.AppCompat.XXX的主题。

项目地址


项目地址

关注下面的标签,发现更多相似文章
评论