前言
RecyclerView的出现让我们可以实现更多更复杂的滑动布局,包括不同的布局类型,不同的数据类型。但是,越是复杂的布局,出现卡顿的现象就会越发的明显。
这其中不乏有以下几点:
- 无效的测量布局绘制
- 模版的重复初始化
onBindView
方法,即使这一模版内容没有任何变化的情况下。如果在这个方法中所要执行的逻辑很多,这将会导致卡顿的出现。原理
那么为何会重新走
onBindView
方法呢,你可能会说去看源码就知道了呀。没错,当你不知道它是如何实现的时候,去看源码往往是最直接有效的。但是今天这个并不是这篇文章的重点,关于RecyclerView的复用和回收网上有很多源码的解析,这里就不一一贴源码解释了,只是做一些简单的介绍。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YBFrLh9k-1640143106872)(https://github.com/lennyup/im...)]
- RecyclerView的回收以及复用的都是
ViewHolder
而不是View。 - RecyclerView只是一个ViewGroup,其真正实现滑动的是在LayoutManager中。
- 回收:当一个itemView不可见时,会将其放到内存中,以便实现复用。
- 复用:四重奏,
mChangedScrap
、mCacheViews
、开发者自定义以及RecycledViewPool
中,都没有才会onCreatViewHolder
。 RecyclerViewPool
中的存储方式是 viewType-Array,也就是对对于每种类型最多存5个。
onBindViewHolder
方法。这也就是回答了我们上面的提问,所以我们的思路就来了,可以通过判断数据的变化来控制onBindView中相应逻辑的执行,来提升性能。DiffUtil
主要是和RecyclerView或者ListView配合使用,由DiffUtil找出每个item的变化,由RecyclerView。Adapter更新UI。这次优化的思路就是在onBindviewHolder中判断新旧item的变化,来做到精准更新。
实现
判断新旧数据的不同,如果数据比较复杂,那么该怎么去判断呢?我们可以用几个主要的字段来概括一下这个数据。
public interface IElement {/**
* 数据内容
* @return 返回该数据体区别于其他数据体的内容
*/
String diffContent();
}
所有的数据bean要实现这个接口,然后在diffContent中定义自己的主要字段。不实现这个接口,用DiffUtils是没有意义的。
我们从设置数据的步骤来一步步讲解,方便理解。
当数据从网络请求回来之后,走
refreshDataSource
方法。/**
* 刷新列表
*
* @param pagedList 新的列表数据
*/
public final void refreshDataSource(List pagedList) {
mDiffer.submitList(pagedList);
}
submitList中对新旧数据进行对比,并将对比结果提供给Adapter。
/**
*比较数据差异,分发差异结果,调用局部刷新API,每次请求接口增加一次版本号
* @param newList新的数据源
*/
public void submitList(final List newList) {
if (newList == mList) {
// 尝试将渲染完成时机通知出去
return;
}final int runGeneration = ++mMaxScheduledGeneration;
// 如果新集合是空 就把老集合所有都remove
if (newList == null) {
int countRemoved = mList.size();
mList = null;
mUpdateCallback.onRemoved(0, countRemoved);
return;
}
// 如果老集合是空 就把新集合所有都insert
if (mList == null) {
mList = newList;
updateDataSource(Collections.unmodifiableList(newList));
mConfig.getBackgroundThreadExecutor()
.execute(
new Runnable() {
@SuppressLint("RestrictedApi")
@Override
public void run() {
for (int i = 0;
i < newList.size();
i++) {
final T t = newList.get(i);
if(t!=null){
dataElementCache.putRecord(new ElementRecord(IDHelper.getUniqueId(t),t));
}
}
dataElementCache.copySelf();
}
});
mUpdateCallback.onInserted(0, newList.size());
return;
}final List oldList = mList;
mConfig.getBackgroundThreadExecutor().execute(new Runnable() {
@SuppressLint("RestrictedApi")
@Override
public void run() {
final DiffUtil.DiffResult result = DiffUtil.calculateDiff(new DiffUtil.Callback() {
@Override
public int getOldListSize() {
return oldList.size();
}@Override
public int getNewListSize() {
return newList.size();
}@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
return mConfig.getDiffCallback().areItemsTheSame(
oldList.get(oldItemPosition), newList.get(newItemPosition));
}@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
return mConfig.getDiffCallback().areContentsTheSame(
oldList.get(oldItemPosition), newList.get(newItemPosition));
}
// payload可以理解为关键的数据,就是新老item的数据中 到底哪里变化了,局部刷新某个item -- 默认返回null
@Override
public Object getChangePayload(int oldItemPosition, int newItemPosition) {
return mConfig.getDiffCallback().getChangePayload(
oldList.get(oldItemPosition), newList.get(newItemPosition));
}
});
mConfig.getMainThreadExecutor().execute(new Runnable() {
@Override
public void run() {
if (mMaxScheduledGeneration == runGeneration) {
//刷新布局
diffResult.dispatchUpdatesTo(mUpdateCallback);
}
}
});
}
});
}
其中
updateDataSource
是用来更新数据源,确保拿到的是最新的。dataElementCache
中存放的是:private volatile ConcurrentMap elementRecords = new ConcurrentHashMap<>();
ElementRecord记录了当前的数据以及唯一标示
UniqueId
,将主要字段以md5方式呈现,减少耗时。这里提供了异步进行数据比较的逻辑,mUpdateCallback实现ListUpdateCallback接口,实现adpter的刷新功能。
@Override
public void onChanged(int position, int count, Object payload) {
recordChanged(position,count);
super.onChanged(position, count, payload);
}@Override
public void onInserted(int position, int count) {
recordChanged(position, count);
super.onInserted(position, count);
}private void recordChanged(int position, int count) {
inttempPosition = position;
for (int i = 0;
i
更新UI必须要在主线程中,但是DiffUtil是一个耗时的操作,所以这次用的是它的一个封装类
AsyncListDifferConfig
首先,在初始化中新建Differ对象。
/**
* 数据项比较工具
*/
private final IDataDiff mDataDiff;
/** 数据比较工具 */
private final AsyncListDifferDelegate mDiffer;
private final IDataCache dataElementCache;
public BaseSwiftAdapter(Context mContext) {
this.mContext = mContext;
dataElementCache = new ElementCache<>();
final DiffCallBack diffCallBack = new DiffCallBack(dataElementCache);
@SuppressLint("RestrictedApi") AsyncDifferConfig config =
new AsyncDifferConfig.Builder<>(diffCallBack)
.setBackgroundThreadExecutor(AppExecutors.backGroudExecutors)
.setMainThreadExecutor(AppExecutors.mainExecutors)
.build();
ChangeListCallback changedPositionCallback = new ChangeListCallback(this);
mDataDiff = new DataDiffImpl<>(changedPositionCallback, dataElementCache);
mDiffer =
new AsyncListDifferDelegate(changedPositionCallback, config, dataElementCache);
}
AsyncListDifferConfig
需要三个参数:DiffUtil的内部类ItemCallback、diffUtil的item比较线程、主线程。ItemCallback
是它的抽象内部类,也就是mConfig.getDiffCallback()
,看下它要实现的几个方法:@Override
public boolean areItemsTheSame(IElement oldItem, IElement newItem) {
return areContentsTheSame(oldItem, newItem);
}/**
* 总体思想是先比较对象地址,在比较内容,提高比较效率
*
* @param oldItem
* @param newItem
* @return
*/
@Override
public boolean areContentsTheSame(IElement oldItem, IElement newItem) {
if (newItem == null) {
return true;
}
if (oldItem == newItem) {
return true;
}
recordNewElement(newItem);
final String newContent = newItem.diffContent();
if(newContent == null || "".equals(newContent)){
return false;
}return newContent.equals(oldItem.diffContent());
}
areItemTheSame和areContentsTheSame,都是用来判断新旧数据是否相同,所以这里用了同一个逻辑,diffContent中存放该数据具有影响的几个字段相拼接的字符串。
dataElementCache用来存储所有数据的集合类型是IElement-ElementRecord的Array。IElement是数据本身,ElementRecord是数据的记录集,包含数据以及数据的唯一标示。
mDiffer会在后续中讲到。
我们来看下关键的
onBindViewHolder
中所做的事情:@Override
public final void onBindViewHolder(VH holder, int position) {
if (null != holder && holder.itemView != null) {
tryBindData(holder, position, this.getItem(position));
}
}private void tryBindData(VH holder, int position, DATA newData) {
final ElementRecord oldDataRecord = holder.content();
boolean needBind ;
if(needBind = (hasPositionDataRefreshChanged(oldDataRecord == null ? null : (DATA) oldDataRecord.getElement(), newData, position) || oldDataRecord == null) ){
Log.d(getClass().getName(),"adapter onBindData 刷新或者新建"+ holder.getItemViewType());
}else if(needBind =hasDataContentChanged(oldDataRecord,newData)){
Log.d(getClass().getName(),"adapter onBindData 滑动内容改变"+ holder.getItemViewType());
}
if(needBind){
refreshAndBind(holder, position, newData);
}else {
Log.d(getClass().getName(),"adapter onBindData 复用不刷新"+ holder.getItemViewType());
}
}
先去判断是否是刷新变化,其次去判断是否是滑动变化,如果有变化就刷新布局,否则什么也不做。
private boolean hasPositionDataRefreshChanged(DATA oldItem, DATA newItem, int position){
returnmDataDiff.areItemsChanged(oldItem, newItem, position);
}
private boolean hasDataContentChanged(ElementRecord oldItem, DATA newItem){
returnmDataDiff.areContentsChanged(oldItem, newItem);
}
可以看出
mDataDiff
主要用来判断新旧数据是否相同。我们来实现mDataDiff中的比较:@Override
public boolean areItemsChanged(T oldItem, T newItem, int position) {
boolean changed = changedPositionCallback.hasPositionChanged(position);
if(changed){
changedPositionCallback.removeChangedPosition(position);
}
return changed;
}@Override
public boolean areContentsChanged(ElementRecord oldElementRecord, T newItem) {
return oldElementRecord !=null&& oldElementRecord.getElement() != newItem && newItem!=null && !sameContent(oldElementRecord,newItem);
}private boolean sameContent(ElementRecord oldElementRecord, T newItem){
final ElementRecord newElementRecord = dataCache.getRecord(newItem);
if(newElementRecord == null){
return false;
}
if(IDHelper.forceRefresh(newElementRecord) || IDHelper.forceRefresh(oldElementRecord)){
return false;
}
return newElementRecord.getUniqueId().equals(oldElementRecord.getUniqueId());
}
其中比较思路为:先判断该viewHolder是否在changedPositions中,changedPositions由ChangeListCallback来提供并实现。其次判断两个对象以及唯一标示。
这里用到了两个比较类:一个是
ItemCallback
的比较类以及mDataDiff
比较类,这里容易看混。最关键的代码在这句:
diffResult.dispatchUpdatesTo(mUpdateCallback);
diffResult会将最小变化量提供给adpter,让其实现局部刷新。
总结
到了这里,我要讲的就差不多要结束了,希望对你们有所帮助。谢谢你们看到了这里。
相关教程 Android基础系列教程:
Android基础课程U-小结_哔哩哔哩_bilibili
Android基础课程UI-布局_哔哩哔哩_bilibili
Android基础课程UI-控件_哔哩哔哩_bilibili
Android基础课程UI-动画_哔哩哔哩_bilibili
Android基础课程-activity的使用_哔哩哔哩_bilibili
Android基础课程-Fragment使用方法_哔哩哔哩_bilibili
Android基础课程-热修复/热更新技术原理_哔哩哔哩_bilibili
【DiffUtils让你的RecyclerView如斯顺滑】本文转自 https://juejin.cn/post/6844903975796342798,如有侵权,请联系删除。
推荐阅读
- 备战金三银四,没有这套逆天的Android面经怎么行()
- 如何学习Flutter(学会这些你就入门了)
- 音视频人才的需求从小众变成了大众(一名合格的音视频开发人员,少则30万起,多则年薪可达百万以上......)
- 什么是“好”的技术(为什么“火”?)
- Android项目实战(浅谈ListView悬浮头部展现效果)
- 什么是音频焦点管理(音频焦点的行为准则是什么?)
- 知乎热议(35岁的中年危机,大龄程序员该何去何从())
- 安卓点击防抖优化手册(非代码层指导)
- 太卷了,公司新来的00后卷王,我们这帮老油条真干不过.....
- 那些年,被我们误解的程序员