ViewPager+Fragment首页布局的加载优化
简介
VIewPager+Fragment是多数APP的首页布局,也是用户打开app后除了广告外第一个看到的页面,所以如果能对ViewPager+Fragment的布局有所优化,就是对app的冷启动时长有所优化。
本文通过设置ViewPager的setOffscreenPageLimit参数和提供适配ViewPager的Fragment来优化加载耗时。
优化思路
setOffscreenPageLimit(int limit)
setOffscreenPageLimit(int limit)是ViewPager提供的一个方法,用于控制提前加载页面的数量。不设置则默认参数是1。
offScreen就是屏幕外的意思,为了用户左右滑动到其他页面时能不卡顿,VIewPager允许提前加载左右相邻一定数目的页面(也就是page),这样切换到其他页面时可以看到加载好的页面。而limit参数就是预加载页面的数目,设置后会加载左右各limit个页面
举个例子,一个ViewPager+ABCDE五个Fragment,默认一开始在C Fragment。 a) 如果设置setOffscreenPageLimit(1),则会提前加载BCD三个 b)如果设置setOffscreenPageLimit(2),则会提前加载ABCDE五个 c) 如果设置setOffscreenPageLimit(1),而从C滑动到D时,E也会被预加载,而B可能会被destroy,也可能只是被remove,具体看adapter实现
优化点 最理想的情况是,启动时我只想加载用户能看到的那个页面,在首帧展示完成后,再利用空闲的时间来预加载剩余的页面,即,我们想达到setOffscreenPageLimit(0)的效果。 可惜的是,ViewPager并不支持设置为0,就算传入了0,也会在内部被纠正为默认值1,这个设计可能是为了用户操作更流畅吧。 既然不能设置为0,我们就退而求其次,设置limit为1,做到启动时尽可能少地加载页面。优化措施大概如下
1、开始时设置setOffscreenPageLimit(1) 2、对点击滑动等可能导致页面切换的操作,需要阻塞并设置setOffscreenPageLimit为最大值,保证全部Fragment被加载了,否则会有Fragment生命周期执行到destroy的风险, 3、在主线程空闲的时候,发消息在主线程执行全量加载,即设置setOffscreenPageLimit为最大值的操作,这样可以做到延迟预加载,减少对用户体验的影响。主线程是否空闲,一是可以把消息丢到idlehandler中处理,但如果handler一直繁忙,会有不会预加载的风险。二是发送延迟固定时间的消息来处理,比如延迟5s来执行,相当于预留了5s给其他业务操作,5s后再触发全量预加载
提供专门用于ViewPager的Fragment
setOffscreenPageLimit无法做到只加载可见页面,那么我们是否可以换个思路,尽量减少加载Fragment时候的耗时,进而做到近似的效果? 要想从这个角度优化,我们需要先知道ViewPager是怎么加载Fragment的 网上关于ViewPager源码的介绍已经是汗牛充栋了,我不展开细讲,只讲优化相关的流程
1、ViewPager#setAdapter时,会触发Fragment对象的创建,但此时Fragment的生命周期还没执行 2、ViewPager#setAdapter后,会触发一次页面绘制,即会有一次doFrame消息的执行,关键流程如下
doFrame
adapter#finishUpdate
FragmentManager#moveToState
Fragment生命周期到onResume
我再贴一下Fragment的生命周期 总的来说,ViewPager加载Fragment时,会执行Fragment的 onAttach、onCreate、onCreateView、onActivityCreated、onStart、onResume 因为offScreenPageLimit不能为0,那么就存在对用户不可见的Fragment,也会在首帧时被执行。那么我们能否增加判断,只有当Fragment可见时,才执行onAttch到onResume等方法呢? 答案是基本可以的,判断Fragment是否对用户可见用userVisibleHint标记即可,我们只需要把onAttch到onResume中首次加载才会用到的代码归到一个方法里,在页面可见时再触发即可。 说太多了,直接上代码吧
public abstract class FragmentForViewPager extends Fragment {
private boolean visible = false;
private boolean loadDone =false;
private boolean firstOnResumeDone = false;
public abstract void loadForViewPager();
public abstract void normalOnResume();
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super. setUserVisibleHint(isVisibleToUser);
this.visible = isVisibleToUser;
}
@Override
public final void onResume() {
super.onResume();
if (!firstOnResumeDone) {
if (visible) {
loadForViewPager();
loadDone=true;
}
firstOnResumeDone = true;
} else {
normalOnResume();
}
}
@Override
public void onDestroy() {
super.onDestroy();
loadDone=false;
firstOnResumeDone=false;
}
public boolean forceLoad() {
if (!loadDone) {
loadForViewPager();
loadDone=true;
}
}
}
我们把Fragment从onAttach到onResume的生命周期拆分为两部分,loadForViewPager和normalOnResume,具体拆分如下
Fragment的onAttach到onStart中的初始化逻辑
Fragment的onResume中初始化需要的逻辑
Fragment的onResume中非初始化相关的逻辑
normalOnResume
拆分后,我们控制loadForViewPager只有在用户可见时才加载,只有loadForViewPager执行过后,后续onResume才走normalOnResume 流程变动如下 优化前
doFrame
onAttach到onStart
初始化相关的onResume逻辑
非初始化的onResume逻辑
优化后
no
no
yes
doFrame
当前Fragment是否用户可见
绘制首帧
主线是否空闲
forceLoad
结束
优化点 1、子类不再需要处理userVisibleHint。很多针对ViewPager+Fragment的文章都介绍通过在setUserVisibleHint里面加逻辑来减少加载耗时,但这样与显得代码很繁杂,因为setUserVisibleHint会被频繁调用,所以需要加是否可见、是否第一次可见等状态,优化后的Fragment子类不再需要获取userVisibleHint状态。内部会自动处理好 2、减少加载不可见Fragment时的耗时,这个是因为onAttach到onStart被架空了,部分onResume逻辑也被架空了,导致首次加载的内容减少了。
总结
针对VIewPager+Fragment的布局加载,本文提供了两种思路进行优化,一是对offscreenPageLimit的动态设置,二是提供了专用于ViewPager的Fragment。
|