前段时间在本身的练习项目中想用到懒加载机制,查看了大多数资料只介绍了在
View Pager
+Fragment
组合的状况下实现的懒加载,可是如今大多数App更多的是Fragmentmanager
去管理主页面多个Fragment
的显示与隐藏,而后主界面的某个或多个Fragment
里又嵌套了多个Fragment
+ViewPager
(详细见下图),对于这种状况,适用于第一种的方式是不能直接解决第二种的状况的,因此写下这篇文章,记录一下踩的几个坑,但愿对同像我同样的初学者提供一种思考方式做为参考(若是有错误或者不合适的地方,但愿各位前辈能在评论区指出,很是感谢!)。git
懒加载也叫延迟加载,在APP中指的是每次只加载当前页面,是一种很好的优化APP性能的一种方式。github
在咱们平时开发中,常用
ViewPager+Fragment
的组合来实现左右滑动的页面设计(如上图),可是ViewPger
有个预加载机制,默认会把ViewPager
当前位置的左右相邻页面预先初始化(俗称预加载),即便设置setOffscreenPageLimit(0)
也无效果,也会预加载。经过点进源码中发现,若是不主动设置setOffscreenPageLimit()
方法,mOffscreenPageLimit
默认值为1,即便设置了0(小于1)的值了,可是还会按照mOffscreenPageLimit=limit=1
处理。bash
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();
}
复制代码
Fragment
有一个非生命周期的setUserVisibleHint(boolean isVisibleToUser)
回调方法,当 ViewPager
嵌套 Fragment
时会起做用,若是切换 ViewPager
则该方法也会被调用,参数isVisibleToUser
为true
表明当前 Fragment
对用户可见,不然不可见。因此最简单的思路:Fragment
可见时才去加载数据,不可见时就不让它加载数据。据咱们建立抽象BaseFragment
,对其进行封装。首先咱们引入isVisibleToUser
变量,负责保存当前Fragment
对用户的可见状态。同时还有几个值得注意的地方:服务器
setUserVisibleHint(boolean isVisibleToUser)
方法的回调时机并无与Fragment
的生命周期有确切的关联,好比说,回调时机有可能在onCreateView()
方法以后,也可能在onCreateView()
方法以前。所以,必须引入一个标志位isPrepareView
判断view是否建立完成,否则,很容易会形成空指针异常。咱们初始化该变量为false
,在onViewCreated()
中,也就是view建立完成后,将其赋值为true
。网络
数据初始化只应该加载一次,所以,引入第二个标志位,isInitData
,初始为false,
在数据加载完成以后,将其赋值为true
,下次返回此页面时不会再自动加载。至此,咱们的懒加载方法考虑了全部条件。也就是当isVisibleToUser
为true
,isInitData
为false
,isPrepareView
为true
时,进行数据加载,而且加载后为了防止重复调用,将isInitData
赋值为true
。ide
将懒加载数据提取成一个方法,那么这个方法该什么时候调用呢?首先 setUserVisibleHint(boolean isVisibleToUser)
方法中是必须调用的,即当Fragment
由可见变为不可见和不可见变为可见时回调。 其次,很容易忽略的一点。对于第一个Fragment
,若是setUserVisibleHint(boolean isVisibleToUser )
方法在onCreateView()
以前调用的话,若是懒加载方法只在setUserVisibleHint(boolean isVisibleToUser )
中调用,那么该Fragment
将只能在被主动切换一次以后才能加载数据,这确定是不可能的,所以,咱们须要在view建立完成以后,也进行一次调用。思来想去,在onActivityCreated()
方法中是最合适的。咱们在继承的时候,在onViewCreated()
方法中进行一些初始化就好了,这样不会引发冲突。布局
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,对于这种由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;
}
}
复制代码
这里个人处理方式是,在lazyInitData()中多加了一段处理逻辑,以下:ui
/**
* 懒加载方法
*/
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里第一种,第二种状况并存的话,这段代码又不适合第一种状况了,由于对于第一种的状况当断定isVisibleToUser
为false
时,虽然不走第一个处理逻辑,可是它的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下,实现懒加载。
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.Light
等Theme.AppCompat.XXX
的主题。