目录java
前言缓存
(1)卡顿缘由:网络
(2)优化提案:ide
(2)处理刷新闪烁问题大数据
前言
RecycleView 是一个可回收复用的列表控件,有着极高的灵活性,能实现ListView、GridView的全部功能。在平常开发中,RecycleView承担着重要的做用,像是淘宝、京东等电商APP都会有商品列表的展现,能够说与用户体验是紧密相连,其重要程度不言而喻。若是RecycleView使用不当将会影响到应用的总体性能拉低,所以它是性能优化系列中“卡顿优化”的重点。
Android性能优化(一)闪退治理、卡顿优化、耗电优化、APK瘦身
本篇,单独了解一下若是对RecycleView 进行性能提高、卡顿优化。
推荐阅读
(一)RecycleView 初探回收复用,onCreateView和onBindView调用关系
(二)RecycleView 实现吸附小标题的Demo(附源码)
(四)RecycleView 滑动到置顶、Adapter局部刷新
1、RecycleView 性能提高
RecyclerView自身有一套完整的缓存机制,很是优秀,对于简单的数据列通常不会有任何问题。但仍然存在不足之处。好比,不能根据滑动状态自行调节数据绑定。遇到开发一些相似商城的应用,当展现大量的商品图片的时候,快速滑动商品列表页面,或频繁增删数据的时候,都颇有可能形成列表的卡顿。那么,形成卡顿的缘由到底是什么呢?
(1)卡顿缘由:
- 界面设计不合理,布局层级嵌套太多,过分绘制。
- bindViewHolder中业务逻辑复杂,数据计算及类型转换等耗时。
- 界面数据改变,一味的全局刷新,致使闪屏卡顿。
- 快速滑动列表,数据加载缓慢。
(2)优化提案:
- 布局、绘制优化。
- 视图绑定与数据处理分离。
- notifyxxx()局部刷新。
- 加大RecyclerView.mCachedViews的缓存。
- 共享RecycledViewPool 。
- 惯性滑动延迟加载。
2、布局、绘制优化
老生常谈的优化方案。就不过多赘述哦~
由于View的测量、布局和绘制是经过遍从来进行操做的,若是布局层级太多极易形成卡顿(官方建议不超过10层)。
能够考虑自定义ViewGroup、<ViewStub>延迟View加载、<merge>标签等方式减小层级;
多层次重叠的 UI 结构中移除底层背景减小过分绘制;
从而提升UI渲染的效率。
3、视图绑定与数据处理分离
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); } }
4、notifyxxx()局部刷新
关于局部刷新,我在第四章里讲解了一点。下面来看看RecycleView的通知子项发生改变的几种方法及处理刷新闪烁。
(1)经常使用的5个列表刷新
- notifyDataSetChanged():所有刷新,(可能会闪)
- notifyItemChanged (int) :指定一个刷新,(必定会闪)
- notifyItemRangeChanged(int, int):指定刷新起始个数(必定会闪)
- notifyItemInserted(int) :插入一个并刷新
- notifyItemRemoved(int) :移除一个并刷新
对于新增、删除、修改数据,能够进行局部数据刷新,而不是一味的全局刷新数据,从而减小数据的绑定,下降卡顿。另外,可参考“
DiffUtil
”,它是support包下新增的一个工具类,判断新数据和旧数据的差异进行局部刷新。
(2)处理刷新闪烁问题
一、为何会出现闪烁呢?
对于指定刷新:会走crateViewHolder和bindViewHolder从新建立和绑定。
对于notifyDataSetChanged:会告知adapter,把全部的数据都从新加载了一遍,有缓存的直接获取,没缓存的从新建立。天然包括从新加载网络图片。
解决办法:
notifyDataSetChanged + setHasStableIds(true) + 复写getItemId() 方法 。(并不是是万能的,注意场景,下面会讲。)
mRecyclerViewAdapter.setHasStableIds(true); @Override public long getItemId(int position) {//position对应数据源集合的索引 return position; }
二、解决的原理详解:
复用缓存中获取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作了一个绑定,就不用从新建立和加载数据,这样就避免了图片闪烁。
三、存在一个大大的坑:
由于getItemId()方法返回值是索引下标值position,当使用数据源集合里的position的话做为返回值的时候,由于业务逻辑集合增删后,数据源的位置就发生了变化,这样进入判断itemId时不能对号入座,再通知子项刷新notifyDataSetChanged()的时候就会仍然出现闪烁。
5、改变mCachedViews的缓存
由于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);
6、共享RecycledViewPool
由于,RecycleViewPool用来存放 mCachedViews 移除的ViewHolder。按照 Type 类型,默认对每一个Type最多缓存 5 个。重点源码中它是被 public static 修饰,表示能够被其余RecyclerView 共享。
(1)嵌套RecycleView卡顿缘由
当使用多层嵌套的RecyclerView极易出现卡顿。好比在一个垂直的RecyclerView中嵌套水平的RecyclerView。
在嵌套RecyclerView中,当用户滚动一个横向RecycleView的时候确定没什么问题,也算流畅,由于它自身一套完整回收复用机制的“神功护体”。
可是,当整个列表垂直滚动时,外层的RecycleView的子项须要建立或复用吧,那么,每个子项中的RecyclerView是否是一样也得处理各自的回收复用机制,内外层的子项数量越庞大,内存消耗就越大,从而形成卡顿甚至,更严重的问题。
(2)解决嵌套RecycleView卡顿
经过调用RecyclerView.setRecycledViewPool()方法,让每个子项里的RecycleView在
同一个RecycledViewPool里作回收复用策略。(固然,前提是子项RecycleView的Adapter是相同的。)
/** * 解决双层嵌套,共用RecycleViewPool */ public class OutShopAdapter extends BaseAdapter<String, RecyclerView.ViewHolder> { 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) { } }
7、惯性滑动延迟加载
(1)快速滑动RecycleView卡顿缘由:
由于,列表上下滑动的时候,RecycleView会在执行复用策略,onCreateViewHolder和onBindViewHolder会执行。item视图建立或数据绑定的方法会随着滑动被屡次执行,容易形成卡顿。
(2)解决快速滑动形成的卡顿
通常都采用滑动关闭数据加载优化:主要是设置
RecyclerView.addOnScrollListener();经过自定义一个滑动监听类继承onScrollListener抽象类,实现
滑动状态改变的方法
onScrollStateChanged(recycleview,state),从而实现
在滑动过程当中不加载,当滚动静止时,刷新界面,实现加载。
缺点:
列表只要一滚动就不加载数据;
列表只要一中止滚动,就刷新数据一次;
无论用户滚动了多少,都会刷新数据。
优化:
只有惯性滚动时才不加载数据;
顶部/底部不刷新数据;
提升列表滑动速度。
技术难点:
如何检测到列表是快速滚动。
如何判断布局是否未加载,若是已加载的就不用重复加载。
列表滑动速度如何改变。(由于是私有的成员变量 private final int mMaxFlingVelocity;)
(3)检测惯性滑动
若是列表滚动中计算一下滚动速度,当速度大于某个值,咱们就认为用户快速滚动列表。
首先,使用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性能优化处理以后,就应该自信点把惯性值加倍,让用户体验翻倍!
(4) 判断是否已加载
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个判断。
一、scroll 为true,表示刚刚发生了“快速”滚动,如今屏幕显示的都是未加载数据的列表项,能够进行加载了。
二、scrolled为true,表示刚刚列表滚动了距离。由于滑到顶部和底部,y轴滚动值为0,容易形成重复刷新数据。
终于写完了。。。。。一看时间,半夜了。。。洗洗睡咯~