前言
RecyclerView的出現讓我們可以實現更多更復雜的滑動佈局,包括不同的佈局型別,不同的資料型別。但是,越是複雜的佈局,出現卡頓的現象就會越發的明顯。
這其中不乏有以下幾點:
- 無效的測量佈局繪製
- 模版的重複初始化
通過滑動的日誌分析,我們可以發現同一模版在上滑下滑的同時,會重新走onBindView
方法,即使這一模版內容沒有任何變化的情況下。如果在這個方法中所要執行的邏輯很多,這將會導致卡頓的出現。
原理
那麼為何會重新走onBindView
方法呢,你可能會說去看原始碼就知道了呀。沒錯,當你不知道它是如何實現的時候,去看原始碼往往是最直接有效的。但是今天這個並不是這篇文章的重點,關於RecyclerView的複用和回收網上有很多原始碼的解析,這裡就不一一貼原始碼解釋了,只是做一些簡單的介紹。
- RecyclerView的回收以及複用的都是
ViewHolder
而不是View。 - RecyclerView只是一個ViewGroup,其真正實現滑動的是在LayoutManager中。
- 回收:當一個itemView不可見時,會將其放到記憶體中,以便實現複用。
- 複用:四重奏,
mChangedScrap
、mCacheViews
、開發者自定義以及RecycledViewPool
中,都沒有才會onCreatViewHolder
。 RecyclerViewPool
中的儲存方式是 viewType-Array,也就是對對於每種型別最多存5個。
大部分的快取是從recyclerViewPool中拿的,recyclerViewPool一定會走onBindViewHolder
方法。這也就是回答了我們上面的提問,所以我們的思路就來了,可以通過判斷資料的變化來控制onBindView中相應邏輯的執行,來提升效能。
DiffUtil
主要是和RecyclerView或者ListView配合使用,由DiffUtil找出每個item的變化,由RecyclerView。Adapter更新UI。
如果對DiffUtil
不熟悉的,可以先去看這篇文章RecyclerView資料更新神器 - DiffUtil。
這次優化的思路就是在onBindviewHolder中判斷新舊item的變化,來做到精準更新。
實現
判斷新舊資料的不同,如果資料比較複雜,那麼該怎麼去判斷呢?我們可以用幾個主要的欄位來概括一下這個資料。
public interface IElement {
/**
* 資料內容
* @return 返回該資料體區別於其他資料體的內容
*/
String diffContent();
}
複製程式碼
所有的資料bean要實現這個介面,然後在diffContent中定義自己的主要欄位。不實現這個介面,用DiffUtils是沒有意義的。
我們從設定資料的步驟來一步步講解,方便理解。
當資料從網路請求回來之後,走refreshDataSource
方法。
/**
* 重新整理列表
*
* @param pagedList 新的列表資料
*/
public final void refreshDataSource(List<DATA> pagedList) {
mDiffer.submitList(pagedList);
}
複製程式碼
submitList中對新舊資料進行對比,並將對比結果提供給Adapter。
/**
* 比較資料差異,分發差異結果,呼叫區域性重新整理API,每次請求介面增加一次版本號
* @param newList 新的資料來源
*/
public void submitList(final List<T> 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<T> 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<IElement,ElementRecord> 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) {
int tempPosition = position;
for (int i = 0; i <count; i++) {
// SparseArray
changedPositions.put(tempPosition,tempPosition);
tempPosition++;
}
}
複製程式碼
更新UI必須要在主執行緒中,但是DiffUtil是一個耗時的操作,所以這次用的是它的一個封裝類AsyncListDifferConfig
首先,在初始化中新建Differ物件。
/**
* 資料項比較工具
*/
private final IDataDiff mDataDiff;
/** 資料比較工具 */
private final AsyncListDifferDelegate<DATA> mDiffer;
private final IDataCache<DATA> 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){
return mDataDiff.areItemsChanged(oldItem, newItem, position);
}
private boolean hasDataContentChanged(ElementRecord oldItem, DATA newItem){
return mDataDiff.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,讓其實現區域性重新整理。
總結
到了這裡,我要講的就差不多要結束了,希望對你們有所幫助。謝謝你們看到了這裡。