一、前言
如果现在用户反馈某个列表很卡,你会怎么排查问题?
这样一个简短的问题,其实考察了我们多方面的知识。要答出其中的一两个小点其实并不难,难的是如何能够由外之内,由浅入深娓娓道来,它考察的是一个程序员发现问题、解决问题、归纳总结的能力。
要回答这个问题,可以从以下四个方面层层深入,整个大纲如下:
- (1) 渲染原理
- 为什么会感知到卡顿
- 理解
VSYNC
- (2) 卡顿的外部因素
- 手机性能
- 系统本身
- 内存抖动
- 在主线程执行耗时操作
- (3) 卡顿的内部因素
- 布局层级
measure
、layout
、draw
耗时- 过度绘制
- (4) 监控卡顿
- 使用
Handler#setMessageLogging
- 使用
这篇文章中穿插着介绍了性能优化工具的使用场景,所有的链接地址为:
- 性能优化工具知识梳理(1) - TraceView
- 性能优化工具知识梳理(2) - Systrace
- 性能优化工具知识梳理(3) - 调试GPU过度绘制 & GPU呈现模式分析
- 性能优化工具知识梳理(4) - Hierarchy Viewer
- 性能优化工具知识梳理(5) - MAT
- 性能优化工具知识梳理(6) - Memory Monitor & Heap Viewer & Allocation Tracker
- 性能优化工具知识梳理(7) - LeakCanary
- 性能优化工具知识梳理(8) - Lint
二、渲染原理
首先我们需要明白 为什么用户会感知到卡顿,要回答这个问题,就需要对渲染的原理有一个基本的了解。
2.1 为什么会感知到卡顿
用户感知到的卡顿主要的根源是因为渲染性能。Android
系统每隔16ms
发出VSYNC
信号,触发对UI
进行渲染,如果每次渲染都成功,这样就能够达到所需要的60fps
,为了能够实现60fps
,这意味着程序的大多数操作都必须在16ms
内完成。
如果你的某个操作是24ms
,系统在得到VSYNC
信号的时候就无法进行正常的渲染,这样就发生了丢帧现象,那么用户在32ms
内看到的是同一帧画面。
2.2 理解 VSYNC
在理解VSYNC
之前,首先需要区分 帧率 和 刷新率:
- 帧率:代表了
GPU
在1s
内 绘制操作 的帧数,例如30fps
、60fps
,属于 软件参数。 - 刷新率:代表了屏幕在
1s
内刷新屏幕的次数,这取决于 硬件的固定参数,例如60Hz
。
GPU
获取图形数据进行渲染,然后硬件负责把渲染后的内容呈现到屏幕上,两者不停地协作。
当帧率和刷新率不一致的时候,就会发生画面上下两部分内容断裂,来自不同的两帧数据发生重叠。因此引入了VSYNC
,在超过60fps
的情况下,GPU
所产生的帧数据会因为等待VSYNC
的刷新信号而被Hold
住,这样能够保持每次刷新都有实际的新的数据可以显示。
但是我们遇到更多的情况是 帧率小于刷新率,也就是我们通常所说的卡顿,如下图所示。
三、卡顿的外部因素
卡顿的 外因 可以归结为以下几个方面:
- 手机性能问题,
CPU
性能不足,内存小。 - 系统本身问题,所有应用都很卡。
- 频繁触发
GC
,导致内存抖动。 - 在主线程中进行了耗时的操作。
3.1 手机性能问题
通过排查反馈用户的机型,如果大部分的反馈都是来自于低端机的用户,那么可以与产品沟通,通过获取硬件的相关参数,例如CPU
核数、内存大小,对于这些低端机型进行特殊的处理,对需求进行简化,避免去实现复杂的动画效果。
3.2 系统本身问题
如果可以联系用户,并且用户反馈不仅是我们的应用,而是整个系统都很卡,那么对于我们来说,其实做不了什么。
如果无法联系用户,那么可以通过trace
文件进行分析。
3.3 内存抖动
内存抖动指的是有大量的对象频繁地进出内存的新生代区域,它往往会伴随着频繁的GC
,而GC
会占用UI
线程和CPU
资源,从而导致应用发生卡顿,因此我们需要尽量这种现象的发生。
3.3.1 排查内存抖动
在排查内存抖动问题的时候,我们可以通过以下几个工具来辅助排查问题:
Memory Monitor
:在列表滑动的时候,实时观察内存的分配情况,定位发生GC
的时间点,确定其是否合理,但是其缺点是 无法列出具体的分配对象。Heap Viewer
:在垃圾回收的时候,呈现出某一时刻的内存快照,帮助我们分析是哪个对象引起了内存泄漏。Allocation Tracker
:分析出一段时间内对象的分配情况,并列出是由什么逻辑导致了这个对象的分配,与Heap Viewer
配合使用,来分析大对象产生的原因。
以上这三种工具的详细使用可以看之前总结的这篇文章:性能优化工具知识梳理(6) - Memory Monitor & Heap Viewer & Allocation Tracker。
3.3.2 容易发生内存抖动的场景
在平时的开发中,我们可以使用以下几点来避免内存抖动的发生:
- 在创建对象的操作,移出到循环体外
- 不要在
onMeasure
、onLayout
、onDraw
方法中频繁地创建对象,例如Paint
、Path
这样的类。 - 在使用
Bitmap
的时候,考虑通过LruCache+inBitmap
的方式进行复用。 - 合理地使用对象池来缓存对象。
3.4 在主线程中,执行了耗时的操作
3.4.1 排查耗时操作
在排查主线程的耗时操作时,最常用的就是TraceView
,通过这个工具可以看到每个方法的具体耗时时间,关于TraceView
的详细使用可以参考 性能优化工具知识梳理(1) - TraceView 这篇文章。
3.4.2 解决主线程耗时问题
在解决主线程耗时问题时,需要根据具体的业务的场景来排查,一般来说,当我们遇到列表卡顿的问题,可以优先从以下几个重要的回调中排查,看下是否在其中执行了耗时的操作,例如IO
、JSON
等。
RecyclerView
的onBindViewHolder
ListView
的getView
。RecyclerView/ListView
的onScrollChanged
。
四、卡顿的内部因素
- 布局层级
measure
、layout
、draw
的耗时- 过度绘制
4.1 布局层级
当我们设计列表的每个Item
项时,应当尽量减少每个Item
的布局层级,因为布局层级越深,每个Item
绘制就越耗时。
4.1.1 排查布局层级问题
在检查布局层级问题时,通常是使用Hierarchy Viewer
工具,通过该工具可以做到以下两点:
- 检查每个
Item
项的布局层级 - 通过每个节点的三个圆点颜色查看其在测量、布局、绘制三个阶段的性能表现,绿色表示
OK
,黄色表示其处于渲染速度比较慢的50%
,红色表示渲染速度非常慢。
更加详细的介绍可以参考 性能优化工具知识梳理(4) - Hierarchy Viewer。
4.1.2 减少布局层级
减少布局层级更多的是需要依赖开发者的习惯,因为有些时候,越少的层级往往需要更复杂的设计逻辑,这意味着需要花更多的时间来思考,在这里强烈推荐ConstraintLayout
控件,对于任何复杂的场景,只需要一层就可以了,使用可以参考 ConstraintLayout 完全解析 快来优化你的布局吧。
对于减少布局层级,有以下几点技巧:
- 首先应当考虑布局层级最小的方案。
- 布局层级相同时,就应当选取合适的父容器,一般来说,有以下几点经验:
- 选取的优先级为:
FrameLayout
、不带layout_weight
参数的LinearLayout
、RelativeLayout
,这里选取的标准为带有layout_weight
的LinearLayout
或者RelativeLayout
会测量两次。 - 当使用
LinearLayout
时,应当尽量避免使用layout_weight
参数。 - 避免使用
RelativeLayout
嵌套RelativeLayout
。 - 如果允许,那么可以使用
Google
的ConstraintLayout
布局。
更多的技巧可以参考 性能优化技巧知识梳理(1) - 布局优化。
4.2 measure、layout、draw 的耗时时间
对于这三个阶段的耗时,可以通过两个工具来排查问题:
Hierarchy Viewer
的节点参数- 设置当中的
GPU
呈现模式分析,参考 性能优化工具知识梳理(3) - 调试GPU过度绘制 & GPU呈现模式分析。
当我们发现在某个阶段耗时过长时,就需要去排查是否在以上三个回调当中做了不当的操作。
4.3 过度绘制
过度绘制其实是 布局层级过深的结果,通过设置中的 调试 GPU 过度绘制,可以直观地看到绘制的重叠情况,检测的结果分为以下四种,严重程度依次递增:
- 蓝色
- 绿色
- 浅红
- 深红
对于过度绘制的部分,需要想办法去优化,详细的使用方式为:性能优化工具知识梳理(3) - 调试GPU过度绘制 & GPU呈现模式分析。
五、监控卡顿
在前面两节中,我们从外因和内因两个部分总结了卡顿问题的排查方法和注意事项,除此之外,还可以通过一些手段实时地监控卡顿问题,这里推荐使用Handler
的setMessageLogging
方法,检测每个消息的耗时时间,当其耗时大于阈值的时候,输出堆栈信息。
简单的实现方式为 Framework 源码解析知识梳理(4) - 从源码角度谈谈 Handler 的应用。
BlockCanary
就是基于这个原理来实现的,具体的使用方式可以参考 AndroidPerformanceMonitor。