记一次RecyclerView滑动卡顿优化 您所在的位置:网站首页 recyclerview快速滑动 记一次RecyclerView滑动卡顿优化

记一次RecyclerView滑动卡顿优化

2024-07-17 10:42| 来源: 网络整理| 查看: 265

背景

项目中使用到了日历选择控件,这个控件直接用的网上现成的轮子,使用的RecyclerView实现。实际使用中,当数据量较大时滑动会很明显的卡顿,严重的影响使用。

调研

问题原因很明显在于RecyclerView滑动时Adapter创建ViewHolder和BindViewHolder中有耗时操作,先搜索一下前人总结的经验:

布局优化减少过度绘制

减少布局层级,可以考虑使用自定义View来减少层级,或者更合理的设置布局来减少层级。

减少xml文件的inflate时间

xml文件包括:layout、drawable的xml,xml文件inflate出ItemView是通过耗时的IO操作。可以使用代码去生成布局,即new View()的方式。这种方式是比较麻烦,但是在布局太过复杂,或对性能要求比较高的时候可以使用。

减少View对象的创建

一个稍微复杂的 Item 会包含大量的 View,而大量的 View 的创建也会消耗大量时间,所以要尽可能简化 ItemView;设计 ItemType 时,对多 ViewType 能够共用的部分尽量设计成自定义 View,减少 View 的构造和嵌套。

设置固定高度

如果item高度是固定的话,可以使用RecyclerView.setHasFixedSize(true);来避免requestLayout浪费资源

滑动优化

参考Glide滑动时停止请求

if (newState == RecyclerView.SCROLL_STATE_DRAGGING || newState == RecyclerView.SCROLL_STATE_SETTLING) { sIsScrolling = true; Glide.with(VipMasterActivity.this).pauseRequests(); } else if (newState == RecyclerView.SCROLL_STATE_IDLE) { if (sIsScrolling == true) { Glide.with(VipMasterActivity.this).resumeRequests(); } sIsScrolling = false; }

其他优化增加RecyclerView的缓存

用空间换时间,来提高滚动的流畅性。

123recyclerView.setItemViewCacheSize(20);recyclerView.setDrawingCacheEnabled(true);recyclerView.setDrawingCacheQuality(View.DRAWING_CACHE_QUALITY_HIGH); 增加RecyclerView预留的额外空间

额外空间:显示范围之外,应该额外缓存的空间

123456new LinearLayoutManager(this) { @Override protected int getExtraLayoutSpace(RecyclerView.State state) { return size; }}; 卡顿分析

了解了卡顿的原因和大致的解决方法后,现在要找到卡顿的元凶对症下药

要分析CPU和内存当然得用到Android Studio自带的Profile工具,先看看文档这个工具的用法。

Call Chart

Call Chart 横轴就是时间线,用来展示方法开始与结束的确切时间,纵轴则自上而下展示了方法间调用和被调用的关系。Call Chart 已经比原数据可读性高很多,但它仍然不方便发现那些运行时间很长的代码,这时我们便需要使用 Flame Chart。

Flame Chart

Flame Chart 提供了一个调用栈的聚合信息。与 Call Chart 不同的是,它的横轴显示的是百分比数值。由于忽略了时间线信息,Flame Chart 可以展示每次调用消耗时间占用整个记录时长的百分比。同时纵轴也被对调了,在顶部展示的是被调用者,底部展示的是调用者。此时的图表看起来越往上越窄,就好像火焰一样,因此得名:

Flame Chart 是基于 Call Chart 来重新组织信息的。从 Call Chat 开始,合并相同的调用栈,以耗时由长至短对调用栈进行排序,就获得了 Flame Chart:

对比两种图表不难看出,左边的 Call Chart 有详细的时间信息,可以展示每次调用是何时发生的;右边的 Flame Chart 所展示的聚合信息,则有助于发现一个总耗时很长的调用路径:

Top Down Tree

前面介绍的两种图表,可以帮助我们从两种角度纵览全局。而如果我们需要更精确的时间信息,就需要使用 Top Down Tree。在 CPU Profiler 中,Top Down 选项卡展示的是一个数据表格,为了便于理解其中各组数据的意义,接下来我们会尝试构建一个 Top Down Tree。

构建一个 Top Down Tree 并不复杂。以 Flame Chart 为基础,您只需要从调用者开始,持续添加被调用者作为子节点,直到整个 Flame Chart 被遍历一遍,您就获得了一个 Top Down Tree:

对于每个节点,我们关注三个时间信息:

Self Time —— 运行自己的代码所消耗的时间; Children Time —— 调用其他方法的时间; Total Time —— 前面两者时间之和。

有了 Top Down Tree,我们能轻易将这三组信息归纳到一个表格之中:

下面我们来看一看这些时间信息是怎么计算的。左手边是和前面一样的 Flame Chart 示例。右边则是一个 Top Down Tree。

我们从 A 节点开始:

A 消耗了 1 秒钟来运行自己的代码,所以 Self Time 是 1; 然后它消耗了 9 秒中去调用其他方法,这意味着它的 Children Time 是 9; 这样就一共消耗了 10 秒钟,Total Time 是 10; B 和 D 以此类推…

值得注意的是,D 节点只是调用了 C,自己没做任何事,这种情况在方法封装时很常见。所以 D 的 Children Time 和 Total Time 都是 2。

下面是表格完全展开的状态。当您在 Android Studio 中分析应用时,CPU Profiler 会完成上面所有的计算,您只要理解这些数字是怎么产生的即可:

对比左右两边: Flame Chart 比较便于发现总耗时很长的调用链,而 Top Down Tree 则方便观察其中每一步所消耗的精确时间。作为一个表格,Top Down Tree 也支持按单独维度进行排序,这点同样非常实用。

Bottom Up Tree

当您希望方便地找到某个方法的调用栈时,Bottom Up Tree 就派上用场了。”树” 如其名,Bottom Up Tree 从底部开始构建,这样我们就能通过在节点上不断添加调用者来反向构建出树。由于每个独立节点都可以构建出一棵树,所以这里其实是森林 (Forest):

让我们再做些计算来搞定这些时间信息。

表格有四行,因为我们有四个树在森林中。从节点 C 开始:

Self Time 是 4 + 2 = 6 秒钟; C 没有调用其他方法,所以 Children Time 是 0; 前面两者相加,总时间为 6 秒钟。

看起来与 Top Bottom Tree 别无二致。接下来展开 C 节点,计算 C 的调用者 B 和 D 的情况。

在计算 B 和 D 节点的相关时间时,情况与前面的 Top Bottom Tree 有所不同:

由于我们在构建基于 C 节点的 Bottom Up Tree,所以所有时间信息也都是基于 C 节点的。这时我们在计算 B 的 Self Time 时,应当计算 C 被 B 调用的时间,而不是 B 自身执行的时间,这里是 4 秒;对于 D 来说,则是 2 秒。 由于只有 B 和 D 调用 C 的方法,它们的 Total Time 之和应与 C 的 Total Time 相等。

下一个树是 B 节点的 Bottom Up Tree,它的 Self Time 是 3 秒,Children Time 是用来调用其他方法的时间,这里只有 C,所以是 2 秒。Total Time 永远都是前两者之和。下面便是整个表格展开的样子:

当您想要观察某个方法如何被调用,比如这个 nanoTime() 方法时,您可以使用 Bottom Up Tree 并观察 nanoTime 方法的子节点列表,通过右边的时间数据,您可以找到那个您所感兴趣的调用:

总结

实战分析

查看上图总结,FlameChart火焰图最适合来查找耗时最长的路径。打开android studio的profile工具录制一段时间,然后打开Flame Chart分析

查看上图发现在RecyclerView Adapter的bingViewHolder中Calendar.getInstance()占据了90%的时间。

查看相关代码

1234567891011121314151617//判断是否包含数据private boolean isContainData(DayTimeEntity dayTimeEntity) { if (dayTimeEntity == null || hasDataDayTimeEntityList.isEmpty()) { return false; } for (DayTimeEntity hasDayTimeEntity : hasDataDayTimeEntityList) { Calendar hasDataCalendar = hasDayTimeEntity.getCalendarTime(); Calendar selectCalendar = dayTimeEntity.getCalendarTime(); if (selectCalendar.getTimeInMillis() == hasDataCalendar.getTimeInMillis()) { return true; } } return false;}

在这个判断当天是否有数据的方法中,遍历了有数据日期的List,每次生成两个Calendar对象去对比是否是当天。当hasDataDayTimeEntityList有几百个时,每次bindViewHolder需要生成hasDataDayTimeEntityList * 2个Calendar对象。

改进

查看DayTimeEntity类结构,包含了年月日的信息。

1234public class DayTimeEntity implements Parcelable { public int day; public int month; public int year;

所有判断逻辑直接改为判断两个DayTimeEntity的年月日字段,不用生成任何对象。

12345678910@Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; DayTimeEntity that = (DayTimeEntity) o; if (day != that.day) return false; if (month != that.month) return false; return year == that.year;} 效果

改完后再去使用发现滑动完全不会卡顿了,效果非常明显。

参考链接

RecyclerView性能优化

使用 Android Studio Profiler 工具解析应用的内存和 CPU 使用数据

使用 CPU Profiler 检查 CPU 活动-



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

      专题文章
        CopyRight 2018-2019 实验室设备网 版权所有