[TOC]
## 緩存機制


**RecyclerView VS Listview**
RecyclerView中mCacheViews(屏幕外)獲取緩存時,是通過匹配pos獲取目標位置的
## 局部刷新
RecyclerView的緩存機制確實更加完善,但還不算質的變化,RecyclerView更大的亮點在于提供了局部刷新的接口,通過局部刷新,就能避免調用許多無用的bindView.結合RecyclerView的緩存機制,看看局部刷新是如何實現的:
以RecyclerView中notifyItemRemoved(1)為例,最終會調用requestLayout(),使整個RecyclerView重新繪制,過程為:
```
onMeasure()-->onLayout()-->onDraw()
```
其中,onLayout()為重點,分為三步:
1. dispathLayoutStep1():記錄RecyclerView刷新前列表項ItemView的各種信息,如Top,Left,Bottom,Right,用于動畫的相關計算;
2. dispathLayoutStep2():真正測量布局大小,位置,核心函數為layoutChildren();
3. dispathLayoutStep3():計算布局前后各個ItemView的狀態,如Remove,Add,Move,Update等,如有必要執行相應的動畫.
其中,layoutChildren()流程圖:


當調用notifyItemRemoved時,會對屏幕內ItemView做預處理,修改ItemView相應的pos以及flag(流程圖中紅色部分):

