前言
今天要说的那个东西其实大家都非常熟悉,那就是RecyclerView,没错大家都会用,但不知道对于RecyclerView的一些优化有多少人专门去研究过,不知道是不是一些开发者还只是停留在只会调用setadapter,然后配合notifyDataSetChanged这种万金油的方式上,又或者说是使用了一些优秀的三方库但是确只是简单停留在调用上就完事,其实RecyclerView做为android开发一个非常常用的控件,可以这么说,一般普通的ui页面都可以通过RecyclerView去实现,个人觉得RecyclerView可以完全去替换掉scrollview,这里说的普通的ui页面特指那些没有酷炫交互方式的页面。深入理解RecyclerView优化方面的技术对于发挥RecyclerView的性能是非常有帮助的。
写这篇文章的缘由还是之前项目在使用同事封装的adapter库时,bugly上报崩溃,在解决问题的过程中有机会深入理解RecyclerView的部分源码,结合网上一些文章,自己总结出来的心得体会,有兴趣的可以去看看我原先的那篇文章bugly关于RecyclerView崩溃问题研究借用一句现在流行的网络用语就是,RecyclerView不止眼前的setadapter和notify,还有诗和远方。闲话扯到这,接下来就来看一下RecyclerView优化方面的东西。
关于RecyclerView的优化,自己会将它们分为两大类,一类是RecyclerView自带的系统优化,另一类就是我们通过代码实现的手动优化,先来介绍下RecyclerView自带的系统优化。系统优化我们不能做太多的干预,但是通过理解RecyclerView的系统优化能够让我们更好的理解RecyclerView的工作机制。
预取功能(Prefetch)
这个功能是rv在版本25之后自带的,也就是说只要你使用了25或者之后版本的rv,那么就自带该功能,并且默认就是处理开启的状态,通过LinearLayoutManager的setItemPrefetchEnabled()我们可以手动控制该功能的开启关闭,但是一般情况下没必要也不推荐关闭该功能,预取功能的原理比较好理解,如图所示
我们都知道android是通过每16ms刷新一次页面来保证ui的流畅程度,现在android系统中刷新ui会通过cpu产生数据,然后交给gpu渲染的形式来完成,从上图可以看出当cpu完成数据处理交给gpu后就一直处于空闲状态,需要等待下一帧才会进行数据处理,而这空闲时间就被白白浪费了,如何才能压榨cpu的性能,让它一直处于忙碌状态,这就是rv的预取功能(Prefetch)要做的事情,rv会预取接下来可能要显示的item,在下一帧到来之前提前处理完数据,然后将得到的itemholder缓存起来,等到真正要使用的时候直接从缓存取出来即可
预取代码理解
虽说预取是默认开启不需要我们开发者操心的事情,但是明白原理还是能加深该功能的理解。下面就说下自己在看预取源码时的一点理解。实现预取功能的一个关键类就是gapworker,可以直接在rv源码中找到该类
GapWorker mGapWorker;
rv通过在ontouchevent中触发预取的判断逻辑,在手指执行move操作的代码末尾有这么段代码
case MotionEvent.ACTION_MOVE: {
......
if (mGapWorker != null && (dx != 0 || dy != 0)) {
mGapWorker.postFromTraversal(this, dx, dy);
}
}
} break;
通过每次move操作来判断是否预取下一个可能要显示的item数据,判断的依据就是通过传入的dx和dy得到手指接下来可能要移动的方向,如果dx或者dy的偏移量会导致下一个item要被显示出来则预取出来,但是并不是说预取下一个可能要显示的item一定都是成功的,其实每次rv取出要显示的一个item本质上就是取出一个viewholder,根据viewholder上关联的itemview来展示这个item。而取出viewholder最核心的方法就是
tryGetViewHolderForPositionByDeadline(int position,boolean dryRun, long deadlineNs)
名字是不是有点长,在rv源码中你会时不时见到这种巨长的方法名,看方法的参数也能找到和预取有关的信息,deadlineNs的一般取值有两种,一种是为了兼容版本25之前没有预取机制的情况,兼容25之前的参数为
static final long FOREVER_NS = Long.MAX_VALUE;
,另一种就是实际的deadline数值,超过这个deadline则表示预取失败,这个其实也好理解,预取机制的主要目的就是提高rv整体滑动的流畅性,如果要预取的viewholder会造成下一帧显示卡顿强行预取的话那就有点本末倒置了。
关于预取成功的条件通过调用
boolean willCreateInTime(int viewType, long approxCurrentNs, long deadlineNs) {
long expectedDurationNs = getScrapDataForType(viewType).mCreateRunningAverageNs;
return expectedDurationNs == 0 || (approxCurrentNs + expectedDurationNs < deadlineNs);
}
来进行判断,approxCurrentNs的值为
long start = getNanoTime();
if (deadlineNs != FOREVER_NS
&& !mRecyclerPool.willCreateInTime(type, start, deadlineNs)) {
// abort - we have a deadline we can't meet
return null;
}
而mCreateRunningAverageNs就是创建同type的holder的平均时间,感兴趣的可以去看下这个值如何得到,不难理解就不贴代码了。关于预取就说到这里,感兴趣的可以自己去看下其余代码的实现方式,可以说google对于rv还是相当重视的,煞费苦心提高rv的各种性能,据说最近推出的viewpager2控件就是通过rv来实现的,大有rv控件一统天下的感觉。
四级缓存
rv设计中另一个提高滑动流畅性的东西就是这个四级缓存了,如果说预取是25版本外来的务工人员,那么这个四级缓存就是一个本地土著了,自rv出现以来就一直存在,相比较listview的2级缓存机制,rv的四级看起来是不是显得更加的高大上。借用一张示意图来看下rv的四级缓存
,rv中通过recycler来管理缓存机制,关于如何使用缓存可以在tryGetViewHolderForPositionByDeadline找到,没错又是这个方法,看来名字起的长存在感也会比较足。
tryGetViewHolderForPositionByDeadline依次会从各级缓存中去取viewholer,如果取到直接丢给rv来展示,如果取不到最终才会执行我们非常熟悉的oncreatviewholder和onbindview方法,一句话就把tryGetViewHolderForPositionByDeadline的功能给讲明白了,内部实现无非是如何从四级缓存中去取肯定有个优先级的顺序。可以先来看下recycler中关于这四级缓存的代码部分
public final class Recycler {
fi