在以前的Android的View繪製機制中咱們講過,對於控件的測量以及佈局會經過 onMeasure() 和 onLayout() 方法來實現。因此這裏咱們將這兩個函數做爲入口來研究RecyclerView的整個佈局過程。java
RecyclerView相對於之前的ListView來講,更加靈活。其所拆分出來的各個類的分工更加明確,很好地體現了咱們常常所說的職責單一原則。咱們這裏先對其中使用到的類進行一下講解緩存
不管是View仍是ViewGroup的子類,都是經過 onMeasure() 來實現測量工做的,那麼咱們對於RecyclerView的源碼解析就把onMeasure看成咱們的切入點app
//RecyclerView.java
protected void onMeasure(int widthSpec, int heightSpec) {
//dispatchLayoutStep1,dispatchLayoutStep2,dispatchLayoutStep3確定會執行,可是會根據具體的狀況來區分是在onMeasure仍是onLayout中執行。
if (mLayout == null) {//LayoutManager爲空,那麼就使用默認的測量策略
defaultOnMeasure(widthSpec, heightSpec);
return;
}
if (mLayout.mAutoMeasure) {
//有LayoutManager,開啓了自動測量
final int widthMode = MeasureSpec.getMode(widthSpec);
final int heightMode = MeasureSpec.getMode(heightSpec);
final boolean skipMeasure = widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
//步驟1 調用LayoutManager的onMeasure方法來進行測量工做
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
//若是width和height都已是精確值,那麼就不用再根據內容進行測量,後面步驟再也不處理
if (skipMeasure || mAdapter == null) {
return;
}
//若是測量過程後的寬或者高都沒有精確,那麼就須要根據child來進行佈局,從而來肯定其寬和高。
// 當前的佈局狀態是start
if (mState.mLayoutStep == State.STEP_START) {
//佈局的第一部 主要進行一些初始化的工做
dispatchLayoutStep1();
}
mLayout.setMeasureSpecs(widthSpec, heightSpec);
mState.mIsMeasuring = true;
//執行佈局第二步。先確認子View的大小與佈局
dispatchLayoutStep2();
// 佈局過程結束,根據Children中的邊界信息計算並設置RecyclerView長寬的測量值
mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
//檢查是否須要再此測量。若是RecyclerView仍然有非精確的寬和高,或者這裏還有至少一個Child還有非精確的寬和高,咱們就須要再次測量。
// 好比父子尺寸屬性互相依賴的狀況,要改變參數從新進行一次
if (mLayout.shouldMeasureTwice()) {
mLayout.setMeasureSpecs(MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY),MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY));
mState.mIsMeasuring = true;
dispatchLayoutStep2();
// now we can get the width and height from the children.
mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
}
} else {
//有LayoutManager,沒有開啓自動測量。通常系統的三個LayoutManager都是自動測量,
// 若是是咱們自定義的LayoutManager的話,能夠經過setAutoMeasureEnabled關閉自動測量功能
//RecyclerView已經設置了固定的Size,直接使用固定值便可
if (mHasFixedSize) {
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
return;
}
//若是在測量過程當中數據發生變化,須要先對數據進行處理
...
// 處理完新更新的數據,而後執行自定義測量操做。
if (mAdapter != null) {
mState.mItemCount = mAdapter.getItemCount();
} else {
mState.mItemCount = 0;
}
eatRequestLayout();
//沒有設置固定的寬高,則須要進行測量
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
resumeRequestLayout(false);
mState.mInPreLayout = false;
}
}
複製代碼
在這個方法裏面,根據不一樣的狀況進行了不一樣的處理。less
咱們先從最簡單的來分析。ide
第一種:沒有設置LayoutManager。函數
由於RecyclerView的全部的測量和佈局工做都是交給LayoutManager來處理的,若是沒有設置的話,只能使用默認的測量方案了。工具
第三種:有LayoutManager,並且關閉了自動測量功能。佈局
關閉測量的狀況下不須要考慮子View的大小和佈局。直接按照正常的流程來進行測量便可。若是直接已經設置了固定的寬高,那麼直接使用固定值便可。若是沒有設置固定寬高,那麼就按照正常的控件同樣,根據父級的要求與自身的屬性進行測量。post
第二種:有LayoutManager,開啓了自動測量。性能
這種狀況是最複雜的,須要根據子View的佈局來調整自身的大小。須要知道子View的大小和佈局。因此RecyclerView將佈局的過程提早到這裏來進行了。
咱們簡化一下代碼再看
//RecyclerView.java
if (mLayout.mAutoMeasure) {
//調用LayoutManager的onMeasure方法來進行測量工做
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
//若是width和height都已是精確值,那麼就不用再根據內容進行測量,後面步驟再也不處理
if (skipMeasure || mAdapter == null) {
return;
}
if (mState.mLayoutStep == State.STEP_START) {
//佈局的第一部 主要進行一些初始化的工做
dispatchLayoutStep1();
}
...
//開啓了自動測量,須要先確認子View的大小與佈局
dispatchLayoutStep2();
...
//再根據子View的狀況決定自身的大小
mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
if (mLayout.shouldMeasureTwice()) {
...
//若是有父子尺寸屬性互相依賴的狀況,要改變參數從新進行一次
dispatchLayoutStep2();
mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
}
}
複製代碼
對於RecyclerView的測量和繪製工做,是須要 dispatchLayoutStep1 , dispatchLayoutStep2 , dispatchLayoutStep3 這三步來執行的,step1裏是進行預佈局,主要跟記錄數據更新時須要進行的動畫所需的信息有關,step2就是實際循環執行了子View的測量佈局的一步,而step3主要是用來實際執行動畫。並且經過 mLayoutStep 記錄了當前執行到了哪一步。在開啓自動測量的狀況下若是沒有設置固定寬高,那麼會執行setp1和step2。在step2執行完後就能夠調用 setMeasuredDimensionFromChildren 方法,根據子類的測量佈局結果來設置自身的大小。
咱們先不進行分析step1,step2和step3的具體功能。直接把 onLayout 的代碼也貼出來,看一下這3步是如何保證都可以執行的。
//RecyclerView.java
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
dispatchLayout();
}
void dispatchLayout() {
if (mAdapter == null) {//沒有設置adapter,返回
Log.e(TAG, "No adapter attached; skipping layout");
// leave the state in START
return;
}
if (mLayout == null) {//沒有設置LayoutManager,返回
Log.e(TAG, "No layout manager attached; skipping layout");
// leave the state in START
return;
}
mState.mIsMeasuring = false;
//在onMeasure階段,若是寬高是固定的,那麼mLayoutStep == State.STEP_START 並且dispatchLayoutStep1和dispatchLayoutStep2不會調用
//因此這裏就會調用一下
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth()|| mLayout.getHeight() != getHeight()) {
//在onMeasure階段,若是執行了dispatchLayoutStep1,可是沒有執行dispatchLayoutStep2,就會執行dispatchLayoutStep2
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else {
mLayout.setExactMeasureSpecsFrom(this);
}
//最終調用dispatchLayoutStep3
dispatchLayoutStep3();
}
複製代碼
能夠看到,其實在 onLayout 階段會根據 onMeasure 階段3個步驟執行到了哪一個,而後會在 onLayout 中把剩下的步驟執行。
OK,到如今整個流程通了,在這3個步驟中,step2就是執行了子View的測量佈局的一步,也是最重要的一環,因此咱們將關注的重點放在這個函數。
//RecyclerView.java
private void dispatchLayoutStep2() {
//禁止佈局請求
eatRequestLayout();
...
mState.mInPreLayout = false;
//調用LayoutManager的layoutChildren方法來佈局
mLayout.onLayoutChildren(mRecycler, mState);
...
resumeRequestLayout(false);
}
複製代碼
這裏調用LayoutManager的 onLayoutChildren 方法,將對於子View的測量和佈局工做交給了LayoutManager。並且咱們在自定義LayoutManager的時候也必需要重寫這個方法來描述咱們的佈局錯略。這裏咱們分析最常用的 LinearLayoutManager(後面簡稱LLM) 。咱們這裏只研究垂直方向佈局的狀況。
//LinearLayoutManager.java
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
// layout algorithm:
// 1) by checking children and other variables, find an anchor coordinate and an anchor
// item position.
// 2) fill towards start, stacking from bottom
// 3) fill towards end, stacking from top
// 4) scroll to fulfill requirements like stack from bottom.
// create layout state
複製代碼
在方法的開始位置,直接就扔給了咱們一段說明文檔,告訴了咱們 LinearLayoutManager 中的佈局策略。簡單翻譯一下:
這裏有個比較關鍵的詞,就是 錨點(AnchorInfo) ,其實 LLM 的佈局並非從上往下一個個進行的。而是極可能從整個佈局的中間某個點開始的,而後朝一個方向一個個填充,填滿可見區域後,朝另外一個方向進行填充。至於先朝哪一個方向填充,是根據具體的變量來肯定的。
AnchorInfo 類須要可以有效的描述一個具體的位置信息,咱們首先類內部的幾個重要的成員變量。
//LinearLayoutManager.java
//簡單的數據類來保存錨點信息
class AnchorInfo {
//錨點參考View在整個數據中的position信息,即它是第幾個View
int mPosition;
//錨點的具體座標信息,填充子View的起始座標。當positon=0的時候,若是隻有一半View可見,那麼這個數據可能爲負數
int mCoordinate;
//是否從底部開始佈局
boolean mLayoutFromEnd;
//是否有效
boolean mValid;
複製代碼
能夠看到,經過 AnchorInfo 就能夠準確的定位當前的位置信息了。那麼在LLM中,這個錨點的位置是如何肯定的呢?
咱們從源碼中去尋找答案。
//LinearLayoutManager.java
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
...
//確認LayoutState存在
ensureLayoutState();
//禁止回收
mLayoutState.mRecycle = false;
//計算是否須要顛倒繪製。是從底部到頂部繪製,仍是從頂部到底部繪製(在LLM的構造函數中,其實能夠設置反向繪製)
resolveShouldLayoutReverse();
//若是當前錨點信息非法,滑動到的位置不可用或者有須要恢復的存儲的SaveState
if (!mAnchorInfo.mValid || mPendingScrollPosition != NO_POSITION || mPendingSavedState != null) {
//重置錨點信息
mAnchorInfo.reset();
//是否從end開始進行佈局。由於mShouldReverseLayout和mStackFromEnd默認都是false,那麼咱們這裏能夠考慮按照默認的狀況來進行分析,也就是mLayoutFromEnd也是false
mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
//計算錨點的位置和座標
updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
//設置錨點有效
mAnchorInfo.mValid = true;
}
複製代碼
在須要肯定錨點的時候,會先將錨點進行初始化,而後經過 updateAnchorInfoForLayout 方法來肯定錨點的信息。
//LinearLayoutManager.java
private void updateAnchorInfoForLayout(RecyclerView.Recycler recycler, RecyclerView.State state, AnchorInfo anchorInfo) {
//從掛起的數據更新錨點信息 這個方法通常不會調用到
if (updateAnchorFromPendingData(state, anchorInfo)) {
return;
}
//**重點方法 從子View來肯定錨點信息(這裏會嘗試從有焦點的子View或者列表第一個位置的View或者最後一個位置的View來肯定)
if (updateAnchorFromChildren(recycler, state, anchorInfo)) {
return;
}
//進入這裏說明如今都沒有肯定錨點(好比設置Data後尚未繪製View的狀況下),就直接設置RecyclerView的頂部或者底部位置爲錨點(按照默認狀況,這裏的mPosition=0)。
anchorInfo.assignCoordinateFromPadding();
anchorInfo.mPosition = mStackFromEnd ? state.getItemCount() - 1 : 0;
}
複製代碼
錨點的肯定方案主要有3個:
最後一種何時會發生呢?其實就是沒有子View讓咱們做爲參考。好比說第一次加載數據的時候,RecyclerView一片空白。這時候確定沒有任何子View可以讓咱們做爲參考。
那麼當有子View的時候,咱們經過 updateAnchorFromChildren 方法來肯定錨點位置。
//LinearLayoutManager.java
//從現有子View中肯定錨定。大多數狀況下,是起始或者末尾的有效子View(通常是未移除,展現在咱們面前的View)。
private boolean updateAnchorFromChildren(RecyclerView.Recycler recycler, RecyclerView.State state, AnchorInfo anchorInfo) {
//沒有數據,直接返回false
if (getChildCount() == 0) {
return false;
}
final View focused = getFocusedChild();
//優先選取得到焦點的子View做爲錨點
if (focused != null && anchorInfo.isViewValidAsAnchor(focused, state)) {
//保持獲取焦點的子view的位置信息
anchorInfo.assignFromViewAndKeepVisibleRect(focused);
return true;
}
if (mLastStackFromEnd != mStackFromEnd) {
return false;
}
//根據錨點的設置信息,從底部或者頂部獲取子View信息
View referenceChild = anchorInfo.mLayoutFromEnd ? findReferenceChildClosestToEnd(recycler, state) : findReferenceChildClosestToStart(recycler, state);
if (referenceChild != null) {
anchorInfo.assignFromView(referenceChild);
...
return true;
}
return false;
}
複製代碼
經過子View肯定錨點座標也是進行了3種狀況的處理
通常狀況下,都會使用第三種方案來肯定錨點,因此咱們這裏也主要關注一下這裏的方法。按照咱們默認的變量信息,這裏會經過 findReferenceChildClosestToStart 方法獲取可見區域中的第一個子View做爲錨點的參考View。而後調用 assignFromView 方法來肯定錨點的幾個屬性值。
//LinearLayoutManager.java
public void assignFromView(View child) {
if (mLayoutFromEnd) {
//若是是從底部佈局,那麼獲取child的底部的位置設置爲錨點
mCoordinate = mOrientationHelper.getDecoratedEnd(child) + mOrientationHelper.getTotalSpaceChange();
} else {
//若是是從頂部開始佈局,那麼獲取child的頂部的位置設置爲錨點座標(這裏要考慮ItemDecorator的狀況)
mCoordinate = mOrientationHelper.getDecoratedStart(child);
}
//mPosition賦值爲參考View的position
mPosition = getPosition(child);
}
複製代碼
mPostion這個變量很好理解,就是子View的位置值,那麼 mCoordinate 是個什麼鬼?咱們 getDecoratedStart 是怎麼處理的就知道了。
//LinearLayoutManager.java
//建立mOrientationHelper。咱們按照垂直佈局來進行分析
if (mOrientationHelper == null) {
mOrientationHelper = OrientationHelper.createOrientationHelper(this, mOrientation);
}
//OrientationHelper.java
public static OrientationHelper createVerticalHelper(RecyclerView.LayoutManager layoutManager) {
return new OrientationHelper(layoutManager) {
@Override
@Override
public int getDecoratedStart(View view) {
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)view.getLayoutParams();
//
return mLayoutManager.getDecoratedTop(view) - params.topMargin;
}
複製代碼
比較難理麼,咱們上個簡陋的圖解釋一下
能夠看到在使用子控件進行錨點信息確認時,通常會選擇屏幕中可見的子View的position爲錨點。這裏會選取屏幕上第一個可見View,也就是positon=1的子View做爲參考點, mCoordinate 被賦值爲1號子View上面的Decor的頂部位置。
回到主線 onLayoutChildren 函數。當咱們的錨點信息確認之後,剩下的就是從這個位置開始進行佈局的填充。
if (mAnchorInfo.mLayoutFromEnd) {//從end開始佈局
//倒着繪製的話,先從錨點往上,繪製完再從錨點往下
//設置繪製方向信息爲從錨點往上
updateLayoutStateToFillStart(mAnchorInfo);
mLayoutState.mExtra = extraForStart;
fill(recycler, mLayoutState, state, false);
startOffset = mLayoutState.mOffset;
final int firstElement = mLayoutState.mCurrentPosition;
if (mLayoutState.mAvailable > 0) {
extraForEnd += mLayoutState.mAvailable;
}
//設置繪製方向信息爲從錨點往下
updateLayoutStateToFillEnd(mAnchorInfo);
mLayoutState.mExtra = extraForEnd;
mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
fill(recycler, mLayoutState, state, false);
endOffset = mLayoutState.mOffset;
if (mLayoutState.mAvailable > 0) {
extraForStart = mLayoutState.mAvailable;
updateLayoutStateToFillStart(firstElement, startOffset);
mLayoutState.mExtra = extraForStart;
fill(recycler, mLayoutState, state, false);
startOffset = mLayoutState.mOffset;
}
} else {//從起始位置開始佈局
// 更新layoutState,設置佈局方向朝下
updateLayoutStateToFillEnd(mAnchorInfo);
mLayoutState.mExtra = extraForEnd;
//開始填充佈局
fill(recycler, mLayoutState, state, false);
//結束偏移
endOffset = mLayoutState.mOffset;
//繪製後的最後一個view的position
final int lastElement = mLayoutState.mCurrentPosition;
if (mLayoutState.mAvailable > 0) {
extraForStart += mLayoutState.mAvailable;
}
//更新layoutState,設置佈局方向朝上
updateLayoutStateToFillStart(mAnchorInfo);
mLayoutState.mExtra = extraForStart;
mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
//再次填充佈局
fill(recycler, mLayoutState, state, false);
//起始位置的偏移
startOffset = mLayoutState.mOffset;
if (mLayoutState.mAvailable > 0) {
extraForEnd = mLayoutState.mAvailable;
updateLayoutStateToFillEnd(lastElement, endOffset);
mLayoutState.mExtra = extraForEnd;
fill(recycler, mLayoutState, state, false);
endOffset = mLayoutState.mOffset;
}
}
複製代碼
能夠看到,根據不一樣的繪製方向,這裏面作了不一樣的處理,只是填充的方向相反而已,具體的步驟是類似的。都是從錨點開始往一個方向進行View的填充,填充滿之後再朝另外一個方向填充。填充子View使用的是 fill() 方法。
由於對於繪製方向都按照默認的來處理,因此這裏咱們看看分析else的代碼,並且第一次填充是朝下填充。
//在LinearLayoutManager中,進行界面重繪和進行滑動兩種狀況下,往屏幕上填充子View的工做都是調用fill()進行
int fill(RecyclerView.Recycler recycler, LayoutState layoutState, RecyclerView.State state, boolean stopOnFocusable) {
//可用區域的像素數
final int start = layoutState.mAvailable;
if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
if (layoutState.mAvailable < 0) {
layoutState.mScrollingOffset += layoutState.mAvailable;
}
//將滑出屏幕的View回收掉
recycleByLayoutState(recycler, layoutState);
}
//剩餘繪製空間=可用區域+擴展空間。
int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
//循環佈局直到沒有剩餘空間了或者沒有剩餘數據了
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
//初始化layoutChunkResult
layoutChunkResult.resetInternal();
//**重點方法 添加一個child,而後將繪製的相關信息保存到layoutChunkResult
layoutChunk(recycler, state, layoutState, layoutChunkResult);
if (layoutChunkResult.mFinished) {//若是佈局結束了(沒有view了),退出循環
break;
}
//根據所添加的child消費的高度更新layoutState的偏移量。mLayoutDirection爲+1或者-1,經過乘法來處理是從底部往上佈局,仍是從上往底部開始佈局
layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
if (!layoutChunkResult.mIgnoreConsumed || mLayoutState.mScrapList != null || !state.isPreLayout()) {
layoutState.mAvailable -= layoutChunkResult.mConsumed;
//消費剩餘可用空間
remainingSpace -= layoutChunkResult.mConsumed;
}
...
}
//返回本次佈局所填充的區域
return start - layoutState.mAvailable;
}
複製代碼
在 fill 方法中,會判斷當前的是否還有剩餘區域能夠進行子View的填充。若是沒有剩餘區域或者沒有子View,那麼就返回。不然就經過 layoutChunk 來進行填充工做,填充完畢之後更新當前的可用區域,而後依次遍歷循環,直到不知足條件爲止。
循環中的填充是經過 layoutChunk 來實現的。
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,LayoutState layoutState, LayoutChunkResult result) {
//經過緩存獲取當前position所須要展現的ViewHolder的View
View view = layoutState.next(recycler);
if (view == null) {
//若是咱們將視圖放置在廢棄視圖中,這可能會返回null,這意味着沒有更多的項須要佈局。
result.mFinished = true;
return;
}
LayoutParams params = (LayoutParams) view.getLayoutParams();
if (layoutState.mScrapList == null) {
//根據方向調用addView方法添加子View
if (mShouldReverseLayout == (layoutState.mLayoutDirection == LayoutState.LAYOUT_START)) {
addView(view);
} else {
addView(view, 0);
}
} else {
if (mShouldReverseLayout == (layoutState.mLayoutDirection == LayoutState.LAYOUT_START)) {
//這裏是即將消失的View,可是須要設置對應的移除動畫
addDisappearingView(view);
} else {
addDisappearingView(view, 0);
}
}
//調用measure測量view。這裏會考慮到父類的padding
measureChildWithMargins(view, 0, 0);
//將本次子View消費的區域設置爲子view的高(或者寬)
result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
//找到view的四個邊角位置
int left, top, right, bottom;
...
//調用child.layout方法進行佈局(這裏會考慮到view的ItemDecorator等信息)
layoutDecoratedWithMargins(view, left, top, right, bottom);
//若是視圖未被刪除或更改,則使用可用空間
if (params.isItemRemoved() || params.isItemChanged()) {
result.mIgnoreConsumed = true;
}
result.mFocusable = view.isFocusable();
}
複製代碼
這裏主要作了5個處理
若是隻是考慮第一次數據加載,那麼到目前爲止,咱們的整個頁面經過兩次 fill 就可以將整個屏幕填充完畢了。
對於RecyclerView的複用機制,咱們就不得不提 RecyclerView.Recycler 類了。它的職責就是負責對於View的回收以及複用工做。Recycler 依次經過 Scrap、CacheView、ViewCacheExtension還有RecycledViewPool 四級緩存機制實現對於RecyclerView的快速繪製工做。
Scrap是RecyclerView中最輕量的緩存,它不參與滑動時的回收複用,只是做爲從新佈局時的一種臨時緩存。它的目的是,緩存當界面從新佈局的先後都出如今屏幕上的ViewHolder,以此省去沒必要要的從新加載與綁定工做。
在RecyclerView從新佈局的時候(不包括RecyclerView初始的那次佈局,由於初始化的時候屏幕上原本並無任何View),先調用**detachAndScrapAttachedViews()將全部當前屏幕上正在顯示的View以ViewHolder爲單位標記並記錄在列表中,在以後的fill()**填充屏幕過程當中,會優先從Scrap列表裏面尋找對應的ViewHolder填充。從Scrap中直接返回的ViewHolder內容沒有任何的變化,不會進行從新建立和綁定的過程。
Scrap列表存在於Recycler模塊中。
public final class Recycler {
final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
ArrayList<ViewHolder> mChangedScrap = null;
...
}
複製代碼
能夠看到,Scrap實際上包括了兩個ViewHolder類型的ArrayList。mAttachedScrap負責保存將會原封不動的ViewHolder,而mChangedScrap負責保存位置會發生移動的ViewHolder,注意只是位置發生移動,內容仍舊是原封不動的。
上圖描述的是咱們在一個RecyclerView中刪除B項,而且調用了**notifyItemRemoved()**時,mAttachedScrap與mChangedScrap分別會臨時存儲的View狀況。此時,A是在刪除先後徹底沒有變化的,它會被臨時放入mAttachedScrap。B是咱們要刪除的,它也會被放進mAttachedScrap,可是會被額外標記REMOVED,而且在以後會被移除。C和D在刪除B後會向上移動位置,所以他們會被臨時放入mChangedScrap中。E在這次操做前並無出如今屏幕中,它不屬於Scrap須要管轄的,Scrap只會緩存屏幕上已經加載出來的ViewHolder。在刪除時,A,B,C,D都會進入Scrap,而在刪除後,A,C,D都會回來,其中C,D只進行了位置上的移動,其內容沒有發生變化。
RecyclerView的局部刷新,依賴的就是Scrap的臨時緩存,咱們須要經過notifyItemRemoved()、notifyItemChanged()等系列方法通知RecyclerView哪些位置發生了變化,這樣RecyclerView就能在處理這些變化的時候,使用Scrap來緩存其它內容沒有發生變化的ViewHolder,因而就完成了局部刷新。須要注意的是,若是咱們使用**notifyDataSetChanged()**方法來通知RecyclerView進行更新,其會標記全部屏幕上的View爲FLAG_INVALID,從而不會嘗試使用Scrap來緩存一下子還會回來的ViewHolder,而是通通直接扔進RecycledViewPool池子裏,回來的時候就要從新走一遍綁定的過程。
Scrap只是做爲佈局時的臨時緩存,它和滑動時的緩存沒有任何關係,它的detach和從新attach只臨時存在於佈局的過程當中。佈局結束時Scrap列表應該是空的,其成員要麼被從新佈局出來,要麼將被移除,總之在佈局過程結束的時候,兩個Scrap列表中都不該該再存在任何東西。
CacheView是一個以ViewHolder爲單位,負責在RecyclerView列表位置產生變更的時候,對剛剛移出屏幕的View進行回收複用的緩存列表。
public final class Recycler {
...
final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
int mViewCacheMax = DEFAULT_CACHE_SIZE;
...
}
複製代碼
咱們能夠看到,在Recycler中,mCachedView與前面的Scrap同樣,也是一個以ViewHolder爲單位存儲的ArrayList。這意味着,它也是對於ViewHolder整個進行緩存,在複用時不須要通過建立和綁定過程,內容不發生改變。並且它有個最大緩存個數限制,默認狀況下是2個。
CacheView.png
從上圖中能夠看出,CacheView將會緩存剛變爲不可見的View。這個緩存工做的進行,是發生在fill()調用時的,因爲佈局更新和滑動時都會調用fill()來進行填充,所以這個場景在滑動過程當中會反覆出現,在佈局更新時也可能由於位置變更而出現。fill()幾經週轉最終會調用recycleViewHolderInternal(),裏面將會出現mCachedViews.add()。上面提到,CacheView有最大緩存個數限制,那麼若是超過了緩存會怎樣呢?
void recycleViewHolderInternal(ViewHolder holder) {
...
if (forceRecycle || holder.isRecyclable()) {
if (mViewCacheMax > 0
&& !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
| ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_UPDATE)) {
// Retire oldest cached view 回收並替換最早緩存的那個
int cachedViewSize = mCachedViews.size();
if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
recycleCachedViewAt(0);
cachedViewSize--;
}
mCachedViews.add(targetCacheIndex, holder);
}
...
}
複製代碼
在**recycleViewHolderInternal()中有這麼一段,若是Recycler發現緩存進來一個新ViewHolder時,會超過最大限制,那麼它將先調用recycleCachedViewAt(0)將最早緩存進來的那個ViewHolder回收進RecycledViewPool池子裏,而後再調用mCachedViews.add()**添加新的緩存。也就是說,咱們在滑動RecyclerView的時候,Recycler會不斷地緩存剛剛滑過變成不可見的View進入CacheView,在達到CacheView的上限時,又會不斷地替換CacheView裏的ViewHolder,將它們扔進RecycledViewPool裏。若是咱們一直朝同一個方向滑動,CacheView其實並無在效率上產生幫助,它只是不斷地在把後面滑過的ViewHolder進行了緩存;而若是咱們常常上下來回滑動,那麼CacheView中的緩存就會獲得很好的利用,畢竟複用CacheView中的緩存的時候,不須要通過建立和綁定的消耗。
前面提到,在Srap和CacheView不肯意緩存的時候,都會丟進RecycledViewPool進行回收,所以RecycledViewPool能夠說是Recycler中的一個終極回收站。
public static class RecycledViewPool {
private SparseArray<ArrayList<ViewHolder>> mScrap =
new SparseArray<ArrayList<ViewHolder>>();
private SparseIntArray mMaxScrap = new SparseIntArray();
private int mAttachCount = 0;
private static final int DEFAULT_MAX_SCRAP = 5;
複製代碼
咱們能夠在RecyclerView中找到RecycledViewPool,能夠看見它的保存形式是和上述的Srap、CacheView都不一樣的,它是以一個SparseArray嵌套一個ArrayList對ViewHolder進行保存的。緣由是RecycledViewPool保存的是以ViewHolder的viewType爲區分(咱們在重寫RecyclerView的**onCreateViewHolder()**時能夠發現這裏有個viewType參數,能夠藉助它來實現展現不一樣類型的列表項)的多個列表。
與前二者不一樣,RecycledViewPool在進行回收的時候,目標只是回收一個該viewType的ViewHolder對象,並無保存下原來ViewHolder的內容,在複用時,將會調用bindViewHolder() 按照咱們在**onBindViewHolder()**描述的綁定步驟進行從新綁定,從而搖身一變變成了一個新的列表項展現出來。
一樣,RecycledViewPool也有一個最大數量限制,默認狀況下是5。在沒有超過最大數量限制的狀況下,Recycler會盡可能把將被廢棄的ViewHolder回收到RecycledViewPool中,以期能被複用。值得一提的是,RecycledViewPool只會按照ViewType進行區分,只要ViewType是相同的,甚至能夠在多個RecyclerView中進行通用的複用,只要爲它們設置同一個RecycledViewPool就能夠了。
總的來看,RecyclerView着重在兩個場景使用緩存與回收複用進行了性能上的優化。一是,在數據更新時,利用Scrap實現局部更新,儘量地減小沒有被更改的View進行無用地從新建立與綁定工做。二是,在快速滑動的時候,重複利用已經滑過的ViewHolder對象,以儘量減小從新建立ViewHolder對象時帶來的壓力。整體的思路就是:只要沒有改變,就直接重用;只要能不建立或從新綁定,就儘量地偷懶。
在研究滑動前,咱們先對 LayoutState 這個類的幾個變量作一下說明。
當滾動發生時,會觸發 scrollHorizontallyBy 方法
//LinearLayoutManager.java
public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler,RecyclerView.State state) {
if (mOrientation == VERTICAL) {
return 0;
}
return scrollBy(dx, recycler, state);
}
int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
if (getChildCount() == 0 || dy == 0) {
return 0;
}
//標記正在滾動
mLayoutState.mRecycle = true;
ensureLayoutState();
//確認滾動方向
final int layoutDirection = dy > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
final int absDy = Math.abs(dy);
//更新layoutState,會更新其展現的屏幕區域,偏移量等。好比說當往上滑動的時候,底部會有dy距離的空白區域,這時候,須要調用fill來填充這個dy距離的區域
updateLayoutState(layoutDirection, absDy, true, state);
//調用fill進行填充展現在客戶面前的view
final int consumed = mLayoutState.mScrollingOffset + fill(recycler, mLayoutState, state, false);
...
//記錄本次滾動的距離
mLayoutState.mLastScrollDelta = scrolled;
return scrolled;
}
複製代碼
當滾動時主要進行了兩個處理
咱們爲了更好的理解layoutState內部的屬性關係,簡單看一下 updateLayoutState 內部的實現。
//LinearLayoutaManager.java
private void updateLayoutState(int layoutDirection, int requiredSpace,boolean canUseExistingSpace, RecyclerView.State state) {
int scrollingOffset;
if (layoutDirection == LayoutState.LAYOUT_END) {
//獲取當前顯示的最底部的View
final View child = getChildClosestToEnd();
//設置當前顯示的子View的底部的偏移量(包括了Decor的高度)
mLayoutState.mOffset = mOrientationHelper.getDecoratedEnd(child);
//底部錨點位置減去RecyclerView的高度的話,剩下的就是咱們滾動scrollingOffset之內,不會繪製新的View
//getEndAfterPadding=RecyclerView的高度-padding的高度
scrollingOffset = mOrientationHelper.getDecoratedEnd(child) - mOrientationHelper.getEndAfterPadding();
}
....
複製代碼
方法裏面的註釋已經很詳細了。
有了複用基礎和對這幾個變量的理解以後,咱們從新回到 fill 中,去理解LLM是如何進行緩存處理的。
首先看一下View的回收。
//LinearLayoutManager.java
int fill(RecyclerView.Recycler recycler, LayoutState layoutState, RecyclerView.State state, boolean stopOnFocusable) {
//重點方法 ** 將滑出屏幕的View回收掉
recycleByLayoutState(recycler, layoutState);
}
private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) {
if (!layoutState.mRecycle || layoutState.mInfinite) {
return;
}
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
//從End端開始回收視圖
recycleViewsFromEnd(recycler, layoutState.mScrollingOffset);
} else {
//從Start端開始回收視圖
recycleViewsFromStart(recycler, layoutState.mScrollingOffset);
}
}
複製代碼
這裏咱們考慮手指上滑的狀況,也就是 recycleViewsFromStart 。另外一種狀況是類似的,能夠本身去理解
//從頭部回收View
private void recycleViewsFromStart(RecyclerView.Recycler recycler, int dt) {
//limit表示滑動多少之內不會繪製
final int limit = dt;
//返回附加到父視圖的當前子View的數量
final int childCount = getChildCount();
...
//遍歷子View
for (int i = 0; i < childCount; i++) {
//獲取到子View
View child = getChildAt(i);
//若是當前的View的底部位置>limit,那麼也就是會有View須要繪製,頂部的View也就須要回收了
//這裏有個邏輯,就是若是底部的View不須要繪製,那麼頂部的View就不會進行回收
if (mOrientationHelper.getDecoratedEnd(child) > limit || mOrientationHelper.getTransformedEndWithDecoration(child) > limit) {
recycleChildren(recycler, 0, i);
return;
}
}
}
}
複製代碼
通過跟蹤最後會進入到
//LinearLayoutManager.java
private void recycleChildren(RecyclerView.Recycler recycler, int startIndex, int endIndex) {
...
removeAndRecycleViewAt(i, recycler);
...
}
}
//RecyclerView.java
public void removeAndRecycleViewAt(int index, Recycler recycler) {
final View view = getChildAt(index);
removeViewAt(index);
recycler.recycleView(view);
}
//RecyclerView.java
public void recycleView(View view) {
recycleViewHolderInternal(holder);
}
複製代碼
最終的回收操做會經過 recycleViewHolderInternal 方法來執行。
void recycleViewHolderInternal(ViewHolder holder) {
//判斷各類沒法回收的狀況
...
if (forceRecycle || holder.isRecyclable()) {
//符合回收條件
if (mViewCacheMax > 0 && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID | ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
// Retire oldest cached view
//滑動的視圖,先保存在mCachedViews中
int cachedViewSize = mCachedViews.size();
if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
//mCachedViews只能緩存mViewCacheMax個,那麼須要將最久的那個移到RecycledViewPool
recycleCachedViewAt(0);
cachedViewSize--;
}
int targetCacheIndex = cachedViewSize;
if (ALLOW_THREAD_GAP_WORK && cachedViewSize > 0 && !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {
// when adding the view, skip past most recently prefetched views
int cacheIndex = cachedViewSize - 1;
while (cacheIndex >= 0) {
int cachedPos = mCachedViews.get(cacheIndex).mPosition;
if (!mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) {
break;
}
cacheIndex--;
}
targetCacheIndex = cacheIndex + 1;
}
//將本次回收的ViewHolder放到mCachedViews中
mCachedViews.add(targetCacheIndex, holder);
cached = true;
}
if (!cached) {//若是已經緩存了。那麼此處不會執行了。
addViewHolderToRecycledViewPool(holder, true);
recycled = true;
}
}
...
mViewInfoStore.removeViewHolder(holder);
if (!cached && !recycled && transientStatePreventsRecycling) {
holder.mOwnerRecyclerView = null;
}
}
複製代碼
這部分代碼屬於在滑動時的View回收的過程
在 fill 填充方法中,不只包含了對於滑出屏幕的View的回收處理,還會將即將展現的界面經過複用ViewHolder來達到快速處理的效果。而對於複用的調用則是在 layoutChunk 中的 layoutState.next(recycler) 來觸發的。
//LinearLayoutManager.java
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,LayoutState layoutState, LayoutChunkResult result) {
//經過緩存獲取當前position所須要展現的ViewHolder的View
View view = layoutState.next(recycler);
//LinearLayoutManager.java
View next(RecyclerView.Recycler recycler) {
...
final View view = recycler.getViewForPosition(mCurrentPosition);
...
}
//RecyclerView.java
public View getViewForPosition(int position) {
return getViewForPosition(position, false);
}
//RecyclerView.java
View getViewForPosition(int position, boolean dryRun) {
return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
}
複製代碼
最終對於ViewHolder的複用邏輯是由 tryGetViewHolderForPositionByDeadline 來處理的。
//RecyclerView.java
ViewHolder tryGetViewHolderForPositionByDeadline(int position,boolean dryRun, long deadlineNs) {
boolean fromScrapOrHiddenOrCache = false;
ViewHolder holder = null;
//嘗試從mChangedScrap中獲取。當數據位置發生變化的時候,會走這個邏輯。不如說notifyItemRemove()後,下面的數據會上移,會走這個邏輯
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder != null;
}
if (holder == null) {
//根據position依次嘗試從mAttachedScrap、隱藏的列表、一級緩存(mCachedViews)中獲取
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
if (holder != null) {
//檢驗獲取的holder是否合法。不合法,就會將holder進行回收。若是合法,則標記fromScrapOrHiddenOrCache爲true。代表holder是從這緩存中獲取的。
if (!validateViewHolderForOffsetPosition(holder)) {
...
holder = null;
} else {
fromScrapOrHiddenOrCache = true;
}
}
}
if (holder == null) {
...
if (mAdapter.hasStableIds()) {
//根據id依次嘗試從mAttachedScrap、一級緩存(mCachedViews)中獲取
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),type, dryRun);
if (holder != null) {
holder.mPosition = offsetPosition;
fromScrapOrHiddenOrCache = true;
}
}
//嘗試從咱們自定義的mViewCacheExtension(二級緩存)中去獲取
if (holder == null && mViewCacheExtension != null) {
final View view = mViewCacheExtension .getViewForPositionAndType(this, position, type);
if (view != null) {
holder = getChildViewHolder(view);
if (holder == null) {
throw new IllegalArgumentException("getViewForPositionAndType returned" + " a view which does not have a ViewHolder");
} else if (holder.shouldIgnore()) {
throw new IllegalArgumentException("getViewForPositionAndType returned" + " a view that is ignored. You must call stopIgnoring before" + " returning this view.");
}
}
}
if (holder == null) {
//從緩存池裏面獲取
holder = getRecycledViewPool().getRecycledView(type);
if (holder != null) {
holder.resetInternal();
if (FORCE_INVALIDATE_DISPLAY_LIST) {
invalidateDisplayListInt(holder);
}
}
}
if (holder == null) {
long start = getNanoTime();
if (deadlineNs != FOREVER_NS&& !mRecyclerPool.willCreateInTime(type, start, deadlineNs)) {
return null;
}
//若是仍然沒法獲取的話,調用Adatper的createViewHolder方法建立一個ViewHolder
holder = mAdapter.createViewHolder(RecyclerView.this, type);
if (ALLOW_THREAD_GAP_WORK) {
// only bother finding nested RV if prefetching
RecyclerView innerView = findNestedRecyclerView(holder.itemView);
if (innerView != null) {
holder.mNestedRecyclerView = new WeakReference<>(innerView);
}
}
long end = getNanoTime();
mRecyclerPool.factorInCreateTime(type, end - start);
}
}
...
boolean bound = false;
if (mState.isPreLayout() && holder.isBound()) {
// do not update unless we absolutely have to.
//數據不須要綁定(通常從mChangedScrap,mAttachedScrap中獲得的緩存Holder是不須要進行從新綁定的)
holder.mPreLayoutPosition = position;
} else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
//holder沒有綁定數據,或者須要更新或者holder無效,則須要從新進行數據的綁定
if (DEBUG && holder.isRemoved()) {
throw new IllegalStateException("Removed holder should be bound and it should"+ " come here only in pre-layout. Holder: " + holder);
}
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
//這裏會進行數據的綁定
bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}
...
return holder;
}
複製代碼
這一段的邏輯就是咱們的ViewHolder的整個複用的流程。能夠彙總一下
當咱們獲取到ViewHolder之後會須要進行綁定,也就是將咱們的數據展現在View中。若是獲取的ViewHolder緩存已經進行了數據綁定的話,則無需再進行處理,不然就須要經過 tryBindViewHolderByDeadline 方法調用 adapter的bindViewHolder 來進行數據的綁定。
整篇文章到這裏結束了,相對來講內容仍是比較多的。先從RecyclerView的測量看成入口,在測量的過程當中,提到了複用機制。最後經過RecyclerView對滑動的處理方法,從源碼層面講解了Holder的回收和複用的實現機制。
彙總一下本次源碼解析學到的新知識:
本文由 開了肯 發佈!
同步公衆號[開了肯]