Android深入理解RecyclerView的缓存机制

目录.png

RecyclerView在项目中的使用已经很普遍了,可以说是项目中最高频使用的一个控件了。除了布局灵活性、丰富的动画,RecyclerView还有优秀的缓存机制,本文尝试通过源码深入了解一下RecyclerView中的缓存机制。

写在前面

RecyclerView是通过内部类Recycler管理的缓存,那么Recycler中缓存的是什么?我们知道RecyclerView在存在大量数据时依然可以滑动的如丝滑般顺畅,而RecyclerView本身是一个ViewGroup,那么滑动时避免不了添加或移除子View(子View通过RecyclerView#Adapter中的onCreateViewHolder创建),如果每次使用子View都要去重新创建,肯定会影响滑动的流 畅性,所以RecyclerView通过Recycler来缓存的是ViewHolder(内部包含子View),这样在滑动时可以复用子View,某些条件下还可以复用子View绑定的数据。所以本质上缓存是为了减少重复绘制View和绑定数据的时间,从而提高了滑动时的性能

四级缓存

Recycler缓存ViewHolder对象有4个等级,优先级从高到底依次为:

  • ArrayList<ViewHolder> mAttachedScrap
  • ArrayList<ViewHolder> mCachedViews
  • ViewCacheExtension mViewCacheExtension
  • RecycledViewPool mRecyclerPool

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

缓存 涉及对象 作用 重新创建视图View(onCreateViewHolder) 重新绑定数据(onBindViewHolder)
一级缓存 mAttachedScrap 缓存屏幕中可见范围的ViewHolder false false
二级缓存 mCachedViews 缓存滑动时即将与RecyclerView分离的ViewHolder,按子View的position或id缓存,默认最多存放2个 false false
三级缓存 mViewCacheExtension 开发者自行实现的缓存 - -
四级缓存 mRecyclerPool ViewHolder缓存池,本质上是一个SparseArray,其中key是ViewType(int类型),value存放的是 ArrayList< ViewHolder>,默认每个ArrayList中最多存放5个ViewHolder false true

RecyclerView滑动时会触发onTouchEvent#onMove,回收及复用ViewHolder在这里就会开始。我们知道设置RecyclerView时需要设置LayoutManager,LayoutManager负责RecyclerView的布局,包含对ItemView的获取与复用。以LinearLayoutManager为例,当RecyclerView重新布局时会依次执行下面几个方法:

  • onLayoutChildren():对RecyclerView进行布局的入口方法
  • fill(): 负责对剩余空间不断地填充,调用的方法是layoutChunk()
  • layoutChunk():负责填充View,该View最终是通过在缓存类Recycler中找到合适的View的

上述的整个调用链:onLayoutChildren()->fill()->layoutChunk()->next()->getViewForPosition(),getViewForPosition()即是是从RecyclerView的回收机制实现类Recycler中获取合适的View,下面主要就来从看这个Recycler#getViewForPosition()的实现。

@NonNull
public View getViewForPosition(int position) {
    return getViewForPosition(position, false);
}

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

他们都会执行tryGetViewHolderForPositionByDeadline函数,继续跟进去:

//根据传入的position获取ViewHolder 
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
        boolean dryRun, long deadlineNs) {
    ---------省略----------
    boolean fromScrapOrHiddenOrCache = false;
    ViewHolder holder = null;
    //预布局 属于特殊情况 从mChangedScrap中获取ViewHolder
    if (mState.isPreLayout()) {
        holder = getChangedScrapViewForPosition(position);
        fromScrapOrHiddenOrCache = holder != null;
    }
    if (holder == null) {
        //1、尝试从mAttachedScrap中获取ViewHolder,此时获取的是屏幕中可见范围中的ViewHolder
        //2、mAttachedScrap缓存中没有的话,继续从mCachedViews尝试获取ViewHolder
        holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
     ----------省略----------
    }
    if (holder == null) {
        final int offsetPosition = mAdapterHelper.findPositionOffset(position);
        ---------省略----------
        final int type = mAdapter.getItemViewType(offsetPosition);
        //如果Adapter中声明了Id,尝试从id中获取,这里不属于缓存
        if (mAdapter.hasStableIds()) {
            holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                    type, dryRun);
        }
        if (holder == null && mViewCacheExtension != null) {
            3、从自定义缓存mViewCacheExtension中尝试获取ViewHolder,该缓存需要开发者实现
            final View view = mViewCacheExtension
                    .getViewForPositionAndType(this, position, type);
            if (view != null) {
                holder = getChildViewHolder(view);
            }
        }
        if (holder == null) { // fallback to pool
            //4、从缓存池mRecyclerPool中尝试获取ViewHolder
            holder = getRecycledViewPool().getRecycledView(type);
            if (holder != null) {
                //如果获取成功,会重置ViewHolder状态,所以需要重新执行Adapter#onBindViewHolder绑定数据
                holder.resetInternal();
                if (FORCE_INVALIDATE_DISPLAY_LIST) {
                    invalidateDisplayListInt(holder);
                }
            }
        }
        if (holder == null) {
            ---------省略----------
          //5、若以上缓存中都没有找到对应的ViewHolder,最终会调用Adapter中的onCreateViewHolder创建一个
            holder = mAdapter.createViewHolder(RecyclerView.this, type);
        }
    }

    boolean bound = false;
    if (mState.isPreLayout() && holder.isBound()) {
        holder.mPreLayoutPosition = position;
    } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
        final int offsetPosition = mAdapterHelper.findPositionOffset(position);
        //6、如果需要绑定数据,会调用Adapter#onBindViewHolder来绑定数据
        bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
    }
    ----------省略----------
    return holder;
}