當調用fill()中RecyclerView.getViewForPosition(pos)時,RecyclerView通過對pos和flag的預處理,使得bindview只調用一次.
需要指出,ListView和RecyclerView最大的區別在于數據源改變時的緩存的處理邏輯,ListView是"一鍋端",將所有的mActiveViews都移入了二級緩存mScrapViews,而RecyclerView則是更加靈活地對每個View修改標志位,區分是否重新bindView。
## 預取
雖說預取是默認開啟不需要我們開發者操心的事情,但是明白原理還是能加深該功能的理解。下面就說下自己在看預取源碼時的一點理解。實現預取功能的一個關鍵類就是gapworker,可以直接在rv源碼中找到該類
~~~undefined
GapWorker mGapWorker;
~~~
rv通過在ontouchevent中觸發預取的判斷邏輯,在手指執行move操作的代碼末尾有這么段代碼
~~~kotlin
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最核心的方法就是
~~~java
tryGetViewHolderForPositionByDeadline(int position,boolean dryRun, long deadlineNs)
~~~
名字是不是有點長,在rv源碼中你會時不時見到這種巨長的方法名,看方法的參數也能找到和預取有關的信息,deadlineNs的一般取值有兩種,一種是為了兼容版本25之前沒有預取機制的情況,兼容25之前的參數為
~~~java
static final long FOREVER_NS = Long.MAX_VALUE;
~~~
,另一種就是實際的deadline數值,超過這個deadline則表示預取失敗,這個其實也好理解,預取機制的主要目的就是提高rv整體滑動的流暢性,如果要預取的viewholder會造成下一幀顯示卡頓強行預取的話那就有點本末倒置了。
關于預取成功的條件通過調用
~~~java
boolean willCreateInTime(int viewType, long approxCurrentNs, long deadlineNs) {
long expectedDurationNs = getScrapDataForType(viewType).mCreateRunningAverageNs;
return expectedDurationNs == 0 || (approxCurrentNs + expectedDurationNs < deadlineNs);
}
~~~
來進行判斷,approxCurrentNs的值為
~~~csharp
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控件一統天下的感覺。
## 業務優化
### 降低item的布局層次
其實這個優化不光適用于rv,activity的布局優化也同樣適用,降低頁面層次可以一定程度降低cpu渲染數據的時間成本,反應到rv中就是降低mCreateRunningAverageNs的時間,不光目前顯示的頁面能加快速度,預取的成功率也能提高,關于如何降低布局層次還是要推薦下google的強大控件ConstraintLayout,具體使用就自行百度吧,比較容易上手,這里吐槽下另一個控件CoordinatorLayout的上手難度確實是有點大啊,不了解CoordinatorLayout源碼可能會遇到一些奇葩問題。降低item的布局層次可以說是rv優化中一個對于rv源碼不需要了解也能完全掌握的有效方式。
### 去除冗余的setitemclick事件
rv和listview一個比較大的不同之處在于rv居然沒有提供setitemclicklistener方法,這是當初自己在使用rv時一個非常不理解的地方,其實現在也不是太理解,但是好在我們可以很方便的實現該功能,一種最簡單的方式就是直接在onbindview方法中設置,這其實是一種不太可取的方式,onbindview在item進入屏幕的時候都會被調用到(cached緩存著的除外),而一般情況下都會創建一個匿名內部類來實現setitemclick,這就會導致在rv快速滑動時創建很多對象,從這點考慮的話setitemclick應該放置到其他地方更為合適
自己的做法就是將setitemclick事件的綁定和viewholder對應的rootview進行綁定,viewholer由于緩存機制的存在它創建的個數是一定的,所以和它綁定的setitemclick對象也是一定的。還有另一種做法可以通過rv自帶的addOnItemTouchListener來實現點擊事件,原理就是rv在觸摸事件中會使用到addOnItemTouchListener中設置的對象,然后配合GestureDetectorCompat實現點擊item,示例代碼如下:
~~~java
recyclerView.addOnItemTouchListener(this);
gestureDetectorCompat = new GestureDetectorCompat(recyclerView.getContext(), new SingleClick());
@Override
public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
if (gestureDetectorCompat != null) {
gestureDetectorCompat.onTouchEvent(e);
}
return false;
}
private class SingleClick extends GestureDetector.SimpleOnGestureListener {
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
View view = recyclerView.findChildViewUnder(e.getX(), e.getY());
if (view == null) {
return false;
}
final RecyclerView.ViewHolder viewHolder = recyclerView.getChildViewHolder(view);
if (!(viewHolder instanceof ViewHolderForRecyclerView)) {
return false;
}
final int position = getAdjustPosition(viewHolder);
if (position == invalidPosition()) {
return false;
}
/****************/
點擊事件設置可以考慮放在這里
/****************/
return true;
}
}
~~~
相對來說這是一個比較優雅點的實現,但是有一點局限在于這種點擊只能設置整個item的點擊,如果item內部有兩個textview都需要實現點擊的話就可能不太適用了,所以具體使用哪種看大家的實際應用場景,可以考慮將這兩種方式都封裝到adapter庫中,目前項目中使用的adapter庫就是采用兩種結合的形式。
### 復用pool緩存
四級緩存中我已經介紹過了,復用本身并不難,調用rv的setRecycledViewPool方法設置一個pool進去就可以,但是并不是說每次使用rv場景的情況下都需要設置一個pool,這個復用pool是針對item中包含rv的情況才適用,如果rv中的item都是普通的布局就不需要復用pool

