美文网首页Android技术知识Android开发经验谈Android开发
面试官常问“RecyclerView”缓存机制?看完这篇让你吊打

面试官常问“RecyclerView”缓存机制?看完这篇让你吊打

作者: 程序老秃子 | 来源:发表于2022-06-02 15:55 被阅读0次

RecyclerView 缓存机制

RecyclerView这个控件几乎所有的Android开发者都使用过(甚至不用加几乎),它是真的很好用,完美取代了ListView和GridView,而RecyclerView之所以好用,得益于它优秀的缓存机制;关于RecyclerView缓存机制,更是需要我们开发者来掌握的

一级缓存

减少绑定数据次数

● RecyclerView 在滑动过程中为了实现平滑的移动,每次移动的距离都是10以下的像素,但是不管移动了多少,只要快速的滑动,就要频繁的遍历数据,但是如果数据遍历完数据再重新bindData 就是一个非常要命的操作,一级缓存attachList 的作用就是在滑动过程保存屏幕中有效的item 放入其中,那么在滑动时从一级缓存中获取的所有item的数据都是有效的,大大的保障了RecyclerView 滑动过程中的平滑性

二级缓存

减少刚刚从相同部分滑出屏幕,再划入屏幕的item bindDate的次数

● 这个过程可以理解为,RecyclerView 向上滑动时,其中一个item 刚刚从一级缓存中被移除,此时RecyclerView 向下滑动让刚刚从一级缓存删除的item 重新显示在屏幕上,此时只需要遍历二级缓存cacheList, 这个list大小只有2个,而且遍历他的时机是有item需要被替换的情况下,所以遍历二级缓存的所消耗的代价还是相当可以的

我们可以一起分析一下一级缓存和二级缓存

● 我们先假设一个item的高度为100 而每次滑动的高度为10.

● 在RecyclerView 向上快速滑动的过程就是第一次滑动10 将所有item 缓存到一级缓存 attachlist 中,反馈滑动距离时,由于没有item 出现替换或者消失的情况,不会遍历二级缓存,直接完整遍历一级缓存即可完成本次滑动,直到 滑动距离达到100,此时出现交替的情况下,假设二级缓存中存在数据,那么则会遍历二级缓存看看其中的数据是否和我们将要显示的数据匹配,如果匹配,则不需要重新绑定数据,如果不匹配则需要重新绑定数据

三级缓存

自定义缓存

● 第三级缓存是用来给复杂的操作用来做不同的缓存策略的

四级缓存

与listView 的viewholder 的用法一致,减少View inflate 的次数

● 其实在RecyclerView 中的第四级缓存从功能上整体上和ListView 的缓存是一致的,就是为了缓存View ,防止每次加载新的item再重新inflate 控件,他比ListView 强大之处就在可以让多个RecyclerView 公用一个4级缓存池,来达到加速显示的效果

RecyclerView 的缓存机制源码

当RecyclerView绘制的时候,会走到LayoutManager里面的next()方法,在next()里面是正式开始使用缓存机制,这里以LinearLayoutManager为例子

        /**
         * Gets the view for the next element that we should layout.
         * Also updates current item index to the next item, based on {@link #mItemDirection}
         *
         * @return The next element that we should layout.
         */
        View next(RecyclerView.Recycler recycler) {
            if (mScrapList != null) {
                return nextViewFromScrapList();
            }
            final View view = recycler.getViewForPosition(mCurrentPosition);
            mCurrentPosition += mItemDirection;
            return view;
        }

在next方法里传入了Recycler对象,这个对象是RecyclerView的内部类。我们先去看一眼这个类

 public final class Recycler {
        final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
        ArrayList<ViewHolder> mChangedScrap = null;
 
        final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
 
        private final List<ViewHolder>
                mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap);
 
        private int mRequestedCacheMax = DEFAULT_CACHE_SIZE;
        int mViewCacheMax = DEFAULT_CACHE_SIZE;
 
        RecycledViewPool mRecyclerPool;
 
        private ViewCacheExtension mViewCacheExtension;
 
        static final int DEFAULT_CACHE_SIZE = 2;
}

