RecyclerView源碼分析

RecyclerView自從出道也有幾年的光景了,大家對于它的贊揚一直絡繹不絕,所以對于如此受歡迎的控件我們必須好好的了解一下。本文嘗試帶領大家更加全面的理解RecyclerView。

設計思路

RecyclerView官網給出的定義是A flexible view for providing a limited window into a large data set.,也就是在限定的試圖內展示大量的數據,來一張通俗明了的圖:

image

RecyclerView的職責就是將Datas中的數據以一定的規則展示在它的上面,但說破天RecyclerView只是一個ViewGroup,它只認識View,不清楚Data數據的具體結構,所以兩個陌生人之間想構建通話,我們很容易想到適配器模式,因此,RecyclerView需要一個Adapter來與Datas進行交流:

image

如上如所示,RecyclerView表示只會和ViewHolder進行接觸,而Adapter的工作就是將Data轉換為RecyclerView認識的ViewHolder,因此RecyclerView就間接地認識了Datas。

事情雖然進展愉快,但RecyclerView是個很懶惰的人,盡管Adapter已經將Datas轉換為RecyclerView所熟知的View,但RecyclerView并不想自己管理些子View,因此,它雇傭了一個叫做LayoutManager的大祭司來幫其完成布局,現在,圖示變成下面這樣:

/pic/understand-recycler/o_1av9iv5731k3422htsd14uess1j.png

如上圖所示,LayoutManager協助RecyclerView來完成布局。但LayoutManager這個大祭司也有弱點,就是它只知道如何將一個一個的View布局在RecyclerView上,但它并不懂得如何管理這些View,如果大祭司肆無忌憚的玩弄View的話肯定會出事情,所以,必須有個管理View的護法,它就是Recycler,LayoutManager在需要View的時候回向護法進行索取,當LayoutManager不需要View(試圖滑出)的時候,就直接將廢棄的View丟給Recycler,圖示如下:

image

到了這里,有負責翻譯數據的Adapter,有負責布局的LayoutManager,有負責管理View的Recycler,一切都很完美,但RecyclerView乃何等神也,它下令說當子View變動的時候姿態要優雅(動畫),所以用雇傭了一個舞者ItemAnimator,因此,舞者也進入了這個圖示:

image

如上,我們就是從宏觀層面來對RecylerView有個大致的了解,可以看到,RecyclerView作為一個View,它只負責接受用戶的各種訊息,然后將信息各司其職的分發出去。接下來我們將深入源碼,看看RecyclerView都是怎么來操作各個組件工作的。

源碼分析

整個RecyclerView還是相當復雜的,我畫了一個與RecyclerView相關類的腦圖:

image

可見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;">

  1. if (mLayout.mAutoMeasure) {
  2. final int widthMode = MeasureSpec.getMode(widthSpec);
  3. final int heightMode = MeasureSpec.getMode(heightSpec);
  4. final boolean skipMeasure = widthMode == MeasureSpec.EXACTLY
  5. && heightMode == MeasureSpec.EXACTLY;
  6. mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
  7. if (skipMeasure || mAdapter == null) {
  8. return;
  9. }
  10. if (mState.mLayoutStep == State.STEP_START) {
  11. dispatchLayoutStep1();
  12. }
  13. mLayout.setMeasureSpecs(widthSpec, heightSpec);
  14. mState.mIsMeasuring = true;
  15. dispatchLayoutStep2();
  16. mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
  17. if (mLayout.shouldMeasureTwice()) {
  18. mLayout.setMeasureSpecs(
  19. MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY),
  20. MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY));
  21. mState.mIsMeasuring = true;
  22. dispatchLayoutStep2();
  23. // now we can get the width and height from the children.
  24. mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
  25. }
  26. }

</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;">

  1. protected void onLayout(boolean changed, int l, int t, int r, int b) {
  2. TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG);
  3. dispatchLayout();
  4. TraceCompat.endSection();
  5. mFirstLayoutComplete = true;
  6. }