如上圖所示紅框就是一個item中嵌套rv的例子,這種場景還是比較常見,如果有多個item都是這種類型那么復用pool就非常有必要了,在封裝adapter庫時需要考慮的一個點就是如何找到item中包含rv,可以考慮的做法就是遍歷item的根布局如果找到包含rv的,那么將對該rv設置pool,所有item中的嵌套rv都使用同一個pool即可,查找item中rv代碼可以如下
~~~php
private List<RecyclerView> findNestedRecyclerView(View rootView) {
List<RecyclerView> list = new ArrayList<>();
if (rootView instanceof RecyclerView) {
list.add((RecyclerView) rootView);
return list;
}
if (!(rootView instanceof ViewGroup)) {
return list;
}
final ViewGroup parent = (ViewGroup) rootView;
final int count = parent.getChildCount();
for (int i = 0; i < count; i++) {
View child = parent.getChildAt(i);
list.addAll(findNestedRecyclerView(child));
}
return list;
}
~~~
得到該list之后接下來要做的就是給里面的rv綁定pool了,可以將該pool設置為adapter庫中的成員變量,每次找到嵌套rv的item時直接將該pool設置給對應的rv即可。
關于使用pool源碼上有一點需要在意的是,當最外層的rv滑動導致item被移除屏幕時,rv其實最終是通過調用
removeview(view)完成的,里面的參數view就是和holder綁定的rootview,如果rootview中包含了rv,也就是上圖所示的情況,會最終調用到嵌套rv的onDetachedFromWindow方法
~~~java
@Override
public void onDetachedFromWindow(RecyclerView view, RecyclerView.Recycler recycler) {
super.onDetachedFromWindow(view, recycler);
if (mRecycleChildrenOnDetach) {
removeAndRecycleAllViews(recycler);
recycler.clear();
}
}
~~~
注意里面的if分支,如果進入該分支里面的主要邏輯就是會清除掉scrap和cached緩存上的holder并將它們放置到pool中,但是默認情況下mRecycleChildrenOnDetach是為false的,這么設計的目的就在于放置到pool中的holder要想被拿來使用還必須調用onbindview來進行重新綁定數據,所以google默認將該參數設置為了false,這樣即使rv會移除屏幕也不會使里面的holder失效,下次再次進入屏幕的時候就可以直接使用避免了onbindview的操作。
但是google還是提供了setRecycleChildrenOnDetach方法允許我們改變它的值,如果要想充分使用pool的功能,最好將其置為true,因為按照一般的用戶習慣滑出屏幕的item一般不會回滾查看,這樣接下來要被滑入的item如果存在rv的情況下就可以快速復用pool中的holder,這是使用pool復用的時候一個需要注意點的地方。
### 保存嵌套rv的滑動狀態
原來開發的時候產品就提出過這種需求,需要將滑動位置進行保存,否則每次位置被重置開起來非常奇怪,具體是個什么問題呢,還是以上圖嵌套rv為例,紅框中的rv可以看出來是滑動到中間位置的,如果這時將該rv移出屏幕,然后再移動回屏幕會發生什么事情,這里要分兩種情況,一種是移出屏幕一點后就直接重新移回屏幕,另一種是移出屏幕一段距離再移回來,你會發現一個比較神奇的事就是移出一點回來的rv會保留原先的滑動狀態,而移出一大段距離后回來的rv會丟失掉原先的滑動狀態,造成這個原因的本質是在于rv的緩存機制,簡單來說就是剛滑動屏幕的會被放到cache中而滑出一段距離的會被放到pool中,而從pool中取出的holder會重新進行數據綁定,沒有保存滑動狀態的話rv就會被重置掉,那么如何才能做到即使放在pool中的holder也能保存滑動狀態。
其實這個問題google也替我們考慮到了,linearlayoutmanager中有對應的onSaveInstanceState和onRestoreInstanceState方法來分別處理保存狀態和恢復狀態,它的機制其實和activity的狀態恢復非常類似,我們需要做的就是當rv被移除屏幕調用onSaveInstanceState,移回來時調用onRestoreInstanceState即可。
需要注意點的是onRestoreInstanceState需要傳入一個參數parcelable,這個是onSaveInstanceState提供給我們的,parcelable里面就保存了當前的滑動位置信息,如果自己在封裝adapter庫的時候就需要將這個parcelable保存起來
~~~cpp
private Map<Integer, SparseArrayCompat<Parcelable>> states;
~~~
map中的key為item對應的position,考慮到一個item中可能嵌套多個rv所以value為SparseArrayCompat,最終的效果

