RecyclerView源碼剖析: 滑動,子View回收再利用

本文緊接着上一篇文章來分析RecyclerView的滾動,從中咱們能夠窺探到RecyclerView緩存的回收與再利用。java

分析源碼,不能太盲目,不然目標過大容易迷失本身,所以我仍是選取上篇文章的例子所示用的代碼,做爲分析的目標緩存

mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
mRecyclerView.setAdapter(new RvAdapter());
複製代碼

爲了分析RecyclerView滾動,如今假設手指從下往上滑動,後面將以此爲根基進行分析。app

  1. 與前面文章約定的習慣一致,我將使用RV表示RecyclerView,用LM表示LayoutManager,用LLM表示LinearLayoutManager。
  2. 閱讀本文須要提早了解RecyclerView源碼剖析: 基本顯示

事件處理

根據事件分發的原理可知,RV的滾動由onTouchEvent()完成,精簡代碼以下佈局

public boolean onTouchEvent(MotionEvent e) {
    // 經過LayoutManager判斷是否能夠水平或者垂直滾動
    final boolean canScrollHorizontally = mLayout.canScrollHorizontally();
    final boolean canScrollVertically = mLayout.canScrollVertically();

    switch (action) {
        case MotionEvent.ACTION_MOVE: {
            int dx = mLastTouchX - x;
            // 1. 獲取手指移動的距離
            // dy大於0,表明手指向上滑動
            int dy = mLastTouchY - y;
                
            // ... 省略判斷是否能執行滾動的代碼
                
            if (mScrollState == SCROLL_STATE_DRAGGING) {
                // ...省略nested scroll的代碼
                    
                // 2. 執行滾動
                if (scrollByInternal(
                        canScrollHorizontally ? dx : 0,
                        canScrollVertically ? dy : 0,
                        e)) {
                    // 成功完成一次滾動,就請求父View再也不截斷後續事件
                    getParent().requestDisallowInterceptTouchEvent(true);
                }
            }
        } break;
    }
    return true;
}
複製代碼

例子中使用的LLM支持的是垂直滾動,而且手指從下往上滑動,所以dx值爲0,dy值大於0(注意,不是小於0)。post

計算完滑動距離後,調用scrollByInternal()來完成滾動。動畫

這裏省略了事件處理的代碼,你們能夠自行分析。ui

滾動實現

boolean scrollByInternal(int x, int y, MotionEvent ev) {
    int consumedX = 0;
    int consumedY = 0;

    if (mAdapter != null) {
        mReusableIntPair[0] = 0;
        mReusableIntPair[1] = 0;
        // 執行滾動,並把滾動消耗的距離放到第二個參數中。
        scrollStep(x, y, mReusableIntPair);
        // 計算滾動消耗的距離
        consumedX = mReusableIntPair[0];
        consumedY = mReusableIntPair[1];
        // ...
    }
        
    // 若是有ItemDecoration當即刷新
    if (!mItemDecorations.isEmpty()) {
        invalidate();
    }

    // ...省略nested scroll和over scroll的代碼
        
    // 通知監聽RV滑動的監聽者
    if (consumedX != 0 || consumedY != 0) {
        dispatchOnScrolled(consumedX, consumedY);
    }
        
    // 若是沒有ItemDecoration,也須要刷新
    if (!awakenScrollBars()) {
        invalidate();
    }
        
    // 只要有滑動,就返回true
    return consumedNestedScroll || consumedX != 0 || consumedY != 0;
}
複製代碼

這裏的邏輯很清楚,執行滾動,通知監聽器,刷新界面。所以咱們把目光集中在scrollStep()便可this

void scrollStep(int dx, int dy, @Nullable int[] consumed) {
    // ...

    int consumedX = 0;
    int consumedY = 0;
    // 滾動交給LayoutManager執行
    if (dx != 0) {
        consumedX = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);
    }
    if (dy != 0) {
        consumedY = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
    }

    // ...
        
    // 保存滾動的距離到第二個參數中
    if (consumed != null) {
        consumed[0] = consumedX;
        consumed[1] = consumedY;
    }
}
複製代碼

原來RV把滾動的邏輯交給了LM,例子中使用的是LLM,並且支持的是垂直滾動,所以咱們來分析LLM的scrollVerticallyBy()方法。spa

LLM的垂直滾動實現

public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
    if (mOrientation == HORIZONTAL) {
        return 0;
    }
    return scrollBy(dy, recycler, state);
}
複製代碼

LLM是經過scrollBy()實現滾動的,因爲代碼邏輯跨度比較大,所以我將分佈講解。code

更新佈局信息

int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) {
    if (getChildCount() == 0 || delta == 0) {
        return 0;
    }
    ensureLayoutState();
    // 表示子View可被回收
    mLayoutState.mRecycle = true;
    // 手指從下往上滑動,delta大於0,取值LayoutState.LAYOUT_END
    final int layoutDirection = delta > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
    final int absDelta = Math.abs(delta);
        
    // 1. 更新mLayoutState信息
    updateLayoutState(layoutDirection, absDelta, true, state);
        
    // ...
    return scrolled;
}
複製代碼

根據前面文章可知,updateLayoutState()爲子View佈局更新mLayouState信息

private void updateLayoutState(int layoutDirection, int requiredSpace, boolean canUseExistingSpace, RecyclerView.State state) {
    // 大部分狀況都爲false
    mLayoutState.mInfinite = resolveIsInfinite();
    // 保存佈局的方向
    mLayoutState.mLayoutDirection = layoutDirection;
        
    mReusableIntPair[0] = 0;
    mReusableIntPair[1] = 0;
    // 計算額外的佈局空間,若是不是smooth scroll,通常爲0
    calculateExtraLayoutSpace(state, mReusableIntPair);
    int extraForStart = Math.max(0, mReusableIntPair[0]);
    int extraForEnd = Math.max(0, mReusableIntPair[1]);
        
    // 根據例子分析,值爲true
    boolean layoutToEnd = layoutDirection == LayoutState.LAYOUT_END;
    // 表明佈局可用的額外空間
    mLayoutState.mExtraFillSpace = layoutToEnd ? extraForEnd : extraForStart;
    // 表明不須要回收的空間
    mLayoutState.mNoRecycleSpace = layoutToEnd ? extraForStart : extraForEnd;
        
    int scrollingOffset;
    if (layoutToEnd) {
        // 增長RV底部的padding
        mLayoutState.mExtraFillSpace += mOrientationHelper.getEndPadding();
        // 獲取最底部的一個子View
        final View child = getChildClosestToEnd();
        // Adapter數據遍歷方向,這裏取ITEM_DIRECTION_TAIL,表示從前日後遍歷
        mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD
                    : LayoutState.ITEM_DIRECTION_TAIL;
        // 計算獲取的下一個View,在Adapter中的position
        mLayoutState.mCurrentPosition = getPosition(child) + mLayoutState.mItemDirection;
        // 計算佈局的起始座標
        mLayoutState.mOffset = mOrientationHelper.getDecoratedEnd(child);

        // 計算在不一樣添加子View的狀況下,RV能夠滾動的距離
        scrollingOffset = mOrientationHelper.getDecoratedEnd(child)
                - mOrientationHelper.getEndAfterPadding();

    } else {

    }
        
    // requiredSpace值爲dy
    // mLayoutState.mAvailable表明佈局的可用空間
    mLayoutState.mAvailable = requiredSpace;
        
    // 此時分析的狀況canUseExistingSpace爲true
    if (canUseExistingSpace) {
        // 我的認爲,這段代碼放在這裏是無心義的
        mLayoutState.mAvailable -= scrollingOffset;
    }
        
        
    mLayoutState.mScrollingOffset = scrollingOffset;
}
複製代碼

這裏有不少變量是在前面文章中介紹過的,可是重點要關注scrollingOffset變量,用一副圖解釋下

scrollingOffset

由圖可知,滑動距離還沒達到scrollingOffset時,RV是不須要填充子View的。

子View的回收與填充

更新完信息後,接下來就要決定這次的滑動是否須要回收不可見子View,以及是否須要填充新View。

int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) {
    // ...
    // 1. 更新mLayoutState信息
    updateLayoutState(layoutDirection, absDelta, true, state);
        
    // 2. 根據狀況回收,建立子View,
    int added = fill(recycler, mLayoutState, state, false);
        
    // ...
    return scrolled;
}
複製代碼

fill()方法的命名很差,它不只僅完成填充子View的任務,還完成了子View的回收任務

int fill(RecyclerView.Recycler recycler, LayoutState layoutState, RecyclerView.State state, boolean stopOnFocusable) {
    final int start = layoutState.mAvailable;
        
    // 1. 在添加子View前,回收那些預計不可見子View
    if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
        // 我的以爲,將剛纔更新mLayout時,計算layoutState.mAvailable的代碼移動到這裏比較恰當
        // TODO ugly bug fix. should not happen
        if (layoutState.mAvailable < 0) {
            layoutState.mScrollingOffset += layoutState.mAvailable;
        }
            recycleByLayoutState(recycler, layoutState);
    }
        
    // 可用空間仍是要包括計算出來的額外空間
    int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
    // 用來保存佈局的結果
    LayoutChunkResult layoutChunkResult = mLayoutChunkResult;

    // 2. 若是有可用空間,而且還有子View能夠填充,那麼就填充子View,並計算是否會輸不可見子View 
    while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
        layoutChunkResult.resetInternal();

        // 2.1 添加子View
        layoutChunk(recycler, state, layoutState, layoutChunkResult);

        // 這裏處理的是全部子View添加完畢的狀況
        if (layoutChunkResult.mFinished) {
            break;
        }

        // 計算下次佈局的座標偏移量
        layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
            
        // 添加子View後,再次計算可用空間
        if (!layoutChunkResult.mIgnoreConsumed || layoutState.mScrapList != null
                || !state.isPreLayout()) {
            layoutState.mAvailable -= layoutChunkResult.mConsumed;
            // we keep a separate remaining space because mAvailable is important for recycling
            remainingSpace -= layoutChunkResult.mConsumed;
        }
            
        // 2.2 添加子View後,在執行滾動前,回收那些預計不可見的子View
        if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
            layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
            if (layoutState.mAvailable < 0) {
                layoutState.mScrollingOffset += layoutState.mAvailable;
            }
            recycleByLayoutState(recycler, layoutState);
        }

    }
    // 表示添加子View使用了多少空間,若是沒有添加子View,值就是0
    return start - layoutState.mAvailable;
}
複製代碼

這段代碼把View的回收以及建立,混合在一塊兒。好在前面的文章已經分析了填充子View的過程,那麼接下來我挑一種情形來分析子View的回收過程。這個情形以下圖

recyle

如圖所示,dy表明手指向上滑動的距離差,很明顯dy是小於scrollingOffset(不添加子View能夠滾動的距離),可是卻大於第一個子View的底部距離。

在這種狀況下,咱們徹底能夠預計,即將到來的滾動,確定會讓第一個子View不可見,所以咱們能夠提早回收這個子View。

回收子View

我把須要的代碼結合在一塊兒來分析提早回收子View的過程,代碼以下

private void updateLayoutState(int layoutDirection, int requiredSpace, boolean canUseExistingSpace, RecyclerView.State state) {
    // requiredSpace的值就是dy
    mLayoutState.mAvailable = requiredSpace;
    // canUseExistingSpace爲true
    if (canUseExistingSpace) {
        // dy小於scrollingOffset時,mLayoutState.mAvailable爲負值
        mLayoutState.mAvailable -= scrollingOffset;
    }
}

int fill(RecyclerView.Recycler recycler, LayoutState layoutState, RecyclerView.State state, boolean stopOnFocusable) {
    // 在添加子View前,回收那些預計不可見子View
    if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
        // 處理負值的狀況
        if (layoutState.mAvailable < 0) {
            // layoutState.mScrollingOffset從新計算後值爲dy
            layoutState.mScrollingOffset += layoutState.mAvailable;
        }
        recycleByLayoutState(recycler, layoutState);
    }
        
    // ...
}
複製代碼

根據前面圖片展現的狀況,dy是小於mScrollingOffset的,代碼中最終計算計算出的layoutState.mScrollingOffset的值就爲dy,後面的代碼將會比較頂部的子View在滾動dy的狀況下,是否不可見,看下recycleByLayoutState()如何實現的

private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) {
    // 此時值爲dy
    int scrollingOffset = layoutState.mScrollingOffset;
    // 不是smooth scroll,值爲0
    int noRecycleSpace = layoutState.mNoRecycleSpace;
    if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {

    } else {
        recycleViewsFromStart(recycler, scrollingOffset, noRecycleSpace);
    }
}
    
private void recycleViewsFromStart(RecyclerView.Recycler recycler, int scrollingOffset, int noRecycleSpace) {
    // 此時值爲dy
    final int limit = scrollingOffset - noRecycleSpace;
        
    final int childCount = getChildCount();
    if (mShouldReverseLayout) {

    } else {
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            // 遍歷獲取第一個底部座標大於dy的子View
            if (mOrientationHelper.getDecoratedEnd(child) > limit
                    || mOrientationHelper.getTransformedEndWithDecoration(child) > limit) {
                // 若是找到這個子View,就刪除前面的全部子View,由於它們都不可見
                recycleChildren(recycler, 0, i);
                return;
            }
        }
    }
}    
複製代碼

根據前面的狀況來分析這段代碼,能夠計算出limit的值就是dy,所以經過遍歷獲取的第一個底部座標大於limit值的子View,就是前面圖片上的第二個子View,所以調用recycleChildren(recycler, 0, i),回收第二個子View前面的全部子View,也就是回收第一個子View,由於它立刻不可見。

recycleChildren()經過removeAndRecycleViewAt()方法逐個回收子View

public void removeAndRecycleViewAt(int index, @NonNull Recycler recycler) {
    // 獲取子View
    final View view = getChildAt(index);
    // 1. 從RV中移除子View
    removeViewAt(index);
    // 2. 使用Recycler回收子View
    recycler.recycleView(view);
}
複製代碼

回收分爲兩步,首先從RV中移除子View,這是一個ViewGroup移除子View的操做。而後利用RecyclerView.Recycler進行回收子View。

Reycler回收

如今咱們把目光聚焦到Recycler是如何回收子View的!!!

此時此刻真是使人激動的時候,由於咱們能夠開始窺探Recycler的緩存。

public void recycleView(@NonNull View view) {
            // 經過佈局參數獲取ViewHolder
            ViewHolder holder = getChildViewHolderInt(view);
            
            // ...
            
            // 回收
            recycleViewHolderInternal(holder);
        }
複製代碼

首先獲取了View的ViewHolder,而後調用recycleViewHolderInternal()來回收ViewHolder

回收的是ViewHolder而不是View,有意思!

void recycleViewHolderInternal(ViewHolder holder) {
    // ... 省略狀態判斷的代碼
            
    // ViewHolder沒有設置FLAG_NOT_RECYCLABLE,而且View處於transient state時,這個變量值爲true
    final boolean transientStatePreventsRecycling = holder
            .doesTransientStatePreventRecycling();
    // Adapter#onFailedToRecycleView()作清理工做後,表明可強制刷新
    final boolean forceRecycle = mAdapter != null
            && transientStatePreventsRecycling
            && mAdapter.onFailedToRecycleView(holder);
                    
    boolean cached = false;
    boolean recycled = false;

    if (forceRecycle || holder.isRecyclable()) {
        // 1. 緩存最大空間大於0而且ViewHolder狀態正常,就把它添加到緩存中
        // mViewCacheMax表明緩存空間大小,默認爲2
        if (mViewCacheMax > 0
                && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
                | ViewHolder.FLAG_REMOVED
                | ViewHolder.FLAG_UPDATE
                | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
                    
            int cachedViewSize = mCachedViews.size();
            // 若是緩存空間已滿,就清理空間
            if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
                // 移除第一個緩存的ViewHolder,並使用Recycler進行回收
                recycleCachedViewAt(0);
                cachedViewSize--;
            }

            int targetCacheIndex = cachedViewSize;
                    
            // ... 省略預獲取信息的操做
                    
            // 緩存空間足夠,就緩存ViewHolder
            mCachedViews.add(targetCacheIndex, holder);
            cached = true;
        }
                
        // 2. 沒法使用緩存,就使用Recycler回收ViewHolder
        if (!cached) {
            addViewHolderToRecycledViewPool(holder, true);
            recycled = true;
        }
    } else {

    }
    // ...
}
複製代碼

咱們來分析下這裏使用的緩存回收策略

  1. ArrayList mCachedViews最大空間大於0,而且ViewHolder狀態正常(沒設置過FLAG_INVALID, FLAG_REMOVED, FLAG_UPDATE, FLAG_ADAPTER_POSITION_UNKNOWN)。
    • 若是達到緩存最大空間,就把index爲0的項,交給Recycler回收。
    • 若是緩存空間足夠,就直接添加到緩存中。
  2. 若是緩存最大空間不夠,或者ViewHolder狀態不正常,那麼就用RecyclerView進行回收。

這裏用到兩個緩存,有什麼區別呢,後面揭曉。

如今來看下addViewHolderToRecycledViewPool()如何回收ViewHolder

void addViewHolderToRecycledViewPool(@NonNull ViewHolder holder, boolean dispatchRecycled) {
            clearNestedRecyclerViewIfNotNested(holder);
    // ...省略Accessbility
    
    // 通知監聽者ViewHolder被回收 
    if (dispatchRecycled) {
        dispatchViewRecycled(holder);
    }
    
    // 被回收前,設置ViewHolder的mOwnerRecyclerView爲null
    holder.mOwnerRecyclerView = null;
    
    // 使用RecylerPool回收ViewHolder
    getRecycledViewPool().putRecycledView(holder);
}
複製代碼

原來使用RecyclerPool進行回收的

public void putRecycledView(ViewHolder scrap) {
    final int viewType = scrap.getItemViewType();
    // 根據類型,獲取回收池中的集合
    final ArrayList<ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap;
    // 若是集合已滿,就不進行這次回收(mMaxScrap默認爲5)
    if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {
        return;
    }
    scrap.resetInternal();
    // 添加須要回收的ViewHolder
    scrapHeap.add(scrap);
}
複製代碼

咱們都直到回收池是爲了節省內存而生,可是它也不能盲目的無限回收,RecyclerPool的回收上限是5。若是達到上限,那麼提交給回收池的ViewHolder就會被忽略掉。

若是沒有達到回收池的上限,首先根據須要被回收的ViewHolder的類型獲取一個集合,而後把須要回收的ViewHolder放到這個集合中。

Recycler再利用

如今咱們已經瞭解了Recycler回收子View的方式,那麼如今咱們來看看如何再利用回收的ViewHolder。

根據前面文章的分析可知,獲取子View是在layoutChunk()中發生的