再看一眼RecycledViewPool的源码

public static class RecycledViewPool {
        private static final int DEFAULT_MAX_SCRAP = 5;
        static class ScrapData {
            final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
            int mMaxScrap = DEFAULT_MAX_SCRAP;
            long mCreateRunningAverageNs = 0;
            long mBindRunningAverageNs = 0;
        }
        SparseArray<ScrapData> mScrap = new SparseArray<>();

其中mAttachedScrap对应Scrap;mCachedViews对应Cache;mViewCacheExtension对应ViewCacheExtension;mRecyclerPool对应RecycledViewPool

注意:mAttachedScrap、mCachedViews和RecycledViewPool里面的mScrapHeap都是ArrayList,缓存被加入到这三个对象里面实际上就是调用的ArrayList.add()方法;复用缓存呢,这里要注意一下不是调用的ArrayList.get()而是ArrayList.remove(),其实这里也很好理解,因为当缓存数据被取出来展示到了屏幕内,自然就应该被移除

我们现在回到刚才的next()方法里,recycler.getViewForPosition(mCurrentPosition); 直接去看getViewForPosition这个方法,接着跟到了这里

 View getViewForPosition(int position, boolean dryRun) {
            return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
        }

接着跟进去

  ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                boolean dryRun, long deadlineNs) {
            if (position < 0 || position >= mState.getItemCount()) {
                throw new IndexOutOfBoundsException("Invalid item position " + position
                        + "(" + position + "). Item count:" + mState.getItemCount()
                        + exceptionLabel());
            }
            boolean fromScrapOrHiddenOrCache = false;
            ViewHolder holder = null;
            // 0) If there is a changed scrap, try to find from there
            if (mState.isPreLayout()) {
                holder = getChangedScrapViewForPosition(position);
                fromScrapOrHiddenOrCache = holder != null;
            }
            // 1) Find by position from scrap/hidden list/cache
            if (holder == null) {
                holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
             
            }
            if (holder == null) {
                final int type = mAdapter.getItemViewType(offsetPosition);
                // 2) Find from scrap/cache via stable ids, if exists
                if (mAdapter.hasStableIds()) {
                    holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                            type, dryRun);
                    if (holder != null) {
                        // update position
                        holder.mPosition = offsetPosition;
                        fromScrapOrHiddenOrCache = true;
                    }
                }
                if (holder == null && mViewCacheExtension != null) {
                    // We are NOT sending the offsetPosition because LayoutManager does not
                    // know it.
                    final View view = mViewCacheExtension
                            .getViewForPositionAndType(this, position, type);
                    if (view != null) {
                        holder = getChildViewHolder(view);
                      
                    }
                }
                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);
                        }
                    }
                }
                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);
                    if (ALLOW_THREAD_GAP_WORK) {
                        // only bother finding nested RV if prefetching
                        RecyclerView innerView = findNestedRecyclerView(holder.itemView);
                        if (innerView != null) {
                            holder.mNestedRecyclerView = new WeakReference<>(innerView);
                        }
                    }
                }
            } 
 
            boolean bound = false;
            if (mState.isPreLayout() && holder.isBound()) {
                // do not update unless we absolutely have to.
                holder.mPreLayoutPosition = position;
            } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
                if (DEBUG && holder.isRemoved()) {
                    throw new IllegalStateException("Removed holder should be bound and it should"
                            + " come here only in pre-layout. Holder: " + holder
                            + exceptionLabel());
                }
                final int offsetPosition = mAdapterHelper.findPositionOffset(position);
                bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
            }  
            return holder;
        }

终于到了缓存机制最核心的地方,为了方便大家阅读,我对这部分源码进行了删减,直接从官方给的注释里面看

// (0) If there is a changed scrap, try to find from there
            if (mState.isPreLayout()) {
                holder = getChangedScrapViewForPosition(position);
                fromScrapOrHiddenOrCache = holder != null;
            }

