1. 程式人生 > 實用技巧 >Android原始碼解讀——RecyclerView回收複用機制

Android原始碼解讀——RecyclerView回收複用機制

問題歸類:

  • 什麼是回收?什麼是複用?
  • 回收什麼?複用什麼?
  • 回收到哪裡去?從哪裡獲得複用?
  • 什麼時候回收?什麼時候複用?

帶著以上幾個問題來分析原始碼,當以上問題都能解釋清楚的時候,對RecyclerView回收複用機制的瞭解也算是完成了。

1、什麼是回收?什麼是複用?

回收:即快取,RecyclerView的快取是將內容存到集合裡面。

複用:即取快取,從集合中去獲取。

2、回收什麼?複用什麼?

回收和複用的物件都是ViewHolder。

什麼是ViewHolder?ViewHolder其實就是用來包裝view的,我們可以將它看成列表的itemview

3、回收到哪裡去?從哪裡獲得複用?

4、什麼時候回收?什麼時候複用?

問題3、4結合原始碼一起分析。

首先對RecyclerView進行普通的使用

public class TestRvAdapter extends RecyclerView.Adapter<TestRvAdapter.ViewHolder> {

    private Context context;
    private List<Star> starList;
    private static final String TAG = "TestRvAdapter";

    public TestRvAdapter(Context context, List<Star> starList) {
        
this.context = context; this.starList = starList; } @NonNull @Override public TestRvAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View view = LayoutInflater.from(context).inflate(R.layout.rv_top_item, null); Log.e(TAG, "onCreateViewHolder: " + getItemCount());
return new ViewHolder(view); } @Override public void onBindViewHolder(@NonNull TestRvAdapter.ViewHolder holder, int position) { holder.tv.setText(starList.get(position).getName()); Log.e(TAG, "onBindViewHolder: " + position); } @Override public int getItemCount() { return starList == null ? 0 : starList.size(); } public class ViewHolder extends RecyclerView.ViewHolder { private TextView tv; public ViewHolder(@NonNull View itemView) { super(itemView); tv = itemView.findViewById(R.id.tv_star); } } }

public class RvTestActivity extends AppCompatActivity {
    private RecyclerView recyclerView;
    private List<Star> starList = new ArrayList<>();

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test_rv);

        initData();

        recyclerView = findViewById(R.id.test_rv);
        /*注意此處*/
        recyclerView.setLayoutManager(new GridLayoutManager(this, 1));
        recyclerView.addItemDecoration(new DividerItemDecoration(this, LinearLayout.VERTICAL));
        recyclerView.setAdapter(new TestRvAdapter(this, starList));

    }

    private void initData() {
        for (int i = 1; i <= 1000; i++) {
            starList.add(new Star(i + "", "快樂家族" + i));
        }
    }
}

以上程式碼相信都看得懂吧?我們看下程式碼細節。

在adapter的onCreateViewHolder和onBindViewHolder兩個方法裡面進行列印,然後在activity裡面我們設定LayoutManager時,用的是GridLayoutManager,數量設定為1,其實跟LinearLayoutManager是一樣的效果,但是這裡為了方便測試,所以用的是GridLayoutManager,進入到GridLayoutManager原始碼也可以看到,他是繼承自LinearLayoutManager的。 OK現在執行看看列印情況。

可以發現,剛進入介面的時候,onCreateViewHolder和onBindViewHolder都進行了列印,而往下滑動之後只會列印onBindViewHolder,不會再進入onCreateViewHolder

我們把程式碼new GridLayoutManager(this, 1) 的1改成8試試會是怎麼樣的列印情況:

可以看到,無論滾動到哪個item,onCreateViewHolder和onBindViewHolder都一直在列印,那麼這是為什麼呢?

接下來分析原始碼。

首先就RecyclerView 的原始碼就有一萬多行,這還沒包括layoutmanager的,分析到底從何入手?那麼我們知道,在觸發onCreateViewHolder和onBindViewHolder的時候,我們都對螢幕進行了滑動,所以我們直接先進入RecyclerView 的onTouchEvent看看RecyclerView 是如何進行滑動處理的。由於是滑動,因此我們直接進入action_move裡面進行檢視。(由於原始碼太多,就不貼上來了,原始碼流程只需要關係入口以及我們需要分析的部分就行了。)

