View 的视觉效果正是通过这一整条复杂的链路一步步展示出来的,有了这个前提,那就可以得出一个结论:上述任意链路发生卡顿,均会造成卡顿。
2.3 生产者和消费者我们再回到 Vsync 的话题,消费 Vsync 的双方分别是 App 和 sf,其中 App 代表的是生产者,sf 代表的是消费者,两者交付的中间产物则是 surface buffer。
再具体一点,生产者大致可以分为两类,一类是以 window 为代表的页面,也就是我们平时所看到的 view 树这一套;另一类是以视频流为代表的可以直接和 surface 完成数据交换的来源,比如相机预览等。
对于一般的生产者和消费者模式,我们知道会存在相互阻塞的问题。比如生产者速度快但是消费者速度慢,亦或是生产者速度慢消费者速度快,都会导致整体速度慢且造成资源浪费。所以 Vsync 的协同以及双缓冲甚至三缓冲的作用就体现出来了。
思考一个问题:是否缓冲的个数越多越好?过多的缓冲会造成什么问题?
答案是会造成另一个严重的问题:lag,响应延迟
这里结合 view 的一生,我们可以把两个流程合在一起,让我们的视角再高一层:
2.4 机制上的保护这里我们来回答第三个问题,从系统的渲染架构上来说,机制上的保护主要有几方面:
- Vsync 机制的协同;
- 多缓冲设计;
- surface 的提供;
- 同步屏障的保护;
- 硬件绘制的支持;
- 渲染线程的支持;
- GPU 合成加速;
这些机制上的保护在系统层面最大程度地保障了 App 体验的流畅性,但是并不能帮我们彻底解决卡顿。为了提供更加流畅的体验,一方面,我们可以加强系统的机制保护,比如 FWatchDog;另一方面,需要我们从 App 的角度入手,治理应用内的卡顿问题。
2.5 再看卡顿的成因经过上面的讨论,我们得出一个卡顿分析的核心理论支撑:渲染机制中的任何流转过程发生异常,均会造成卡顿。
那么接下来,我们逐个分析,看看都会有哪些原因可能造成卡顿。
2.5.1 渲染流程- Vsync 调度:这个是起始点,但是调度的过程会经过线程切换以及一些委派的逻辑,有可能造成卡顿,但是一般可能性比较小,我们也基本无法介入;
- 消息调度:主要是 doframe Message 的调度,这就是一个普通的 Handler 调度,如果这个调度被其他的 Message 阻塞产生了时延,会直接导致后续的所有流程不会被触发。这里直播建立了一个 FWtachDog 机制,可以通过优化消息调度达到插帧的效果,使得界面更加流畅;
- input 处理:input 是一次 Vsync 调度最先执行的逻辑,主要处理 input 事件。如果有大量的事件堆积或者在事件分发逻辑中加入大量耗时业务逻辑,会造成当前帧的时长被拉大,造成卡顿。抖音基础技术同学也有尝试过事件采样的方案,减少 event 的处理,取得了不错的效果;
- 动画处理:主要是 animator 动画的更新,同理,动画数量过多,或者动画的更新中有比较耗时的逻辑,也会造成当前帧的渲染卡顿。对动画的降帧和降复杂度其实解决的就是这个问题;
- view 处理:主要是接下来的三大流程,过度绘制、频繁刷新、复杂的视图效果都是此处造成卡顿的主要原因。比如我们平时所说的降低页面层级,主要解决的就是这个问题;
- measure/layout/draw:view 渲染的三大流程,因为涉及到遍历和高频执行,所以这里涉及到的耗时问题均会被放大,比如我们会降不能在 draw 里面调用耗时函数,不能 new 对象等等;
- DisplayList 的更新:这里主要是 canvas 和 displaylist 的映射,一般不会存在卡顿问题,反而可能存在映射失败导致的显示问题;
- OpenGL 指令转换:这里主要是将 canvas 的命令转换为 OpenGL 的指令,一般不存在问题。不过这里倒是有一个可以探索的点,会不会存在一类特殊的 canvas 指令,转换后的 OpenGL 指令消耗比较大,进而导致 GPU 的损耗?有了解的同学可以探讨一下;
- buffer 交换:这里主要指 OpenGL 指令集交换给 GPU,这个一般和指令的复杂度有关。一个有意思的事儿是这里一度被我们作为线上采集 GPU 指标的数据源,但是由于多缓冲的因素数据准确度不够被放弃了;
- GPU 处理:顾名思义,这里是 GPU 对数据的处理,耗时主要和任务量和纹理复杂度有关。这也就是我们降低 GPU 负载有助于降低卡顿的原因;
- layer 合成:这里主要是 layer 的 compose 的工作,一般接触不到。偶尔发现 sf 的 vsync 信号被 delay 的情况,造成 buffer 供应不及时,暂时还不清楚原因;
- 光栅化/Display:这里暂时忽略,底层系统行为;
- Buffer 切换:主要是屏幕的显示,这里 buffer 的数量也会影响帧的整体延迟,不过是系统行为,不能干预。
除了上述的渲染流程引起的卡顿,还有一些其他的因素,典型的就是视频流。
- 渲染卡顿:主要是 TextureView 渲染,textureview 跟随 window 共用一个 surface,每一帧均需要一起协同渲染并相互影响,UI 卡顿会造成视频流卡顿,视频流的卡顿有时候也会造成 UI 的卡顿;
- 解码:解码主要是将数据流解码为 surface 可消费的 buffer 数据,是除了网络外最重要的耗时点。现在我们一般都会采用硬解,比软解的性能高很多。但是帧的复杂度、编码算法的复杂度、分辨率等也会直接导致解码耗时被拉长;
- OpenGL 处理:有时会对解码完成的数据做二次处理,这个如果比较耗时会直接导致渲染卡顿;
- 网络:这个就不再赘述了,包括 DNS 节点优选、cdn 服务、GOP 配置等;
- 推流异常:这个属于数据源出了问题,这里暂时以用户侧的视角为主,暂不讨论。
2.5.3 系统负载
- 内存:内存的吃紧会直接导致 GC 的增加甚至 ANR,是造成卡顿的一个不可忽视的因素;
- CPU:CPU 对卡顿的影响主要在于线程调度慢、任务执行的慢和资源竞争,比如降频会直接导致应用卡顿;
- GPU:GPU 的影响见渲染流程,但是其实还会间接影响到功耗和发热;
- 功耗/发热:功耗和发热一般是不分家的,高功耗会引起高发热,进而会引起系统保护,比如降频、热缓解等,间接的导致卡顿。
我们此处再整体整理并归类,为了更完备一些,这里将推流也放了上来。在一定程度上,我们遇到的所有卡顿问题,均能在这里找到理论依据,这也是指导我们优化卡顿问题的理论支撑。
3. 如何评价卡顿3.1 线上指标指标 | 释义 | 计算方式 | 数据来源 |
FPS | 帧率 | 取 vsync 到来的时间为起点,doFrame 执行完成的事件为终点,作为每帧的渲染耗时,同时利用渲染耗时/刷新率可以得出每次渲染的丢帧数。平均 FPS = 一段时间内渲染帧的个数 * 60 / (渲染帧个数 丢帧个数) | vsync |
stall_video_ui_rate | 总卡顿率 | (UI 卡顿时长 流卡顿时长) / 采集时长 | vsync |
stall_ui_rate | UI 卡顿率 | 【> 3 帧】UI 卡顿时长 / 采集时长 | vsync |
stall_video_rate | 流卡顿率 | 流卡顿时长 / 采集时长 | vsync |
stall_ui_slight_rate | 轻微卡顿率 | 【3 - 6】帧丢帧时长 / 采集时长 | vsync |
stall_ui_moderate_rate | 中等卡顿率 | 【7 - 13】帧丢帧时长 / 采集时长 | vsync |
stall_ui_serious_rate | 严重卡顿率 | 【> 14】帧丢帧时长 / 采集时长 | vsync |
Diggo 是字节自研的一个开放的开发调试工具平台,是一个集「评价、分析、调试」为一体的,一站式工具平台。内置性能测评、界面分析、卡顿分析、内存分析、崩溃分析、即时调试等基础分析能力,可为产品开发阶段提供强大助力。
指标 | 释义 | 计算方式 | 数据来源 |
FPS | 时机渲染帧率 | 数据获取时间周期内,实际渲染帧数/ 数据获取间隔时间 | SF & GFXInfo |
RFPS | 相对帧率 | 数据获取时间周期内,(理论满帧-实际掉帧数)/ 数据获取间隔时间 | GFXInfo |
Stutter | 卡顿率 | 卡顿比。当发生 jank 的帧的累计时长与区间时长的比值。 | SF |
Janky Count | 普通卡顿次数 | 单帧绘制耗时大于 MOVIE_FRAME_TIME 时,计一次 janky。 | SF |
Big Janky Count | 严重卡顿次数 | 单帧绘制耗时大于 3*MOVIE_FRAME_TIME 时,计一次 big janky。 | SF |
名称 | 释义 |
正式包慢函数 | 相对于灰度包,过滤了比较多监控,对性能损耗比较小,但是需要手动打开,单点反馈中不能保留反馈现场 |
灰度包慢函数 | 灰度上全量打开,针对版本间的数据对比和新增卡顿问题解决比较有效 |
ANR | ANR 的及时响应和处理 |
工具名 | 备注 |
Systrace | 暂不赘述 |
perfetto | 加强版 systrace,可定制,可以参考官方文档 |
Rhea | 最常用也是最好用的工具,方便发现下下问题和归因,和 perfetto 一起使用绝配,感兴趣的同学可以移步 github 搜索 btrace |
profiler | Androidstudio 自带工具,比较方便,但是数据准确度不高 |
sf / gfxinfo | 主要用于脚本和工具 |
这里主要针对 UI 卡顿和 UI/流相互影响打来的卡顿。
对于 UI 卡顿来说,我们手握卡顿优化的 8 板大斧子,所向披靡:
- 下线代码;
- 减少执行次数;
- 异步;
- 打散;
- 预热;
- 复用;
- 方案优化;
- 硬件加速;
总体思路就是「能不干就不干、能少干就少干、能早点干就早点儿干、能晚点儿干就晚点儿干、能让别人干就让别人干、能干完一次当 10 次就只干一次,实在不行,再考虑自己大干一场」。
这里例举出一些常见的优化思路,注意这一定也不可能是全部,如果有其他好的优化思路,我们可以一起交流。