(七)RecycleView 性能提高、卡顿优化(绝对干货!!)

目录java

前言缓存

1、RecycleView 性能提高性能优化

(1)卡顿缘由:网络

(2)优化提案:ide

2、布局、绘制优化函数

 3、视图绑定与数据处理分离工具

4、notifyxxx()局部刷新布局

(1)经常使用的5个列表刷新性能

(2)处理刷新闪烁问题大数据

5、改变mCachedViews的缓存

6、共享RecycledViewPool

(1)嵌套RecycleView卡顿缘由

(2)解决嵌套RecycleView卡顿

7、惯性滑动延迟加载

(1)快速滑动RecycleView卡顿缘由:

(2)解决快速滑动形成的卡顿

(3)检测惯性滑动

(4) 判断是否已加载


 

前言

RecycleView 是一个可回收复用的列表控件,有着极高的灵活性,能实现ListView、GridView的全部功能。在平常开发中,RecycleView承担着重要的做用,像是淘宝、京东等电商APP都会有商品列表的展现,能够说与用户体验是紧密相连,其重要程度不言而喻。若是RecycleView使用不当将会影响到应用的总体性能拉低,所以它是性能优化系列中“卡顿优化”的重点。

Android性能优化(一)闪退治理、卡顿优化、耗电优化、APK瘦身

本篇,单独了解一下若是对RecycleView 进行性能提高、卡顿优化。

推荐阅读 

(一)RecycleView 初探回收复用,onCreateView和onBindView调用关系

(二)RecycleView 实现吸附小标题的Demo(附源码)

(三)RecycleView 自定义下拉刷新,上拉加载监听

(四)RecycleView 滑动到置顶、Adapter局部刷新

(五)RecycleView 动态设置改变列表显示的高度

(六)RecycleView 回收复用机制总结

(七)RecycleView 性能提高、卡顿优化


1、RecycleView 性能提高

RecyclerView自身有一套完整的缓存机制,很是优秀,对于简单的数据列通常不会有任何问题。但仍然存在不足之处。好比,不能根据滑动状态自行调节数据绑定。遇到开发一些相似商城的应用,当展现大量的商品图片的时候,快速滑动商品列表页面,或频繁增删数据的时候,都颇有可能形成列表的卡顿。那么,形成卡顿的缘由到底是什么呢?

 

(1)卡顿缘由:

  • 界面设计不合理,布局层级嵌套太多,过分绘制。
  • bindViewHolder中业务逻辑复杂,数据计算及类型转换等耗时。
  • 界面数据改变,一味的全局刷新,致使闪屏卡顿。
  • 快速滑动列表,数据加载缓慢。

 

(2)优化提案:

  • 布局、绘制优化。
  • 视图绑定与数据处理分离。
  • notifyxxx()局部刷新。
  • 加大RecyclerView.mCachedViews的缓存。
  • 共享RecycledViewPool 。
  • 惯性滑动延迟加载。

 


2、布局、绘制优化

老生常谈的优化方案。就不过多赘述哦~

由于View的测量、布局和绘制是经过遍从来进行操做的,若是布局层级太多极易形成卡顿(官方建议不超过10层)。

能够考虑自定义ViewGroup、<ViewStub>延迟View加载、<merge>标签等方式减小层级;

多层次重叠的 UI 结构中移除底层背景减小过分绘制;

从而提升UI渲染的效率。

 


 3、视图绑定与数据处理分离

onBindViewHolder()就是RecyclerView对item视图进行数据绑定的方法。

由于,RecyclerView的onBindViewHolder()方法是在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视图建立或数据绑定的方法会随着滑动被屡次执行,容易形成卡顿。

能够查看我第一章:(一)RecycleView 初探回收复用,onCreateView和onBindView调用关系

(2)解决快速滑动形成的卡顿

通常都采用滑动关闭数据加载优化:主要是设置RecyclerView.addOnScrollListener();经过自定义一个滑动监听类继承onScrollListener抽象类,实现滑动状态改变的方法onScrollStateChanged(recycleview,state),从而实现在滑动过程当中不加载,当滚动静止时,刷新界面,实现加载

缺点:

  • 列表只要一滚动就不加载数据;

  • 列表只要一中止滚动,就刷新数据一次;

  • 无论用户滚动了多少,都会刷新数据。

优化:

  • 只有惯性滚动时才不加载数据;

  • 顶部/底部不刷新数据;

  • 提升列表滑动速度。

技术难点:

  1. 如何检测到列表是快速滚动。

  2. 如何判断布局是否未加载,若是已加载的就不用重复加载。

  3. 列表滑动速度如何改变。(由于是私有的成员变量 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,容易形成重复刷新数据。

 


终于写完了。。。。。一看时间,半夜了。。。洗洗睡咯~

相关文章
相关标签/搜索