美文网首页Android技术知识
Android中 RecyclerView的回收复用机制

Android中 RecyclerView的回收复用机制

作者: 搬砖小老弟 | 来源:发表于2022-05-11 14:41 被阅读0次

    作者:金大人的梦
    转载地址:https://juejin.cn/post/7094497660983312397

    问题归类:

    • 什么是回收?什么是复用?
    • 回收什么?复用什么?
    • 回收到哪里去?从哪里获得复用?
    • 什么时候回收?什么时候复用?

    带着以上几个问题来分析源码,当以上问题都能解释清楚的时候,对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>, RecycledViewPool 会根据ViewType来进行分类,ArrayList对应的就是一个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最多存储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 进行保存。

    除了MOVE事件会进行缓存以外,还有一个动作也会进行缓存,那就是在刚开始布局调用onLayout的时候。

    RecyclerView.onLayout --> dispatchLayout --》 dispatchLayoutStep2 --》 onLayoutChildren 到了这一步,就跟上面的分析一模一样了。

    相关文章

      网友评论

        本文标题:Android中 RecyclerView的回收复用机制

        本文链接:https://www.haomeiwen.com/subject/zlbdurtx.html