入口:滑動 Move 事件 --> scrollByInternal --> scrollStep --> mLayout.scrollVerticallyBy(mLayout就是LayoutManager,所以我們要看他的實現類LinearLayoutManager.scrollVerticallyBy) --> scrollBy --> fill --> layoutChunk --> layoutState.next獲取view --> addView(view);

view就是從這裡載入進RecyclerView 的,那麼在addView之前有一個獲取view的動作layoutState.next,我們具體分析一下到底是如何獲取的。

layoutState.next --> getViewForPosition --> tryGetViewHolderForPositionByDeadline

tryGetViewHolderForPositionByDeadline就是我們最終需要找到的一個方法,在這個方法裡面,RecyclerView 通過快取取出viewHolder,我們可以看到裡面有各種if (holder == null) 的判斷,那麼到底是如何去取的呢?

分以下幾種情況去獲取ViewHolder

  1. getChangedScrapViewForPosition -- mChangeScrap 與動畫相關
  2. getScrapOrHiddenOrCachedHolderForPosition -- mAttachedScrap 、mCachedViews
  3. getScrapOrCachedViewForId -- mAttachedScrap 、mCachedViews (ViewType,itemid)
  4. mViewCacheExtension.getViewForPositionAndType -- 自定義快取
  5. getRecycledViewPool().getRecycledView -- 從緩衝池裡面獲取

歸納一下我們可以得出,RecyclerView 大致分為四級快取

  • mChangeScrap與 mAttachedScrap,用來快取還在螢幕內的 ViewHolder
  • mCachedViews,用來快取移除螢幕之外的 ViewHolder
  • mViewCacheExtension,開發給使用者的自定義擴充套件快取,需要使用者自己管理 View 的建立和快取(通常用不到,至少目前為止我沒用到過)
  • RecycledViewPool,ViewHolder 快取池

多級快取的最終目的就是為了提升效能

看到原始碼最後一個if語句,也就是當所有的快取都沒有viewHolder的時候,這個時候我們就需要建立,

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

createViewHolder方法裡面自然的就呼叫到了adapter的onCreateViewHolder方法

也就是:當沒有快取的時候: mAdapter.createViewHolder --> onCreateViewHolder

    建立ViewHolder 後 繫結: tryBindViewHolderByDeadline--> mAdapter.bindViewHolder--> onBindViewHolder

以上為複用(取快取)的過程,那麼回收(存快取)是個什麼樣的機制呢?

首先也是一樣要找到入口

當我們重新整理佈局的時候,RecyclerView 會呼叫到 LinearLayoutManager 的onLayoutChildren 方法。

LinearLayoutManager.onLayoutChildren --> detachAndScrapAttachedViews --> scrapOrRecycleView

    private void scrapOrRecycleView(Recycler recycler, int index, View view) {
            final ViewHolder viewHolder = getChildViewHolderInt(view);
            if (viewHolder.shouldIgnore()) {
                if (DEBUG) {
                    Log.d(TAG, "ignoring view " + viewHolder);
                }
                return;
            }
            if (viewHolder.isInvalid() && !viewHolder.isRemoved()
                    && !mRecyclerView.mAdapter.hasStableIds()) {
                removeViewAt(index);
                recycler.recycleViewHolderInternal(viewHolder);
            } else {
                detachViewAt(index);
                recycler.scrapView(view);
                mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
            }
        }

那麼在scrapOrRecycleView 方法裡面分為了兩種情況:

  • recycler.recycleViewHolderInternal(viewHolder);
  • recycler.scrapView(view);

分析recycler.recycleViewHolderInternal(viewHolder);

進入到recycleViewHolderInternal方法,可以看到有如下的一個判斷:

if (mViewCacheMax > 0
                        && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
                        | ViewHolder.FLAG_REMOVED
                        | ViewHolder.FLAG_UPDATE
                        | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
                    // Retire oldest cached view
                    int cachedViewSize = mCachedViews.size();
                    if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
                        recycleCachedViewAt(0);
                        cachedViewSize--;
                    }

                    int targetCacheIndex = cachedViewSize;
                    if (ALLOW_THREAD_GAP_WORK
                            && cachedViewSize > 0
                            && !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {
                        // when adding the view, skip past most recently prefetched views
                        int cacheIndex = cachedViewSize - 1;
                        while (cacheIndex >= 0) {
                            int cachedPos = mCachedViews.get(cacheIndex).mPosition;
                            if (!mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) {
                                break;
                            }
                            cacheIndex--;
                        }
                        targetCacheIndex = cacheIndex + 1;
                    }
                    mCachedViews.add(targetCacheIndex, holder);
                    cached = true;
                }

