RecyclerView自從出道也有幾年的光景了,大家對于它的贊揚一直絡繹不絕,所以對于如此受歡迎的控件我們必須好好的了解一下。本文嘗試帶領大家更加全面的理解RecyclerView。
設計思路
RecyclerView官網給出的定義是A flexible view for providing a limited window into a large data set.
,也就是在限定的試圖內展示大量的數據,來一張通俗明了的圖:
RecyclerView的職責就是將Datas中的數據以一定的規則展示在它的上面,但說破天RecyclerView只是一個ViewGroup,它只認識View,不清楚Data數據的具體結構,所以兩個陌生人之間想構建通話,我們很容易想到適配器模式
,因此,RecyclerView需要一個Adapter來與Datas進行交流:
如上如所示,RecyclerView表示只會和ViewHolder進行接觸,而Adapter的工作就是將Data轉換為RecyclerView認識的ViewHolder,因此RecyclerView就間接地認識了Datas。
事情雖然進展愉快,但RecyclerView是個很懶惰的人,盡管Adapter已經將Datas轉換為RecyclerView所熟知的View,但RecyclerView并不想自己管理些子View,因此,它雇傭了一個叫做LayoutManager的大祭司來幫其完成布局,現在,圖示變成下面這樣:
如上圖所示,LayoutManager協助RecyclerView來完成布局。但LayoutManager這個大祭司也有弱點,就是它只知道如何將一個一個的View布局在RecyclerView上,但它并不懂得如何管理這些View,如果大祭司肆無忌憚的玩弄View的話肯定會出事情,所以,必須有個管理View的護法,它就是Recycler,LayoutManager在需要View的時候回向護法進行索取,當LayoutManager不需要View(試圖滑出)的時候,就直接將廢棄的View丟給Recycler,圖示如下:
到了這里,有負責翻譯數據的Adapter,有負責布局的LayoutManager,有負責管理View的Recycler,一切都很完美,但RecyclerView乃何等神也,它下令說當子View變動的時候姿態要優雅(動畫),所以用雇傭了一個舞者ItemAnimator,因此,舞者也進入了這個圖示:
如上,我們就是從宏觀層面來對RecylerView有個大致的了解,可以看到,RecyclerView作為一個View,它只負責接受用戶的各種訊息,然后將信息各司其職的分發出去。接下來我們將深入源碼,看看RecyclerView都是怎么來操作各個組件工作的。
源碼分析
整個RecyclerView還是相當復雜的,我畫了一個與RecyclerView相關類的腦圖:
可見RecyclerView涉及的類相當多,所以看代碼的時候很容易迷失。因此我們需要抽絲剝繭,按照主線來進行分析。
既然RecyclerView是一個View,那無外乎還是measure、layout、draw幾個過程,所以我們依次來看一下。
onMeasure
前面說過,RecyclerView會將測量與布局交給LayoutManager來做,并且LayoutManager有一個叫做mAutoMeasure的屬性,這個屬性用來控制LayoutManager是否開啟自動測量,開啟自動測量的話布局就交由RecyclerView使用一套默認的測量機制,否則,自定義的LayoutManager需要重寫onMeasure來處理自身的測量工作。RecyclerView目前提供的幾種LayoutManager都開啟了自動測量,所以這里我們關注一下自動測量部分的邏輯:
<pre class="highlight prettyprint linenums prettyprinted" style="box-sizing: border-box; padding: 2px; font-style: normal; font-variant: normal; font-weight: normal; font-stretch: normal; font-size: 0.9em; line-height: 1.5; font-family: Consolas, Monaco, "Andale Mono", monospace; border: 1px solid rgb(136, 136, 136); border-radius: 3px; background: rgb(255, 255, 255); overflow: auto; margin: 1.715em 0px;">
if (mLayout.mAutoMeasure) {
final int widthMode = MeasureSpec.getMode(widthSpec);
final int heightMode = MeasureSpec.getMode(heightSpec);
final boolean skipMeasure = widthMode == MeasureSpec.EXACTLY
&& heightMode == MeasureSpec.EXACTLY;
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
if (skipMeasure || mAdapter == null) {
return;
}
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
}
mLayout.setMeasureSpecs(widthSpec, heightSpec);
mState.mIsMeasuring = true;
dispatchLayoutStep2();
mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
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);
}
}
</pre>
自動測量的原理如下:
當RecyclerView的寬高都為EXACTLY時,可以直接設置對應的寬高,然后返回,結束測量。
如果寬高的測量規則不是EXACTLY的,則會在onMeasure()中開始布局的處理,這里首先要介紹一個很重要的類:RecyclerView.State ,這個類封裝了當前RecyclerView的有用信息。State的一個變量mLayoutStep表示了RecyclerView當前的布局狀態,包括STEP_START、STEP_LAYOUT 、 STEP_ANIMATIONS三個,而RecyclerView的布局過程也分為三步,其中,STEP_START表示即將開始布局,需要調用dispatchLayoutStep1來執行第一步布局,接下來,布局狀態變為STEP_LAYOUT,表示接下來需要調用dispatchLayoutStep2里進行第二步布局,同理,第二步布局后狀態變為STEP_ANIMATIONS,需要執行第三步布局dispatchLayoutStep3。
這三個步驟的工作也各不相同,step1負責記錄狀態,step2負責布局,step3則與step1進行比較,根據變化來觸發動畫。
RecyclerView將布局劃分的如此細致必然是有其原因的,在開啟自動測量模式的情況,RecyclerView是支持WRAP_CONTENT屬性的,比如我們可以很容易的在RecyclerView的下面放置其它的View,RecyclerView會根據子View所占大小動態調整自己的大小,這時候,RecyclerView就會將子控件的measure與layout提前到Recycler的onMeasure中,因為它需要確定子空間的大小與位置后,再來設置自己的大小。所以這時候就會在onMeasure中完成step1與step2。否則,就需要在onLayout中去完成整個布局過程。
綜上,整個mLayout.mAutoMeasure就是在做前兩步的布局,可見RecylerView的measure與layout是緊密相關的,所以我們來趕快瞧一瞧RecyclerView是如何layout的。
onLayout
我們直接看下onLayout的代碼:
<pre class="highlight prettyprint linenums prettyprinted" style="box-sizing: border-box; padding: 2px; font-style: normal; font-variant: normal; font-weight: normal; font-stretch: normal; font-size: 0.9em; line-height: 1.5; font-family: Consolas, Monaco, "Andale Mono", monospace; border: 1px solid rgb(136, 136, 136); border-radius: 3px; background: rgb(255, 255, 255); overflow: auto; margin: 1.715em 0px;">
protected void onLayout(boolean changed, int l, int t, int r, int b) {
TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG);
dispatchLayout();
TraceCompat.endSection();
mFirstLayoutComplete = true;
}
</pre>
直接追進dispatchLayout:
<pre class="highlight prettyprint linenums prettyprinted" style="box-sizing: border-box; padding: 2px; font-style: normal; font-variant: normal; font-weight: normal; font-stretch: normal; font-size: 0.9em; line-height: 1.5; font-family: Consolas, Monaco, "Andale Mono", monospace; border: 1px solid rgb(136, 136, 136); border-radius: 3px; background: rgb(255, 255, 255); overflow: auto; margin: 1.715em 0px;">
void dispatchLayout() {
...
mState.mIsMeasuring = false;
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth() ||mLayout.getHeight() != getHeight()) {
// First 2 steps are done in onMeasure but looks like we have to run again due to
// changed size.
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else {
// always make sure we sync them (to ensure mode is exact)
mLayout.setExactMeasureSpecsFrom(this);
}
dispatchLayoutStep3();
}
</pre>
通過查看dispatchLayout的代碼正好驗證了我們前文關于RecyclerView的layout三步走原則,如果在onMeasure中已經完成了step1與step2,則只會執行step3,否則三步會依次觸發。接下來我們一步一步的進行分析
dispatchLayoutStep1
<pre class="highlight prettyprint linenums prettyprinted" style="box-sizing: border-box; padding: 2px; font-style: normal; font-variant: normal; font-weight: normal; font-stretch: normal; font-size: 0.9em; line-height: 1.5; font-family: Consolas, Monaco, "Andale Mono", monospace; border: 1px solid rgb(136, 136, 136); border-radius: 3px; background: rgb(255, 255, 255); overflow: auto; margin: 1.715em 0px;">
private void dispatchLayoutStep1(){
...
if (mState.mRunSimpleAnimations) {
int count = mChildHelper.getChildCount();
for (int i = 0; i < count; ++i) {
final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
final ItemHolderInfo animationInfo = mItemAnimator
.recordPreLayoutInformation(mState, holder,
ItemAnimator.buildAdapterChangeFlagsForAnimations(holder),
holder.getUnmodifiedPayloads());
mViewInfoStore.addToPreLayout(holder, animationInfo);
...
}
}
...
mState.mLayoutStep = State.STEP_LAYOUT;
}
</pre>
step的第一步目的就是在記錄View的狀態,首先遍歷當前所有的View依次進行處理,mItemAnimator會根據每個View的信息封裝成一個ItemHolderInfo,這個ItemHolderInfo中主要包含的就是當前View的位置狀態等。然后ItemHolderInfo 就被存入mViewInfoStore中,注意這里調用的是mViewInfoStore的addToPre
Layout方法,我們追進:
<pre class="highlight prettyprint linenums prettyprinted" style="box-sizing: border-box; padding: 2px; font-style: normal; font-variant: normal; font-weight: normal; font-stretch: normal; font-size: 0.9em; line-height: 1.5; font-family: Consolas, Monaco, "Andale Mono", monospace; border: 1px solid rgb(136, 136, 136); border-radius: 3px; background: rgb(255, 255, 255); overflow: auto; margin: 1.715em 0px;">
void addToPreLayout(ViewHolder holder, ItemHolderInfo info) {
InfoRecord record = mLayoutHolderMap.get(holder);
if (record == null) {
record = InfoRecord.obtain();
mLayoutHolderMap.put(holder, record);
}
record.preInfo = info;
record.flags |= FLAG_PRE;
}
</pre>
addToPreLayout方法中會根據holder來查詢InfoRecord信息,如果沒有,則生成,然后將info信息賦值給InfoRecord的preInfo變量。最后標記FLAG_PRE信息,如此,完成函數。
所以縱觀整個layout的第一步,就是在記錄當前的View信息,因為進入第二步后,View的信息就將被改變了。我們來看第二步:
dispatchLayoutStep2
<pre class="highlight prettyprint linenums prettyprinted" style="box-sizing: border-box; padding: 2px; font-style: normal; font-variant: normal; font-weight: normal; font-stretch: normal; font-size: 0.9em; line-height: 1.5; font-family: Consolas, Monaco, "Andale Mono", monospace; border: 1px solid rgb(136, 136, 136); border-radius: 3px; background: rgb(255, 255, 255); overflow: auto; margin: 1.715em 0px;">
private void dispatchLayoutStep2() {
...
mLayout.onLayoutChildren(mRecycler, mState);
...
mState.mLayoutStep = State.STEP_ANIMATIONS;
}
</pre>
layout的第二步主要就是真正的去布局View了,前面也說過,RecyclerView的布局是由LayoutManager負責的,所以第二步的主要工作也都在LayoutManager中,由于每種布局的方式不一樣,這里我們以常見的LinearLayoutManager為例。我們看其onLayoutChildren方法:
<pre class="highlight prettyprint linenums prettyprinted" style="box-sizing: border-box; padding: 2px; font-style: normal; font-variant: normal; font-weight: normal; font-stretch: normal; font-size: 0.9em; line-height: 1.5; font-family: Consolas, Monaco, "Andale Mono", monospace; border: 1px solid rgb(136, 136, 136); border-radius: 3px; background: rgb(255, 255, 255); overflow: auto; margin: 1.715em 0px;">
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
...
if (!mAnchorInfo.mValid || mPendingScrollPosition != NO_POSITION ||
mPendingSavedState != null) {
updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
}
...
if (mAnchorInfo.mLayoutFromEnd) {
firstLayoutDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_TAIL :
LayoutState.ITEM_DIRECTION_HEAD;
} else {
firstLayoutDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD :
LayoutState.ITEM_DIRECTION_TAIL;
}
...
onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);
...
if (mAnchorInfo.mLayoutFromEnd) {
...
} else {
// fill towards end
updateLayoutStateToFillEnd(mAnchorInfo);
fill(recycler, mLayoutState, state, false);
...
// fill towards start
updateLayoutStateToFillStart(mAnchorInfo);
...
fill(recycler, mLayoutState, state, false);
...
}
...
}
</pre>
整個onLayoutChildren過程還是很復雜的,這里我盡量省略了一些與流程關系不大的細節處理代碼。整個onLayoutChildren過程可以大致整理如下:
- 找到anchor點
- 根據anchor一直向前布局,直至填充滿anchor點前面的所有區域
- 根據anchor一直向后布局,直至填充滿anchor點后面的所有區域
anchor點的尋找是由updateAnchorInfoForLayout函數負責的:
<pre class="highlight prettyprint linenums prettyprinted" style="box-sizing: border-box; padding: 2px; font-style: normal; font-variant: normal; font-weight: normal; font-stretch: normal; font-size: 0.9em; line-height: 1.5; font-family: Consolas, Monaco, "Andale Mono", monospace; border: 1px solid rgb(136, 136, 136); border-radius: 3px; background: rgb(255, 255, 255); overflow: auto; margin: 1.715em 0px;">
private void updateAnchorInfoForLayout(RecyclerView.Recycler recycler, RecyclerView.State state,
AnchorInfo anchorInfo) {
...
if (updateAnchorFromChildren(recycler, state, anchorInfo)) {
return;
}
...
anchorInfo.assignCoordinateFromPadding();
anchorInfo.mPosition = mStackFromEnd ? state.getItemCount() - 1 : 0;
}
</pre>
函數內首先通過子view來獲取anchor,如果沒有獲取到,就根據就取頭/尾點來作為anchor。所以這里我們主要關注updateAnchorFromChildren函數:
<pre class="highlight prettyprint linenums prettyprinted" style="box-sizing: border-box; padding: 2px; font-style: normal; font-variant: normal; font-weight: normal; font-stretch: normal; font-size: 0.9em; line-height: 1.5; font-family: Consolas, Monaco, "Andale Mono", monospace; border: 1px solid rgb(136, 136, 136); border-radius: 3px; background: rgb(255, 255, 255); overflow: auto; margin: 1.715em 0px;">
private boolean updateAnchorFromChildren(RecyclerView.Recycler recycler,
RecyclerView.State state, AnchorInfo anchorInfo) {
if (getChildCount() == 0) {
return false;
}
final View focused = getFocusedChild();
if (focused != null && anchorInfo.isViewValidAsAnchor(focused, state)) {
anchorInfo.assignFromViewAndKeepVisibleRect(focused);
return true;
}
if (mLastStackFromEnd != mStackFromEnd) {
return false;
}
View referenceChild = anchorInfo.mLayoutFromEnd
? findReferenceChildClosestToEnd(recycler, state)
: findReferenceChildClosestToStart(recycler, state);
if (referenceChild != null) {
anchorInfo.assignFromView(referenceChild);
...
return true;
}
return false;
}
</pre>
updateAnchorFromChildren內部做的事情也很容易理解,首先尋找被focus的child,找到的話以此child作為anchor,否則根據布局的方向尋找最合適的child來作為anchor,如果找到則將child的信息賦值給anchorInfo,其實anchorInfo主要記錄的信息就是view的物理位置與在adapter中的位置。找到后返回true,否則返回false則交由上一步的函數做處理。
綜上,剛剛的所追蹤的代碼都是在尋找anchor點。在我們尋找后,LinearLayoutManager還給了我們更改anchor的時機,就是onAnchorReady
函數,我們可以繼承LinearLayoutManager 來重寫onAnchorReady方法,就可以實現某些特定的功能,比如進入RecyclerView時定位在某一項等等。
總之,我們現在找到了anchor信息,接下來就是根據anchor來布局了。無論從上到下還是從下到上布局,都調用的是fill方法,我們進入fill方法來查看一番:
<pre class="highlight prettyprint linenums prettyprinted" style="box-sizing: border-box; padding: 2px; font-style: normal; font-variant: normal; font-weight: normal; font-stretch: normal; font-size: 0.9em; line-height: 1.5; font-family: Consolas, Monaco, "Andale Mono", monospace; border: 1px solid rgb(136, 136, 136); border-radius: 3px; background: rgb(255, 255, 255); overflow: auto; margin: 1.715em 0px;">
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
final int start = layoutState.mAvailable;
if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
recycleByLayoutState(recycler, layoutState);
}
int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
layoutChunk(recycler, state, layoutState, layoutChunkResult);
...
}
return start - layoutState.mAvailable;
}
</pre>
這里同樣省略了很多代碼,我們關注重點:
首先比較重要的函數是recycleByLayoutState,這個函數就厲害了,它會根據當前信息對不需要的View進行回收:
<pre class="highlight prettyprint linenums prettyprinted" style="box-sizing: border-box; padding: 2px; font-style: normal; font-variant: normal; font-weight: normal; font-stretch: normal; font-size: 0.9em; line-height: 1.5; font-family: Consolas, Monaco, "Andale Mono", monospace; border: 1px solid rgb(136, 136, 136); border-radius: 3px; background: rgb(255, 255, 255); overflow: auto; margin: 1.715em 0px;">
private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) {
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
...
} else {
recycleViewsFromStart(recycler, layoutState.mScrollingOffset);
}
}
</pre>
我們繼續追進recycleViewsFromStart:
<pre class="highlight prettyprint linenums prettyprinted" style="box-sizing: border-box; padding: 2px; font-style: normal; font-variant: normal; font-weight: normal; font-stretch: normal; font-size: 0.9em; line-height: 1.5; font-family: Consolas, Monaco, "Andale Mono", monospace; border: 1px solid rgb(136, 136, 136); border-radius: 3px; background: rgb(255, 255, 255); overflow: auto; margin: 1.715em 0px;">
private void recycleViewsFromStart(RecyclerView.Recycler recycler, int dt) {
final int limit = dt;
final int childCount = getChildCount();
if (mShouldReverseLayout) {
...
} else {
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (mOrientationHelper.getDecoratedEnd(child) > limit
|| mOrientationHelper.getTransformedEndWithDecoration(child) > limit) {
recycleChildren(recycler, 0, i);
return;
}
}
}
}
</pre>
這個函數的作用就是遍歷所有的子View,找出逃離邊界的View進行回收,回收函數我們鎖定在recycleChildren里,而這個函數最后又會調到removeAndRecycleViewAt:
<pre class="highlight prettyprint linenums prettyprinted" style="box-sizing: border-box; padding: 2px; font-style: normal; font-variant: normal; font-weight: normal; font-stretch: normal; font-size: 0.9em; line-height: 1.5; font-family: Consolas, Monaco, "Andale Mono", monospace; border: 1px solid rgb(136, 136, 136); border-radius: 3px; background: rgb(255, 255, 255); overflow: auto; margin: 1.715em 0px;">
public void removeAndRecycleViewAt(int index, Recycler recycler) {
final View view = getChildAt(index);
removeViewAt(index);
recycler.recycleView(view);
}
</pre>
這個函數首先調用removeViewAt函數,這個函數的作用是將View從RecyclerView中移除, 緊接著我們看到,是recycler執行了view的回收邏輯。這里我們暫且打住,關于recycler我們會單獨進行說明,這里我們只需要理解,在fill函數的一開始會去回收逃離出屏幕的view。我們再次回到fill函數,關注這里:
<pre class="highlight prettyprint linenums prettyprinted" style="box-sizing: border-box; padding: 2px; font-style: normal; font-variant: normal; font-weight: normal; font-stretch: normal; font-size: 0.9em; line-height: 1.5; font-family: Consolas, Monaco, "Andale Mono", monospace; border: 1px solid rgb(136, 136, 136); border-radius: 3px; background: rgb(255, 255, 255); overflow: auto; margin: 1.715em 0px;">
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
layoutChunk(recycler, state, layoutState, layoutChunkResult);
...
}
</pre>
這段代碼很容易理解,只要還有剩余空間,就會執行layoutChunk方法:
<pre class="highlight prettyprint linenums prettyprinted" style="box-sizing: border-box; padding: 2px; font-style: normal; font-variant: normal; font-weight: normal; font-stretch: normal; font-size: 0.9em; line-height: 1.5; font-family: Consolas, Monaco, "Andale Mono", monospace; border: 1px solid rgb(136, 136, 136); border-radius: 3px; background: rgb(255, 255, 255); overflow: auto; margin: 1.715em 0px;">
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
View view = layoutState.next(recycler);
...
LayoutParams params = (LayoutParams) view.getLayoutParams();
if (layoutState.mScrapList == null) {
if (mShouldReverseLayout == (layoutState.mLayoutDirection
== LayoutState.LAYOUT_START)) {
addView(view);
} else {
addView(view, 0);
}
} else {
...
}
...
layoutDecoratedWithMargins(view, left, top, right, bottom);
...
}
</pre>
我們首先看到,layoutState的next方法返回了一個View,憑空變出一個View,好神奇,追進去看一下:
<pre class="highlight prettyprint linenums prettyprinted" style="box-sizing: border-box; padding: 2px; font-style: normal; font-variant: normal; font-weight: normal; font-stretch: normal; font-size: 0.9em; line-height: 1.5; font-family: Consolas, Monaco, "Andale Mono", monospace; border: 1px solid rgb(136, 136, 136); border-radius: 3px; background: rgb(255, 255, 255); overflow: auto; margin: 1.715em 0px;">
View next(RecyclerView.Recycler recycler) {
...
final View view = recycler.getViewForPosition(mCurrentPosition);
return view;
}
</pre>
可見,view的獲取邏輯也由recycler來負責,所以,這里我們同樣打住,只需要清楚recycler可以根據位置返回一個view即可。
再回到layoutChunk看一下對剛剛生成的view作何處理:
<pre class="highlight prettyprint linenums prettyprinted" style="box-sizing: border-box; padding: 2px; font-style: normal; font-variant: normal; font-weight: normal; font-stretch: normal; font-size: 0.9em; line-height: 1.5; font-family: Consolas, Monaco, "Andale Mono", monospace; border: 1px solid rgb(136, 136, 136); border-radius: 3px; background: rgb(255, 255, 255); overflow: auto; margin: 1.715em 0px;">
if (mShouldReverseLayout == (layoutState.mLayoutDirection
== LayoutState.LAYOUT_START)) {
addView(view);
} else {
addView(view, 0);
}
</pre>
很明顯調用了addView方法,雖然這個方法是LayoutManager的,但這個方法最終會多次輾轉調用到RecyclerView的addView方法,將view添加在RecyclerView中。
綜上,我們就梳理了整個第二步布局的過程,此過程完成了子View的測量與布局,任務還是相當繁重。
dispatchLayoutStep3
接下來,就到了布局的最后一步了,我們直接看下dispatchLayoutStep3方法:
<pre class="highlight prettyprint linenums prettyprinted" style="box-sizing: border-box; padding: 2px; font-style: normal; font-variant: normal; font-weight: normal; font-stretch: normal; font-size: 0.9em; line-height: 1.5; font-family: Consolas, Monaco, "Andale Mono", monospace; border: 1px solid rgb(136, 136, 136); border-radius: 3px; background: rgb(255, 255, 255); overflow: auto; margin: 1.715em 0px;">
private void dispatchLayoutStep3() {
mState.mLayoutStep = State.STEP_START;
if (mState.mRunSimpleAnimations) {
for (int i = mChildHelper.getChildCount() - 1; i >= 0; i--) {
...
final ItemHolderInfo animationInfo = mItemAnimator
.recordPostLayoutInformation(mState, holder);
mViewInfoStore.addToPostLayout(holder, animationInfo);
}
mViewInfoStore.process(mViewInfoProcessCallback);
}
...
}
</pre>
這一步是與第一步呼應的的,此時由于子View都已完成布局,所以子View的信息都發生了變化。我們會看到第一步出現的mViewInfoStore和mItemAnimator再次登場,這次mItemAnimator調用的是recordPostLayoutInformation方法,而mViewInfoStore調用的是addToPostLayout方法,還記得剛剛我強調的嗎,之前是pre,也就是真正布局之前的狀態,而現在要記錄布局之后的狀態,我們追進addToPostLayout:
<pre class="highlight prettyprint linenums prettyprinted" style="box-sizing: border-box; padding: 2px; font-style: normal; font-variant: normal; font-weight: normal; font-stretch: normal; font-size: 0.9em; line-height: 1.5; font-family: Consolas, Monaco, "Andale Mono", monospace; border: 1px solid rgb(136, 136, 136); border-radius: 3px; background: rgb(255, 255, 255); overflow: auto; margin: 1.715em 0px;">
void addToPostLayout(ViewHolder holder, ItemHolderInfo info) {
InfoRecord record = mLayoutHolderMap.get(holder);
if (record == null) {
record = InfoRecord.obtain();
mLayoutHolderMap.put(holder, record);
}
record.postInfo = info;
record.flags |= FLAG_POST;
}
</pre>
和第一步的addToPreLayout類似,不過這次info信息被賦值給了record的postInfo變量,這樣,一個record中就包含了布局前后view的狀態。
最后,mViewInfoStore調用了process方法,這個方法就是根據mViewInfoStore中的View信息,來執行動畫邏輯,這又是一個可以展看很多的點,這里不做探討,感興趣的可以深入的看一下,會對動畫流程有更直觀的體會。
緩存邏輯
前面的章節對于Recycler這個類相關的操作我們都直接進行了忽略,這里我們好好的來看下RecylerView是如何工作的。
與ListView不同,RecyclerView的緩存是分為多級的,但其實整個的緩存邏輯還是很容易理解的,我們首先看一下剛剛獲取View的方法getViewForPosition:
<pre class="highlight prettyprint linenums prettyprinted" style="box-sizing: border-box; padding: 2px; font-style: normal; font-variant: normal; font-weight: normal; font-stretch: normal; font-size: 0.9em; line-height: 1.5; font-family: Consolas, Monaco, "Andale Mono", monospace; border: 1px solid rgb(136, 136, 136); border-radius: 3px; background: rgb(255, 255, 255); overflow: auto; margin: 1.715em 0px;">
View getViewForPosition(int position, boolean dryRun) {
boolean fromScrap = false;
ViewHolder holder = null;
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
fromScrap = holder != null;
}
if (holder == null) {
holder = getScrapViewForPosition(position, INVALID_TYPE, dryRun);
...
}
if (holder == null) {
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
final int type = mAdapter.getItemViewType(offsetPosition);
if (mAdapter.hasStableIds()) {
holder = getScrapViewForId(mAdapter.getItemId(offsetPosition), type, dryRun);
}
if (holder == null && mViewCacheExtension != null) {
final View view = mViewCacheExtension
.getViewForPositionAndType(this, position, type);
...
}
if (holder == null) { // fallback to recycler
holder = getRecycledViewPool().getRecycledView(type);
if (holder != null) {
holder.resetInternal();
if (FORCE_INVALIDATE_DISPLAY_LIST) {
invalidateDisplayListInt(holder);
}
}
}
if (holder == null) {
holder = mAdapter.createViewHolder(RecyclerView.this, type);
}
}
//生成LayoutParams的代碼 ...
return holder.itemView;
}
}
</pre>
獲取View的邏輯可以整理成如下:
- 搜索mChangedScrap,如果找到則返回相應holder。
- 搜索mAttachedScrap與mCachedViews,如果找到且holder有效則返回相應holder。
- 如果設置了mViewCacheExtension,對其調用getViewForPositionAndType方法進行獲取,若該方法返回結果則生成對應的holder。
- 搜索mRecyclerPool,如果找到到則返回相應holder
- 如果上述過程都沒有找到對應的holder,則執行我們熟悉的Adapter.createViewHolder(),創建新的holder實例
綜上,只要數據合法,該方法最終肯定會返回符合條件的View。這里可能大家比較關注的是這么多級別的緩存到底有什么區別,這個問題可能大家看完回收View的代碼會有更深的理解:
<pre class="highlight prettyprint linenums prettyprinted" style="box-sizing: border-box; padding: 2px; font-style: normal; font-variant: normal; font-weight: normal; font-stretch: normal; font-size: 0.9em; line-height: 1.5; font-family: Consolas, Monaco, "Andale Mono", monospace; border: 1px solid rgb(136, 136, 136); border-radius: 3px; background: rgb(255, 255, 255); overflow: auto; margin: 1.715em 0px;">
void recycleViewHolderInternal(ViewHolder holder) {
...
if (holder.isRecyclable()) {
if (!holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID | ViewHolder.FLAG_REMOVED
| ViewHolder.FLAG_UPDATE)) {
int cachedViewSize = mCachedViews.size();
if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
recycleCachedViewAt(0);
cachedViewSize --;
}
if (cachedViewSize < mViewCacheMax) {
mCachedViews.add(holder);
cached = true;
}
}
if (!cached) {
addViewHolderToRecycledViewPool(holder);
recycled = true;
}
}
...
}
</pre>
View的回收并不像View的創建那么復雜,這里只涉及了兩層緩存mCachedViews與mRecyclerPool,mCachedViews相當于一個先進先出的數據結構,每當有新的View需要緩存時都會將新的View存入mCachedViews,而mCachedViews則會移除頭元素,并將頭元素放入mRecyclerPool,所以mCachedViews相當于一級緩存,mRecyclerPool則相當于二級緩存,并且mRecyclerPool是可以多個RecyclerView共享的,這在類似于多Tab的新聞類應用會有很大的用處,因為多個Tab下的多個RecyclerView可以共用一個二級緩存。減少內存開銷。
如此,就是對RecyclerView的緩存邏輯的簡要分析。
與AdapterView比較
談到RecyclerView,總避免不了與它的前輩AdapterView家族進行一撕,這里我整理了一下RecylerView與AdapterView的各自特點:
前面四點兩位都提供了各自的實現,但也各有各自的特點:
-
點擊事件
ListView原生提供Item單擊、長按的事件, 而RecyclerView則需要使用onTouchListener,相對自己實現會比較復雜。
-
分割線
ListView可以很輕松的設置divider屬性來顯示Item之間的分割線,RecyclerView需要我們自己實現ItemDecoration,前者使用簡單,后者可定制性強。
-
布局類型
AdapterView提供了ListView與GridView兩種類型,分別對應流式布局與網格式布局。RecyclerView提供了LinearLayoutManager、GridLayoutManager與之抗衡,相對而言,使用RecyclerView來進行更換布局方式更為輕松。只需要更換一個變量即可,而對于AdapterView而言則是需要更換一個View了。
-
緩存方式
ListView使用了一個名為RecyclerBin的類負責試圖的緩存,而Recycler則使用Recycler來進行緩存,原理上兩者基本一致。
在對比了幾個相近點之外,我們再分別來看一下兩者的不同點,先看RecyclerView:
-
局部刷新
這是一個很有用的功能,在ListView中我們想局部刷新某個Item需要自己來編寫刷新邏輯,而在RecyclerView中我們可以通過
notifyItemChanged(position)
來刷新單個Item,甚至可以通過notifyItemChanged(position, payload)
來傳入一個payload信息來刷新單個Item中的特定內容。 -
動畫
作為視覺動物,我相信很多人喜歡RecylerView都和它簡單的動畫API有關,因為之前對ListView做動畫比較困難,并且不舒服。
-
嵌套布局
嵌套布局也是最近比較火的一個概念,RecyclerView實現了
NestedScrollingChild
接口,使得它可以和一些嵌套組件很好的工作。
我們再來看ListView原生獨有的幾個特點:
-
頭部與尾部的支持
ListView原生支持添加頭部與尾部,雖然RecyclerView可以通過定義不同的Type來做支持,但實際應用中,如果封裝的不好,是很容易出問題的,因為Adapter中的數據位置與物理數據位置發生了偏移。
-
多選
支持多選、單選也是ListView的一大長處,其實如果要我們自己在RecyclerView中去做支持還是需要不少代碼量的。
-
多數據源的支持
ListView提供了CursorAdapter、ArrayAdapter,可以讓我們很方便的從數據庫或者數組中獲取數據,這在測試的時候很有用。
綜上,我們會發現RecycerView的最大特點就是靈活,正因為這種靈活,因此會犧牲了某些便利性。而AdapterView相對來講就比較刻板,但它原生為我們提供了很多有用的方法來便于我們快速開發。ListView并不像當年的ActivityGroup,在Fragment出來后就被標記為Deprecated。兩者目前還是一種互補的關系,起碼在短時間內RecyclerView還并不能完全替代AdapterView,個人感覺原因有兩個,一是目前太多的應用使用了ListView,并且ListView向RecyclerView轉變也沒有無損的方法。第二點,比如我就是想添加個頭部,每個item帶個點擊事件這類簡單的需求,ListView完全可以很輕松的勝任,沒必要舍近求遠來使用RecyclerView。因此,在實際應用中選擇更適合自己的就好。
當然,從Google最近幾次的更新來看,RecyclerView的進化還是很迅速的,而ListView則幾乎沒什么變動,所以RecyclerView絕對是大大的潛力股呀。
設計精巧的類
在翻看RecycleView源碼的過程中也遇見了許多之前沒有注意過的類,這些類都可以復用在我們的日常工作當中。這里列舉出其中具有代表性的幾位。
Bucket
如果一個對象有大量的是與非的狀態需要表示,通常我們會使用BitMask 技術來節省內存,在 Java 中,一個 byte 類型,有 8 位(bit),可以表達 8 個不同的狀態,而 int 類型,則有 32 位,可以表達 32 種狀態。再比如Long類型,有64位,則可以表達64中狀態。一般情況下使用一個Long已經足夠我們使用了。但如果有不設上限的狀態需要我們表示呢?
在ChildHelper里有一個靜態內部類Bucket,基本源碼如下:
<pre class="highlight prettyprint linenums prettyprinted" style="box-sizing: border-box; padding: 2px; font-style: normal; font-variant: normal; font-weight: normal; font-stretch: normal; font-size: 0.9em; line-height: 1.5; font-family: Consolas, Monaco, "Andale Mono", monospace; border: 1px solid rgb(136, 136, 136); border-radius: 3px; background: rgb(255, 255, 255); overflow: auto; margin: 1.715em 0px;">
static class Bucket {
final static int BITS_PER_WORD = Long.SIZE;
final static long LAST_BIT = 1L << (Long.SIZE - 1);
long mData = 0;
Bucket next;
void set(int index) {
if (index >= BITS_PER_WORD) {
ensureNext();
next.set(index - BITS_PER_WORD);
} else {
mData |= 1L << index;
}
}
...
}
</pre>
可以看到,Bucket是一個鏈表結構,當index大于64的時候,它便會去下一個Bucket去尋找,所以,Bucket可以不設上限的表示狀態。
Pools
熟悉Message回收機制的朋友可能了解,在使用Message對象時最好通過Message.obtain()
方法來獲取,這樣可以在很多情況下避免創建新對象。在使用完之后調用message.recycle()
來回收消息。
谷歌為這種機制也提供了抽象的實現,就是位于v4包下Pools類, 內部接口Pool提供了acquire
與release
兩個方法,不過需要注意的是這個acquire
方法可能返回空,畢竟Pools不是業務類,它不應該清楚對象的具體創建邏輯.
還有一點是Pools與Message類的實現機制不同,每個Message對象內部都持有一個引用下一個message的指針,相當于一個鏈表結構,而Pool的實現類SimplePool
中使用的是數組.
Pool機制在 RecycleView 中有如下幾處應用:
RecycleView將item的增刪改封裝為
UpdateOp
類。ViewInfoStore
類中靜態內部類InfoRecord
。
缺點
RecyclerView也不是萬能的,它的靈活性也是有一定限制的,比如我就遇到了一不是很好解決的問題:
Recyler的緩存級別是一個Item的整個View,而我們沒辦法自定義緩存級別,這樣說比較抽象,舉個例子,我的某些Item的某個子View加載很耗時,所以我希望我在上下滑動的時候,Item的其它View是可以被回收利用的,但這個加載很耗時的View是不要重復使用的。即我希望用空間換取時間來獲取滑動的流暢性。當然,這樣的需求不常見,RecyclerView也不能很好的滿足這一點。
總結
RecyclerView也應該算作一個明星控件了,自從其誕生開始就備受歡迎,仔細的學習也能讓我們在工作中更容易的、更恰當的使用。本文也只是分析了RecyclerView的一部分,關于動畫、滑動、嵌套滑動等等還需要大家自行去研究。