可以看到幾個rv在被移出屏幕后再移回來能夠正確保存滑動的位置信息,并且在刪除其中一個item后states中的信息也能得到同步的更新,更新的實現就是利用rv的registerAdapterDataObserver方法,在adapter調用完notify系列方法后會在對應的回調中響應,對于map的更新操作可以放置到這些回調中進行處理。
### 視情況設置itemanimator動畫
使用過listview的都知道listview是沒有item改變動畫效果的,而rv默認就是支持動畫效果的,之前說過rv內部源碼有1萬多行,其實除了rv內部做了大量優化之外,為了支持item的動畫效果google也沒少下苦功夫,也正是因為這樣才使得rv源碼看起來非常復雜。默認在開啟item動畫的情況下會使rv額外處理很多的邏輯判斷,notify的增刪改操作都會對應相應的item動畫效果,所以如果你的應用不需要這些動畫效果的話可以直接關閉掉,這樣可以在處理增刪改操作時大大簡化rv的內部邏輯處理,關閉的方法直接調用setItemAnimator(null)即可。
### diffutil一個神奇的工具類
diffutil是配合rv進行差異化比較的工具類,通過對比前后兩個data數據集合,diffutil會自動給出一系列的notify操作,避免我們手動調用notifiy的繁瑣,看一個簡單的使用示例
~~~csharp
data = new ArrayList<>();
data.add(new MultiTypeItem(R.layout.testlayout1, "hello1"));
data.add(new MultiTypeItem(R.layout.testlayout1, "hello2"));
data.add(new MultiTypeItem(R.layout.testlayout1, "hello3"));
data.add(new MultiTypeItem(R.layout.testlayout1, "hello4"));
data.add(new MultiTypeItem(R.layout.testlayout1, "hello5"));
data.add(new MultiTypeItem(R.layout.testlayout1, "hello6"));
data.add(new MultiTypeItem(R.layout.testlayout1, "hello7"));
newData = new ArrayList<>();
//改
newData.add(new MultiTypeItem(R.layout.testlayout1, "new one"));
newData.add(new MultiTypeItem(R.layout.testlayout1, "hello2"));
newData.add(new MultiTypeItem(R.layout.testlayout1, "hello3"));
newData.add(new MultiTypeItem(R.layout.testlayout1, "hello4"));
//增
newData.add(new MultiTypeItem(R.layout.testlayout1, "add one"));
newData.add(new MultiTypeItem(R.layout.testlayout1, "hello5"));
newData.add(new MultiTypeItem(R.layout.testlayout1, "hello6"));
newData.add(new MultiTypeItem(R.layout.testlayout1, "hello7"));
~~~
先準備兩個數據集合分別代表原數據集和最新的數據集,然后實現下Callback接口
~~~java
private class DiffCallBack extends DiffUtil.Callback {
@Override
public int getOldListSize() {
return data.size();
}
@Override
public int getNewListSize() {
return newData.size();
}
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
return data.get(oldItemPosition).getType() == newData.get(newItemPosition).getType();
}
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
String oldStr = (String) DiffUtilDemoActivity.this.data.get(oldItemPosition).getData();
String newStr = (String) DiffUtilDemoActivity.this.newData.get(newItemPosition).getData();
return oldStr.equals(newStr);
}
}
~~~
實現的方法比較容易看懂,diffutil之所以能判斷兩個數據集的差距就是通過調用上述方法實現,areItemsTheSame表示的就是兩個數據集對應position上的itemtype是否一樣,areContentsTheSame就是比較在itemtype一致的情況下item中內容是否相同,可以理解成是否需要對item進行局部刷新。實現完callback之后接下來就是如何調用了。
~~~cpp
DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffCallBack(), true);
diffResult.dispatchUpdatesTo(adapter);
adapter.setData(newData);
~~~
上述就是diffutil一個簡單的代碼范例,其實最開始的時候自己想將diffutil封裝到adapter庫,但實際在使用后發現了幾個自認為的弊端,所以放棄使用該工具類,這也可能是自己沒有完全掌握diffutil精髓所導致的吧,這里就直接說下我對diffutil使用的看法。
弊端一:
看示例代碼應該也能察覺到,要想使用diffutil必須準備兩個數據集,這就是一個比較蛋疼的事情,原先我們只需要維護一個數據集就可以,現在就需要我們同時維護兩個數據集,兩個數據集都需要有一份自己的數據,如果只是簡單將數據從一個集合copy到另一個集合是可能會導致問題的,會涉及到對象的深拷貝和淺拷貝問題,你必須保證兩份數據集都有各自獨立的內存,否則當你修改其中一個數據集可能會造成另一個數據集同時被修改掉的情況。
弊端二:
為了實現callback接口必須實現四個方法,其中areContentsTheSame是最難實現的一個方法,因為這里涉及到對比同type的item內容是否一致,這就需要將該item對應的數據bean進行比較,怎么比較效率會高點,目前能想到的方法就是將bean轉換成string通過調用equals方法進行比較,如果item的數據bean對應的成員變量很少如示例所示那倒還好,這也是網上很多推薦diffutil文章避開的問題。但是如果bean對應的成員很多,或者成員變量含有list,里面又包含各種對象元素,想想就知道areContentsTheSame很難去實現,為了引入一個diffutil額外增加這么多的邏輯判斷有點得不償失。
弊端三:
diffutil看起來讓人捉摸不透的item動畫行為,以上面代碼為例
~~~csharp
newData.add(new MultiTypeItem(R.layout.testlayout1, "hello1"));
newData.add(new MultiTypeItem(R.layout.testlayout1, "hello2"));
// newData.add(new MultiTypeItem(R.layout.testlayout1, "hello3"));
newData.add(new MultiTypeItem(R.layout.testlayout1, "hello4"));
newData.add(new MultiTypeItem(R.layout.testlayout1, "hello5"));
newData.add(new MultiTypeItem(R.layout.testlayout1, "hello6"));
newData.add(new MultiTypeItem(R.layout.testlayout1, "hello7"));
~~~
新的數據集和原有數據集唯一的不同點就在于中間刪除了一條數據,按照原先我們對于rv的理解,執行的表現形式應該是hello3被刪除掉,然后hello3下面的所有item整體上移才對,但在使用diffutil后你會發現并不是這樣的,它的表現比較怪異會移除第一條數據,這種怪異的行為應該和diffutil內部復雜的算法有關。
基于上述幾個弊端所以最終自己并沒有在adapter庫去使用diffutil,比較有意思的是之前在看關于diffutil文章的時候特意留言問過其中一個作者在實際開發中是否有使用過diffutil,得到的答案是并沒有在實際項目使用過,所以對于一些工具類是否真的好用還需要實際項目來檢驗,當然上面所說的都只是我的理解,不排除有人能透徹理解diffutil活用它的開發者,只是我沒有在網上找到這種文章。
### setHasFixedSize
**設置setHasFixedSize,這么做的一個最大的好處就是嵌套的rv不會觸發requestLayout,從而不會導致外層的rv進行重繪。**
又是一個google提供給我們的方法,主要作用就是設置固定高度的rv,避免rv重復measure調用。這個方法可以配合rv的wrap\_content屬性來使用,比如一個垂直滾動的rv,它的height屬性設置為wrap\_content,最初的時候數據集data只有3條數據,全部展示出來也不能使rv撐滿整個屏幕,如果這時我們通過調用notifyItemRangeInserted增加一條數據,在設置setHasFixedSize和沒有設置setHasFixedSize你會發現rv的高度是不一樣的,設置過setHasFixedSize屬性的rv高度不會改變,而沒有設置過則rv會重新measure它的高度,這是setHasFixedSize表現出來的外在形式,我們可以從代碼層來找到其中的原因。
notifiy的一系列方法除了notifyDataSetChanged這種萬金油的方式,還有一系列進行局部刷新的方法可供調用,而這些方法最終都會執行到一個方法
~~~cpp
void triggerUpdateProcessor() {
if (POST_UPDATES_ON_ANIMATION && mHasFixedSize && mIsAttached) {
ViewCompat.postOnAnimation(RecyclerView.this, mUpdateChildViewsRunnable);
} else {
mAdapterUpdateDuringMeasure = true;
requestLayout();
}
}
~~~
區別就在于當設置過setHasFixedSize會走if分支,而沒有設置則進入到else分支,else分支直接會調用到requestLayout方法,該方法會導致視圖樹進行重新繪制,onmeasure,onlayout最終都會被執行到,結合這點再來看為什么rv的高度屬性為wrap\_content時會受到setHasFixedSize影響就很清楚了,根據上述源碼可以得到一個優化的地方在于,當item嵌套了rv并且rv沒有設置wrap\_content屬性時,我們可以對該rv設置setHasFixedSize,這么做的一個最大的好處就是嵌套的rv不會觸發requestLayout,從而不會導致外層的rv進行重繪。
# 參考資料
[RecyclerView 最深最全剖析,所有面試點都get到了](https://blog.csdn.net/xJ032w2j4cCjhOW8s8/article/details/89007962)
[RecyclerView剖析](https://blog.csdn.net/qq_23012315/article/details/50807224)
[RecyclerView剖析——續一](https://blog.csdn.net/qq_23012315/article/details/51096696)
[Android RecyclerView 局部刷新分析](https://blog.csdn.net/fei20121106/article/details/108121169)
[RecyclerView一些你可能需要知道的優化技術](https://www.jianshu.com/p/1d2213f303fc)
[Android ListView與RecyclerView對比淺析--緩存機制](https://mp.weixin.qq.com/s/_1-5REzMQibPLcK79Hz4gg)
- Android
- 四大組件
- Activity
- Fragment
- Service
- 序列化
- Handler
- Hander介紹
- MessageQueue詳細
- 啟動流程
- 系統啟動流程
- 應用啟動流程
- Activity啟動流程
- View
- view繪制
- view事件傳遞
- choreographer
- LayoutInflater
- UI渲染概念
- Binder
- Binder原理
- Binder最大數據
- Binder小結
- Android組件
- ListView原理
- RecyclerView原理
- SharePreferences
- AsyncTask
- Sqlite
- SQLCipher加密
- 遷移與修復
- Sqlite內核
- Sqlite優化v2
- sqlite索引
- sqlite之wal
- sqlite之鎖機制
- 網絡
- 基礎
- TCP
- HTTP
- HTTP1.1
- HTTP2.0
- HTTPS
- HTTP3.0
- HTTP進化圖
- HTTP小結
- 實踐
- 網絡優化
- Json
- ProtoBuffer
- 斷點續傳
- 性能
- 卡頓
- 卡頓監控
- ANR
- ANR監控
- 內存
- 內存問題與優化
- 圖片內存優化
- 線下內存監控
- 線上內存監控
- 啟動優化
- 死鎖監控
- 崩潰監控
- 包體積優化
- UI渲染優化
- UI常規優化
- I/O監控
- 電量監控
- 第三方框架
- 網絡框架
- Volley
- Okhttp
- 網絡框架n問
- OkHttp原理N問
- 設計模式
- EventBus
- Rxjava
- 圖片
- ImageWoker
- Gilde的優化
- APT
- 依賴注入
- APT
- ARouter
- ButterKnife
- MMKV
- Jetpack
- 協程
- MVI
- Startup
- DataBinder
- 黑科技
- hook
- 運行期Java-hook技術
- 編譯期hook
- ASM
- Transform增量編譯
- 運行期Native-hook技術
- 熱修復
- 插件化
- AAB
- Shadow
- 虛擬機
- 其他
- UI自動化
- JavaParser
- Android Line
- 編譯
- 疑難雜癥
- Android11滑動異常
- 方案
- 工業化
- 模塊化
- 隱私合規
- 動態化
- 項目管理
- 業務啟動優化
- 業務架構設計
- 性能優化case
- 性能優化-排查思路
- 性能優化-現有方案
- 登錄
- 搜索
- C++
- NDK入門
- 跨平臺
- H5
- Flutter
- Flutter 性能優化
- 數據跨平臺