Android 懒加载

为什么要使用懒加载?

我们经常会在项目中使用 ViewPager + Fragment 加载Tab数据,但是由于 ViewPager 有预加载的功能,所以当我们第一次进入时会加载不止第一页的数据。假如我们在其他页面做了一些耗时操作,就有可能导致页面不流畅,为了解决这种情况,所以就需要使用到懒加载。

什么是懒加载?

懒加载说白了就是当Fragment可见时,才进行网络请求或者耗时操作;当Fragment 不可见时,将当前页面的网络请求或者耗时操作暂停(比如Handler)。通过这种做法提高程序的性能和优化用户体验。

懒加载解决方式
1.设置setOffscreenPageLimit
// 默认的缓存页面数量(常量)
private static final int DEFAULT_OFFSCREEN_PAGES = 1;

// 缓存页面数量(变量)
private int mOffscreenPageLimit = DEFAULT_OFFSCREEN_PAGES;

public void setOffscreenPageLimit(int limit) {
    // 当我们手动设置的limit数小于默认值1时,limit值会自动被赋值为默认值1(即DEFAULT_OFFSCREEN_PAGES)
    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) {
        // 经过前面的拦截判断后,将limit的值设置给mOffscreenPageLimit
        mOffscreenPageLimit = limit;
        populate();
    }
}

通过源码可以看出 ViewPagersetOffscreenPageLimit 最小值为1,所以 ViewPager 在通常情况下都会加载左右各一个页面,也就是说无法通过这种系统的方法实现懒加载效果。

2.自己实现一个懒加载

1.首先创建 ViewPager + Fragment 的页面,适配器选择使用FragmentStatePagerAdapter

2.setUserVisibleHint()
在Fragment创建时会先被调用一次,传入 isVisibleToUser = false
如果当前 Fragment 可见,那么 setUserVisibleHint() 会再次被调用一次传入 isVisibleToUser = true;
如果 Fragment 从可见->不可见,那么 setUserVisibleHint()也会被调用,传入 isVisibleToUser = false
总结:setUserVisibleHint() 除了 Fragment 的可见状态发生变化时会被回调外,在 new Fragment() 时也会被回调,此方法先于生命周期方法执行
如果我们想要在 Fragment 可见与不可见时做一些操作,就需要做一些判断重新封装。

3.先看一下正常启动时 setUserVisibleHint() 怎么执行的,先写一个BaseLazyLoadFragment 类继承自 Fragment
BaseLazyLoadFragment部分代码如下

 @Override
 public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,  
 @Nullable Bundle savedInstanceState) {
     Log.d(getClass().getSimpleName(), "onCreateView: ");
      if (view == null) {
            view = inflater.inflate(getLayoutId(), null);
        }
        initView(view);
        return view;
    }
 @Override
 public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        Log.d(getClass().getSimpleName(), "setUserVisibleHint: " + isVisibleToUser);
    }

 protected abstract void initView(View view);

 protected abstract int getLayoutId();

Fragment代码如下

public class TestFragment3 extends BaseLazyLoadFragment {
    private static final String TAG = "TestFragment3";
    @Override
    protected void initView(View view) {
    }
    @Override
    protected int getLayoutId() {
        return R.layout.fragment_test3;
    }
    
  
}

进入程序依次打开三个页面运行结果如下
在这里插入图片描述
于此可见上面对 setUserVisibleHint() 总结是对的。
接下来分析一些这三屏的日志
1.除去第一页,第二页、第三页、第四页的 onCreateViewsetUserVisibleHint()=false 都已经在前一个页面调用了,第一次加载时滑到当前页面(除第一页)实际调用了上一个页面的 setUserVisibleHint()=false 当前页面的setUserVisibleHint()=true 以及下一个页面的 onCreateView 。针对这种情况可以在onCreateView 中定义一个 isViewCreateboolean 变量记录,然后在 setUserVisibleHint() 方法中,根据 isViewCreate值,再去调用页面自己定义的可见和不可见方法
这个时候代码就是下面这样

 private boolean isViewCreate = false;//  View 是否创建

    @Override
    public View onCreateView(@NonNull LayoutInflater inflater,   
    @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        Log.d(getClass().getSimpleName(), "onCreateView: ");
        if (view == null) {
            view = inflater.inflate(getLayoutId(), null);
        }
        initView(view);
        isViewCreate = true;
        return view;
    }
    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        if (isViewCreate) {
            if (isVisibleToUser) {
                dispatchVisibleHint(true);
            } else {
                dispatchVisibleHint(false);
            }
        }
   }
    public void dispatchVisibleHint(boolean isVisible) {
        if (isVisible) {
            onFragmentResume();
        } else {
            onFragmentPause();
        }
    }
    public void onFragmentPause() {
    }
    public void onFragmentResume() {
    }