这里面只有设置动画以后才会为true

 // 1) Find by position from scrap/hidden list/cache
            if (holder == null) {
                holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
            }

这里就开始拿第一级和第二级缓存了getScrapOrHiddenOrCachedHolderForPosition()这个方法可以深入去看以下,注意这里传的参数是position(dryRun这个参数不用管),就跟我之前说的,Scrap和Cache是根据position拿到缓存

if (holder == null && mViewCacheExtension != null) {
                    // We are NOT sending the offsetPosition because LayoutManager does not
                    // know it.
                    final View view = mViewCacheExtension
                            .getViewForPositionAndType(this, position, type);
                    if (view != null) {
                        holder = getChildViewHolder(view);
                      
                    }
                }

这里开始拿第三级缓存了,这里我们不自定义ViewCacheExtension就不会进入判断条件

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

这里到了第四级缓存RecycledViewPool,getRecycledViewPool().getRecycledView(type);通过type拿到ViewHolder,接着holder.resetInternal();重置ViewHolder,让其变成一个全新的ViewHolder

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);
                    if (ALLOW_THREAD_GAP_WORK) {
                        // only bother finding nested RV if prefetching
                        RecyclerView innerView = findNestedRecyclerView(holder.itemView);
                        if (innerView != null) {
                            holder.mNestedRecyclerView = new WeakReference<>(innerView);
                        }
                    }
                }

到这里如果ViewHolder还为null的话,就会create view了,创建一个新的ViewHolder

boolean bound = false;
            if (mState.isPreLayout() && holder.isBound()) {
                // do not update unless we absolutely have to.
                holder.mPreLayoutPosition = position;
            } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
                if (DEBUG && holder.isRemoved()) {
                    throw new IllegalStateException("Removed holder should be bound and it should"
                            + " come here only in pre-layout. Holder: " + holder
                            + exceptionLabel());
                }
                final int offsetPosition = mAdapterHelper.findPositionOffset(position);
                bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
            }  

这里else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid())是判断这个ViewHolder是不是有效的,也就是可不可以复用,如果不可以复用就会进入tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);这个方法,在这里面调用了bindViewHolder()方法

点进去看一眼

 private boolean tryBindViewHolderByDeadline(@NonNull ViewHolder holder, int offsetPosition,
                int position, long deadlineNs) {
            ....................
            mAdapter.bindViewHolder(holder, offsetPosition);
            ....................
            return true;
        }

再点进去就到了我们熟悉的onBindViewHolder()

  public final void bindViewHolder(@NonNull VH holder, int position) {
            .......................
            onBindViewHolder(holder, position, holder.getUnmodifiedPayloads());
           .........................
        }

至此,缓存机制的整体流程就全部分析完毕了

小结

ListView有两级缓存,分别是Active View和Scrap View,缓存的对象是ItemView;而RecyclerView有四级缓存,分别是Scrap、Cache、ViewCacheExtension和RecycledViewPool,缓存的对象是ViewHolder

Scrap和Cache分别是通过position去找ViewHolder可以直接复用;ViewCacheExtension自定义缓存,目前来说应用场景比较少却需慎用;RecycledViewPool通过type来获取ViewHolder,获取的ViewHolder是个全新,需要重新绑定数据;当你看到这里的时候,RecyclerView的性能比ListView优化在哪里,我想你已经有答案

尾述

点击 底层源码 即可 免费获取 完整代码 以及 更多学习笔记 面试视频

技术是无止境的,你需要对自己提交的每一行代码、使用的每一个工具负责,不断挖掘其底层原理,才能使自己的技术升华到更高的层面

Android 架构师之路还很漫长,与君共勉

PS:有问题欢迎指正,可以在评论区留下你的建议和感受;
欢迎大家点赞评论,觉得内容可以的话,可以转发分享一下

相关文章

网友评论

    本文标题:面试官常问“RecyclerView”缓存机制?看完这篇让你吊打

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