【App】RecycleView优化

本文详细探讨了RecyclerView的缓存机制,包括三级缓存、RecycledViewPool的使用,以及notifyDataSetChanged的过程。在优化方面,介绍了设置setHasFixedSize(true)、局部刷新、惯性滑动延迟加载等策略。此外,还讨论了数据预处理、SnapHelper、DiffUtil等提高性能的方法,以及在实际使用中遇到的图片错乱和内存泄漏问题及其解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1、Recycle缓存

1.1三级缓存

a)Attached scrap & Changed scrap
ArrayList mAttachedScrap可见ViewHolder;
ArrayList mChangedScrap主要用到刷新屏幕上的itemView数据;
b)cache Views
ArrayList mCachedViews :保存最近移出屏幕的ViewHolder,包含数据和position信息,复用时必须是相同位置的ViewHolder才能复用,应用场景在那些需要来回滑动的列表中,当往回滑动时,能直接复用ViewHolder数据,不需要重新bindView。用一个数组保存ViewHolder;
c)RecyclerViewPool
SparseArray<ArrayList> mScrap:缓存池,当cacheView满了后,将cacheView中移出的ViewHolder放到Pool中,放之前会把ViewHolder数据清除掉,所以复用时需要重新bindView。按viewType来保存ViewHolder,每种类型最大缓存个数默认为5

1.2调用notifyDataSetChanged过程

如果没有设置Stable Ids,RecyclerView不知道哪些改变,它假设所有都改变了,会将每一个ViewHolder设置成无效并且放到缓存池Pool中。如果我们仅是把屏幕上的第四条itemView移到第六条的位置,屏幕上所有itemView都会重新layout一遍,这样只能从缓存池RecycledViewPool池中取缓存的ViewHolder,如果不够时,需要重新create ViewHolder.
如果设置了Satable Ids,即每一个itemView都有一个唯一的id来标识,通过getItemId()来获取这个唯一标识id,当然我们不能用position来标识,因为itemView会复用,位置会乱序。当调用notifyDataSetChanged()方法时,ViewHolder会进入上面的一级缓存mAttachedScrap中,而不是进入缓存池pool中,这样的好处:1)不会存在缓存池pool满的问题,不需要重新createViewHolder; 2) 不需要重新bindView了

2、Recycle优化:

Recycle优化这一块有两个角度的优化,一个UI级别的,还有一个就是数据级别的,下面分开介绍:
UI优化

2.1 View优化

纯UI布局的优化,这里不讲太多,删除无用层级,减少过度绘制,ViewStub的使用等等。这里额外讲一个优化点:AsyncLayoutInflater。我们打开一个list界面,显示刷新数据,然后在交给RecycleView渲染数据,我们观察页面的GPU Rending的渲染时间柱状图,发现RecycleView第一次加载展示数据会比较耗时,主要是这个有onCreateViewHolder进行View的初始化创建。如果item的view比较复杂,没有优化空间导致了卡顿,我们可以利用AsyncLayoutInflater预加载UI,具体的实现可以参考之前的App启动时间优化中的View提前加载,这里不多提。

2.2 setHasFixedSize(true)

当我们确定Item的改变不会影响RecyclerView的宽高的时候可以设置setHasFixedSize(true),并通过Adapter的增删改插方法去刷新RecyclerView,而不是通过notifyDataSetChanged(),这样RecyclerView就不会被requestLayout()。
尤其是RecycleView嵌套的时候,我们对子RecycleView中的ViewHolder的View最好进行宽高限制并setHasFixedSize(true),否则整个页面的刷新就比较频繁,尤其耗时。

2.3 getExtraLayoutSpace

我们有时候会遇到这样的问题,如果Item填充满整个屏幕,对RecycleView初始化的时候,第一次进入,滑动到第二个元素会有明显卡顿。整个卡顿系统是在干什么?原因是,当只有一张卡片可见,当滚动的时候,RecyclerView找不到可以重用的view了,它将创建一个新的,因此在滑动到第二个feed的时候就会有一定的延时,但是第二个feed之 后的滚动是流畅的,因为这个时候RecyclerView已经有能重用的view了。RecycleView给我们提供了一个这样的Api来解决这样的问题:

LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this) {
    @Override
    protected int getExtraLayoutSpace(RecyclerView.State state) {
    	//300是一个UI数据;
        return 300;
    }
};

2.4 局部刷新

我们知道RecycleView本身是支持局部刷新的,他封装的Adapter的监听接口都已经实现了,这里不介绍了。这里介绍另外的一个局部刷新,颗粒度到UI级别的,如果我们改变了RecycleView中的Item中的某些数据,我们ViewHolder会执行onBind(),但是onBind会重置整个Item的View,这里notifyItemChanged(int position, @Nullable Object payload),会出现onBindViewHolder(@NonNull ViewHolder holder, int position, @NonNull List payloads)

2.5 惯性滑动延迟加载

有这样的一个场景,如果我们快速的滑动列表,有时候(复杂列表),我们会发现列表滑动不流畅,也就是出险了卡顿,这里的卡顿又是什么原因?列表上下滑动的时候,RecycleView会在执行复用策略,onCreateViewHolder和onBindViewHolder会执行。item视图创建或数据绑定的方法会随着滑动被多次执行,容易造成卡顿。
对于大量图片的RecyclerView,滑动暂停后再加载;RecyclerView中存在几种绘制复杂,占用内存高的楼层类型,但是用户只是快速滑动到底部,并没有必要绘制计算这几种复杂类型,所以也可以考虑对滑动速度,滑动状态进行判断,满足条件后再加载这几种复杂的。
在滑动过程中不加载,当滚动静止时,刷新界面,实现加载。这样的缺点很明显:列表只要一滚动就不加载数据、列表只要一停止滚动,就刷新数据一次、不管用户滚动了多少,都会刷新数据。解决这些问题我们应该怎么做?
只有惯性滚动时才不加载数据、顶部/底部不刷新数据、提高列表滑动速度,实现起来有这样的几个问题:如何检测到列表是惯性滚动?如何判断布局是否未加载,如果已加载的就不用重复加载?列表滑动速度如何改变?
判断是否惯性滑动以及设置惯性滑动速度,这样用到了一个反射:

public static void setMaxFlingVelocity(RecyclerView recyclerView, final BaseAdapter adapter, final int velocity) {
        try {
            Field field = recyclerView.getClass().getDeclaredField("mMaxFlingVelocity");
            field.setAccessible(true);
            field.set(recyclerView, velocity);
        } catch (Exception e) {
            e.printStackTrace();
        }

        recyclerView.setOnFlingListener(new RecyclerView.OnFlingListener() {
            @Override
            public boolean onFling(int xv, int yv) {//xv是x方向滑动速度,yv是y方向滑动速度。    
                if (yv >= velocity) {
                    adapter.setScrolling(true);
                }else{
                    adapter.setScrolling(false);
                }
                return false;
            }
        });
 }

针对判断布局是否未加载,上面的onBind onUpdate这个时候就起到了作用,在对应的Viewholder中加入isScroll参数就可以实现上述问题。

2.6 setOnClickListener

onCreateViewHolder 和 onBindViewHolder 对时间都比较敏感,尽量避免繁琐的操作和循环创建对象。RecycleView没有提供onItemClickListener()方法,需要我们自己实现。这里就需要注意,我们什么时候创建View.onItemClickListener()以及怎么创建?我这边得建议是全局只创建一个listener绑定到Adapter中。具体的实现这里不表述。

2.7 RecyclerView缓存优化

1、setItemViewCacheSize(int)空间换时间,注意根据机型版本不同,做好差异化配置;
2、复用RecycledViewPool注意复用场景,比如RecycleView嵌套和ViewPage+fragment+RecycleView的界面框架;

下面就是数据级别的

2.7 数据预处理