</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;">

  1. void dispatchLayout() {
  2. ...
  3. mState.mIsMeasuring = false;
  4. if (mState.mLayoutStep == State.STEP_START) {
  5. dispatchLayoutStep1();
  6. mLayout.setExactMeasureSpecsFrom(this);
  7. dispatchLayoutStep2();
  8. } else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth() ||mLayout.getHeight() != getHeight()) {
  9. // First 2 steps are done in onMeasure but looks like we have to run again due to
  10. // changed size.
  11. mLayout.setExactMeasureSpecsFrom(this);
  12. dispatchLayoutStep2();
  13. } else {
  14. // always make sure we sync them (to ensure mode is exact)
  15. mLayout.setExactMeasureSpecsFrom(this);
  16. }
  17. dispatchLayoutStep3();
  18. }

</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;">

  1. private void dispatchLayoutStep1(){
  2. ...
  3. if (mState.mRunSimpleAnimations) {
  4. int count = mChildHelper.getChildCount();
  5. for (int i = 0; i < count; ++i) {
  6. final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
  7. final ItemHolderInfo animationInfo = mItemAnimator
  8. .recordPreLayoutInformation(mState, holder,
  9. ItemAnimator.buildAdapterChangeFlagsForAnimations(holder),
  10. holder.getUnmodifiedPayloads());
  11. mViewInfoStore.addToPreLayout(holder, animationInfo);
  12. ...
  13. }
  14. }
  15. ...
  16. mState.mLayoutStep = State.STEP_LAYOUT;
  17. }

</pre>

step的第一步目的就是在記錄View的狀態,首先遍歷當前所有的View依次進行處理,mItemAnimator會根據每個View的信息封裝成一個ItemHolderInfo,這個ItemHolderInfo中主要包含的就是當前View的位置狀態等。然后ItemHolderInfo 就被存入mViewInfoStore中,注意這里調用的是mViewInfoStore的addToPreLayout方法,我們追進:

<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;">

  1. void addToPreLayout(ViewHolder holder, ItemHolderInfo info) {
  2. InfoRecord record = mLayoutHolderMap.get(holder);
  3. if (record == null) {
  4. record = InfoRecord.obtain();
  5. mLayoutHolderMap.put(holder, record);
  6. }
  7. record.preInfo = info;
  8. record.flags |= FLAG_PRE;
  9. }

</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;">

  1. private void dispatchLayoutStep2() {
  2. ...
  3. mLayout.onLayoutChildren(mRecycler, mState);
  4. ...
  5. mState.mLayoutStep = State.STEP_ANIMATIONS;
  6. }

</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;">

  1. public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {

  2. ...

  3. if (!mAnchorInfo.mValid || mPendingScrollPosition != NO_POSITION ||

  4. mPendingSavedState != null) {

  5. updateAnchorInfoForLayout(recycler, state, mAnchorInfo);

  6. }

  7. ...

  8. if (mAnchorInfo.mLayoutFromEnd) {

  9. firstLayoutDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_TAIL :

  10. LayoutState.ITEM_DIRECTION_HEAD;

  11. } else {

  12. firstLayoutDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD :

  13. LayoutState.ITEM_DIRECTION_TAIL;

  14. }

  15. ...

  16. onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);

  17. ...

  18. if (mAnchorInfo.mLayoutFromEnd) {

  19. ...

  20. } else {

  21. // fill towards end

  22. updateLayoutStateToFillEnd(mAnchorInfo);

  23. fill(recycler, mLayoutState, state, false);

  24. ...

  25. // fill towards start

  26. updateLayoutStateToFillStart(mAnchorInfo);

  27. ...

  28. fill(recycler, mLayoutState, state, false);

  29. ...

  30. }

  31. ...

  32. }

</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;">

  1. private void updateAnchorInfoForLayout(RecyclerView.Recycler recycler, RecyclerView.State state,
  2. AnchorInfo anchorInfo) {
  3. ...
  4. if (updateAnchorFromChildren(recycler, state, anchorInfo)) {
  5. return;
  6. }
  7. ...
  8. anchorInfo.assignCoordinateFromPadding();
  9. anchorInfo.mPosition = mStackFromEnd ? state.getItemCount() - 1 : 0;
  10. }

