RecyclerView

作者: Johnson_Coding | 来源:发表于2021-03-09 16:57 被阅读0次
    RecyclerView为什么强制我们实现ViewHolder模式?

    关于这个问题,我们首先看一下ListView。ListView是不强制我们实现ViewHolder的,但是后来google建议我们实现ViewHolder模式。我们先分别看一下这两种不同的方式。

    image image

    其实这里我已经用红框标出来了,ListView使用ViewHolder的好处就在于可以避免每次getView都进行findViewById()操作,因为findViewById()利用的是DFS算法(深度优化搜索),是非常耗性能的。而对于RecyclerView来说,强制实现ViewHolder的其中一个原因就是避免多次进行findViewById()的处理,另一个原因就是因为ItemView和ViewHolder的关系是一对一,也就是说一个ViewHolder对应一个ItemView。这个ViewHolder当中持有对应的ItemView的所有信息,比如说:position;view;width等等,拿到了ViewHolder基本就拿到了ItemView的所有信息,而ViewHolder使用起来相比itemView更加方便。RecyclerView缓存机制缓存的就是ViewHolder(ListView缓存的是ItemView),这也是为什么RecyclerView为什么强制我们实现ViewHolder的原因。

    Listview的缓存机制

    在正式讲RecyclerView的缓存机制之前还需要提一嘴ListView的缓存机制,不多BB,先上图

    image

    ListView的缓存有两级,在ListView里面有一个内部类 RecycleBin,RecycleBin有两个对象Active View和Scrap View来管理缓存,Active View是第一级,Scrap View是第二级。

    • Active View:是缓存在屏幕内的ItemView,当列表数据发生变化时,屏幕内的数据可以直接拿来复用,无须进行数据绑定。

    • Scrap view:缓存屏幕外的ItemView,这里所有的缓存的数据都是"脏的",也就是数据需要重新绑定,也就是说屏幕外的所有数据在进入屏幕的时候都要走一遍getView()方法。
      再来一张图,看看ListView的缓存流程

      image

      当Active View和Scrap View中都没有缓存的时候就会直接create view。

    小结

    ListView的缓存机制相对比较好理解,它只有两级缓存,一级缓存Active View是负责屏幕内的ItemView快速复用,而Scrap View是缓存屏幕外的数据,当该数据从屏幕外滑动到屏幕内的时候需要走一遍getView()方法。

    RecyclerView的缓存机制

    先上图

    image

    RecyclerView的缓存分为四级

    • Scrap
    • Cache
    • ViewCacheExtension
    • RecycledViewPool

    Scrap对应ListView 的Active View,就是屏幕内的缓存数据,就是相当于换了个名字,可以直接拿来复用。
    Cache 刚刚移出屏幕的缓存数据,默认大小是2个,当其容量被充满同时又有新的数据添加的时候,会根据FIFO原则,把优先进入的缓存数据移出并放到下一级缓存中,然后再把新的数据添加进来。Cache里面的数据是干净的,也就是携带了原来的ViewHolder的所有数据信息,数据可以直接来拿来复用。需要注意的是,cache是根据position来寻找数据的,这个postion是根据第一个或者最后一个可见的item的position以及用户操作行为(上拉还是下拉)。
    举个栗子:当前屏幕内第一个可见的item的position是1,用户进行了一个下拉操作,那么当前预测的position就相当于(1-1=0),也就是position=0的那个item要被拉回到屏幕,此时RecyclerView就从Cache里面找position=0的数据,如果找到了就直接拿来复用。
    ViewCacheExtension是google留给开发者自己来自定义缓存的,这个ViewCacheExtension我个人建议还是要慎用,因为我扒拉扒拉网上其他的博客,没有找到对应的使用场景,而且这个类的api设计的也有些奇怪,只有一个public abstract View getViewForPositionAndType(@NonNull Recycler recycler, int position, int type);让开发者重写通过position和type拿到ViewHolder的方法,却没有提供如何产生ViewHolder或者管理ViewHolder的方法,给人一种只出不进的赶脚,还是那句话慎用。
    RecycledViewPool刚才说了Cache默认的缓存数量是2个,当Cache缓存满了以后会根据FIFO(先进先出)的规则把Cache先缓存进去的ViewHolder移出并缓存到RecycledViewPool中,RecycledViewPool默认的缓存数量是5个。RecycledViewPool与Cache相比不同的是,从Cache里面移出的ViewHolder再存入RecycledViewPool之前ViewHolder的数据会被全部重置,相当于一个新的ViewHolder,而且Cache是根据position来获取ViewHolder,而RecycledViewPool是根据itemType获取的,如果没有重写getItemType()方法,itemType就是默认的。因为RecycledViewPool缓存的ViewHolder是全新的,所以取出来的时候需要走onBindViewHolder()方法。

    再来张图看看整体流程

    image

    这里大家先记住主要流程,并且记住各级缓存是根据什么拿到ViewHolder以及ViewHolder能否直接拿来复用,先有一个整体的认识,下面我会带着大家再简单分析一下RecyclerView缓存机制的源码。

    阅读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优化在哪里,我想你已经有答案。

    通过Demo再理解一遍

    担心你看完上面的内容,倒头就忘,我们写个简单的demo通过打印log的方式来巩固一下学到的知识。
    简单说一下Demo里面需要注意的代码,下面是对RecyclerView的一个包装

    public class RecyclerViewWrapper extends RecyclerView {
        private  LayoutListener layoutListener;
    
        public RecyclerViewWrapper(@NonNull Context context) {
            super(context);
        }
    
        public RecyclerViewWrapper(@NonNull Context context, @Nullable AttributeSet attrs) {
            super(context, attrs);
        }
    
        public RecyclerViewWrapper(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) {
            super(context, attrs, defStyle);
        }
    
        public void setLayoutListener(LayoutListener layoutListener) {
            this.layoutListener = layoutListener;
        }
    
        @Override
        protected void onLayout(boolean changed, int l, int t, int r, int b) {
            if (layoutListener != null) {
                layoutListener.onBeforeLayout();
            }
            super.onLayout(changed, l, t, r, b);
    
            if (layoutListener != null) {
                layoutListener.onAfterLayout();
            }
        }
    
        public interface LayoutListener {
    
            void onBeforeLayout();
    
            void onAfterLayout();
        }
    
    }
    
    

    其实很简单,在RecyclerView执行onLayout()方法前后执行一下咱们打印缓存变化的方法

    再看一眼打印缓存变化的方法,利用反射的技术

    /**
         * 利用java反射机制拿到RecyclerView内的缓存并打印出来
         * */
        private void showMessage(RecyclerViewWrapper rv) {
            try {
                Field mRecycler =
                        Class.forName("androidx.recyclerview.widget.RecyclerView").getDeclaredField("mRecycler");
                mRecycler.setAccessible(true);
                RecyclerView.Recycler recyclerInstance = (RecyclerView.Recycler) mRecycler.get(rv);
    
                Class<?> recyclerClass = Class.forName(mRecycler.getType().getName());
                Field mViewCacheMax = recyclerClass.getDeclaredField("mViewCacheMax");
                Field mAttachedScrap = recyclerClass.getDeclaredField("mAttachedScrap");
                Field mChangedScrap = recyclerClass.getDeclaredField("mChangedScrap");
                Field mCachedViews = recyclerClass.getDeclaredField("mCachedViews");
                Field mRecyclerPool = recyclerClass.getDeclaredField("mRecyclerPool");
                mViewCacheMax.setAccessible(true);
                mAttachedScrap.setAccessible(true);
                mChangedScrap.setAccessible(true);
                mCachedViews.setAccessible(true);
                mRecyclerPool.setAccessible(true);
    
                int mViewCacheSize = (int) mViewCacheMax.get(recyclerInstance);
                ArrayListWrapper<RecyclerView.ViewHolder> mAttached =
                        (ArrayListWrapper<RecyclerView.ViewHolder>) mAttachedScrap.get(recyclerInstance);
                ArrayList<RecyclerView.ViewHolder> mChanged =
                        (ArrayList<RecyclerView.ViewHolder>) mChangedScrap.get(recyclerInstance);
                ArrayList<RecyclerView.ViewHolder> mCached =
                        (ArrayList<RecyclerView.ViewHolder>) mCachedViews.get(recyclerInstance);
                RecyclerView.RecycledViewPool recycledViewPool =
                        (RecyclerView.RecycledViewPool) mRecyclerPool.get(recyclerInstance);
    
                Class<?> recyclerPoolClass = Class.forName(mRecyclerPool.getType().getName());
    
                Log.e(TAG, "mAttachedScrap(一缓) size is:" + mAttached.maxSize + ", \n" + "mCachedViews(二缓) max size is:" + mViewCacheSize + ","
                        + getMCachedViewsInfo(mCached) + getRVPoolInfo(recyclerPoolClass, recycledViewPool));
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
    

    核心的代码呢就这两块,文章的最后我会把我的demo上传到github上。
    注意:本文使用的RecyclerView的版本是androidx,在调onAttachedToWindow()方法的时候会进行版本判断,如果是5.0以及以上的系统(即大于等于21),GapWorker会把RecyclerView自己加入到GapWorker。在RenderThread线程执行预取操作的时候会mPrefetchMaxCountObserved = 1,这就会导致你使用5.0以及以上系统的手机打印缓存数量的时候会比你预想的多一个。这里为了不造成这种问题,本文使用4.4系统的Android模拟器来演示Demo。

    Demo演示效果截图

    • 启动App,第一次加载的情况

      image

      初始化加载只有屏幕内的一级缓存7个

    • 把position = 0 和position=1 两个item移除屏幕

      image

      看蓝色框出来的,position = 0 和position = 1的item被加入到了Cache缓存中,Cache的缓存数量我没有修改,默认2个,也就说现在已经满了

    • 再把position = 2的item也移除屏幕

      image

    因为上一步Cache里面的缓存已经慢了,此时position = 2又被加入缓存,根据FIFO的原则,cache里面position = 0 被remove掉并加入到了四级缓存RecycledView里面,此时RecycledView也有了缓存并且该缓存没有任何有效数据信息。

    • 再上一步的基础上下拉一下,把position = 2的item显示出来

      image

      此时position = 2的item将要被显示出来,会先从cache里面找,发现Cache正好有position = 2的缓存就直接拿出来复用了,并且原来在屏幕里的position= 9 的item被移除了,就会加入到Cache的缓存里
      -----------------------------------分割线-------------------------------------------
      现在看一下onCreateViewHolder()和onBindViewHolder()的情况

    • 还是启动App,第一次加载后,再把position = 0和position =1的item移除屏幕再移回来

      image

      onBindViewHolder()方法没有被重复执行

    总结

    recyclerview拓展性强:

    可以通过LayoutManager形成线性、网格、瀑布流布局。
    通过getItemViewType来生成不同的视图。
    通过OnItemTouchListener监听 Item 的事件,虽然比
    提供了notifyItemInserted、notifyItemRemoved、notifyItemChanged、notifyItemMoved来提高局部刷新的效率。
    ListView.OnItemClickListener麻烦了点,但是可以实现更复杂的功能,比如item滑动。

    四级缓存,效率大大增加

    1.屏幕内缓存

    屏幕中显示的ViewHolder会缓存在mAttachedScrap、mChangedScrap中 :

    • mChangedScrap 表示数据已经改变的ewHolder列表
    • mAttachedScrap 仍与RecyclerView存在绑定关系,并未分离的ViewHolder列表
    2.屏幕外缓存

    当列表滑动出了屏幕时,ViewHolder会被缓存在 mCachedViews ,默认DEFAULT_CACHE_SIZE为2,可通过Recyclerview.setItemViewCacheSize()动态设置。

    3.自定义缓存

    空实现,可以自己实现ViewCacheExtension类实现自定义缓存,可通过Recyclerview.setViewCacheExtension()设置。可能google留着以后会继续增加。

    4.缓存池

    ViewHolder在首先会缓存在 mCachedViews 中,当超过了个数(比如默认为2), 就会添加到 RecycledViewPool 中。RecycledViewPool 会根据每个ViewType把ViewHolder分别存储在不同的列表中,每个ViewType最多缓存DEFAULT_MAX_SCRAP = 5 个ViewHolder,recycleviewPool可以被多个recycleview共享。

    性能优化

    1、数据处理与视图加载分离,bindviewHolder方法是在UI线程进行的,此方法不能耗时操作,不然将会影响滑动流畅性。2、数据优化,从网络获取的数据分页加载时进行缓存,提高二次加载速度。3、用Diffutil工具包判断新数据和老数据区别,局部刷新。4、布局优化 减少xml文件用代码生成布局,因为android都是通过inflate解析xml,有IO操作。固定item高度,避免requestLayout计算高度浪费资源。5、共用缓存池recyclerViewPool。6,滑动过程中不需要加载图片,停止后再加载。在recycleview的onScrollStateChanged方法中滑动时和惯性滚动时调用glide.pauseRequset停止加载,停止滑动时调用glide.resumeRequset开始加载。

    相关文章

      网友评论

        本文标题:RecyclerView

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