Android 3分钟彻底搞懂 RecyclerView 的缓存机制,再也不怕面试被虐了 RecyclerView卡顿优化 刷新闪烁优化(绝对干货!!)

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),从而实现在滑动过程中不加载,当滚动静止时,刷新界面,实现加载

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容