上述逻辑用流程图表示:

image.png

总结一下上述流程:通过mAttachedScrap、mCachedViews及mViewCacheExtension获取的ViewHolder不需要重新创建布局及绑定数据;通过缓存池mRecyclerPool获取的ViewHolder不需要重新创建布局,但是需要重新绑定数据;如果上述缓存中都没有获取到目标ViewHolder,那么就会回调Adapter#onCreateViewHolder创建布局,以及回调Adapter#onBindViewHolder来绑定数据。

ViewCacheExtension

我们已经知道ViewCacheExtension属于第三级缓存,需要开发者自行实现,那么ViewCacheExtension在什么场景下使用?又是如何实现的呢?

首先我们要明确一点,那就是Recycler本身已经设置了好几级缓存了,为什么还要留个接口让开发者去自行实现缓存呢?关于这一点,谈一谈我的理解:来看看Recycler中的其他缓存,其中mAttachedScrap用来处理可见屏幕的缓存;mCachedViews里存储的数据虽然是根据position来缓存,但是里面的数据随时可能会被替换的;再来看mRecyclerPoolmRecyclerPool里按viewType去存储ArrayList< ViewHolder>,所以mRecyclerPool并不能按position去存储ViewHolder,而且从mRecyclerPool取出的View每次都要去走Adapter#onBindViewHolder去重新绑定数据。假如我现在需要在一个特定的位置(比如position=0位置)一直展示某个View,且里面的内容是不变的,那么最好的情况就是在特定位置时,既不需要每次重新创建View,也不需要每次都去重新绑定数据,上面的几种缓存显然都是不适用的,这种情况该怎么办呢?可以通过自定义缓存ViewCacheExtension实现上述需求。

  • ViewCacheExtension适用场景:ViewHolder位置固定、内容固定、数量有限时使用
  • ViewCacheExtension使用举例:
    比如在position=0时展示的是一个广告,位置不变,内容不变,来看看如何实现:
    DemoRvActivity.java:
  public class DemoRvActivity extends AppCompatActivity {
    private RecyclerView recyclerView;
    private DemoAdapter adapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_demo_rv);
        recyclerView = findViewById(R.id.rv_view);
        recyclerView.setLayoutManager(new LinearLayoutManager(this));
        recyclerView.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL));
        adapter = new DemoAdapter();
        recyclerView.setAdapter(adapter);

        //viewType类型为TYPE_SPECIAL时,设置四级缓存池RecyclerPool不存储对应类型的数据 因为需要开发者自行缓存
        recyclerView.getRecycledViewPool().setMaxRecycledViews(DemoAdapter.TYPE_SPECIAL, 0);
        //设置ViewCacheExtension缓存
        recyclerView.setViewCacheExtension(new MyViewCacheExtension());
    }

    //实现自定义缓存ViewCacheExtension
    class MyViewCacheExtension extends RecyclerView.ViewCacheExtension {
        @Nullable
        @Override
        public View getViewForPositionAndType(@NonNull RecyclerView.Recycler recycler, int position, int viewType) {
            //如果viewType为TYPE_SPECIAL,使用自己缓存的View去构建ViewHolder
            // 否则返回null,会使用系统RecyclerPool缓存或者从新通过onCreateViewHolder构建View及ViewHolder
            return viewType == DemoAdapter.TYPE_SPECIAL ? adapter.caches.get(position) : null;
        }
    }
 }

在看下Adapter的代码:

public class DemoAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {

    //viewType类型 TYPE_COMMON代表普通类型 TYPE_SPECIAL代表特殊类型(此处的View和数据一直不变)
    public static final int TYPE_COMMON = 1;
    public static final int TYPE_SPECIAL = 101;

    public SparseArray<View> caches = new SparseArray<>();//开发者自行维护的缓存

    private List<String> mDatas = new ArrayList<>();

    DemoAdapter() {
        initData();
    }

    private void initData() {
        for (int i = 0; i < 50; i++) {
            if (i == 0) {
                mDatas.add("我是一条特殊的数据,我的位置固定、内容不会变");
            } else {
                mDatas.add("这是第" + (i + 1) + "条数据");
            }
        }
    }

    public List<String> getData() {
        return mDatas;
    }

