当前位置:   article > 正文

Android基础-RecyclerView详解_安卓recyclerview

安卓recyclerview

资料

ItemDecoration深入解析与实战(一)——源码分析
【1】关于Recyclerview的缓存机制的理解
【2】RecyclerView缓存机制
【3】ViewCacheExtension使用
【4】RecyclerView 必知必会

【5】https://juejin.im/post/5a7569676fb9a063435eaf4c
【6https://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缓存原理,有图有真相] (https://juejin.im/post/5b79a0b851882542b13d204b#heading-7)
Android深入理解RecyclerView的缓存机制

在这里插入图片描述

一款不错的流式布局LayoutManager-FlowLayout

ItemDecoration

ItemDecoration

LayoutManager

LayoutManager分析与实践

RecyclerView缓存机制(scrap view)
【Android】让HeaderView也参与回收机制,自我感觉是优雅的为 RecyclerView 添加 HeaderView (FooterView)的解决方案
RecyclerView 里的自定义 LayoutManager 的一种设计与实现
Android 掌握自定义LayoutManager(一) 系列开篇 常见误区、问题、注意事项,常用API。

RecyclerView问题汇总

源码解析

资料
【进阶】RecyclerView源码解析(一)——绘制流程
【进阶】RecyclerView源码解析(二)——缓存机制
【进阶】RecyclerView源码解析(三)——深度解析缓存机制
【进阶】RecyclerView源码解析(四)——RecyclerView进阶优化使用
【框架】基于AOP的RecyclerView复杂楼层样式的开发框架,楼层打通,支持组件化,支持MVP(不用每次再写Adapter了~)

RecyclerView的多级缓存机制,每一级缓存具体作用是什么,分别在什么场景下会用到哪些缓存

https://zhooker.github.io/2017/08/14/%E5%85%B3%E4%BA%8ERecyclerview%E7%9A%84%E7%BC%93%E
https://www.wanandroid.com/wenda/show/14222
https://blog.csdn.net/u013700502/article/details/105058771
https://juejin.cn/post/6854573221702795277#heading-9

Scrap、Cache、ViewCacheExtension 、RecycledViewPool
Scrap缓存用在RecyclerView布局时,布局完成之后就会清空
添加到Cache缓存和RecyclerViewPool缓存的item,他们的View必须已经从RecyclerView中detached或 removed
一级缓存:mAttachedScrap 和 mChangedScrap 二级缓存:mCachedViews 三级缓存:ViewCacheExtension
四级缓存:RecycledViewPool 然后说怎么用,就是先从 1 级找,然后 2 级…然后4 级,找不到 create ViewHolder。
https://www.jianshu.com/p/467ae8a7ca6e

mAttachedScrap/mChangedScrap
屏幕内缓存
RecyclerView 的滑动场景来说,新卡位的复用以及旧卡位的回收机制, 不会涉及到 mChangedScrap 和
mAttachedScrap
notifyItemChanged/rangeChange,此时如果Holder发生了改变那么就放入changeScrap中,反之放入到
AttachScrap。
mCachedViews
当列表滑动出了屏幕时,ViewHolder会被缓存在 mCachedViews ,其大小由mViewCacheMax决定,默认
DEFAULT_CACHE_SIZE为2,可通过Recyclerview.setItemViewCacheSize()动态设置。
ViewCacheExtension
可以自己实现ViewCacheExtension类实现自定义缓存,可通过Recyclerview.setViewCacheExtension()设置。
缓存池
ViewHolder在首先会缓存在 mCachedViews 中,当超过了个数(比如默认为2), 就会添加到
RecycledViewPool 中。RecycledViewPool 会根据每个ViewType把ViewHolder分别存储在不同的列表中,每个
ViewType最多缓存DEFAULT_MAX_SCRAP = 5 个ViewHolder

RecyclerView的滑动回收复用机制

https://www.jianshu.com/p/467ae8a7ca6e
RecyclerView 滑动的场景触发的回收复用机制工作时, 并不需要四级缓存都参与的。
1.RecyclerView 向下滑动操作的日志,第三行5个卡位的显示都是重新创建的 ViewHolder
新一行5个卡位和复用不可能会用到刚移出屏幕的5个卡位, 因为先复用再回收,新一行的5个卡位先去目前的
mCachedViews 和 ViewPool 的缓存中寻找复用,没有就重新创建,然后移出屏幕的那行的5个卡位再回收缓存到
mCachedViews 和 ViewPool 里面,
2.RecyclerView 再次向上滑动重新显示第一行的5个卡位时,只有后面3个卡位触发了 onBindViewHolder() 方
法,重新绑定数据。
滑动场景下涉及到的回收和复用的结构体是 mCachedViews 和 ViewPool,前者默认大小为2,后者为5。所
以,当第三行显示出来后,第一行的5个卡位被回收,回收时先缓存在 mCachedViews,满了再移出旧的到
ViewPool 里,所有5个卡位有2个缓存在 mCachedViews 里,3个缓存在 ViewPool,所以最新的两个卡位是0、1,
会放在 mCachedViews 里,而2、3、4的卡位则放在 ViewPool 里。
3.而至于为什么会创建了17个 ViewHolder,那是因为再第四行的卡位要显示出来时,ViewPool 里只有3个缓
存,而第四行的卡位又用不了 mCachedViews 里的2个缓存,因为这两个缓存的是6、7卡位的 ViewHolder,所以就
需要再重新创建2个 ViewHodler 来给第四行最后的两个卡位使用。

