In-depth understanding of RecyclerView and actual combat (1) basic concepts

introduce

It's just a personal study note, if there is anything wrong, please leave a message


Blog recommendation

https://blog.csdn.net/zhangqilugrubby/article/details/53463875
https://www.cnblogs.com/dasusu/p/7746946.html

conceptual knowledge

RecyclerView's recycling mechanism

Recycler

Recycler is an inner class of RecyclerView. main member variable

key member variable

Member variables describe
mAttachedScrap List of ViewHolder not detached from RecyclerView
mChangedScrap A list of ViewHolders indicating that the data has changed
mCachedViews The size of the ViewHolder cache list is determined by mViewCacheMax. The default DEFAULT_CACHE_SIZE is 2, which can be dynamically set.
mViewCacheExtension A layer of cache that developers can customize is an instance of the virtual class ViewCacheExtension. Developers can implement the method getViewForPositionAndType(Recycler recycler, int position, int type) to implement their own cache.
mRecyclerPool ViewHolder cache pool, if ViewHolder cannot be stored in limited mCachedViews, ViewHolder will be stored in RecyclerViewPool.

How to take out the reused ViewHodler

key method describe
View getViewForPosition(int position) Provided to the external multiplexing mechanism interface, as to whether the returned View is reused or created, it is determined internally

The above method has been going inward to find
View getViewForPosition(int position)

ViewHolder tryGetViewHolderForPositionByDeadline(int position,
        boolean dryRun, long deadlineNs) {
    if (position < 0 || position >= mState.getItemCount()) {
        // 数组越界抛出异常,不执行以下代码
    }
    boolean fromScrapOrHiddenOrCache = false;
    ViewHolder holder = null;
    // 0) 从 mChangedScrap 查找数据已经改变的ViewHolder列表
    if (mState.isPreLayout()) {
        holder = getChangedScrapViewForPosition(position);
        ..
    }
    // 1) 从 mAttachedScrap/隐藏View(动画中)/mCachedViews 查找ViewHodler
    if (holder == null) {
        holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
        if (holder != null) {
            if (!validateViewHolderForOffsetPosition(holder)) {
                //就算 position 匹配找到了 ViewHolder,还需要判断一下这个 ViewHolder 是否已经被 remove 掉,type 类型一致不一致;
                holder = null;
            } else {
                fromScrapOrHiddenOrCache = true;
            }
        }
    }
    if (holder == null) {
        final int type = mAdapter.getItemViewType(offsetPosition);
        // 2) 这里跟 1) 做得事情一致,只是通过 id 方式
        if (mAdapter.hasStableIds()) {
            holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                    type, dryRun);
           ....
        }
        if (holder == null && mViewCacheExtension != null) {
            // 3) 开发者可自定义的一层缓存
            final View view = mViewCacheExtension
                    .getViewForPositionAndType(this, position, type);
        }
        if (holder == null) {
            // 4)  从RecyclerViewPool 里取 ViewHolder,ViewPool 会根据不同的 item type 创建不同的 List,每个 List 默认大小为5个。
            //(注意:这里是将不同type 的最后一个 返回并从缓存列表中移除)
            holder = getRecycledViewPool().getRecycledView(type);
            if (holder != null) {
                // 重置ViewHolder,这里也就说明了 为什么需要重新调用 onBindViewHolder 了
                holder.resetInternal();
                // ...
            }
        }
        if (holder == null) {
            // 5) 如果缓存中都没找到,创建一个全新的
            holder = mAdapter.createViewHolder(RecyclerView.this, type);
            ...
        }

    }

    boolean bound = false;
    if (mState.isPreLayout() && holder.isBound()) {
        // 6) 是否需要重新调用 ViewHodler
        final int offsetPosition = mAdapterHelper.findPositionOffset(position);
        bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
    }

    // // 7) 设置布局 layoutparams
    final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
    final LayoutParams rvLayoutParams;
    if (lp == null) {
        rvLayoutParams = (LayoutParams) generateDefaultLayoutParams();
        holder.itemView.setLayoutParams(rvLayoutParams);
    } else if (!checkLayoutParams(lp)) {
        rvLayoutParams = (LayoutParams) generateLayoutParams(lp);
        holder.itemView.setLayoutParams(rvLayoutParams);
    } else {
        rvLayoutParams = (LayoutParams) lp;
    }
    rvLayoutParams.mViewHolder = holder;
    rvLayoutParams.mPendingInvalidate = fromScrapOrHiddenOrCache && bound;
    return holder;
}

Recycling mechanism triggered when swiping

There are many entrances to the recycling mechanism, because Recycler has various structures, such as mAttachedScrap, mCachedViews, etc. The timing of recycling different structures is different, and there are more entrances.

When the RecyclerView slides, it will be handled by scrollVerticallyBy() of LinearLayoutManager, then LayoutManager will then call the fill() method to process the card positions that need to be reused and recycled, and finally call the recyclerView() method to start recycling.