    @NonNull
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) {
        Log.e("TTT", "-----onCreateViewHolder:" + "viewType is " + viewType + "-----");
        Context context = viewGroup.getContext();
        if (viewType == TYPE_SPECIAL) {
            View view = LayoutInflater.from(context)
                    .inflate(R.layout.item_special_layout, viewGroup, false);
            return new SpecialHolder(view);
        } else {
            View view = LayoutInflater.from(context)
                    .inflate(R.layout.item_common_layout, viewGroup, false);
            return new CommonHolder(view);
        }
    }

    @Override
    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
        Log.e("TTT", "-----onBindViewHolder:" + "position is " + position + "-----");
        if (holder instanceof SpecialHolder) {
            SpecialHolder sHolder = (SpecialHolder) holder;
            sHolder.tv_ad.setText(mDatas.get(position));
            //这里是重点,根据position将View放到自定义缓存中
            caches.put(position, sHolder.itemView);
        } else if (holder instanceof CommonHolder) {
            CommonHolder cHolder = (CommonHolder) holder;
            cHolder.tv_textName.setText(mDatas.get(position));
        }
    }

    @Override
    public int getItemViewType(int position) {
        if (position == 0) {
            return TYPE_SPECIAL;//第一个位置View和数据固定
        } else {
            return TYPE_COMMON;
        }
    }

    @Override
    public long getItemId(int position) {
        return super.getItemId(position);
    }

    @Override
    public int getItemCount() {
        return mDatas.size();
    }

    class SpecialHolder extends RecyclerView.ViewHolder {
        TextView tv_ad;

        public SpecialHolder(@NonNull View itemView) {
            super(itemView);
            tv_ad = itemView.findViewById(R.id.tv_special_ad);
        }
    }

    class CommonHolder extends RecyclerView.ViewHolder {

        TextView tv_textName;

        public CommonHolder(@NonNull View itemView) {
            super(itemView);
            tv_textName = itemView.findViewById(R.id.tv_text);
        }
    }
}

运行界面如下:


image.png

重点关注第一条数据,当第一次运行时,在针对于第一条数据会执行Adapter#onCreateViewHolderAdapter#onBindViewHolder,想想也对,毕竟第一次执行,肯定要有一个创建View和绑定数据的过程。此时向下滑动到底部再滑上来,通过debug发现不再走这两个方法了,而是在getViewForPositionAndType回调中根据position拿到了我们自定义缓存中的View及数据,所以可以直接展示。再看我们自己维护的缓存是什么时候设置的,其实我这里是在Adapter#onBindViewHolder中根据position设置的缓存:

caches.put(position, sHolder.itemView);

假如我们把上面这行代码删除了呢,再次执行上述滑动操作,自定义缓存对应失效了,Adapter#onCreateViewHolderAdapter#onBindViewHolder都会被执行,这里可能大家可能会有个疑问,自定义缓存失效,为什么RecyclerPool里也没有对这个viewType进行缓存呢(因为如果缓存了,是不会重新执行onCreateViewHolder的)?猜想这是因为我在代码中设置了

recyclerView.getRecycledViewPool().setMaxRecycledViews(DemoAdapter.TYPE_SPECIAL, 0)

viewType类型为TYPE_SPECIAL时,设置缓存池RecyclerPool不存储对应类型的数据,因为开发者自行缓存了,所以没必要再往RecyclerPool存储了,如果把上面这行代码注释掉,重新执行上述滑动操作,会发现针对第一条数据只执行了Adapter#onBindViewHolder,因为即使自定义缓存失效了,默认还是会往RecyclerPool存储的嘛,这也验证了我们的猜想。

RecyclerView & ListView缓存机制对比

结论援引自:Android ListView 与 RecyclerView 对比浅析--缓存机制

ListView和RecyclerView缓存机制基本一致:
1). mActiveViews和mAttachedScrap功能相似,意义在于快速重用屏幕上可见的列表项ItemView,而不需要重新createView和bindView;
2). mScrapView和mCachedViews + mReyclerViewPool功能相似,意义在于缓存离开屏幕的ItemView,目的是让即将进入屏幕的ItemView重用.
3). RecyclerView的优势在于a.mCacheViews的使用,可以做到屏幕外的列表项ItemView进入屏幕内时也无须bindView快速重用;b.mRecyclerPool可以供多个RecyclerView共同使用,在特定场景下,如viewpaper+多个列表页下有优势.客观来说,RecyclerView在特定场景下对ListView的缓存机制做了补强和完善。
不同使用场景:列表页展示界面,需要支持动画,或者频繁更新,局部刷新,建议使用RecyclerView,更加强大完善,易扩展;其它情况(如微信卡包列表页)两者都OK,但ListView在使用上会更加方便,快捷。

参考

【1】关于Recyclerview的缓存机制的理解
【2】RecyclerView缓存机制
【3】ViewCacheExtension使用
【4】RecyclerView 必知必会

【5】https://juejin.im/post/5a7569676fb9a063435eaf4c
【6】https://github.com/gyzboy/AndroidSamples/blob/master/app/src/main/java/com/gyz/androidsamples/view/ASRecyclerView.java
【7】https://blog.csdn.net/HJsir/article/details/81485653
【8】RecyclerView缓存原理,有图有真相

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

推荐阅读更多精彩内容