.RecyclerView的刷新回收复用机制

notifyXxx后会RecyclerView会进行两次布局,一次预布局,一次实际布局,然后执行动画操作
dispatchLayoutStep1
查找改变holder,并保存在mChangedScrap中;其他未改变的保存到mAttachedScrap中 (mChangedScrap
保存的holder信息只有预布局时才会被复用)
dispatchLayoutStep2
此步骤会创建一个新的holder并执行绑定数据,充当改变位置的holder,其他位置holder从mAttachedScrap中
获取

RecyclerView 为什么要预布局

https://juejin.cn/post/6890288761783975950#heading-0
why
这种负责执行动画的View在原布局或新布局中不存在的动画,就是预测动画。
因为RecyclerView 要执行预测动画。比如有A,B,C三个itemView,其中A和B被加载到屏幕上,这时候删除B后,
按照最终效果我们会看到C移动到B的位置;因为我们只知道 C 最终的位置,但是不知道 C 的起始位置在哪里(即C还
未被加载)。
用户有 A、B、C 三个 item,A,B 刚好显示在屏幕中,这个时候,用户把 B 删除了,那么最终 C 会显示在 B 原
来的位置
因为我们只知道 C 最终的位置,但是不知道 C 的起始位置在哪里,无法确定 C 应该从哪里滑动过来。
在其他 LayoutManager 中,它可能是从侧面或者是其他地方滑动过来的。
what
当 Adapter 发生变化的时候,RecyclerView 会让 LayoutManager 进行两次布局。
第一次,预布局,为动画前的表项先执行一次pre-layout,根据 Adapter 的 notify 信息,我们知道哪些 item 即将
变化了,将不可见的表项 3 也加载到布局中,形成一张布局快照(1、2、3)。
第二次,实际布局,也就是变化完成之后的布局同样形成一张布局快照(1、3)。
这样只要比较前后布局的变化,就能得出应该执行什么动画了,就称为预测动画。

ListView 与 RecyclerView区别

1.布局效果
ListView 的布局比较单一,只有一个纵向效果; RecyclerView 的布局效果丰富, 可以在 LayoutMananger 中
设置:线性布局(纵向,横向),表格布局,瀑布流布局
2.局部刷新
RecyclerView中可以实现局部刷新,例如:notifyItemChanged();
如果要在ListView实现局部刷新,依然是可以实现的,当一个item数据刷新时,我们可以在Adapter中,实现一
个notifyItemChanged()方法,在方法里面通过这个 item 的 position,刷新这个item的数据
3.缓存区别
ListView有两级缓存,在屏幕与非屏幕内。 RecyclerView比ListView多两级缓存 ListView缓存View。
RecyclerView缓存RecyclerView.ViewHolder

RecyclerView性能优化

1.数据处理与视图加载分离
简单来说就是在onBindViewHolder()只设置UI显示,不做任何逻辑判断,需要的业务逻辑在得到javabean之前
处理好,
2.布局优化
减少过渡绘制 减少布局层级
3.设置RecyclerView.addOnScrollListener()来在滑动过程中停止加载的操作。

缓存

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

  • mAttachedScrap:ArrayList< ViewHolder>类型
  • mCachedViews:ArrayList< ViewHolder>类型
  • mViewCacheExtension:ViewCacheExtension类型
  • mRecyclerPool:RecycledViewPool类型
    在这里插入图片描述

用到的几个方法

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

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

他们都会执行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;
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64

上述逻辑流程图
在这里插入图片描述

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

ViewCacheExtension

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

首先我们要明确一点,那就是Recycler本身已经设置了好几级缓存了,为什么还要留个接口让开发者去自行实现缓存呢?关于这一点,谈一谈我的理解:来看看Recycler中的其他缓存,其中mAttachedScrap用来处理可见屏幕的缓存;mCachedViews里存储的数据虽然是根据position来缓存,但是里面的数据随时可能会被替换的;再来看mRecyclerPool,mRecyclerPool里按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;
        }
    }
 }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

在看下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);
        }
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97

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

caches.put(position, sHolder.itemView);
  • 1

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

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

即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在使用上会更加方便,快捷。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/weixin_40725706/article/detail/174680?site
推荐阅读
相关标签
  

闽ICP备14008679号