RecyclerView性能优化实战

在Android中RecyclerView的使用随处可见,它的性能优化程度跟用户体验息息相关。

性能优化实战的例子如下,是获取手机所有已安装app列表:


RecyclerView的一些优化方案和使用技巧:

  • recyclerView.setHasFixedSize(true)

当Item的高度如是固定的,设置这个属性为true可以提高性能,尤其是当RecyclerView有条目插入、删除时性能提升更明显。RecyclerView在条目数量改变,会重新测量、布局各个item,如果设置了setHasFixedSize(true),由于item的宽高都是固定的,adapter的内容改变时,RecyclerView不会整个布局都重绘。

 void onItemsInsertedOrRemoved() {
   if (hasFixedSize) layoutChildren();
   else requestLayout();
}
  • 使用getExtraLayoutSpace为LayoutManager设置更多的预留空间

在RecyclerView的元素比较高,一屏只能显示一个元素的时候,第一次滑动到第二个元素会卡顿。

RecyclerView (以及其他基于adapter的view,比如ListView、GridView等)使用了缓存机制重用子 view(即系统只将屏幕可见范围之内的元素保存在内存中,在滚动的时候不断的重用这些内存中已经存在的view,而不是新建view)。

这个机制会导致一个问题,启动应用之后,在屏幕可见范围内,如果只有一张卡片可见,当滚动的时 候,RecyclerView找不到可以重用的view了,它将创建一个新的,因此在滑动到第二个feed的时候就会有一定的延时,但是第二个feed之 后的滚动是流畅的,因为这个时候RecyclerView已经有能重用的view了。

        val linearLayoutManager: LinearLayoutManager = object : LinearLayoutManager(applicationContext, LinearLayoutManager.VERTICAL, false) {
            override fun getExtraLayoutSpace(state: RecyclerView.State): Int {
                return 300
            }
        }
        recyclerView.layoutManager = linearLayoutManager
  • 避免创建过多监听器

onCreateViewHolder 和 onBindViewHolder 对时间都比较敏感,尽量避免繁琐的操作和循环创建对象。例如创建 OnClickListener,可以全局创建一个。同时onBindViewHolder调用次数会多于onCreateViewHolder的次数,如从RecyclerViewPool缓存池中取到的View都需要重新bindView,所以我们可以把监听放到CreateView中进行。

优化前:
注意,反复滑动列表,会一直调用onBindViewHolder方法,所以这里会一直创建OnClickListener对象。

    override fun onBindViewHolder(holder: AppViewHolder, position: Int) {
        holder.ivIcon?.background = context.packageManager.getActivityIcon(datas[position].intent)
        holder.tvName?.text = datas[position].name
        holder.tvPkg?.text = "包名:" + datas[position].pkg

        holder.itemView.setOnClickListener(object: OnClickListener {
            override fun onClick(v: View?) {
                context.startActivity(datas[position].intent)
            }
        })
    }

优化后:

    class AppViewHolder(itemView: View): ViewHolder(itemView) {
        var ivIcon: ImageView?= null
        var tvName: TextView?= null
        var tvPkg: TextView?= null

        init {
            ivIcon = itemView.findViewById(R.id.iv_icon)
            tvName = itemView.findViewById(R.id.tv_name)
            tvPkg = itemView.findViewById(R.id.tv_pkg)
            itemView.setOnClickListener(onClickListener)
        }

        var onClickListener: OnClickListener = object: OnClickListener {
            override fun onClick(v: View?) {
                
            }
        }
    }
数据处理与视图绑定分离

RecyclerView的 bindViewHolder方法是在UI线程进行的,如果在该方法进行耗时操作,将会影响滑动的流畅性。
比如:

mTextView.setText(Html.fromHtml(data).toString());

这里的 Html.fromHtml(data) 方法可能就是比较耗时的,存在多个
TextView 的话耗时会更为严重,这样便会引发掉帧、卡顿,而如果把这
一步与网络异步线程放在一起,站在用户角度,最多就是网络刷新时间稍
长一点。

局部刷新

可以用一下一些方法,替代notifyDataSetChanged,达到局部刷新的目的。notifyDataSetChanged会触发所有item的detached回调再触发onAttached回调。

notifyItemChanged(int position)
notifyItemInserted(int position)
notifyItemRemoved(int position)
notifyItemMoved(int fromPosition, int toPosition) 
notifyItemRangeChanged(int positionStart, int itemCount)
notifyItemRangeInserted(int positionStart, int itemCount) 
notifyItemRangeRemoved(int positionStart, int itemCount) 
复用RecycledViewPool