</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;">

  1. private boolean updateAnchorFromChildren(RecyclerView.Recycler recycler,
  2. RecyclerView.State state, AnchorInfo anchorInfo) {
  3. if (getChildCount() == 0) {
  4. return false;
  5. }
  6. final View focused = getFocusedChild();
  7. if (focused != null && anchorInfo.isViewValidAsAnchor(focused, state)) {
  8. anchorInfo.assignFromViewAndKeepVisibleRect(focused);
  9. return true;
  10. }
  11. if (mLastStackFromEnd != mStackFromEnd) {
  12. return false;
  13. }
  14. View referenceChild = anchorInfo.mLayoutFromEnd
  15. ? findReferenceChildClosestToEnd(recycler, state)
  16. : findReferenceChildClosestToStart(recycler, state);
  17. if (referenceChild != null) {
  18. anchorInfo.assignFromView(referenceChild);
  19. ...
  20. return true;
  21. }
  22. return false;
  23. }

</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;">

  1. int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
  2. RecyclerView.State state, boolean stopOnFocusable) {
  3. final int start = layoutState.mAvailable;
  4. if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
  5. recycleByLayoutState(recycler, layoutState);
  6. }
  7. int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
  8. LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
  9. while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
  10. layoutChunk(recycler, state, layoutState, layoutChunkResult);
  11. ...
  12. }
  13. return start - layoutState.mAvailable;
  14. }

</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;">

  1. private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) {
  2. if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
  3. ...
  4. } else {
  5. recycleViewsFromStart(recycler, layoutState.mScrollingOffset);
  6. }
  7. }

</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;">

  1. private void recycleViewsFromStart(RecyclerView.Recycler recycler, int dt) {
  2. final int limit = dt;
  3. final int childCount = getChildCount();
  4. if (mShouldReverseLayout) {
  5. ...
  6. } else {
  7. for (int i = 0; i < childCount; i++) {
  8. View child = getChildAt(i);
  9. if (mOrientationHelper.getDecoratedEnd(child) > limit
  10. || mOrientationHelper.getTransformedEndWithDecoration(child) > limit) {
  11. recycleChildren(recycler, 0, i);
  12. return;
  13. }
  14. }
  15. }
  16. }

</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;">

  1. public void removeAndRecycleViewAt(int index, Recycler recycler) {
  2. final View view = getChildAt(index);
  3. removeViewAt(index);
  4. recycler.recycleView(view);
  5. }

</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;">

  1. while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
  2. layoutChunk(recycler, state, layoutState, layoutChunkResult);
  3. ...
  4. }

</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;">

  1. void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
  2. LayoutState layoutState, LayoutChunkResult result) {
  3. View view = layoutState.next(recycler);
  4. ...
  5. LayoutParams params = (LayoutParams) view.getLayoutParams();
  6. if (layoutState.mScrapList == null) {
  7. if (mShouldReverseLayout == (layoutState.mLayoutDirection
  8. == LayoutState.LAYOUT_START)) {
  9. addView(view);
  10. } else {
  11. addView(view, 0);
  12. }
  13. } else {
  14. ...
  15. }
  16. ...
  17. layoutDecoratedWithMargins(view, left, top, right, bottom);
  18. ...
  19. }

</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;">

  1. View next(RecyclerView.Recycler recycler) {
  2. ...
  3. final View view = recycler.getViewForPosition(mCurrentPosition);
  4. return view;
  5. }

</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;">

  1. if (mShouldReverseLayout == (layoutState.mLayoutDirection
  2. == LayoutState.LAYOUT_START)) {
  3. addView(view);
  4. } else {
  5. addView(view, 0);
  6. }

</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;">

  1. private void dispatchLayoutStep3() {

  2. mState.mLayoutStep = State.STEP_START;

  3. if (mState.mRunSimpleAnimations) {

  4. for (int i = mChildHelper.getChildCount() - 1; i >= 0; i--) {

  5. ...

  6. final ItemHolderInfo animationInfo = mItemAnimator

  7. .recordPostLayoutInformation(mState, holder);

  8. mViewInfoStore.addToPostLayout(holder, animationInfo);

  9. }

  10. mViewInfoStore.process(mViewInfoProcessCallback);

  11. }

  12. ...

  13. }

</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;">

  1. void addToPostLayout(ViewHolder holder, ItemHolderInfo info) {
  2. InfoRecord record = mLayoutHolderMap.get(holder);
  3. if (record == null) {
  4. record = InfoRecord.obtain();
  5. mLayoutHolderMap.put(holder, record);
  6. }
  7. record.postInfo = info;
  8. record.flags |= FLAG_POST;
  9. }

