(前言暂位符号)
没有实现 View holder 的 getView()
的例子如下,大量的 findViewById()
方法被调用,看似不是很大的开销,但其方法的底层实现是深度优先搜索,时间复杂度是O(n)。
完全可以避免以上不必要开支,见下例子,通过setTag()
、getTag()
复用view,只有在第一次convertView为空的时候去创建ViewHolder,并调用findViewById
将convertView中的view赋值到holder中,并且将holder存起来留给下次复用,避免重复找view操作。这也是 ViewHolder名字的来历:用来保存View的容器。
setTag()
为什么可以绑定convertView和holder?其实这里的内部实现很简单,把任意一个对象Object作为参数(成员变量)存到View当中,另一种方法则是用Key、Value的方式进行绑定。
Item view 和 view holder 是一一对应的关系;
View holder解决的是防止重复进行findViewById
,提升效率;
没什么关系,对比以上2个例子,第一个效率不高的getView()
方法中可见即使没有使用View holder的存在,但是通过convertView的空判断选择创建或直接findViewById
,这就是在复用,只是比较耗性能!
ViewType种类过多,尽量减少在onBindViewHolder(VireHolder holder, int position)
里if else的冗杂判断,可将具体的数据绑定逻辑放到 ViewHolder内部。
如下图,RecyclerBin是ListView的回收站,专门负责管理ListView的缓存。RecyclerBin中有两层缓存:
结合下图理解,当ListView需要创建一个view,也就是Adapter.getView时,首先从两层缓存中找,如果没有就创建一个view返回。
结合下图再来加深一下ListView的二层缓存理解:屏幕内外的View,和复用、缓存有什么关系?
Android系统的屏幕刷新机制16.6ms/1帧,每次刷新的时候会把最新的View渲染到屏幕上,也就是说在下一帧画面时,ListView会将**屏幕上所有的item**的数据清空,再根据最新的业务逻辑状态结合ListView内部机制(及layout、draw等等),重新绘制到屏幕上。
那么,被清空的这些item呢?例如滑动列表,数据没变,只是item的位置改变!因此这些item view是可以拿回来继续复用的,并不需要重新bind数据,即ListView直接跳过adapter.getView步骤的。正因为ListView内部已经处理过了,也无需开发者在getView中处理这些逻辑。
诶嘿,上述讲解还有一个重点:凡是跳过getView
方法的item不需要重新bind数据,那么执行过getView
方法的item view是肯定要重新bind数据的!
再来看Srap View,上滑动列表后,右再回到原来位置,这时ListView会在Scrap View,即被废弃的View中找到item view,既然是脏数据,找到后就需要重新bind数据。(注:如果脏数据中没有找到,就重新创建)
留一个问题:Srap View内部使用的什么数据结构?
如下图,这对比起来相较于ListView似乎复杂了些,但其实二者还是有些许相同之处,只是RV的缓存机制更加完善高效。
RecyclerView内部是由Recycler来管理缓存机制,注意:RecyclerView和ListView缓存本质上很重要的一点差异,前者缓存的是ViewHolder、而后者缓存的是View对象!但是但是,在文章一开始我们就分析过ViewHolder和item view的关系,一一对应,相互绑定的,其实差别也不大。
RV接收到创建一个item view的需求时,首先从四层缓存中找,没找到才会去创建。来看这四层缓存:
从下图屏幕的角度,来理解这四层缓存:
首先来看第一层 Scrap,即屏幕内的item view,回顾ListView讲解的缓存内容,这部分是可以直接被复用的。注意:这类型“可以直接被复用的view” 是通过数据集的position来查找对应的viewholder。例如在滑动的过程中需要position为5的view(注意:position为5的item view一直在屏幕内的!只是每次刷新数据重新渲染),这时可以直接去Scrap干净数据中找,直接拿来复用,跳过bind数据步骤。
再来看第二层 Cache,即刚移出屏幕的item view,直接被回收到Cache,再拿来复用时就需要重新bind数据。例如往上滑动列表,position为2、3的item view被移出屏幕(被回收到Cache),再往下滑,position为2、3的item view在Cache中找到拿回来复用。同Scrap一样,也是通过position找到viewholder,直接拿回来复用,无需重新bind数据,即跳过onBindViewHolder
方法。
第三层 ViewCacheExtension,说实话使用场景很少,估计很多人闻所未闻,其API设计更是有些奇怪,要知道RecyclerView的缓存机制目标是ViewHolder,读取ViewHolder中有效信息(可从它获取item view)。但是ViewCacheExtension源码自行实现返回的对象却是item view,那ViewHolder?ViewCacheExtension内部还会去检查item view 是否绑定ViewHolder,没有绑定则直接crash。
第四层 RecycledViewPool ,内部保存的都是被废弃的脏(dirty)数据,通过viewType 找到对应数据后需要重新绑定数据(注意这一层缓存是通过viewType查找),即虽然缓存读取跳过了onCreateViewHolder
步骤,但是需要重新绑定数据onBindViewHolder
。
底层的数据结构?
注意:列表中item/广告 的impression统计:
getView()
统计,该方法被调用就相当于item view曝光;onBindViewHolder()
方法统计?数据有误!经过上述缓存讲解,在第二层Cache时,item重新曝光,在Cache中找到并复用item,此方法并不会被调用,因此数据会有些许不精准。
onViewAttachedToWindow()
统计;OnClickListener对象多次被创建,观察者模式
在onCreateViewHolder
里设置点击监听,View、ViewHolder、View.OnClickListener三者一一对应!
LinerLayoutManager.setInitialPrefetchItemCount()
横向列表初次显示时可见的item个数
//伪代码解释
void onContentsChanged() {
if(mHasFiedSize) {
layoutChildren();
} else {
requestLayout();
}
}
如果Adapter的数据变化(例如item增加、删除)不会导致RV的大小变化,那么可以为RV设置此属性;这样当RV内部的item变化时,可简化重走整个绘制过程。
重合的viewtype,即可走第四层缓存复用item
使用方法
RecyclerView.RecycledViewPool recycledViewPool =
new RecyclerView.RecycledViewPool();
rv1.setRecycledViewPool(recycledViewPool);
rv2.setRecycledViewPool(recycledViewPool);
rv3.setRecycledViewPool(recycledViewPool);
notifyDataSetChange()
会导致整个布局重新绘制,所有ViewHolder被重新绑定,而且会失去可能的动画效果。内部算法 Myers Diff Algorith 是动态规划,不要求掌握其具体实现,给出的链接是对该算法的可视化讲解,感兴趣的朋友可以深入研究下:Myers Diff Algorithm - Code & Interactive Visualization
那具体怎么运用DiffUtil到实际操作中,首先来介绍一个 重点抽象类DiffUtil.Callback,实现Callback里的方法告诉系统如何计算:
//DiffUtil.class
public abstract static class Callback {
public abstract int getOldListSize();
public abstract int getNewListSize();
public abstract boolean areItemsTheSame(int oldItemPosition, int newItemPosition);
public abstract boolean areContentsTheSame(int oldItemPosition, int newItemPosition);
@Nullable
public Object getChangePayload(int oldItemPosition, int newItemPosition) {
return null;
}
}
areItemsTheSame()
返回true时会调用。
areItemsTheSame()
返回true时此方法才会被调用,这个时候再来做这2个人具体信息比较,例如姓名、性别、出身地等等。areItemsTheSame()
返回 true ,并且 areContentsTheSame()
返回 false 时被调用。
以上几个方法的调用前提、顺序通过下图总结归纳:
上代码,实现一个简单的UserDiffCallback例子,来比较两个用户列表:
public class UserDiffCallback extends DiffUtil.Callback {
private List<User> oldList;
private List<User> newList;
public UserDiffCallback(List<User> oldList, List<User> newList) {
this.oldList = oldList;
this.newList = newList;
}
@Override
public int getOldListSize() {return oldList.size();}
@Override
public int getNewListSize() {return newList.size();}
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
//唯一标识符比较
return oldList.get(oldItemPosition).id == newList.get(newItemPosition).id;
}
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
final User oldUser = oldList.get(oldItemPosition);
final User newUser = newList.get(oldItemPosition);
//内容比较
//优化:可以在User类中实现equals方法,根据Model自身特性实现比较“相等”逻辑判断
return oldUser.id == newUser.id &&
oldUser.name.equals(newUser.name) &&
oldUser.sex.equals(newUser.sex) &&
oldUser.address.equals(newUser.address);
}
}
上述UserDiffCallback类继承于DiffUtil.Callback抽象类,并实现了其中4个抽象方法,其中areItemsTheSame()
、areContentsTheSame()
实现不同层度的item比较逻辑。此例子简单易懂,无需多余赘述,值得注意的是上述例子并未实现getChangePayload()
方法。
如果没有实现此方法,那么就享受不到“增量更新”的便利,例如User类中可能只有名字name字段修改了,其余的数据都未变化,由于此方法没有被实现,因此整个item还是会被重新刷新。也就是“增量更新”和“全量更新”的区别。
如下代码,我们来实现getChangePayload()
方法,其内部主要逻辑看似与内容比较方法areContentsTheSame()
相似,但还有一点不同是:将两个对象的差异部分存储到Bundle中返回。
public class UserDiffCallback extends DiffUtil.Callback {
......
@Nullable
@Override
public Object getChangePayload(int oldItemPosition, int newItemPosition) {
final User oldUser = oldList.get(oldItemPosition);
final User newUser = newList.get(oldItemPosition);
final Bundle payload = new Bundle();
if(oldUser.id != newUser.id){
payload.putInt(User.KEY_ID, newUser.id);
}
if(oldUser.sex != newUser.sex){
payload.putString(User.KEY_SEX, newUser.sex);
}
if(oldUser.address != newUser.address){
payload.putString(User.KEY_ADDRESS, newUser.address);
}
if(payload.size() == 0){
return null;
}
return payload;
}
......
}
外界调用
实现了 DiffUtil.Callback 后,我们就可以在自定义Adapter中根据自身逻辑选择使用增量更新来update列表,封装方法如下:
public class ShowcaseAdapter extends RecyclerView.Adapter<ShowcaseAdapter.ViewHolder> {
private List<User> userList;
ShowcaseAdapter(List<User> userList){
this.userList = userList;
}
......
public void swapData(List<User> newList, boolean diff){
if(diff){
DiffUtil.Callback diffUtilCallback;
DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(
new UserDiffCallback(userList, newList), false);
userList = newList;
//增量更新
diffResult.dispatchUpdatesTo(this);
} else {
userList = newList;
//全量更新
notifyDataSetChanged();
}
}
}
看以上swapData(List
方法封装设计:
那么,来看**“增量更新”**的具体操作,此时就使用到了之前封装的 “计算两个列表差异”的工具类UserDiffCallback:
DiffUtil.calculateDiff()
方法,即计算需要进行更新操作的列表,而此方法返回的结果则是:旧列表转换成新列表需要更新的部分。
dispatchUpdatesTo()
方法,即所谓的“增量更新”,将列表更换的差异分发给Adapter,使其根据接收到的差异数据做更新。(而并非“全量更新”中的所有item刷新)总结
getChangePayload()
返回的差异数据 DiffResult,DiffResult 再分发给 notifyItemRangeChanged(position, count, payload)
方法,最终交给 Adapter 的 onBindViewHolder(… List< Object > payloads)
处理。
DiffUtil 一般通过这四个方法通知 Adapter 来更新数据。
notifyItemChanged()
notifyItemMoved()
notifyItemRangeChanged()
notifyItemRangeInserted()
notifyItemRangeRemoved()
DiffUtil 的效率?
上述例子调用calculate
计算方法是在主线程
Android系统每次刷新频率是 1帧/16.6ms,若计算时间过长反而会导致掉帧现象,本想是RV的优化?却…
因此在列表数据比较大的时候,异步计算Diff。 说到异步,这事儿就好办了,以下三个方法推荐:
前两种方式不必多少,来看下第三种官方推荐,可查看: