RecyclerView源碼剖析: 數據刷新

本片文章來分析RecyclerView數據刷新,你將會看到緩存在數據刷新過程當中的使用,以及不一樣的數據刷新方法對性能的影響。java

閱讀本文前,你須要前面的兩篇文章的基礎數組

  1. RecyclerView源碼剖析: 基本顯示
  2. RecyclerView源碼剖析: 滑動,子View回收再利用

設置數據觀察者

RecyclerView設置Adapter的時候,會給Adapter設置一個數據觀察者RecyclerViewDataObserver mObserver。在Adapter通知數據更新的時候,這個觀察者會根據狀況完成界面刷新工做。緩存

全局刷新

首先來分析最簡單粗暴,並且也是最經常使用的數據刷新方法,Adapter#notifyDataSetChanged(),它表示數據徹底改變,界面須要全局刷新。app

Adapter#notifyDataSetChanged()會調用RecyclerViewDataObserver#onChanged()方法ide

private class RecyclerViewDataObserver extends AdapterDataObserver {
    @Override
    public void onChanged() {
        // 表示數據的結構已經徹底改變
        mState.mStructureChanged = true;
        // 1.預處理工做,響應數據集改變
        processDataSetCompletelyChanged(true);
        // 2.若是沒有等待更新的操做,那麼就當即請求從新佈局
        if (!mAdapterHelper.hasPendingUpdates()) {
            requestLayout();
        }
    }
}
複製代碼

RecyclerViewDataObserver接收到數據徹底改變的消息後,它首先作一些預處理工做,以響應數據集改變,而後請求從新佈局來刷新界面。工具

預處理

首先來看看數據集改變的預處理工做到底作了啥佈局

void processDataSetCompletelyChanged(boolean dispatchItemsChanged) {
    // 參數傳入的值爲true,代表須要分發數據改變的事件
    mDispatchItemsChangedEvent |= dispatchItemsChanged;
    // 代表在layout後,數據集徹底改變了
    mDataSetHasChangedAfterLayout = true;
    // 標記已知的View爲無效的
    markKnownViewsInvalid();
}
複製代碼

processDataSetCompletelyChanged()首先作了一些狀態標記,而後調用markKnownViewsInvalid()來標記已知的View爲無效的。post

void markKnownViewsInvalid() {
    final int childCount = mChildHelper.getUnfilteredChildCount();
    for (int i = 0; i < childCount; i++) {
        final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
        if (holder != null && !holder.shouldIgnore()) {
            // 1. 遍歷RecyclerView全部子View,設置FLAG_UPDATE和FLAG_INVALID標籤
            holder.addFlags(ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID);
        }
    }
    // 2. 標記ItemDecoration區域爲dirty
    markItemDecorInsetsDirty();
    // 3. 對mCachedViews中保存的View,設置FLAG_UPDATE和FLAG_INVALID標籤,甚至用RecyclerPool回收這些View
    mRecycler.markKnownViewsInvalid();
}
複製代碼

markKnownViewsInvalid()處理的目標不只僅只有RecyclerView的子View,並且還包括mCachedViews緩存中的View。性能

界面因爲滑動,致使某些不可見的子View被移除,而且優先使用mCachedViews緩存它。當再次因爲滑動須要顯示這個子View時,就會從mCachedViews中獲取。優化

markKnownViewsInvalid()作了兩件事

  1. 標記View爲FLAG_UPDATEFLAG_INVALID
  2. 標記View的ItemDecoration區域爲dirty,也就是設置View的佈局參數的mInsetsDirty值爲true,表示須要刷新View的ItemDecoration區域。

刷新界面

預處理工做作完了,就會調用requestLayout()來從新佈局,咱們把主要精力放在layout過程。

layout過程分爲了三步,dispatchLayoutStep1()處理更新操做以及保存動畫信息,dispatchLayoutStep3()執行動畫並作一些清理工做,而dispatchLayoutStep2()是完成了數據刷新的工做。

dispatchLayoutStep2()把子View的佈局工做交給了LayoutManager#onLayoutChildren()完成,這裏以LinearLayoutManager#onLayoutChidren()爲例進行分析。

public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
    // ...
    
    // 1. 分離/廢棄並緩存子View
    detachAndScrapAttachedViews(recycler);
    // 2. 填充子View,由layoutChunk()實現
    fill(recycler, mLayoutState, state, false);
    
    // ...
}
複製代碼

LinearLayoutManager對子View的佈局工做大體分爲兩步。首先是分離/移除子View,並緩存它。而後是獲取子View並填充給RecyclerView

緩存

首先咱們來分析分離/移除和緩存這一步,調用的是LayoutManager#detachAndScrapAttachedViews()方法,它會遍歷RecyclerView全部子View,而後調用LayoutManager#scrapOrRecycleView()方法來執行分離/移除和緩存子View

private void scrapOrRecycleView(Recycler recycler, int index, View view) {
    final ViewHolder viewHolder = getChildViewHolderInt(view);
    if (viewHolder.shouldIgnore()) {
        return;
    }
    // 如下條件是知足的
    // 子View的ViewHolder設置過FLAG_INVALID
    // 子View的ViewHolder沒有設置過FLAG_REMOVED
    // Adapter默認沒有開啓stable id功能
    if (viewHolder.isInvalid() && !viewHolder.isRemoved()
            && !mRecyclerView.mAdapter.hasStableIds()) {
        // 1. 從RecyclerView中移除子View
        removeViewAt(index);
        // 2. Recycler回收子View
        recycler.recycleViewHolderInternal(viewHolder);
    } else {
        detachViewAt(index);
        recycler.scrapView(view);
        mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
    }
}
複製代碼

在預處理工做環節,把View標記爲FLAG_INVALID,所以就會先把這個View從RecyclerView中移除,使用的是ViewGroup#removeViewAt()方法。而後使用Recycler來回收這些被移除的子View。

咱們如今來看下回收的過程

void recycleViewHolderInternal(ViewHolder holder) {
            // ...
            
            if (forceRecycle || holder.isRecyclable()) {
                // 因爲View被標記爲FLAG_INVALID,因此沒法使用mCachedViews這個緩存
                if (mViewCacheMax > 0
                        && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
                        | ViewHolder.FLAG_REMOVED
                        | ViewHolder.FLAG_UPDATE
                        | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
                    // ...
                }
                // 不能使用mCachedViews這個緩存,就交給RecyclerPool回收
                if (!cached) {
                    addViewHolderToRecycledViewPool(holder, true);
                    recycled = true;
                }
            } else {

            }
            
            // ...
        }
複製代碼

因爲子View被標記爲FLAG_INVALID,所以子View只能交給RecyclerPool進行回收。

填充

如今全部子View都被RecyclerPool回收了,那麼接下來分析如何給RecyclerView填充子View。這一步是由LinearLayoutManager#layoutChunk()實現的

void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, LayoutState layoutState, LayoutChunkResult result) {
    // ...
        
    // 1. 從Recycler中獲取View
    View view = layoutState.next(recycler);
        
    // 2. 把子View添加到RecyclerView中
    addView(view);
    
    // 3. 測量子View
    measureChildWithMargins(view, 0, 0);
    
    // 4. 佈局子View
    layoutDecoratedWithMargins(view, left, top, right, bottom);
        
    // ...
}
複製代碼

填充子View經歷了這四步,可是咱們把目光放在如何從Recycler中獲取View。它是由Recycler#tryGetViewHolderForPositionByDeadline()實現的

ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) {
    boolean fromScrapOrHiddenOrCache = false;
    ViewHolder holder = null;
            
    // 1. 首先從mChangedScrap中獲取
    // LinearLayoutManager在大部分狀況下是支持predictive item animations
    if (mState.isPreLayout()) {
        holder = getChangedScrapViewForPosition(position);
        fromScrapOrHiddenOrCache = holder != null;
    }

    // 2. 從mAttachedScrap, hidden view, mCachedViews中獲取
    if (holder == null) {
        holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
        if (holder != null) {
            if (!validateViewHolderForOffsetPosition(holder)) {
                // recycle holder (and unscrap if relevant) since it can't be used
                if (!dryRun) {
                    // we would like to recycle this but need to make sure it is not used by
                    // animation logic etc.
                    holder.addFlags(ViewHolder.FLAG_INVALID);
                    if (holder.isScrap()) {
                        removeDetachedView(holder.itemView, false);
                        holder.unScrap();
                    } else if (holder.wasReturnedFromScrap()) {
                        holder.clearReturnedFromScrapFlag();
                    }
                    recycleViewHolderInternal(holder);
                }
                holder = null;
            } else {
                fromScrapOrHiddenOrCache = true;
            }
        }
    }

    if (holder == null) {
        final int offsetPosition = mAdapterHelper.findPositionOffset(position);
        final int type = mAdapter.getItemViewType(offsetPosition);
        // 3. 經過stable id從mAttachedScrap, mCachedViews中獲取
        if (mAdapter.hasStableIds()) {
            holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                    type, dryRun);
            if (holder != null) {
                // update position
                holder.mPosition = offsetPosition;
                fromScrapOrHiddenOrCache = true;
            }
        }
        if (holder == null && mViewCacheExtension != null) {
            // 4. 從自定義緩存中獲取
            final View view = mViewCacheExtension
                    .getViewForPositionAndType(this, position, type);
            if (view != null) {
                holder = getChildViewHolder(view);
            }
        }
        if (holder == null) { // fallback to pool
            // 5. 從RecyclerPool中獲取
            holder = getRecycledViewPool().getRecycledView(type);
            if (holder != null) {
                // 重置全部的標誌位
                holder.resetInternal();
                // ...
            }
        }
        if (holder == null) {
            // 6. 利用Adapter建立
            holder = mAdapter.createViewHolder(RecyclerView.this, type);
        }
    }

    // ...

    // 7. 根據條件決定是否執行綁定操做
    boolean bound = false;
    if (mState.isPreLayout() && holder.isBound()) {
        // 若是已經綁定,就只更新predictive item animations的位置信息
        holder.mPreLayoutPosition = position;
    } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
        // 若是沒有綁定,就須要執行綁定操做
        final int offsetPosition = mAdapterHelper.findPositionOffset(position);
        bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
    }

    // 8. 校訂佈局參數,並更新它
    // ...

    return holder;
}
複製代碼

這裏我把全部的獲取路徑都標註了,可是因爲View被標記爲FLAG_INVALID,因此只能從RecyclerPool進行獲取(第6步)。而從RecylerPool獲取ViewHolder後,它的全部標誌位都被重置了,所以還須要進行綁定(第7步)。

獲取到了View,就把它添加到RecyclerView中,而後測量、佈局、繪製,所以完成了界面刷新過程。

小結

如今咱們來總結下Adapter#notifyDataSetChanged()方法的優缺點。

優勢就是簡單無腦。缺點就是影響繪製性能,由於它要把全部子View移除、回收、獲取、再綁定。

而在實際中,數據每每只改變一小部分,例如某幾項數據更新了,某幾項數據刪除了等等。這個時候咱們但願只刷新受影響的子View便可,而不是指望全部子View都刷新。

Adapter提供了不少局部刷新的方法,例如notifyItemChanged()用來處理數據更新,notifyItemInserted()處理數據增長,notifyItemRemoved()處理數據移除,notifyItemMove()處理數據移動,而且還提供了相應的範圍操做的方法notifyRangXXX()。這樣咱們就沒必要無腦使用notifyDataSetChanged()方法,可是須要咱們本身比較數據,而後決定調用那種佈局刷新的方法。

局部刷新

全局刷新影響繪製性能,那麼咱們來看看局部刷新是如何優化繪製性能的。

咱們挑選Adapter#notifyItemChanged()方法來分析,它會調用觀察者的onItemRangeChanged()方法

private class RecyclerViewDataObserver extends AdapterDataObserver {
    public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
        // 1. 通知AdapterHelper,某個範圍內數據有更新
        if (mAdapterHelper.onItemRangeChanged(positionStart, itemCount, payload)) {
            // 2. 觸發更新
            triggerUpdateProcessor();
        }
    }
}
複製代碼

首先會通知AdapterHelper,某個範圍的數據有改變

boolean onItemRangeChanged(int positionStart, int itemCount, Object payload) {
    if (itemCount < 1) {
        return false;
    }
    // 保存UPDATE操做
    mPendingUpdates.add(obtainUpdateOp(UpdateOp.UPDATE, positionStart, itemCount, payload));
    // 保存操做類型
    mExistingUpdateTypes |= UpdateOp.UPDATE;
    // 當只有一個待處理的更新操做時,表示須要當即處理
    return mPendingUpdates.size() == 1;
}
複製代碼

AdapterHelper會建立一個相應操做類型的UpdateOp對象保存,而後也會保存這次操做的類型。

從返回值能夠看出,若是等待更新的操做只有一個,就表明須要理解處理。咱們假設如今只有一個更新操做,那麼會調用triggerUpdateProcessor()來處理

void triggerUpdateProcessor() {
    // POST_UPDATES_ON_ANIMATION在sdk大於16的時候爲true
    // mHasFixedSize表示是否有固定尺寸
    // mIsAttached表示是否RecyclerView是否添加到Window中
    if (POST_UPDATES_ON_ANIMATION && mHasFixedSize && mIsAttached) {
        ViewCompat.postOnAnimation(RecyclerView.this, mUpdateChildViewsRunnable);
    } else {
        mAdapterUpdateDuringMeasure = true;
        requestLayout();
    }
}
複製代碼

觸發更新操做受條件限制,這裏的限制條件基本上只有mHasFixedSize,它是經過RecyclerView#setHasFixedSize()設置的。若是RecyclerView的寬高設置爲固定尺寸,例如100dp,或者match_parent,那麼能夠調用setHasFixedSize(true)設置RecyclerView有固定寬高。這樣能夠在某些時候避免RecyclerView自我測量這一步。

可是不管使用哪一種方式執行更新操做,都會經歷layout過程。在dispatchLayoutStep1()中會調用processAdapterUpdatesAndSetAnimationFlags()處理這些更新操做,而且決定是否執行動畫。

本文不分析動畫部分的源碼。

private void processAdapterUpdatesAndSetAnimationFlags() {
    // LinearLayoutManager若是不是在狀態恢復中,是支持可預測動畫特性的
    if (predictiveItemAnimationsEnabled()) {
        mAdapterHelper.preProcess();
    } else {
        mAdapterHelper.consumeUpdatesInOnePass();
    }
    // 省略動畫相關的代碼...
}
複製代碼

這裏又根據LayoutManager是否支持Predictive item animations,份內了兩種處理方式,可是它們異曲同工,最終它們都會處理受影響的子View。

Predictive item animations: 對於添加,移除,移動操做(不包括改變操做),會自動建立一個動畫,這個動畫會顯示View從哪裏來,到哪裏去。

對於數據改變操做,processAdapterUpdatesAndSetAnimationFlags()最終會經過AdapterHelper的以下代碼來處理受影響的子View

mCallback.markViewHoldersUpdated(op.positionStart, op.itemCount, op.payload);
複製代碼

這個mCallback是在RecyclerView中實現的

void initAdapterManager() {
    mAdapterHelper = new AdapterHelper(new AdapterHelper.Callback() {
        public void markViewHoldersUpdated(int positionStart, int itemCount, Object payload) {
            // 處理範圍數據改變操做
            viewRangeUpdate(positionStart, itemCount, payload);
            // 表示是數據改變操做
            mItemsChanged = true;
        }
    }
}
複製代碼

viewRangeUpdate()處理了範圍數據改變的操做,比較簡單,總結以下

  1. RecyclerView處於數據改變範圍內的子View,被標記爲FLAG_UPDATE,而且標記它的ItemDecoration區域爲dirty
  2. mCachedViews中緩存的,且處於數據改變範圍內View,被標記爲FLAG_UPDATE,而且被RecyclerPool回收。

如今,dispatchLayoutStep1()已經把受數據改變影響的View(包括mCachedView緩存的)所有標記爲FLAG_UPDATE,而後在dispatchLayoutStep2()中爲子View進行從新佈局,它是由LayoutManager#onLayoutChildren()實現的,咱們這裏仍然以LinearLayoutManager#onLayoutChildren()爲例進行分析。

public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
    // ...
    
    // 1. 分離/廢棄並緩存子View
    detachAndScrapAttachedViews(recycler);
    // 2. 填充子View,由layoutChunk()實現
    fill(recycler, mLayoutState, state, false);
    
    // ...
}
複製代碼

這段代碼是否是似曾相識,沒錯,咱們剛在前面分析過,只不過此次分析的狀況是局部數據改變,而非全局刷新。

緩存

首先咱們來看下如何分離/廢棄並緩存子View的。

RecyclerView#detachAndScrapAttachedViews()會遍歷全部子View,而後經過ReyclerView#scrapOrRecycleView()來處理

private void scrapOrRecycleView(Recycler recycler, int index, View view) {
    final ViewHolder viewHolder = getChildViewHolderInt(view);
    if (viewHolder.shouldIgnore()) {
        return;
    }
    // 如今的狀況是View只被標記爲FLAG_UPDATE
    if (viewHolder.isInvalid() && !viewHolder.isRemoved()
            && !mRecyclerView.mAdapter.hasStableIds()) {

    } else {
        // 1. 從RecyclerView分離子View
        detachViewAt(index);
        // 2. 緩存被分離的子View
        recycler.scrapView(view);
        // 爲動畫保存信息
        mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
    }
}
複製代碼

如今的狀況是View只被標記爲FLAG_UPDATE,這與全局刷新的狀況不同了,這裏第一步是把子View從RecyclerView中分離(detach)而不是移除(remove)。它經過RecyclerView中的以下回調實現

private void initChildrenHelper() {
        mChildHelper = new ChildHelper(new ChildHelper.Callback() {
            public void detachViewFromParent(int offset) {
                final View view = getChildAt(offset);
                if (view != null) {
                    final ViewHolder vh = getChildViewHolderInt(view);
                    if (vh != null) {
                        // 1.添加FLAG_TMP_DETACHED標誌位
                        vh.addFlags(ViewHolder.FLAG_TMP_DETACHED);
                    }
                }
                // 2.調用ViewGroup#detachViewFromParent()分離子View
                RecyclerView.this.detachViewFromParent(offset);
            }
        }
    }
複製代碼

首先把子View標記爲FLAG_TMP_DETACHED,而後分離子View。

移除(remove)和分離(detach)有何區別?

  1. 移除會致使從新佈局,也就是requestLayout()
  2. 分離只是從ViewGroup#mChildren數組中移除引用,可是必須在同一個繪製週期內,把分離的View從新附着上去或者刪除。所以並不會引起從新佈局。

如今全部的子View都已經從RecyclerView中分離了,接下來就會使用Recycler來緩存它們,調用的是RecyclerView#scrapView()方法

void scrapView(View view) {
    final ViewHolder holder = getChildViewHolderInt(view);
    // 根據View的狀態標記,用不一樣方式緩存
    if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
            || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
        holder.setScrapContainer(this, false);
        mAttachedScrap.add(holder);
    } else {
        if (mChangedScrap == null) {
            mChangedScrap = new ArrayList<ViewHolder>();
        }
        holder.setScrapContainer(this, true);
        mChangedScrap.add(holder);
    }
}
複製代碼

RecyclerView的全部子View中,對於數據改變範圍內的子View,會被標記爲FLAG_UPDATE,它會被mChangedScrap緩存,而其餘子View會被mAttachedScrap緩存。

填充

如今咱們已經瞭解了局部刷新的緩存是如何使用的,那麼如今咱們來看看LinearLayoutManager是如何實現子View填充的,它是由LinearLayoutManager#layoutChunk()實現的

void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, LayoutState layoutState, LayoutChunkResult result) {
    // ...
        
    // 1. 從Recycler中獲取View
    View view = layoutState.next(recycler);
        
    // 2. 把子View添加/附着到RecyclerView中
    addView(view);
    
    // 3. 測量子View
    measureChildWithMargins(view, 0, 0);
    
    // 4. 佈局子View
    layoutDecoratedWithMargins(view, left, top, right, bottom);
        
    // ...
}
複製代碼

又是一段熟悉的代碼,咱們仍是把目光聚焦到如何從Recycler獲取子View的過程,它是經過Recycler#tryGetViewHolderForPositionByDeadline()實現的

ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) {
    boolean fromScrapOrHiddenOrCache = false;
    ViewHolder holder = null;
            
    // 1. 首先從mChangedScrap中獲取
    // LinearLayoutManager在大部分狀況下是支持predictive item animations
    if (mState.isPreLayout()) {
        holder = getChangedScrapViewForPosition(position);
        fromScrapOrHiddenOrCache = holder != null;
    }

    // 2. 從mAttachedScrap, hidden view, mCachedViews中獲取
    if (holder == null) {
        holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
        if (holder != null) {
            if (!validateViewHolderForOffsetPosition(holder)) {
                // recycle holder (and unscrap if relevant) since it can't be used
                if (!dryRun) {
                    // we would like to recycle this but need to make sure it is not used by
                    // animation logic etc.
                    holder.addFlags(ViewHolder.FLAG_INVALID);
                    if (holder.isScrap()) {
                        removeDetachedView(holder.itemView, false);
                        holder.unScrap();
                    } else if (holder.wasReturnedFromScrap()) {
                        holder.clearReturnedFromScrapFlag();
                    }
                    recycleViewHolderInternal(holder);
                }
                holder = null;
            } else {
                fromScrapOrHiddenOrCache = true;
            }
        }
    }

    if (holder == null) {
        final int offsetPosition = mAdapterHelper.findPositionOffset(position);
        final int type = mAdapter.getItemViewType(offsetPosition);
        // 3. 經過stable id從mAttachedScrap, mCachedViews中獲取
        if (mAdapter.hasStableIds()) {
            holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                    type, dryRun);
            if (holder != null) {
                // update position
                holder.mPosition = offsetPosition;
                fromScrapOrHiddenOrCache = true;
            }
        }
        if (holder == null && mViewCacheExtension != null) {
            // 4. 從自定義緩存中獲取
            final View view = mViewCacheExtension
                    .getViewForPositionAndType(this, position, type);
            if (view != null) {
                holder = getChildViewHolder(view);
            }
        }
        if (holder == null) { // fallback to pool
            // 5. 從RecyclerPool中獲取
            holder = getRecycledViewPool().getRecycledView(type);
            if (holder != null) {
                // 重置全部的標誌位
                holder.resetInternal();
                // ...
            }
        }
        if (holder == null) {
            // 6. 利用Adapter建立
            holder = mAdapter.createViewHolder(RecyclerView.this, type);
        }
    }

    // ...

    // 7. 根據條件決定是否執行綁定操做
    boolean bound = false;
    if (mState.isPreLayout() && holder.isBound()) {
        // 若是已經綁定,就只更新predictive item animations的位置信息
        holder.mPreLayoutPosition = position;
    } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
        // 若是沒有綁定,就須要執行綁定操做
        final int offsetPosition = mAdapterHelper.findPositionOffset(position);
        bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
    }

    // 8. 校訂佈局參數,並更新它
    // ...

    return holder;
}
複製代碼

對於部分數據改變操做,被分離的子View,只被標記爲FLAG_UPDATEFLAG_TMP_DETACHED,所以能用到的獲取View的途徑只有第1步和第2步,也就是從mChangedScrapmAttachedScrap中獲取。然而,對於其餘的操做,例如添加,移除,移動,可能就會從不一樣路徑獲取View。

對於數據改變這種狀況,從mChangedScrapmAttachedScrap中獲取ViewHolder的條件基本上只要知足一個條件便可,這個條件是從ViewHolder獲取的位置要與填充的位置相等。

mChangedScrapmAttachedScrap中獲取ViewHolder後,它仍是已經綁定的狀態。可是對於數據改變受影響的子View,因爲被標記爲FALG_UPDATE,所以還須要再綁定一次數據,這樣就能夠達到數據更新的效果。而對於那些沒有受數據改變影響的子View,就不須要再綁定。

如今咱們已經從Recycler中獲取了View,而且數據改變的View已經從新綁定數據,如今須要把這些分離的子View從新附着(attach)到RecyclerView上,它是經過LayoutManager#addViewInt()實現的

private void addViewInt(View child, int index, boolean disappearing) {
    final ViewHolder holder = getChildViewHolderInt(child);
    // ...
    final LayoutParams lp = (LayoutParams) child.getLayoutParams();
    if (holder.wasReturnedFromScrap() || holder.isScrap()) {
        if (holder.isScrap()) {
            holder.unScrap();
        } else {
            holder.clearReturnedFromScrapFlag();
        }
        // 處理分離子View的狀況,它會把子View從新attach到RecyclerView中
        mChildHelper.attachViewToParent(child, index, child.getLayoutParams(), false);
        if (DISPATCH_TEMP_DETACH) {
            ViewCompat.dispatchFinishTemporaryDetach(child);
        }
    } else if (child.getParent() == mRecyclerView) { // it was not a scrap but a valid child
        // 處理子View移動的狀況
        
    } else {
        // 對於其餘的狀況,都是把子View添加到RecyclerView中
        mChildHelper.addView(child, index, false);
        lp.mInsetsDirty = true;
        if (mSmoothScroller != null && mSmoothScroller.isRunning()) {
            mSmoothScroller.onChildAttachedToWindow(child);
        }
    }
    // ...
}
複製代碼

對於由於數據改變而分離的子View,會從新附着(attach)到RecyclerView上。然而對於移動,添加操做,還會有不一樣的操做,這裏就不依依分析了。

如今被分離的子View已經從新附着到RecyclerView上,而且數據改變的部分也相應的更新了,剩下的就是繪製工做了。

小結

局部刷新,是以分離和再附着的方式處理那些不受影響的子View,而只處理受影響的子View,或從新綁定後再附着,或直接建立在添加到RecyclerView。總之,相對於全局刷新,提高了繪製性能。

DiffUtil

要使用局部刷新,就必須比較先後的數據差別,而後決定使用哪一種數據刷新方式。比較這個過程每每是複雜的,因此後來Google又推出了一個工具類DiffUtil,它把這個比較過程抽象出來,經過它能夠計算先後數據差別,而後精準的調用局部刷新的方法。

若是以爲個人文章寫的還不錯,點個贊,關注我。

相關文章
相關標籤/搜索