void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, LayoutState layoutState, LayoutChunkResult result) {
    // 1. 獲取子View
    View view = layoutState.next(recycler);
    // 2. 添加子View
    // 3. 測量子View
    // 4. 佈局子View

    // ...
}
複製代碼

獲取子View是從Recycler中獲取的

ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) {
    // ...
            
    ViewHolder holder = null;
 
    // 1. 從mAttachedScrap,hidden(正在消失的View), mCachedViews中獲取
    if (holder == null) {
        holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
        if (holder != null) {
            // 對於不是pre-layout過程,這裏主要就是檢查ViewHolder類型是否匹配
            if (!validateViewHolderForOffsetPosition(holder)) {
                        
            } else {
                romScrapOrHiddenOrCache = true;
            }
        }
    }
            
            
    if (holder == null) {
        final int offsetPosition = mAdapterHelper.findPositionOffset(position);
        final int type = mAdapter.getItemViewType(offsetPosition);
        // 2. 若是Adapter支持stable ids,就根據stable id從mAttachedScrap, mCachedViews中獲取
        if (mAdapter.hasStableIds()) {

        }
                
        // 3. 從自定義的緩存mViewCacheExtension中獲取
        if (holder == null && mViewCacheExtension != null) {

        }
        if (holder == null) { // fallback to pool
            // 4. 從RecylerPool中獲取
            holder = getRecycledViewPool().getRecycledView(type);
            if (holder != null) {
                holder.resetInternal();
                if (FORCE_INVALIDATE_DISPLAY_LIST) {
                    invalidateDisplayListInt(holder);
                }
            }
        }
                
        // 5. 若是都獲取不到,只能調用Adapter建立
        if (holder == null) {
            holder = mAdapter.createViewHolder(RecyclerView.this, type);
        }
    }

    // ...

    boolean bound = false;
    if (mState.isPreLayout() && holder.isBound()) {

    } 
    // 6. 根據狀況決定是否從新綁定
    else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
        final int offsetPosition = mAdapterHelper.findPositionOffset(position);
        bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
    }

    // 7. 更新佈局參數
    // ....
}
複製代碼

第二步,涉及到Adapterstable id功能,大部分狀況下用不到,因此暫時不考慮。

第三步使用的是自定義緩存,目前不分析,你們能夠本身去研究下自定義緩存如何使用。

第一步,從mAttachedScraphidden view(正在消失的View),mCachedViews中獲取ViewHolder。其中與咱們相關的就是緩存mCachedViews,代碼以下

ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
            final int scrapCount = mAttachedScrap.size();

    // 1. 從mAttachedScrap獲取

    // 2. 從hidden views中獲取

    // 3. 從mCachedViews中獲取
    final int cacheSize = mCachedViews.size();
    for (int i = 0; i < cacheSize; i++) {
        final ViewHolder holder = mCachedViews.get(i);
        // 獲取ViewHolder的條件
        // 1. ViewHolder沒設置過FLAG_INVALID
        // 2. ViewHolder的佈局位置要和添加的位置同樣
        // 3. 沒有被過渡動畫所使用
        if (!holder.isInvalid() && holder.getLayoutPosition() == position
                && !holder.isAttachedToTransitionOverlay()) {
            // 本次分析中dryRun爲false,表明獲取ViewHolder以前,須要從緩存中移除
            if (!dryRun) {
                mCachedViews.remove(i);
            }
            return holder;
        }
    }
    return null;
}
複製代碼

咱們發現從mCachedViews獲取ViewHolder仍是有條件的

  1. ViewHolder有效,也就是沒設置過FLAG_INVALID。
  2. ViewHolder的佈局位置要和添加的位置同樣。
  3. 沒有被過渡動畫所使用。

第一個和第三個條件,對於此時的分析是知足的。第二個條件很顯然不必定知足,它是處理這樣的狀況,假如手指向上滑動致使第一個View被回收,此時手指立刻向下滑動,這個時候就須要讓剛被回收的第一個View再顯示出來,所以就能夠再利用剛回收的ViewHolder