2.上面的分析是在排除了第一页之后写的逻辑,所以需要对第一页加载做一些判断。第一页显示时的日志是,先调用 setUserVisibleHint()=false =>setUserVisibleHint()= true => onCreateView , 其他页面显示当前页面的日志只是 setUserVisibleHint()=true 。也就是说在加载第一页的时候 setUserVisibleHint()=trueonCreateView 才都会调用,因此我们只需要在 onCreateView 中做一下判断。
这时onCreateView中的代码就变成了

	@Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        if (view == null) {
            view = inflater.inflate(getLayoutId(), null);
        }
        initView(view);
        isCreateView = true;
        if (getUserVisibleHint() && !isHidden()) {
            dispatchVisibleHint(true);
        }
        return view;
    }

我们有时需要在页面第一次初始化时做一些加载动画,之后就不再显示的逻辑,这时我们也是可以使用一个 boolean 类型的变量 isFirstEnter 在页面可见的时候触发页面第一次可见的方法
这时候的完整代码就变成了

public abstract class BaseLazyLoadFragment extends Fragment {
    private View view;
    private boolean isCreateView = false;// View 是否创建
    private boolean isFirstEnter = false;// View 第一次可见

    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, 		@Nullable Bundle savedInstanceState) {
        Log.d(getClass().getSimpleName(), "onCreateView: ");
        if (view == null) {
            view = inflater.inflate(getLayoutId(), null);
        }
        initView(view);
        isCreateView = true;
        isFirstEnter = true;
        if (getUserVisibleHint() && !isHidden()) {
            dispatchVisibleHint(true);
        }
        return view;
    }

    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        if (isCreateView) {
            if (isVisibleToUser) {
                dispatchVisibleHint(true);
            } else {
                dispatchVisibleHint(false);
            }
        }
        Log.d(getClass().getSimpleName(), "setUserVisibleHint: " + isVisibleToUser);
    }

    public void dispatchVisibleHint(boolean isVisible) {
        if (isVisible) {
            onFragmentResume();
            if (isFirstEnter) {
          	    isFirstEnter = false;
                onFragmentFirstVisible();
            }
        } else {
            onFragmentPause();
        }
    }
    // 页面首次可见
    public void onFragmentFirstVisible() {
    }
    // 页面不可见
    public void onFragmentPause() {
    }
    // 页面可见
    public void onFragmentResume() {
    }
    protected abstract void initView(View view);
    protected abstract int getLayoutId();

}
接下来我们看一些特殊情况

1.如果我们适配器使用FragmentPagerAdapter加载时会怎么样:
如果对 FragmentPagerAdapterFragmentStatePagerAdapter 不太清楚的可以看一下
FragmentPagerAdapter和FragmentStatePagerAdapter源码中的三宝
为了方便查看,我在 onFragmentPauseonFragmentResume 加了打印

    // 页面不可见
    public void onFragmentPause() {
        Log.d(getClass().getSimpleName(), "onFragmentPause: ");
    }
    // 页面可见
    public void onFragmentResume() {
        Log.d(getClass().getSimpleName(), "onFragmentResume: ");
    }

在这里插入图片描述
依次从1->4 可以看到此时日志都是正常的,如果跨页面跳转会怎么样?
在这里插入图片描述
分析一下4->1和1->3的日志:
正常情况下如果从 4->1 我们只需要第四页的 onFragmentPause 和第一页的 onFragmentResume 但是实际上多调用了第一页和第二页的 onFragmentPause ,1->3 也是同样多调用了第三页和第四页的 onFragmentPause
其实多调用的 onFragmentPause 页面都有2个共同点:
1.调用了页面的 setUserVisibleHint()=false
2.调用 onFragmentPause 的页面对用户不可见;
因此我们可以定义一个变量 currentFragmentVisibleState=false 记录 Fragment 的显示状态,根据 currentFragmentVisibleStatesetUserVisibleHint 判断触发页面的可见和不可见方法 。
代码如下

	// 保存 fragmet 显示状态(默认隐藏)
    private boolean currentFragmentVisibleState = false;
    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        if (isViewCreate) {
            if (isVisibleToUser && !currentFragmentVisibleState) {
                dispatchVisibleHint(true);
            } else if (currentFragmentVisibleState && !isVisibleToUser) {
                dispatchVisibleHint(false);
            }
        }
    }

    public void dispatchVisibleHint(boolean isVisible) {
        if (currentFragmentVisibleState == isVisible) {
            return;
        }
        currentFragmentVisibleState = isVisible;
        if (isVisible) {
            onFragmentResume();
            if (isFirstEnter) {
            	isFirstEnter=true;
                onFragmentFirstVisible();
            }
        } else {
            onFragmentPause();
        }
    }