public void recycleView(View view) {
    // This public recycle method tries to make view recycle-able since layout manager
    // intended to recycle this view (e.g. even if it is in scrap or change cache)
    ViewHolder holder = getChildViewHolderInt(view);
    if (holder.isTmpDetached()) {
        removeDetachedView(view, false);
    }
    if (holder.isScrap()) {
        holder.unScrap();
    } else if (holder.wasReturnedFromScrap()) {
        holder.clearReturnedFromScrapFlag();
    }
    // 进入里面查看
    recycleViewHolderInternal(holder);
}

The recycling process of mCachedViews

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)) {
            int cachedViewSize = mCachedViews.size();
            if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
                // 从 mCached中移除,加入到 RecyclerPool中
                recycleCachedViewAt(0);
                cachedViewSize--;
            }

            int targetCacheIndex = cachedViewSize;
            if (ALLOW_THREAD_GAP_WORK
                    && cachedViewSize > 0
                    && !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {
            ....
            // 然后讲最新的 viewholder加入到 mCacheScrap中去  
            mCachedViews.add(targetCacheIndex, holder);
            cached = true;
        }
        if (!cached) {
            // cache条件不满足 加入 RecycledViewPool 缓存中
            addViewHolderToRecycledViewPool(holder, true);
            recycled = true;
        }
    } else {
        ...
    }
    ....
}

The recycling process of RecycledViewPool

When the above code is added to the RecyclerViewPool method, enter this method

void addViewHolderToRecycledViewPool(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);
}

mMaxScrap==default 5

This also shows that the cached ViewHodler is first cached in the cachedView, and if it is not satisfied or exceeded, it is
added to the RecyclerPool

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);
}

Summarize

The overall flow chart is as follows: (can be enlarged to view)
steal a picture, and then add your own analysis
write picture description here

Multiplexed search process

write picture description here

summary of a problem

Q1: If you slide down, the display of the 5 card positions in the new row will de-multiplex the cached ViewHolder, and the 5 card positions in the first row will be moved out of the screen to be recycled. Recycle? Or recycle and reuse? Or recycle and reuse? That is to say, is it possible that the ViewHolder reused for the 5 card positions in the new row is the 5 card positions that were recycled in the first row? A: Reuse first and then recycle. The 5 card slots of a new row go to the current cache of mCachedViews and ViewPool to find reuse, if not, recreate them, and then move the 5 card slots of the row out of the screen and then recycle the cache to mCachedViews And inside the ViewPool, so the new line of 5 card positions and multiplexing cannot use the 5 card positions that have just been moved out of the screen.

Q2: During this process, when the RecyclerView slides up again to redisplay the 5 card positions in the first row, only the last 3 card positions trigger the onBindViewHolder() method to re-bind the data? Obviously 5 card slots are multiplexed.
A: The recycling and reuse structures involved in the sliding scene are mCachedViews and ViewPool. The default size of the former is 2, and the latter is 5. Therefore, when the third line is displayed, the 5 card slots in the first row are recycled. When recycling, they are first cached in mCachedViews, and when they are full, the old ones are moved out to ViewPool. All 5 card slots have 2 cached in mCachedViews. , 3 are cached in ViewPool, as for which 2 are cached in mCachedViews, this is controlled by LayoutManager. The example explained above uses GridLayoutManager, and the recycling logic during sliding is implemented in the parent class LinearLayoutManager. When recycling the first row of card positions, it is recycled from back to front, so the latest two card positions are 0 and 1. It is placed in mCachedViews, and the card positions 2, 3, and 4 are placed in ViewPool.

Therefore, when swiping up again, the five card positions in the first row will be reused in the two structures. As I said before, the ViewHolder stored in mCachedViews can only be reused in the card position in the original position, so 0 and 1 are two. Each card slot can be directly reused with ViewHolder in mCachedViews, and the ViewHolder here does not need to re-bind data. As for the 2, 3, and 4 card slots, go to ViewPool to find it. It happens that there are 3 ViewHolders cached in ViewPool, so The five card slots in the first row are all reused, and the multiplexing from ViewPool needs to rebind data, so that only three card slots need to rebind data.

Q3: Next, no matter whether it is swiping up or down, and swiping several times, there will be no more onCreateViewHolder() logs. That is to say, RecyclerView has created a total of 17 ViewHolders, but sometimes there are only 3 of 5 card slots in a row. One card slot needs to rebind data, but sometimes 5 card slots need to rebind data, why is this? A: Sometimes there are only 3 card slots in a row for the same reason as Q2, because the ViewHolder at the current position is cached in mCachedView. Of course, the ViewHolder that is originally it can be used directly. As for why 17 ViewHolders are created, it is because when the card position of the fourth row is to be displayed, there are only 3 caches in the ViewPool, and the card position of the fourth row cannot use the 2 caches in mCachedViews, because These two caches are ViewHolders of 6 and 7 card positions, so it is necessary to recreate 2 ViewHodlers for the last two card positions of the fourth row.


Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324782509&siteId=291194637