流水不争先,争的是滔滔不绝

IM 会话列表卡顿优化实践

IM安全 macgrady 77℃

一.优化背景:

IM日常使用中,会话列表是用户最直观的界面,会话列表是否流畅对用户的体验有着很大的影响。随着功能的不断增加,会话列表上要展示的信息也越来越多。市面上一些其它的IM有的会出现严重的卡顿现象,我们来分析下其中的原因。

1.卡顿的产生过程:卡顿的原因是16MS内无法完成渲染所导致的,那么为什么需要在 16ms 内完成呢?以及在 16ms 以内需要完成哪些工作呢?

2.刷新率(RefreshRate)与帧率(FrameRate)

刷新率指的是屏幕每秒刷新的次数,是针对硬件而言的。目前大部分的手机刷新率都在 60Hz(屏幕每秒钟刷新 60 次),有部分高端机采用的 120Hz(比如 iPad Pro)。

帧率是每秒绘制的帧数,是针对软件而言的。通常只要帧率与刷新率保持一致,我们看到的画面就是流畅的。所以帧率在 60FPS 时我们就不会感觉到卡。

如果帧率为每秒钟 60 帧,而屏幕刷新率为 30Hz,那么就会出现屏幕上半部分还停留在上一帧的画面,屏幕的下半部分渲染出来的就是下一帧的画面 —— 这种情况被称为画面撕裂;相反,如果帧率为每秒钟 30 帧,屏幕刷新率为 60Hz,那么就会出现相连两帧显示的是同一画面,这就出现了卡顿。所以单方面的提升帧率或者刷新率是没有意义的,需要两者同时进行提升。

由于目前大部分 Android 机屏幕都采用的 60Hz 的刷新率,为了使帧率也能达到 60FPS,那么就要求在 16.67ms 内完成一帧的绘制(1000ms/60Frame = 16.666ms / Frame)。

2.2 VSYNC

由于显示器是从最上面一行像素开始,向下逐行刷新,所以从最顶端到最底部的刷新是有时间差的。

如果帧率(FPS)大于刷新率,那么就会出现前文提到的画面撕裂,如果帧率再大一点,那么下一帧的还没来得及显示,下下一帧的数据就覆盖上来了,中间这帧就被跳过了,这种情况被称为跳帧。

为了解决这种帧率大于刷新率的问题,引入了垂直同步的技术,简单来说就是显示器每隔 16ms 发送一个垂直同步信号(VSYNC),系统会等待垂直同步信号的到来,才进行一帧的渲染和缓冲区的更新,这样就把帧率与刷新率锁定。

2.3 系统是如何生成一帧的

在 Android4.0 以前,处理用户输入事件,绘制、栅格化都由 CPU 中应用主线程执行,很容易造成卡顿。主要原因在于主线程的任务太重,要处理很多事件,其次 CPU 中只有少量的 ALU 单元(算术逻辑单元),并不擅长做图形计算。Android4.0 以后应用默认开启硬件加速。开启硬件加速以后,CPU 不擅长的图像运算就交给了 GPU 来完成,GPU 中包含了大量的 ALU 单元,就是为实现大量数学运算设计的(所以挖矿一般用 GPU)。硬件加速开启后还会将主线程中的渲染工作交给单独的渲染线程(RenderThread),这样当主线程将内容同步到 RenderThread 后,主线程就可以释放出来进行其他工作,渲染线程完成接下来的工作。

2.4 三缓冲区(Triple Buffer)

为了解决帧率(FPS)小于屏幕刷新率导致的掉帧问题,Android4.1 引入了三级缓冲区。

在双缓冲区的时候,由于 Display 和 GPU 各占用了一个缓冲区,导致在垂直同步信号到来时 CPU 没有办法进行绘制。那么现在新增一个缓冲区,CPU 就能在垂直同步信号到来时进行绘制工作。

在第二个 16ms 内,虽然还是重复显示了一帧,但是在 Display 占用了 A 缓冲区,GPU 占用了 B 缓冲区的情况下,CPU 依然可以使用 C 缓冲区完成绘制工作,这样 CPU 也被充分地利用起来。后续的显示也比较顺畅,有效地避免了 Jank 进一步的加剧。

通过上面的流程我们知道,出现卡顿是因为掉帧了,而掉帧的原因在于垂直同步信号到来时,还没有准备好数据用于显示。所以我们要处理卡顿,就要尽量缩短 CPU/GPU 绘制的时间,这样就能保证在 16ms 内完成一帧的渲染。

二、优化方案及实践

2.1 异步

有了大概的了解以后,我们开始对会话列表进行优化。

在问题分析中,我们发现 Vsync 延迟占比很大,所以我们首先想到的是将主线程中的耗时任务剥离出来,放到工作线程中执行。为了更快地定位主线程方法耗时,可以使用滴滴的 Dokit 或者腾讯的 Matrix 进行慢函数定位。