</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;">

  1. View getViewForPosition(int position, boolean dryRun) {

  2. boolean fromScrap = false;

  3. ViewHolder holder = null;

  4. if (mState.isPreLayout()) {

  5. holder = getChangedScrapViewForPosition(position);

  6. fromScrap = holder != null;

  7. }

  8. if (holder == null) {

  9. holder = getScrapViewForPosition(position, INVALID_TYPE, dryRun);

  10. ...

  11. }

  12. if (holder == null) {

  13. final int offsetPosition = mAdapterHelper.findPositionOffset(position);

  14. final int type = mAdapter.getItemViewType(offsetPosition);

  15. if (mAdapter.hasStableIds()) {

  16. holder = getScrapViewForId(mAdapter.getItemId(offsetPosition), type, dryRun);

  17. }

  18. if (holder == null && mViewCacheExtension != null) {

  19. final View view = mViewCacheExtension

  20. .getViewForPositionAndType(this, position, type);

  21. ...

  22. }

  23. if (holder == null) { // fallback to recycler

  24. holder = getRecycledViewPool().getRecycledView(type);

  25. if (holder != null) {

  26. holder.resetInternal();

  27. if (FORCE_INVALIDATE_DISPLAY_LIST) {

  28. invalidateDisplayListInt(holder);

  29. }

  30. }

  31. }

  32. if (holder == null) {

  33. holder = mAdapter.createViewHolder(RecyclerView.this, type);

  34. }

  35. }

  36. //生成LayoutParams的代碼 ...

  37. return holder.itemView;

  38. }

  39. }

</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;">

  1. void recycleViewHolderInternal(ViewHolder holder) {

  2. ...

  3. if (holder.isRecyclable()) {

  4. if (!holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID | ViewHolder.FLAG_REMOVED

  5. | ViewHolder.FLAG_UPDATE)) {

  6. int cachedViewSize = mCachedViews.size();

  7. if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {

  8. recycleCachedViewAt(0);

  9. cachedViewSize --;

  10. }

  11. if (cachedViewSize < mViewCacheMax) {

  12. mCachedViews.add(holder);

  13. cached = true;

  14. }

  15. }

  16. if (!cached) {

  17. addViewHolderToRecycledViewPool(holder);

  18. recycled = true;

  19. }

  20. }

  21. ...

  22. }

</pre>

View的回收并不像View的創建那么復雜,這里只涉及了兩層緩存mCachedViews與mRecyclerPool,mCachedViews相當于一個先進先出的數據結構,每當有新的View需要緩存時都會將新的View存入mCachedViews,而mCachedViews則會移除頭元素,并將頭元素放入mRecyclerPool,所以mCachedViews相當于一級緩存,mRecyclerPool則相當于二級緩存,并且mRecyclerPool是可以多個RecyclerView共享的,這在類似于多Tab的新聞類應用會有很大的用處,因為多個Tab下的多個RecyclerView可以共用一個二級緩存。減少內存開銷。

如此,就是對RecyclerView的緩存邏輯的簡要分析。

與AdapterView比較

談到RecyclerView,總避免不了與它的前輩AdapterView家族進行一撕,這里我整理了一下RecylerView與AdapterView的各自特點:

image

前面四點兩位都提供了各自的實現,但也各有各自的特點:

  • 點擊事件

    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;">

  1. static class Bucket {

  2. final static int BITS_PER_WORD = Long.SIZE;

  3. final static long LAST_BIT = 1L << (Long.SIZE - 1);

  4. long mData = 0;

  5. Bucket next;

  6. void set(int index) {

  7. if (index >= BITS_PER_WORD) {

  8. ensureNext();

  9. next.set(index - BITS_PER_WORD);

  10. } else {

  11. mData |= 1L << index;

  12. }

  13. }

  14. ...

  15. }

</pre>

可以看到,Bucket是一個鏈表結構,當index大于64的時候,它便會去下一個Bucket去尋找,所以,Bucket可以不設上限的表示狀態。

Pools

熟悉Message回收機制的朋友可能了解,在使用Message對象時最好通過Message.obtain()方法來獲取,這樣可以在很多情況下避免創建新對象。在使用完之后調用message.recycle()來回收消息。

谷歌為這種機制也提供了抽象的實現,就是位于v4包下Pools類, 內部接口Pool提供了acquirerelease兩個方法,不過需要注意的是這個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的一部分,關于動畫、滑動、嵌套滑動等等還需要大家自行去研究。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容