這段程式碼表示:如果ViewHolder不改變(有時候一個列表可能用到了多個ViewHolder),那麼先判斷mCachedViews的大小

       mCachedViews.size 大於預設可快取的大小(預設為2),執行recycleCachedViewAt,實際上就是將cacheView裡面的資料拿到RecycledViewPool快取池裡面,然後再把新的快取存入到cacheView裡面,採取的是先進先出的原則。

       快取池裡面儲存的只是 ViewHolder 型別,沒有資料,而cacheView是包含資料的(經過binder了的ViewHolder)。這也是為什麼分級的原因,最終還是為了執行效率。

而緩衝池是與Map類似。他的快取形式就如:ArrayList<ArrayList<viewholder>>,RecycledViewPool會根據ViewType來進行分類,ArrayList<viewholder>對應的就是一個ViewType

我們進入recycleCachedViewAt-->addViewHolderToRecycledViewPool(快取到快取池裡面)

如果上面的條件不滿足,那麼會執行到下面的if語句塊,直接快取到快取池:

if (!cached) {
    addViewHolderToRecycledViewPool(holder, true);
    recycled = true;
}

直接執行addViewHolderToRecycledViewPool,可以看到與上面呼叫的方法一樣,在這個方法裡面會執行getRecycledViewPool().putRecycledView(holder)

    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; } if (DEBUG && scrapHeap.contains(scrap)) { throw new IllegalArgumentException("this scrap item already exists"); } scrap.resetInternal(); scrapHeap.add(scrap); }

通過上面原始碼可以得出,每個ArrayList<viewholder>最多儲存5個ViewHolder,多餘的會直接丟棄不儲存。

為什麼資料滿了之後,會直接丟棄呢?接著分析:

假如沒有滿的話,也就是沒有多餘的話,那麼就會執行scrap.resetInternal();

        void resetInternal() {
            mFlags = 0;
            mPosition = NO_POSITION;
            mOldPosition = NO_POSITION;
            mItemId = NO_ID;
            mPreLayoutPosition = NO_POSITION;
            mIsRecyclableCount = 0;
            mShadowedHolder = null;
            mShadowingHolder = null;
            clearPayload();
            mWasImportantForAccessibilityBeforeHidden = ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO;
            mPendingAccessibilityState = PENDING_ACCESSIBILITY_STATE_NOT_SET;
            clearNestedRecyclerViewIfNotNested(this);
        }        

這裡面是將ViewHolder進行清空,然後再通過scrapHeap.add(scrap); 進行儲存,這也是為什麼緩衝池裡面只是 ViewHolder型別,而沒有資料的原因,沒有資料的話,我們快取太多沒有意義。

這也是為什麼前面我們執行的時候,當值=8的時候,會不停的重新整理onCreateViewHolder和onBindViewHolder,而為1的時候只重新整理onBindViewHolder,因為其預設上限值為2+5=7,超過這個數,多餘的就沒進入快取了。

以上分析完畢之後我們知道了,下面兩種情況是如何快取的了

而第三種是系統不管的,也就是我們自定義的快取

分析程式碼:recycler.scrapView(view),mAttachedScrap 和 mChangedScrap 這種情況

    void scrapView(View view) {
            final ViewHolder holder = getChildViewHolderInt(view);
            if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
                    || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
                if (holder.isInvalid() && !holder.isRemoved() && !mAdapter.hasStableIds()) {
                    throw new IllegalArgumentException("Called scrap view with an invalid view."
                            + " Invalid views cannot be reused from scrap, they should rebound from"
                            + " recycler pool." + exceptionLabel());
                }
                holder.setScrapContainer(this, false);
                mAttachedScrap.add(holder);
            } else {
                if (mChangedScrap == null) {
                    mChangedScrap = new ArrayList<ViewHolder>();
                }
                holder.setScrapContainer(this, true);
                mChangedScrap.add(holder);
            }
        }

我們發現,該方法一進入的時候就直接在處理了

mAttachedScrap.add(holder);
mChangedScrap.add(holder);

只是需要清楚上面的判斷是如何判斷的,其意思就是 if 的情況 ,當我們的標記沒移除、或者失效、更新、動畫不復用或者沒動畫的時候,就會利用mAttachedScrap 進行儲存,否則就用mChangedScrap 進行儲存。

完。