这部分逻辑在主线程中执行,耗时大概在 80ms 左右,如果会话列表多,数据库表数据变更大,这部分的耗时还会增加。

我们还发现每次进入会话列表时都需要从数据库中获取会话列表数据,加载更多时也会从数据库中读取会话数据。读取到会话数据以后,我们会对获取到的会话进行过滤操作,比如不是同一个组织下的会话则应该过滤掉。过滤完成以后会进行去重,如果该会话已经存在,则更新当前会话;如果不存在,则创建一个新的会话并添加到会话列表,然后还需要对会话列表按一定规则进行排序,最后再通知 UI 进行刷新。

这部分的耗时为 500ms-600ms,并且随着数据量的增大耗时还会增加,所以这部分必须放到子线程中执行。但是这里必须注意线程安全问题,否则会出现数据多次被添加,会话列表上出现多条重复的数据。

2.2 增加缓存

在检查代码的时候,我们发现有很多地方会获取当前用户的信息,而当前用户信息保存在了本地 SP 中(后改为MMKV),并且以 Json 格式存储。那么在获取用户信息的时候会从 SP 中先读取出来(IO 操作),再反序列化为对象(反射)。

每次都这样获取当前用户的信息会非常的耗时。为了解决这个问题,我们将第一次获取的用户信息进行缓存,如果内存中存在当前用户的信息则直接返回,并且在每次修改当前用户信息的时候,更新内存中的对象。

2.3 减少刷新次数

在这个方案里,一方面要减少不合理的刷新,另外一方面要将部分全局刷新改为局部刷新。 将通知页面刷新的代码提取到循环外面,等待数据更新完毕以后刷新一次即可。

2.4 onCreateViewHolder 优化:

在分析 Systrace 报告时,我们发现了新的情况 —— 一次滑动伴随着大量的 CreateView 操作。为什么会出现这种情况呢?我们知道 RecyclerView 本身是存在缓存机制的,滑动中如果新展示的 item 布局跟老的一致,就不会再执行 CreateView,而是复用老的 item,执行 bindView 来设置数据,这样可减少创建 view 时的 IO 和反射耗时。

2.5 预加载+全局缓存

虽然我们减少了 CreateView 的次数,但是我们在首次进入时第一屏还是需要 CreateView,并且我们发现 CreateView 的耗时也挺长。那么我们就考虑将创建 View 的操作提前来执行并且缓存下来。

2.6 布局优化

除了减少 BindView 的耗时以外,布局的层级也影响着 onMeasure 和 onLayout 的耗时。我们在使用 GPU 呈现模式分析工具时发现测量和布局花费了大量的时间,所以我们打算减少 item 的布局层级。

除了去掉重复的背景,我们还可以尽量减少使用透明度,Android 系统在绘制透明度时会将同一个区域绘制两次,第一次是原有的内容,第二次是新加的透明度效果。基本上 Android 中的透明度动画都会造成过度绘制,所以可以尽量减少使用透明度动画,在 View 上面也尽量不要使用 alpha 属性。

2.7其他优化

除了上面所说的优化点,还有一些小的优化点:

(1)比如使用高版本的 RecyclerView,会默认开启预取功能。

UI 线程完成数据处理交给 Render 线程以后就一直处于空闲状态,需要等待个 Vsync 信号的到来才会进行数据处理,而这空闲时间就被白白浪费了,开启预取以后就能合理地使用这段空闲时间。

(2)将 RecyclerView 的 setHasFixedSize 方法设置为 true。当我们的 item 宽高固定时,使用 Adapter 的 onItemRangeChanged()、onItemRangeInserted()、onItemRangeRemoved()、onItemRangeMoved() 这几个方法更新 UI,不会重新计算大小。

(3)如果不使用 RecyclerView 的动画,可以通过 ((SimpleItemAnimator) rv.getItemAnimator()).setSupportsChangeAnimations(false)  把默认动画关闭来提升效率。

三、总结

在开发过程中,随着业务的不断新增,我们的方法和逻辑复杂度也会不断增加,这时候一定要注意方法耗时,耗时严重的尽量提取到子线程中执行。使用 Recyclerview 时千万不要无脑刷新,能局部刷的绝不全局刷,能延迟刷的绝不马上刷。在分析卡顿的时候可以结合工具进行,这样效率会提高很多,通过 Systrace 发现大概的问题和排查方向以后,可以通过 Android Studio 自带的 Profiler 来进行具体代码的定位。

=====================================================

————————————————

版权声明:本文为51CTO博主「融云」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

原文链接:https://blog.51cto.com/u_14206262/3865058

版权声明:部分文章、图片等内容为用户发布或互联网整理而来,仅供学习参考。如有侵犯您的版权,请联系我们,将立刻删除。
点击这里给我发消息