美文网首页Android
Android 3分钟彻底搞懂 RecyclerView 的缓

Android 3分钟彻底搞懂 RecyclerView 的缓

作者: 鹏城十八少 | 来源:发表于2021-04-11 14:32 被阅读0次

    1.RecyclerView缓存原理

    2.ListView和RecyclerView区别

    3.为什么RecyclerView加载首屏会慢一些

    4.如何让两个 RecyclerView 共用一个缓存,今日头条页面实例

    5.如何解决RecyclerView滑动卡顿问题

    6.快速滑动RecycleView卡顿解决办法

    1.RecyclerView缓存原理

    RecyclerView 是 ListView 的升级版本,更加先进和灵活。看名字我们就能看出一点端倪,没错,它主要的特点就是复用。回收的类在LayoutManager

    回收原理:

    注:官网上貌似把mAttachedScrap、mCachedViews当成一级了,为了方便区分,本文还是把他们当成两级缓存。

    缓存涉及对象作用重新创建视图View(onCreateViewHolder)重新绑定数据(onBindViewHolder)

    一级缓存mAttachedScrap缓存屏幕中可见范围的ViewHolderfalsefalse

    二级缓存mCachedViews缓存滑动时即将与RecyclerView分离的ViewHolder,按子View的position或id缓存,默认最多存放2个falsefalse

    三级缓存mViewCacheExtension开发者自行实现的缓存--

    四级缓存mRecyclerPoolViewHolder缓存池,本质上是一个SparseArray,其中key是ViewType(int类型),value存放的是 ArrayList< ViewHolder>,默认每个ArrayList中最多存放5个ViewHolderfalsetrue

    RecyclerView滑动时会触发onTouchEvent#onMove,回收及复用ViewHolder在这里就会开始

    mAttachedScrap(第一屏,可见)----mCachedViews(刚刚移除的)--------mRecyclerPool(总的)

    1).它会先在mAttachedScrap中找,看要的View是不是刚刚剥离的,如果是就直接返回使用,

    2).如果不是,先在mCachedViews中查找,因为在mCachedViews中精确匹配,如果匹配到,就说明这个HolderView是刚刚被移除的,也直接返回,

    3).如果匹配不到就会最终到mRecyclerPool找,如果mRecyclerPool有现成的holderView实例,这时候就不再是精确匹配了,只要有现成的holderView实例就返回给我们使用,只有在mRecyclerPool为空时,才会调用onCreateViewHolder新建。

    具体分析

    一.mAttachedScrap到底有什么用?

    (第一屏,可见),第一次存放。用于插入一个数据进去的时候用到。滑动的时候不用到

    二.mCachedViews它的作用就是保存最新被移除的HolderView

    自定义ViewCacheExtension缓存作用,适用场景:ViewHolder位置固定、内容固定、数量有限时使用

    缓存的存和取的过程:

    取的原则:mCachedViews > mRecyclerPool

    mAttachedScrap不参与回收复用,只保存从在重新布局时,从RecyclerView中剥离的当前在显示的HolderView列表。

    所以,mCachedViews、mViewCacheExtension、mRecyclerPool组成了回收复用的三级缓存,当RecyclerView要拿一个复用的HolderView时,获取优先级是mCachedViews > mViewCacheExtension > mRecyclerPool。由于一般而言我们是不会自定义mViewCacheExtension的。所以获取顺序其实就是mCachedViews > mRecyclerPool,

    存放过程:mCachedViews------mRecyclerPool(一个静态类)

    在我们标记为Removed以为,会把这个HolderView移到mCachedViews中,如果mCachedViews已满,就利用先进先出原则,将mCachedViews中老的holderView移到mRecyclerPool中,然后再把新的HolderView加入到mCachedViews中。

    举例:

    上滑动:上面不可见的移动到mCachedViews然后是mRecyclerPool=========调用的方法getViewForPosition()

    下面新的可见, 会从到mCachedViews找然后是mRecyclerPool============调用的方法removeAndRecycleView(child, recycler)

    为什么这么设计多个缓存?优化效率:

    这里需要注意的是,在mAttachedScrap和mCachedViews中拿到的HolderView,因为都是精确匹配的,所以都是直接使用,不会调用onBindViewHolder重新绑定数据,只有在mRecyclerPool中拿到的HolderView才会重新绑定数据。正是有mCachedViews的存在,所以只有在RecyclerView来回滚动时,池子的使用效率最高,因为凡是从mCachedViews中取的HolderView是直接使用的,不需要重新绑定数据。

    mRecyclerPool容量是5

    mCachedViews容量是2,他们最多是7个,为什么后面一直不用创建了呢?一般只创建一屏!

    后面移出一个,然后就填充一个。

    源码分析:

    ViewgetViewForPosition(int position, boolean dryRun) {

    return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;

    }

    ViewHoldertryGetViewHolderForPositionByDeadline(int position,

            boolean dryRun, long deadlineNs) {

    if (position <0 || position >=mState.getItemCount()) {

    throw new IndexOutOfBoundsException("Invalid item position " + position

    +"(" + position +"). Item count:" +mState.getItemCount());

        }

    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) {

    if (!validateViewHolderForOffsetPosition(holder)) {

    // recycle holder (and unscrap if relevant) since it can't be used

                    if (!dryRun) {

    // we would like to recycle this but need to make sure it is not used by

    // animation logic etc.

                        holder.addFlags(ViewHolder.FLAG_INVALID);

                        if (holder.isScrap()) {

    removeDetachedView(holder.itemView, false);

                            holder.unScrap();

                        }else if (holder.wasReturnedFromScrap()) {

    holder.clearReturnedFromScrapFlag();

                        }

    recycleViewHolderInternal(holder);

                    }

    holder =null;

                }else {

    fromScrapOrHiddenOrCache =true;

                }

    }

    }

    if (holder ==null) {

    final int offsetPosition =mAdapterHelper.findPositionOffset(position);

            if (offsetPosition <0 || offsetPosition >=mAdapter.getItemCount()) {

    throw new IndexOutOfBoundsException("Inconsistency detected. Invalid item "

                        +"position " + position +"(offset:" + offsetPosition +")."

                        +"state:" +mState.getItemCount());

            }

    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) {

    throw new IllegalArgumentException("getViewForPositionAndType returned"

                                +" a view which does not have a ViewHolder");

                    }else if (holder.shouldIgnore()) {

    throw new IllegalArgumentException("getViewForPositionAndType returned"

                                +" a view that is ignored. You must call stopIgnoring before"

                                +" returning this 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);

                    }

    }

    long end = getNanoTime();

                mRecyclerPool.factorInCreateTime(type, end - start);

                if (DEBUG) {

    Log.d(TAG, "tryGetViewHolderForPositionByDeadline created new ViewHolder");

                }

    }

    }

    Demo地址:https://github.com/pengcaihua123456/shennandadao/tree/master

    onCreateViewHolder()方法执行次数

    onBindViewHolder()方法的执行次数

    2.ListView和RecyclerView区别

    1).缓存机制不一样

     RecyclerView中mCacheViews(屏幕外)获取缓存时,是通过匹配pos获取目标位置的缓存,这样做的好处是,当数据源数据不变的情况下,无须重新bindView,而同样是离屏缓存,ListView从mScrapViews根据pos获取相应的缓存,但是并没有直接使用,而是重新getView(即必定会重新bindView)

    ListView和RecyclerView最大的区别在于数据源改变时的缓存的处理逻辑,ListView是”一锅端”,将所有的mActiveViews都移入了二级缓存mScrapViews,而RecyclerView则是更加灵活地对每个View修改标志位,区分是否重新bindView。

    2).Listview支持,HeaderView 和 FooterView   而RecyclerView支持横竖滑动LayoutManager

    3). RecyclerView支持动画

    4).局部刷新方式

    3.为什么RecyclerView加载首屏会慢一些

    第一次要createview和bindview()。没有任何缓存

    4.如何让两个 RecyclerView 共用一个缓存

    通过RecyclewView直接获回收池

    RecyclerView.RecycledViewPool recycledViewPool=mRecyclerView.getRecycledViewPool();

    使用多个RecyclerView,并且里面有相同item布局时,这时就可以通过setRecycledViewPool()设置同一个RecycledViewPool;

    5.如何解决RecyclerView滑动卡顿问题

    1)、根据需求修改RecyclerView默认的绘制缓存选项

    recyclerView.setItemViewCacheSize(20);recyclerView.setDrawingCacheEnabled(true);recyclerView.setDrawingCacheQuality(View.DRAWING_CACHE_QUALITY_HIGH);

    当item会出现频繁的来回滑动时,可以通过setItemViewCacheSize()设置mCachedViews的数量,这个缓存主要是不需要重新进行绑定数据;

    典型的是:用空间换时间的方法。

    2).使用多个RecyclerView,并且里面有相同item布局时,这时就可以通过setRecycledViewPool()设置同一个RecycledViewPool;

    因为,RecycleViewPool用来存放 mCachedViews 移除的ViewHolder。按照 Type 类型,默认对每个Type最多缓存 5 个。重点源码中它是被 public static 修饰,表示可以被其他RecyclerView 共享。

    3).当是网格布局的时候,如果一行的item超过五个,需要通过setMaxRecycledViews()去重新设置缓存的最大个数;

    4).可以使用setHasStableIds(true)进行设置(同时重写Adapter的getItemID()方法),这时会复用到scrap缓存;

    源码里面:

    ViewHoldertryGetViewHolderForPositionByDeadline(int position,

            boolean dryRun, long deadlineNs) {

    if (position <0 || position >=mState.getItemCount()) {

    throw new IndexOutOfBoundsException("Invalid item position " + position

    +"(" + position +"). Item count:" +mState.getItemCount());

        }

    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;

        }

    5).局部刷新替代全局刷新。避免整个列表的数据更新,只更新受影响的布局。例如,加载更多时,不使用notifyDataSetChanged(),而是使用notifyItemRangeInserted(rangeStart, rangeEnd)

    6).滑动监听

    主要就是对onScrollStateChanged方法进行监听,然后通知adapter是否加载图片或复杂布局

    7).measure()优化和减少requestLayout()调用

    当RecyclerView宽高的测量模式都是EXACTLY时,onMeasure()方法不需要执行dispatchLayoutStep1()等方法来进行测量。而当RecyclerView的宽高不确定并且至少一个child的宽高不确定时,要measure两遍。

    因此将RecyclerView的宽高模式都设置为EXACTLY有助于优化性能。

        protected void onMeasure(int widthSpec, int heightSpec) {

            // ......

            if (mLayout.isAutoMeasureEnabled()) {

                final int widthMode = MeasureSpec.getMode(widthSpec);

                final int heightMode = MeasureSpec.getMode(heightSpec);

                           mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);

                final boolean measureSpecModeIsExactly =

                        widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;

                if (measureSpecModeIsExactly || mAdapter == null) {

                    return;

                }

                // ......

        }

    还有一个方法RecyclerView.setHasFixedSize(true)可以避免数据改变时重新计算RecyclerView的大小

    6.快速滑动RecycleView卡顿解决办法

    (1)快速滑动RecycleView卡顿原因:

    因为,列表上下滑动的时候,RecycleView会在执行复用策略,onCreateViewHolder和onBindViewHolder会执行。item视图创建或数据绑定的方法会随着滑动被多次执行,容易造成卡顿。

    (2)解决快速滑动造成的卡顿

    一般都采用滑动关闭数据加载优化:主要是设置RecyclerView.addOnScrollListener();通过自定义一个滑动监听类继承onScrollListener抽象类,实现滑动状态改变的方法onScrollStateChanged(recycleview,state),从而实现在滑动过程中不加载,当滚动静止时,刷新界面,实现加载

    相关文章

      网友评论

        本文标题:Android 3分钟彻底搞懂 RecyclerView 的缓

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