2.有时候我们会在 Fragment 中打开一个新的 Activity 或者我们点击 Home 键回到桌面了,这时候就需要调用 onFragmentPause() 做一些暂停操作;同样在回到当前页面时也需要调用 onFragmentResume() 做一些恢复操作。

    @Override
    public void onPause() {
        super.onPause();
        if (currentFragmentVisibleState && getUserVisibleHint() && !isHidden()) {
            dispatchVisibleHint(false);
        }
    }

    @Override
    public void onResume() {
        super.onResume();
        if (!currentFragmentVisibleState && getUserVisibleHint() && !isHidden()) {
            dispatchVisibleHint(true);
        }
    }

3.以上说的都是单个的 ViewPagerFragment ,还有很多都会在嵌套一层,这个时候应该怎么处理呢?
我在 TestFragment2 中新增了3个 ChildFragment ,看一下日志打印
在这里插入图片描述
通过日志可以看出,相邻页面跳转和跨页面跳转都是没有问题的.如果我们切换外层的 Fragment ,这个时候会怎么样呢?
在这里插入图片描述
通过日志我们可以看出从 TestFragment2 ->TestFragment3 ,然后返回到 TestFragment2 ,这时 ChildFragment1onFragmentPause() 方法并没有调用,所以针对这种情况就需要在父类 显示/隐藏 的时候通知所有的子类。

public void dispatchVisibleHint(boolean isVisible) {
        // 记录当前 Fragment 的显示状态
        currentFragmentVisibleState = isVisible;
        if (isVisible) {
            if (isFirstEnter) {
                // 第一次进入的标识改为false
                isFirstEnter = false;
                // 当前fragment首次可见
                onFragmentFirstVisible();
            }
            onFragmentResume();
            dispatchChildVisibleState(true);
        } else {
            onFragmentPause();
            dispatchChildVisibleState(false);
        }
    }
    public void dispatchChildVisibleState(boolean isChildVisible) {
        List<Fragment> fragments = getChildFragmentManager().getFragments();
        Log.d(getClass().getSimpleName(), "dispatchChildVisibleState: " 
              + (fragments != null && fragments.size() > 0));
        if (fragments != null && fragments.size() > 0) {
            for (Fragment fragment : fragments) {
                Log.d(getClass().getSimpleName(), "dispatchChildVisibleState: " + 
                        fragment.getUserVisibleHint() + !fragment.isHidden());
                if (fragment instanceof BaseLazyLoadFragment 
                && fragment.getUserVisibleHint() && !fragment.isHidden()) {
                    ((BaseLazyLoadFragment) fragment).dispatchVisibleHint(isChildVisible);
                }
            }

        }

    }

这时候我们在打印一下日志看一下是否有问题
在这里插入图片描述
日志是从第一页滑到有子 Fragment 的第二页,可以明显看到 ChildFragment1 被调用了两次,这样肯定是有问题的,所以需要我们解决。通过日志我们可以看到这样的流程 ChildFragment1 -> TestFragment2 -> ChildFragment1 ,因为子ChildFragment1调用是优先于父 Fragment ,所以在 父 Fragment 在显示的时候又会分发给子 Fragment 所以就会出现子 Fragment 被调用两次,但是此时Fragment 已经显示了所以我们只用在分发的之前判断当前页面的显示状态就可以了。


    public void dispatchVisibleHint(boolean isVisible) {
        // 如果当前的 Fragment 的分发状态和 isVisible 相同,此时就没有必要分发了
        if (currentVisiableState == isVisible) {
            return;
        }
        // 记录当前 Fragment 的显示状态
        currentVisiableState = isVisible;

        if (isVisible) {
            if (isFirstEnter) {
                isFirstEnter = false;
                onFragmentFirstVisible();
            }
            dispatchChildVisibleStete(true);
            onFragmentResume();
        } else {
            dispatchChildVisibleStete(false);
            onFragmentPause();
        }

    }

至此懒加载逻辑就算是完整的,希望能对您理解懒加载有一定的帮助。如果有什么疏漏或者错误,欢迎留言讨论。
github地址

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值