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裡面進行檢視。(由於原始碼太多,就不貼上來了,原始碼流程只需要關係入口以及我們需要分析的部分就行了。)
tryGetViewHolderForPositionByDeadline就是我們最終需要找到的一個方法,在這個方法裡面,RecyclerView 通過快取取出viewHolder,我們可以看到裡面有各種if (holder == null) 的判斷,那麼到底是如何去取的呢?
分以下幾種情況去獲取ViewHolder
- getChangedScrapViewForPosition -- mChangeScrap 與動畫相關
- getScrapOrHiddenOrCachedHolderForPosition -- mAttachedScrap 、mCachedViews
- getScrapOrCachedViewForId -- mAttachedScrap 、mCachedViews (ViewType,itemid)
- mViewCacheExtension.getViewForPositionAndType -- 自定義快取
- 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 進行儲存。