目录
前言
一、RecycleView 性能提升
(1)卡顿原因:
(2)优化提案:
二、布局、绘制优化
三、视图绑定与数据处理分离
四、notifyxxx()局部刷新
(1)常用的5个列表刷新
(2)处理刷新闪烁问题
五、改变mCachedViews的缓存
六、惯性滑动延迟加载策略
(1)快速滑动RecycleView卡顿原因:
(2)解决快速滑动造成的卡顿
(3)检测惯性滑动
(4) 判断是否已加载
七、共享RecycledViewPool
(1)嵌套RecycleView卡顿原因
(2)解决嵌套RecycleView卡顿
RecycleView 是一个可回收复用的列表控件,有着极高的灵活性,能实现ListView、GridView的所有功能。在日常开发中,RecycleView承担着重要的作用,像是淘宝、京东等电商APP都会有商品列表的展示,可以说与用户体验是紧密相连,其重要程度不言而喻。如果RecycleView使用不当将会影响到应用的整体性能拉低,因此它是性能优化系列中“卡顿优化”的重点。
Android性能优化(一)闪退治理、卡顿优化、耗电优化、APK瘦身
本篇,单独了解一下如果对RecycleView 进行性能提升、卡顿优化。
推荐阅读
(一)RecycleView 初探回收复用,onCreateView和onBindView调用关系
(二)RecycleView 实现吸附小标题的Demo(附源码)
(三)RecycleView 自定义下拉刷新,上拉加载监听
(四)RecycleView 滑动到置顶、Adapter局部刷新
(五)RecycleView 动态设置改变列表显示的高度
(六)RecycleView 回收复用机制总结
(七)RecycleView 性能提升、卡顿优化
RecyclerView自身有一套完整的缓存机制,非常优秀,对于简单的数据列一般不会有任何问题。但仍然存在不足之处。比如,不能根据滑动状态自行调节数据绑定。遇到开发一些类似商城的应用,当展示大量的商品图片的时候,快速滑动商品列表页面,或频繁增删数据的时候,都很有可能造成列表的卡顿。那么,造成卡顿的原因究竟是什么呢?
- 界面设计不合理,布局层级嵌套太多,过度绘制。
- bindViewHolder中业务逻辑复杂,数据计算及类型转换等耗时。
- 界面数据改变,一味的全局刷新,导致闪屏卡顿。
- 快速滑动列表,数据加载缓慢。
- 布局、绘制优化。
- 视图绑定与数据处理分离。
- notifyxxx()局部刷新。
- 加大RecyclerView.mCachedViews的缓存。
- 共享RecycledViewPool 。
- 惯性滑动延迟加载。
老生常谈的优化方案。就不过多赘述哦~
因为View的测量、布局和绘制是通过遍历来进行操作的,如果布局层级太多极易造成卡顿(官方建议不超过10层)。
可以考虑自定义ViewGroup、
延迟View加载、 标签等方式减少层级; 多层次重叠的 UI 结构中移除底层背景减少过度绘制;
从而提高UI渲染的效率。
onBindViewHolder()就是RecyclerView对item视图进行数据绑定的方法。
因为,RecyclerView的onB
indViewHolder()
方法是在UI线程运行的,而该方法做了耗时操作就会影响滑动的流畅性。比如,下载文件操作、网络连接操作、类型转换操作(日期转换、音频格式转换等)、文件操作、较大数据的初始化、sleep函数等。例如,我要在item里面根据日期显示背景颜色和年月日文字:
class ItemBean{
Date dateDue;
String title;
String description;
}
static Date today = new Date();
static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd", Locale.CHINA);
class MyRecyclerView.Adapter extends RecyclerView.Adapter {
public onBindViewHolder(RecyclerView.ViewHolder tv, int position) {
ItemBean bean = getItem(position);
//日期的比较
if (today.compareTo(bean.dateDue) > 0) {
tv.backgroundView.setColor(Color.GREEN);
} else {
tv.backgroundView.setColor(Color.RED);
}
//日期的转换
String mDateSdf = sdf.format(bean.dateDue);
tv.dateTextView.setDate(mDateSdf);
}
}
案例中,在
onBindViewHolder
方法中进行了日期的比较和日期的格式化是很耗时的。然而,onBindViewHolder
方法中应该只是将数据显示到视图中,而不应进行业务的处理。正确的做法是: 将日期的比较和日期的转换在和RecycleView数据绑定之前提前计算完毕。大致表达的意思,如下:
class ItemBean{
int backColor;
String mDateSdf
}
class MyRecyclerView.Adapter extends RecyclerView.Adapter {
public onBindViewHolder(RecyclerView.ViewHolder tv, int position) {
ItemBean bean = getItem(position);
tv.backgroundView.setColor(bean.backColor);
tv.dateTextView.setDate(mDateSdf);
}
}
关于局部刷新,我在第四章里讲解了一点。下面来看看RecycleView的通知子项发生改变的几种方法及处理刷新闪烁。
- notifyDataSetChanged():全部刷新,(可能会闪)
- notifyItemChanged (int) :指定一个刷新,(一定会闪)
- notifyItemRangeChanged(int, int):指定刷新起始个数(一定会闪)
- notifyItemInserted(int) :插入一个并刷新
- notifyItemRemoved(int) :移除一个并刷新
对于新增、删除、修改数据,可以进行局部数据刷新,而不是一味的全局刷新数据,从而减少数据的绑定,降低卡顿。另外,可参考“
DiffUtil
”,它是support包下新增的一个工具类,判断新数据和旧数据的差别进行局部刷新。
1、为什么会出现闪烁呢?
对于指定刷新:会走crateViewHolder和bindViewHolder重新创建和绑定。
对于notifyDataSetChanged:会告知adapter,把所有的数据都重新加载了一遍,有缓存的直接获取,没缓存的重新创建。自然包括重新加载网络图片。
解决办法:
notifyDataSetChanged + setHasStableIds(true) + 复写getItemId() 方法 。(并非是万能的,注意场景,下面会讲。)
mRecyclerViewAdapter.setHasStableIds(true);
@Override
public long getItemId(int position) {//position对应数据源集合的索引
return position;
}
2、解决的原理详解:
复用缓存中获取ViewHolder调用链的方法入口,源码如下:
ViewHolder tryGetViewHolderForPositionByDeadline(int position, ...) {
//...省略
if (holder == null) {
final int type = mAdapter.getItemViewType(offsetPosition);
if (mAdapter.hasStableIds()) {
// 通过type 和 ItemId从 mAttachedScrap 和 mCachedViews 寻找
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition), type, dryRun);
}
if (holder == null) {
// 没有,那只好 create 一个新的咯
holder = mAdapter.createViewHolder(RecyclerView.this, type);
}
}
RecyclerView.ViewHolder getScrapOrCachedViewForId(long id, int type, boolean dryRun) {
int count = this.mAttachedScrap.size(); //先从mAttachScrap 寻找
for(int i = count - 1; i >= 0; --i) {
RecyclerView.ViewHolder holder = this.mAttachedScrap.get(i);
if (holder.getItemId() == id && !holder.wasReturnedFromScrap()) {
if (type == holder.getItemViewType()) {
//..
return holder;//拿到了!
}
}
}
count = this.mCachedViews.size(); //再从mCachedViews 寻找
for(int ix = count - 1; ix >= 0; --ix) {
RecyclerView.ViewHolder holderx = (RecyclerView.ViewHolder)this.mCachedViews.get(ix);
if (holderx.getItemId() == id) {
if (type == holderx.getItemViewType()) {
return holderx;//拿到了!
}
}
}
return null;//没找到,返回null
}
源码中,当hasStableIds()为true,进入getScrapOrCachedViewForId(..itemId),再判断itemId拿到缓存实例。相当于用itemId做了一个绑定,就不用重新创建和加载数据,这样就避免了图片闪烁。
3、存在一个大大的坑:
因为getItemId()方法返回值是索引下标值position,当使用数据源集合里的position的话作为返回值的时候,因为业务逻辑集合增删后,数据源的位置就发生了变化,这样进入判断itemId时不能对号入座,再通知子项刷新notifyDataSetChanged()的时候就会仍然出现闪烁。
因为mCachedViews默认缓存容量是 2 个。存在这里的ViewHolder绑定的数据信息也都在,可以直接添加到 RecyclerView 中进行显示,不需要再次重新 onBindViewHolder()。
因此,我们可以通过 setViewCacheSize(int)方法改变缓存的容量大小,减少视图绑定数据的次数。
原理:
典型的是:用空间换时间的方法。
recyclerView.setItemViewCacheSize(20);
recyclerView.setDrawingCacheEnabled(true);//保存绘图,提高速度
//*public static final int DRAWING_CACHE_QUALITY_HIGH = 1048576;
recyclerView.setDrawingCacheQuality(View.DRAWING_CACHE_QUALITY_HIGH);
因为,列表上下滑动的时候,RecycleView会在执行复用策略,onCreateViewHolder和onBindViewHolder会执行。item视图创建或数据绑定的方法会随着滑动被多次执行,容易造成卡顿。
可以查看我第一章:(一)RecycleView 初探回收复用,onCreateView和onBindView调用关系
一般都采用滑动关闭数据加载优化:主要是设置
RecyclerView.addOnScrollListener();通过自定义一个滑动监听类继承onScrollListener抽象类,实现
滑动状态改变的方法
onScrollStateChanged(recycleview,state),从而实现
在滑动过程中不加载,当滚动静止时,刷新界面,实现加载。
缺点:
列表只要一滚动就不加载数据;
列表只要一停止滚动,就刷新数据一次;
不管用户滚动了多少,都会刷新数据。
优化:
只有惯性滚动时才不加载数据;
顶部/底部不刷新数据;
提高列表滑动速度。
技术难点:
如何检测到列表是快速滚动。
如何判断布局是否未加载,如果已加载的就不用重复加载。
列表滑动速度如何改变。(因为是私有的成员变量 private final int mMaxFlingVelocity;)
如果列表滚动中计算一下滚动速度,当速度大于某个值,我们就认为用户快速滚动列表。
首先,使用GestureDetector.OnGestureListener的监听onFling()方法。(不推荐)
//创建手势
GestureDetector detector = new GestureDetector(this, new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onFling(MotionEvent motionEvent, MotionEvent motionEvent1, float v, float v1) {
if (Math.abs(v1) > 8000) {//惯性值
simpleAdapter.setScrolling(true);
}
return false;
}
});
//监听手势
recyclerView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View view, MotionEvent event) {
detector.onTouchEvent(event);
return false;
}
});
其实,RecycleView内部就有惯性滑动的监听。(推荐)
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;
}
});
}
系统默认惯性滑动最大值mMaxFlingVelocity是8000,这个值是可以通过反射修改的。值越大,惯性滑动距离越远,越丝滑。因此,做了前面一套比较完善的RecycleView性能优化处理之后,就应该自信点把惯性值加倍,让用户体验翻倍!
adapter:
protected boolean scroll;
public boolean getScrolling(){return scroll;}
public void setScrolling(boolean scroll){this.scroll = scroll;}
@Override
protected void setOnBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
ShopBean item = mlist.get(position);
if(scroll){//未加载图片
((ViewHolde) viewHolder).imageView.setImageResource(0);
}else {//加载图片
Glide.with(mContext).
load(item.getPictureUrl())
.centerCrop()
.into(((ViewHolde) viewHolder).imageView);
}
}
scrollListener:
private boolean scrolled;//是否已滚动
private BaseAdapter mAdapter;
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
//y轴值发生改变
if (dy != 0) { scrolled = true;}
}
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
switch (newState) {
case RecyclerView.SCROLL_STATE_IDLE: //(静止)
// 未加载数据
if (mAdapter.getScrolling() && scrolled) {
mAdapter.setScrolling(false);//正常加载数据
mAdapter.notifyDataSetChanged();
}
scrolled = false;
break;
}
super.onScrollStateChanged(recyclerView, newState);
}
首先,根据一个Boolean类型变量scroll来控制ImageView是否加载图片。 true 表示滚动中,不加载;false,停止滚动,正常显示。默认为false。
然后,滑动静止加入2个判断。
1、scroll 为true,表示刚刚发生了“快速”滚动,现在屏幕显示的都是未加载数据的列表项,可以进行加载了。
2、scrolled为true,表示刚刚列表滚动了距离。因为滑到顶部和底部,y轴滚动值为0,容易造成重复刷新数据。
因为,RecycleViewPool用来存放 mCachedViews 移除的ViewHolder。按照 Type 类型,默认对每个Type最多缓存 5 个。重点源码中它是被 public static 修饰,表示可以被其他RecyclerView 共享。
当使用多层嵌套的RecyclerView极易出现卡顿。比如在一个垂直的RecyclerView中嵌套水平的RecyclerView。
在嵌套RecyclerView中,当用户滚动一个横向RecycleView的时候肯定没什么问题,也算流畅,因为它自身一套完整回收复用机制的“神功护体”。
但是,当整个列表垂直滚动时,外层的RecycleView的子项需要创建或复用吧,那么,每一个子项中的RecyclerView是不是同样也得处理各自的回收复用机制,内外层的子项数量越庞大,内存消耗就越大,从而造成卡顿甚至,更严重的问题。
通过调用RecyclerView.setRecycledViewPool()方法,让每一个子项里的RecycleView在
同一个RecycledViewPool里做回收复用策略。(当然,前提是子项RecycleView的Adapter是相同的。)
/**
* 解决双层嵌套,共用RecycleViewPool
*/
public class OutShopAdapter extends BaseAdapter {
RecyclerView.RecycledViewPool mSharedPool = new RecyclerView.RecycledViewPool();
public OutShopAdapter(Context context, List mMessages) {
super(context, mMessages);
}
@Override
protected RecyclerView.ViewHolder createViewHolder(int viewType, ViewGroup parent) {
RecyclerView childRecycleView = new RecyclerView(context);
childRecycleView.setRecycledViewPool(mSharedPool);
return null;
}
@Override
protected void setOnBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
}
}