我们经常在onBind方法中去对model进行在加工,然后展示,这里是有优化点的,我们在数据获取到的时候,可以提前对数据进行加工,包装成我们需要展示的数据。这里就不会再UI展示的过程中去处理数据。

2.8 SnapHelper

优化滑动效果,这里不过多解释,都是官方推荐使用的。

2.8 数据预取Prefetch

数据预取的思想就是:将闲置的UI线程利用起来,提前加载计算下一帧的Frame Buffer。
预取优化是在 Support Library v25中引入,在 v25.1.0中改进的。如果你使用 RecyclerView 提供的默认 layout manager,你将自动获得这种优化。然而,如果你使用嵌套 RecyclerView 或者自己写 layout manager,你需要改变你的代码来利用这个特性。对于嵌套 RecyclerView 而言,要获取最佳的性能,在内部的 LayoutManager 中调用 LinearLayoutManager 的setInitialItemPrefetchCount()方法(25.1版本起可用)。例如,如果你竖直方向的list至少展示三个条目,调用 setInitialItemPrefetchCount(4)。如果你实现了自己的 LayoutManager,你需要重写 LayoutManager.collectAdjacentPrefetchPositions()方法。该方法在数据预取开启时被 RecyclerView 调用(LayoutManager 的默认实现什么都不做)。第二,在嵌套的内层 RecyclerView 中,如果你想让你的 LayoutManager 预取数据,你同样应当实现 LayoutManager.collectInitialPrefetchPositions()。

2.8 DiffUtil

DiffUtil是support-v7:24.2.0中的新工具类,它用来比较两个数据集,寻找出旧数据集—>新数据集的最小变化量。 说到数据集,相信大家知道它是和谁相关的了,就是我的最爱,RecyclerView。 就我使用的这几天来看,它最大的用处就是在RecyclerView刷新时,不再无脑mAdapter.notifyDataSetChanged()。 以前无脑mAdapter.notifyDataSetChanged()有两个缺点:1、不会触发RecyclerView的动画(删除、新增、位移、change动画)2、性能较低,毕竟是无脑的刷新了一遍整个RecyclerView , 极端情况下:新老数据集一模一样,效率是最低的。使用DiffUtil后,改为如下代码:

DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffCallBack(mDatas, newDatas), true);
diffResult.dispatchUpdatesTo(mAdapter);

它会自动计算新老数据集的差异,并根据差异情况,自动对Adapter进行刷新,显然,这个四个方法在执行时都是伴有RecyclerView的动画的,且都是定向刷新方法,刷新效率蹭蹭的上升了。
注意getChangePayload的使用。
使用DiffUtils的时候,也有需要注意的地方://TODO
在子线程中计算DiffResult,在主线程中刷新RecyclerView。

3、遇到的问题

3.1 RecyclerView缓存复用view属性动画变换后复用导致图片错乱

Recyclerview的缓存机制,作者主要在对RecyclerView的ItemView某些图片进行了属性动画变换,这样就改变了ViewHolder中ImageView的属性,在滑动时,RecyclerView的缓存复用机制可能导致ViewHolder不会重新创建,也不会重新bindView,这样某些ItemView的图片是View属性动画变换后的图片,导致不是自己想要的结果。

3.2 RecyclerView关联的GapWorker导致内存泄漏

RecyclerView导致内存泄漏问题分析,其实主要是RecyclerView关联的GapWorker中有一个静态的ThreadLocal对象,静态属性生命周期和应用进程生命周期一致,发生内存泄漏肯定是因为GapWorker的引用链一直关联到Activity中,且没有在相应的时候释放这条引用链。按道理RecyclerView内部onAttachedToWindow和onDetachedFromWindow分别进行了引用和释放引用,是不会发生内存泄漏的,但是由于开发者应对的环境不一样,遇到的坑也不一样。作者这种分析办法还是很值得学习。
作为Android开发者,一定要注意RecyclerView的使用,千万不要跟ListView一起使用,慎用!自定义View的时候设计到RecyclerView也要注意是否调用RecyclerView的onDetachedFromWindow方法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值