在TabLayout+ViewPager+RecyclerView的场景中,当多个RecyclerView有相同的item布局结构时,多个RecyclerView共用一个RecycledViewPool可以避免创建ViewHolder的开销,避免GC。RecycledViewPool对象可通过RecyclerView对象获取,也可以自己实现。
如果LayoutManager是LinearLayoutManager或其子类,需要手动开启这个特性: layout.setRecycleChildrenOnDetach(true)

        val recycledViewPool = recyclerView.recycledViewPool
        recyclerView1.setRecycledViewPool(recycledViewPool)
        recyclerView2.setRecycledViewPool(recycledViewPool)
使用DiffUtil局部刷新

DiffUtil是androidx.recyclerview.widget包下的一个工具类,当你的RecyclerView需要更新数据时,将新旧数据集传给它,它就能快速告知adapter有哪些数据需要更新。就相当于如果改变了就对某个item刷新,没改变就没刷新,可以简称为局部刷新。

mAdapter.notifyDataSetChanged()有两个缺点:
1.不会触发RecyclerView的动画(删除、新增、位移、change动画)
2.性能较低,毕竟是无脑的刷新了一遍整个RecyclerView , 极端情况下:新老数据集一模一样,效率是最低的。

它会自动计算新老数据集的差异,并根据差异情况,自动调用以下四个方法

adapter.notifyItemRangeInserted(position, count);
adapter.notifyItemRangeRemoved(position, count);
adapter.notifyItemMoved(fromPosition, toPosition);
adapter.notifyItemRangeChanged(position, count, payload);

简单使用DiffUtil,我们需要且仅需要额外编写一个类。

class AticalDiff: DiffUtil.ItemCallback<ArticleItem>() {
    override fun areItemsTheSame(oldItem: ArticleItem, newItem: ArticleItem): Boolean {
        return oldItem.userId == newItem.userId
    }

    override fun areContentsTheSame(oldItem: ArticleItem, newItem: ArticleItem): Boolean {
        return (oldItem == newItem && oldItem.userId == newItem.userId && oldItem.link == newItem.link &&
                oldItem.title == newItem.title)
    }

}

使用方式:
adapter继承androidx.recyclerview.widget.ListAdapter包下的ListAdapter,并在构造方法中传入自定义的DiffUtil.ItemCallback,代码如下:

class Adapter(val context: Context): ListAdapter<ArticleItem, Adapter.MyViewHolder>(AticalDiff()) {

}
优化滑动操作

如果RecyclerView加载很多大图,快速滑动卡顿解决方案:
考虑滚动的时候不做复杂布局及图片的加载,尽量减少滚动过程中的耗时操作,这样滚动停止的时候再加载可见区域的布局。

onScrollStateChanged的几种状态:

SCROLL_STATE_IDLE 屏幕停止滚动

SCROLL_STATE_DRAGGING 屏幕滚动且用户使用的触碰或手指还在屏幕上

SCROLL_STATE_SETTLING 由于用户的操作,屏幕产生惯性滑动

Gilde同时也为我们提供了两个方法:

resumeRequests() 开始加载图片
pauseRequests() 停止加载图片
public class AutoLoadRecyclerView extends RecyclerView {

    private void init() {
        addOnScrollListener(new ImageAutoLoadScrollListener());
    }

    //监听滚动来对图片加载进行判断处理
    public class ImageAutoLoadScrollListener extends OnScrollListener {
        @Override
        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
            super.onScrolled(recyclerView, dx, dy);
        }

        @Override
        public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
            super.onScrollStateChanged(recyclerView, newState);
            switch (newState) {
                case SCROLL_STATE_IDLE: // The RecyclerView is not currently scrolling.
                    //当屏幕停止滚动,加载图片
                    try {
                        if (getContext() != null) Glide.with(getContext()).resumeRequests();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    break;
                case SCROLL_STATE_DRAGGING: // The RecyclerView is currently being dragged by outside input such as user touch input.
                    //当屏幕滚动且用户使用的触碰或手指还在屏幕上,停止加载图片
                    try {
                        if (getContext() != null) Glide.with(getContext()).pauseRequests();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    break;
                case SCROLL_STATE_SETTLING: // The RecyclerView is currently animating to a final position while not under outside control.
                    //由于用户的操作,屏幕产生惯性滑动,停止加载图片
                    try {
                        if (getContext() != null) Glide.with(getContext()).pauseRequests();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    break;
            }
        }
    }
}

方案二:在正在滚动和停止滚动时给adapter设置是否滚动的属性值,在adapter判断值的状态去过略加载图片逻辑。


参考:
https://blog.csdn.net/GracefulGuigui/article/details/103646864
https://blog.csdn.net/yaojie5519/article/details/117174114

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容