從這裏咱們能夠明白,mCachedViews是用來處理剛剛被回收的子View,而後又要讓這個子View再顯示的狀況。

從前面回收ViewHolder的分析可知,回收還用到了RecyclerPool,所以這裏咱們還要分析從RecyclerPool獲取ViewHolder的狀況,也就是第四步,它調用的是RecyclerPool#getRecyclerView()方法

public ViewHolder getRecycledView(int viewType) {
    // 根據類型獲取緩存的集合
    final ScrapData scrapData = mScrap.get(viewType);
    if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
        final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
        for (int i = scrapHeap.size() - 1; i >= 0; i--) {
            // ViewHolder沒有被過渡動畫使用,那麼默認返回的就是第一個ViewHolder
            if (!scrapHeap.get(i).isAttachedToTransitionOverlay()) {
                return scrapHeap.remove(i);
            }
        }
    }
    return null;
}
複製代碼

這裏與前面分析RecyclerPool的緩存原理一致,首先根據類型獲取緩存集合,而後返回一個沒有被過渡動畫使用的ViewHolder。可是這個ViewHolder中保存的數據是不可靠的,所以須要從新綁定一次,也就是要調用Adapter#bindViewHolder()方法,從而會調用子類AdapteronBindViewHolder()方法。

咱們來總結下mCachedViewsRecyclerPool兩個緩存的的特色

  1. mCachedViews適用於子View剛被回收,而後又立刻要顯示的狀況。從這個緩存中獲取的ViewHolder不須要再綁定。
  2. RecyclerPool是爲了節約內存。從這個緩存中獲取的ViewHolder是須要從新綁定的。

那麼若是緩存中獲取不到ViewHolder呢?天然須要經過Adapter建立了,這個過程在前面的文章已經分析過了。

實現RV的滾動

如今,咱們已經明白了子View如何回收再利用,那麼如今咱們來進行最後一步,分析RV如何實現滾動

可能你已經忘記從哪裏分析了,不要緊,已經分析到了LLM的scrollBy()中的第三步

int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) {
    // ...
    // 1. 更新mLayoutState信息
    updateLayoutState(layoutDirection, absDelta, true, state);
        
    // 2. 根據狀況回收,建立子View,
    int added = fill(recycler, mLayoutState, state, false);
        
    // 3. 計算實際須要滾動的距離
    final int consumed = mLayoutState.mScrollingOffset + added;
    if (consumed < 0) {
        return 0;
    }
    final int scrolled = absDelta > consumed ? layoutDirection * consumed : delta;
        
    // 4. 執行RV的滾動
    mOrientationHelper.offsetChildren(-scrolled);
        
    return scrolled;
}
複製代碼

第三步,計算實際須要滾動的距離。這裏分多種狀況,請你們自行分析。

第四步,實現RV的滾動,這裏是經過基類LayoutManager#offsetChildrenVertical(),再由RecyclerView#offsetChildrenVertical()實現的

public void offsetChildrenVertical(@Px int dy) {
    final int childCount = mChildHelper.getChildCount();
    for (int i = 0; i < childCount; i++) {
        mChildHelper.getChildAt(i).offsetTopAndBottom(dy);
    }
}
複製代碼

原來是經過位移RV的每個子View完成來完成RV的總體滾動。

感想

RecyclerView每個過程的分析,都須要極大的耐心和毅力,爲寫這篇文章,我醞釀了每個細節,只但願能講解明白。可是限於篇幅,文章中不少小細節只給出了註釋,須要你們在源碼中本身去分析去體會,才能真正作到融會貫通。

下集預告

下一篇文章,我將講述ItemDecoration的原理,以及實現一個很是有意思的案例,效果以下

聯繫人索引

不過,這個準備過程可能有點長,喜歡我文章的朋友能夠點個贊,關注一波。

相關文章
相關標籤/搜索