引言:iOS和Android各有自己的列表組件。衆所周知,列表組件一直都是移動端各個端中,組件重用、內存優化的重點。今天就來分析下iOS和Android各自的重用機制。
Android:RecyclerView的緩存機制
先來熟悉下ViewHolder的幾個狀態
- isInvalid:表示當前
ViewHolder
是否已經失效。通常來說,在3種情況下會出現這種情況:1.調用了Adapter
的notifyDataSetChanged
方法;2. 手動調用RecyclerView
的invalidateItemDecorations
方法;3. 調用RecyclerView
的setAdapter
方法或者swapAdapter
方法。- isRemoved:表示當前的
ViewHolder
是否被移除。通常來說,數據源被移除了部分數據,然後調用Adapter
的notifyItemRemoved
方法。- isBound:表示當前
ViewHolder
是否已經調用了onBindViewHolder
。- isTmpDetached:表示當前的
ItemView
是否從RecyclerView
(即父View
)detach
掉。通常來說有兩種情況下會出現這種情況:1.手動了RecyclerView
的detachView
相關方法;2. 在從mHideViews
裏面獲取ViewHolder
,會先detach
掉這個ViewHolder
關聯的ItemView
。- isScrap:表示是否在
mAttachedScrap
或者mChangedScrap
數組裏面,進而表示當前ViewHolder
是否被廢棄。- isUpdated:表示當前
ViewHolder
是否已經更新。通常來說,在3種情況下會出現情況:1.isInvalid
方法存在的三種情況;2.調用了Adapter
的onBindViewHolder
方法;3. 調用了Adapter
的notifyItemChanged
方法
mAttachedScrap、mChangedScrap
mAttachedScrap
存儲的是當前還在屏幕中的ViewHolder
,用不是人話來描述,就是是從屏幕上分離出來,但是又即將添加到屏幕上去的ViewHolder(什麼鬼?)。
打個比方,RecyclerView
上下滑動,此時會重新調用LayoutManager
的onLayoutChildren
方法,屏幕上所有的ViewHolder
先scrap
掉,添加到mAttachedScrap
裏面去,然後在重新佈局每個ItemView
時,會從優先mAttachedScrap
裏面獲取。而mChangedScrap
存儲的是數據被更新的ViewHolder,
比如說調用了Adapter
的notifyItemChanged
方法。個人理解,這兩個緩存個雖然也算是緩存重用,但和我們平時接觸的有區別(這話講的有點扭捏),它並不是傳統的刪除對象後緩存起來待到需要新建對象時複用的機制。
我們通過RecyclerView的源碼來了解一下這兩個緩存機制的區別
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);
}
}
這個方法簡單來說就是通過holder的tag來區分應該放入mAttachedScrap還是mChangedScrap。
當holder滿足以下幾個條件之一,會被放入mAttachedScrap
- 被同時標記爲
remove
和invalid
- 完全沒有被改變
- canReuseUpdatedViewHolder方法的返回值爲true 。canReuseUpdatedViewHolder方法嵌套了好幾層函數調用,這裏不展開,簡單來說holder的mItemAnimator爲空,或者mItemAnimator的
canReuseUpdatedViewHolder
方法爲true
當這些條件都不滿足時,或者說holder的isUpdated
方法返回爲true時(即調用Adapter
的notifyItemChanged
方法時),會放入到mChangedScrap
裏面去
mCachedViews
可以理解爲RecyclerView的一級緩存,默認大小爲2,也就是緩存2個ViewHolder。
有兩點需要注意:
- mCachedViews只能複用同一位置的ViewHolder,什麼概念呢?比如屏幕上有3個ViewHolder,此時向下滑動,出現了第4個ViewHolder,而第1個ViewHolder被移出了屏幕。然後再向上滑動,第1個ViewHolder重新回到屏幕內,第1個ViewHolder會被mCachedViews中取出並複用。而如果你在第1個ViewHolder被移除屏幕後,繼續向下滑動,出現第5個ViewHolder,新出現的ViewHolder並不會從mCachedViews中複用,因爲位置不同。
- mCachedViews中複用的ViewHolder中的數據不會被清除,複用時不會重新跑onBindViewHolder方法(因爲位置相同,可以理解爲是同一個,所以也不需要刷新數據)。
來看下源碼:
void recycleViewHolderInternal(ViewHolder holder) {
//方法有點長,前面的掠過,看核心部分
if (forceRecycle || holder.isRecyclable()) {
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;
}
if (!cached) {
addViewHolderToRecycledViewPool(holder, true);
recycled = true;
}
}
注意第二行的判斷條件,前面一段分析了holder在什麼tag情況下會被加入到mAttachedScrap、mChangedScrap。很明顯,這裏明確定義了holder的tag不符合加入mAttachedScrap、mChangedScrap的條件的情況下,纔會被加入到mCachedViews
mRecyclerPool
RecyclerView的二級緩存,根據不同的 item type 創建不同的 List,每個 List 默認大小爲5個(也就是複用5個ViewHolder)。不同於mCachedViews,mRecyclerPool沒有位置要求,只有type要求。但是複用的ViewHolder中的數據會被清除,因此複用時,會重跑onBindViewHolder方法。
注意剛纔貼出的源碼中,有這麼一段:
if (!cached) {
addViewHolderToRecycledViewPool(holder, true);
recycled = true;
}
可以理解爲,如果不滿足加入mAttachedScrap、mChangedScrap的條件,而又沒有加入到mCachedViews的情況下,holder啓動加入到mRecyclerPool的流程。
再來看一下addViewHolderToRecycledViewPool的內容
void addViewHolderToRecycledViewPool(@NonNull ViewHolder holder, boolean dispatchRecycled) {
clearNestedRecyclerViewIfNotNested(holder);
if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_SET_A11Y_ITEM_DELEGATE)) {
holder.setFlags(0, ViewHolder.FLAG_SET_A11Y_ITEM_DELEGATE);
ViewCompat.setAccessibilityDelegate(holder.itemView, null);
}
if (dispatchRecycled) {
dispatchViewRecycled(holder);
}
holder.mOwnerRecyclerView = null;
getRecycledViewPool().putRecycledView(holder);
}
public void putRecycledView(ViewHolder scrap) {
final int viewType = scrap.getItemViewType();
final ArrayList<ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap;
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);
}
注意getScrapDataForType這個方法,證明了之前的說法,mRecyclerPool是基於type運作的
mViewCacheExtension
自定義緩存,通常用不到
來看複用
RecyclerView複用的核心方法是
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs)
這個方法比較長,我們只貼部分代碼出來。
首先,是關於mChangedScrap的:
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
...
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder != null;
}
...
}
其次是mAttachedScrap和mCachedViews
if (holder == null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
...
}
if (mAdapter.hasStableIds()) {
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
type, dryRun);
這兩個方法從源碼分析,都是先從其次是mAttachedScrap,然後再從mCachedViews裏複用holder。從方法名可以看出,一個是基於位置複用,另一個是基於id複用
再者是mRecyclerPool
if (holder == null) { // fallback to pool
if (DEBUG) {
Log.d(TAG, "tryGetViewHolderForPositionByDeadline("
+ position + ") fetching from shared pool");
}
holder = getRecycledViewPool().getRecycledView(type);
if (holder != null) {
holder.resetInternal();
if (FORCE_INVALIDATE_DISPLAY_LIST) {
invalidateDisplayListInt(holder);
}
}
}
最後,如果完全沒有holder可以複用,會調用mAdapter.createViewHolder來創建holder
if (holder == null) {
long start = getNanoTime();
if (deadlineNs != FOREVER_NS
&& !mRecyclerPool.willCreateInTime(type, start, deadlineNs)) {
// abort - we have a deadline we can't meet
return null;
}
holder = mAdapter.createViewHolder(RecyclerView.this, type);
iOS:UITableView
iOS的UITableView相對而言邏輯就簡單了很多,創建屏幕可顯示最大個數+1的cell,當一個cell被移動出屏幕,自動進入到緩存池。
用網上的一張圖片來描述:
機制相當簡單,就不描述了
和RecyclerView相比,UITableView有兩個最大的不同:
RecyclerView的複用機制是自動的,而UITableView是需要手動啓動的:
看代碼:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
// 0.重用標識
// 被static修飾的局部變量:只會初始化一次,在整個程序運行過程中,只有一份內存
static NSString *ID = @"cell";
// 1.先根據cell的標識去緩存池中查找可循環利用的cell
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ID];
// 2.如果cell爲nil(緩存池找不到對應的cell)
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:ID];
}
// 3.覆蓋數據
cell.textLabel.text = [NSString stringWithFormat:@"testdata - %zd", indexPath.row];
return cell;
}
注意dequeueReusableCellWithIdentifier:ID這個方法,從字面意思就能理解是從緩存池中獲取一個cell來複用。如果你在創建cell時不是調用這個方法,而是直接新建一個cell,那麼UITableView的緩存機制就沒有用了。
數據清零
UITableView無視位置,cell只要回收就會清除數據,這一點其實從代碼裏就能一目瞭然,每個cell,都需要當成新建的cell來重新賦值。