编程进阶网编程进阶网
  • 基础组成体系
  • 程序编程原理
  • 异常和IO系统
  • 六大设计原则
  • 设计模式导读
  • 创建型设计模式
  • 结构型设计模式
  • 行为型设计模式
  • 设计模式案例
  • 面向对象思想
  • 基础入门
  • 高级进阶
  • JVM虚拟机
  • 数据集合
  • Java面试题
  • C语言入门
  • C综合案例
  • C标准库
  • C语言专栏
  • C++入门
  • C++综合案例
  • C++专栏
  • HTML
  • CSS
  • JavaScript
  • 前端专栏
  • Swift
  • iOS入门
  • 基础入门
  • 开源库解读
  • 性能优化
  • Framework
  • 方案设计
  • 媒体音视频
  • 硬件开发
  • Groovy
  • 常用工具
  • 大厂面试题
  • 综合案例
  • 网络底层
  • Https
  • 网络请求
  • 故障排查
  • 专栏
  • 数组
  • 链表
  • 栈
  • 队列
  • 树
  • 递归
  • 哈希
  • 排序
  • 查找
  • 字符串
  • 其他
  • Bash脚本
  • Linux入门
  • 嵌入式开发
  • 代码规范
  • Markdown
  • 开发理论
  • 开发工具
  • Git管理
  • 百宝箱
  • 开源协议
  • 技术招聘
  • 测试经验
  • 职场提升
  • 技术模版
  • 关于我
  • 目标清单
  • 学习框架
  • 育儿经验
  • 我的专栏
  • 底层能力
  • 读书心得
  • 随笔笔记
  • 职场思考
  • 中华历史
  • 经济学故事
  • 基础组成体系
  • 程序编程原理
  • 异常和IO系统
  • 六大设计原则
  • 设计模式导读
  • 创建型设计模式
  • 结构型设计模式
  • 行为型设计模式
  • 设计模式案例
  • 面向对象思想
  • 基础入门
  • 高级进阶
  • JVM虚拟机
  • 数据集合
  • Java面试题
  • C语言入门
  • C综合案例
  • C标准库
  • C语言专栏
  • C++入门
  • C++综合案例
  • C++专栏
  • HTML
  • CSS
  • JavaScript
  • 前端专栏
  • Swift
  • iOS入门
  • 基础入门
  • 开源库解读
  • 性能优化
  • Framework
  • 方案设计
  • 媒体音视频
  • 硬件开发
  • Groovy
  • 常用工具
  • 大厂面试题
  • 综合案例
  • 网络底层
  • Https
  • 网络请求
  • 故障排查
  • 专栏
  • 数组
  • 链表
  • 栈
  • 队列
  • 树
  • 递归
  • 哈希
  • 排序
  • 查找
  • 字符串
  • 其他
  • Bash脚本
  • Linux入门
  • 嵌入式开发
  • 代码规范
  • Markdown
  • 开发理论
  • 开发工具
  • Git管理
  • 百宝箱
  • 开源协议
  • 技术招聘
  • 测试经验
  • 职场提升
  • 技术模版
  • 关于我
  • 目标清单
  • 学习框架
  • 育儿经验
  • 我的专栏
  • 底层能力
  • 读书心得
  • 随笔笔记
  • 职场思考
  • 中华历史
  • 经济学故事
  • 01.崩溃捕获设计实践
  • 02.崩溃治理优化总结
  • 03.Native崩溃治理实践
  • 04.ANR监控设计实践
  • 05.CPU消耗优化实践
  • 06.卡顿监控设计实践
  • 07.卡顿治理优化实践
  • 08.网络分析与优化实践
  • 09.线程优化实践操作
  • 10.高性能图片优化方案
  • 11.OOM异常优化实践
  • 12.内存监控优化方案
  • 13.内存治理优化实践
  • 14.FPS监测设计实践
  • 15.进程优化设计实践
  • 16.App启动优化实践
  • 17.App页面UI优化实践
  • 18.App稳定性专项实践
  • 19.App瘦身优化实践
  • 20.常见代码优化实践
  • 21.移动端防抓包实践
  • 22.App磁盘沙盒实践
  • 23.Ping工具开发实践
  • 24.Gradle构建优化实践
  • 25.CodeReview实践总结

07.卡顿治理优化实践

目录介绍

  • 01.卡顿基础介绍
    • 1.1 项目背景
    • 1.2 遇到问题
    • 1.3 基础概念
    • 1.4 卡顿指标
  • 02.治理卡顿的流程
    • 2.1 排查卡顿流程
    • 2.2 排查卡顿工具
    • 2.3 制定卡顿指标
  • 03.卡顿的成因分析
    • 3.1 渲染机制中卡顿点
    • 3.2 卡顿的分类
    • 3.3 核心关注的卡顿
  • 04.UI卡顿
    • 4.1 View卡顿点
    • 4.2 UI卡顿优化
    • 4.3 过渡绘制卡顿
  • 05.线程卡顿
    • 5.1 卡顿实践思路
    • 5.2 UI线程被阻塞

01.卡顿基础介绍

1.1 项目背景

1.2 遇到问题

1.3 基础概念

1.4 卡顿指标

02.治理卡顿的流程

2.1 排查卡顿流程

  • 优化卡顿的步骤
    • 监控卡顿的发生 ---> 准确定位到哪个模块(甚至哪个方法导致了卡) ---> 分析代码并解决卡顿
  • 监控卡顿的发生
    • 主要使用工具监控
  • 定位到卡顿的点
    • 待完善
  • 分析代码并解决卡顿
    • UI卡顿
    • 线程卡顿
    • 过渡绘制卡顿

2.2 排查卡顿工具

  • 卡顿一些工具
    • Systrace方法
    • Android CPU Profiler工具分析卡顿
    • 线上性能监控方法熟悉
    • FPS帧率检测

2.3 制定卡顿指标

  • FPS:帧率;取 vsync 到来的时间为起点,doFrame 执行完成的事件为终点,作为每帧的渲染耗时
    • 同时利用渲染耗时/刷新率可以得出每次渲染的丢帧数。平均 FPS = 一段时间内渲染帧的个数 * 60 / (渲染帧个数 + 丢帧个数)
  • 卡顿指标说明
    • stall_video_ui_rate:总卡顿率;(UI 卡顿时长 + 流卡顿时长) / 采集时长
    • stall_ui_rate:UI 卡顿率;【> 3 帧】UI 卡顿时长 / 采集时长
    • stall_video_rate:流卡顿率;流卡顿时长 / 采集时长
    • stall_ui_slight_rate:轻微卡顿率;【3 - 6】帧丢帧时长 / 采集时长
    • stall_ui_moderate_rate:中等卡顿率;【7 - 13】帧丢帧时长 / 采集时长
    • stall_ui_serious_rate:严重卡顿率;【> 14】帧丢帧时长 / 采集时长vsync

03.卡顿的成因分析

3.1 渲染机制中卡顿点

  • View渲染的一生。可以理解为在16.6ms做了那些事情。
    • image
      image
  • 卡顿的点有哪些呢
    • 渲染机制中的任何流转过程发生异常,均会造成卡顿。逐个分析,看看都会有哪些原因可能造成卡顿。
  • 渲染流程中的卡顿分析
    • Vsync 调度:这个是起始点,但是调度的过程会经过线程切换以及一些委派的逻辑,有可能造成卡顿,但是一般可能性比较小,我们也基本无法介入;
    • 消息调度:主要是 doframe Message 的调度,这就是一个普通的 Handler 调度,如果这个调度被其他的 Message 阻塞产生了时延,会直接导致后续的所有流程不会被触发。这里建立了一个 WatchDog 机制,可以通过优化消息调度达到插帧的效果,使得界面更加流畅;
    • 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 配置等;
    • 推流异常:这个属于数据源出了问题,这里暂时以用户侧的视角为主,暂不讨论。
  • 系统负载
    • 内存:内存的吃紧会直接导致 GC 的增加甚至 ANR,是造成卡顿的一个不可忽视的因素;
    • CPU:CPU 对卡顿的影响主要在于线程调度慢、任务执行的慢和资源竞争,比如降频会直接导致应用卡顿;
    • GPU:GPU 的影响见渲染流程,但是其实还会间接影响到功耗和发热;
    • 功耗/发热:功耗和发热一般是不分家的,高功耗会引起高发热,进而会引起系统保护,比如降频、热缓解等,间接的导致卡顿。

3.2 卡顿的分类

  • 在一定程度上,我们遇到的所有卡顿问题,均能在这里找到理论依据,这也是指导我们优化卡顿问题的理论支撑。
    • image
      image

3.3 核心关注的卡顿

04.UI卡顿

4.1 View卡顿点

  • 产生卡顿原因有很多,主要有以下几点:
    • 布局Layout过于复杂,无法在16ms内完成渲染。
    • 同一时间动画执行的次数过多,导致CPU和GPU负载过重。
    • View过渡绘制,导致某些像素在同一帧时间内被绘制多次。
    • 在UI线程中做了稍微耗时的操作。
    • GC回收时暂停时间过长或者频繁的GC产生大量的暂停时间。

4.2 UI卡顿优化

  • 对于 UI 卡顿来说,我们手握卡顿优化的 8 板大斧子,所向披靡:
    • 下线代码;
    • 减少执行次数;
    • 异步;
    • 打散;
    • 预热;
    • 复用;
    • 方案优化;
    • 硬件加速;

05.线程卡顿

5.2 UI线程被阻塞

  • 如果UI线程受到阻塞,显而易见的是,我们的Traversal过程也将受阻塞!画面卡顿是妥妥的发生啊。这就是为什么大家一直在强调不要在UI线程做耗时操作的原因。通常UI线程的阻塞和以下原因脱不了关系。
    • 在UI线程中进行IO读写数据的操作。这是一个很费时的过程好吗?千万别这么干。如果不想获得一个卡到爆的App的话,把IO操作统统放到子线程中去。
    • 在UI线程中进行复杂的运算操作。运算本身是一个耗时的操作,当然简单的运算几乎瞬间完成,所以不会让你感受到它在耗时。但是对于十分复杂的运算,对时间的消耗是十分辣眼睛的!如果不想获得一个卡到爆的App的话,把复杂的运算操作放到子线程中去。
    • 在UI线程中进行复杂的数据处理。我说的是比如数据的加密、解密、编码等等。这些操作都需要进行复杂运算,特别是在数据比较复杂的时候。如果不想获得一个卡到爆的App的话,把复杂数据的处理工作放到子线程中去。
    • 频繁的发生GC,导致UI线程被频繁中断。在Java中,发生GC(垃圾回收)意味着Stop-The-World,就是说其它线程全部会被暂停啊。好可怕!正常的GC导致偶然的画面卡顿是可以接受的,但是频繁发生就让人很蛋疼了!频繁GC的罪魁祸首是内存抖动。简单的说就是在短时间内频繁的创建大量对象,导致达到GC的阀值,然后GC就发生了。如果不想获得一个卡到爆的App的话,把内存的管理做好,即使这是Java。

05.内存抖动

  • 什么是内存抖动?
    • 是由于短时间内有大量对象进出Young Generiation区导致的,它伴随着频繁的GC。在Java内存管理机制中我提到过内存抖动会引起频繁的GC,从而使UI线程被频繁阻塞,导致画面卡顿。
  • 避免发生内存抖动的几点建议:
    • 尽量避免在循环体内创建对象,应该把对象创建移到循环体外。
    • 注意自定义View的onDraw()方法会被频繁调用,所以在这里面不应该频繁的创建对象。
    • 当需要大量使用Bitmap的时候,试着把它们缓存在数组中实现复用。
    • 对于能够复用的对象,同理可以使用对象池将它们缓存起来。
  • 内存抖动是由于大量对象在短时间内被配置而引起的,所以要做的就是谨慎对待那些可能会大量创建对象的情况。
    • 这块可以看我的这个开源项目,有效使用对象池避免对象大量创建。图片缩放控件

2.3 卡顿根本原因

  • 视图刷新原理:
    • View 的 requestLayout 和 ViewRootImpl##setView 最终都会调用 ViewRootImpl 的 requestLayout 方法,然后通过 scheduleTraversals 方法向 Choreographer 提交一个绘制任务,然后再通过 DisplayEventReceiver 向底层请求 vsync 垂直同步信号,当 vsync 信号来的时候,会通过 JNI 回调回来,在通过 Handler 往消息队列 post 一个异步任务,最终是 ViewRootImpl 去执行绘制任务,最后调用 performTraversals 方法,完成绘制。
  • 卡顿的根本原因:
    • 从刷新原理来看卡顿的根本原理是有两个地方会造成掉帧:
    • 一个是主线程有其它耗时操作,导致doFrame 没有机会在 vsync 信号发出之后 16 毫秒内调用;
    • 还有一个就是当前doFrame方法耗时,绘制太久,下一个 vsync 信号来的时候这一帧还没画完,造成掉帧。

02.复杂的视图树

  • 如果视图树复杂,会使整个Traversal过程变长。
    • 因此,我们在开发过程中要控制视图树的复杂程度。减少不必要的层级嵌套。比如使用RelativeLayout可以减少复杂布局的嵌套。
  • 频繁的触发 requestLayout()
    • 就可能会导致在一帧的周期内,频繁的发生布局计算,这也会导致整个Traversal过程变长。
    • 有的ViewGroup类型的控件,比如RelativeLayout,在一帧的周期内会通过两次layout()操作来计算确认子View的位置,这种少量的操作并不会引起能够被注意到的性能问题。
    • 但是如果在一帧的周期内频繁的发生layout()计算,就会导致严重的性能,每次计算都是要消耗时间的!
    • 而requestLayout()操作,会向ViewRootImpl中一个名为mLayoutRequesters的List集合里添加需要重新Layout的View,这些View将在下一帧中全部重新layout()一遍。通常在一个控件加载之后,如果没什么变化的话,它不会在每次的刷新中都重新layout()一次,因为这是一个费时的计算过程。所以,如果每一帧都有许多View需要进行layout()操作,可想而知你的界面将会卡到爆!卡到爆!需要注意,setLayoutParams()最终也会调用requestLayout(),所以也不能烂用!

05.View卡顿原因

  • 产生卡顿原因有很多,主要有以下几点:
  • 布局Layout过于复杂,无法在16ms内完成渲染。
  • 同一时间动画执行的次数过多,导致CPU和GPU负载过重。
  • View过渡绘制,导致某些像素在同一帧时间内被绘制多次。
  • 在UI线程中做了稍微耗时的操作。
  • GC回收时暂停时间过长或者频繁的GC产生大量的暂停时间。

06.自定义View优化策略

  • 为了加速你的view,对于频繁调用的方法,需要尽量减少不必要的代码。先从onDraw开始,需要特别注意不应该在这里做内存分配的事情,因为它会导致GC,从而导致卡顿。在初始化或者动画间隙期间做分配内存的动作。不要在动画正在执行的时候做内存分配的事情。
  • 还需要尽可能的减少onDraw被调用的次数,大多数时候导致onDraw都是因为调用了invalidate().因此请尽量减少调用invaildate()的次数。如果可能的话,尽量调用含有4个参数的invalidate()方法而不是没有参数的invalidate()。没有参数的invalidate会强制重绘整个view。
  • 另外一个非常耗时的操作是请求layout。任何时候执行requestLayout(),会使得Android UI系统去遍历整个View的层级来计算出每一个view的大小。如果找到有冲突的值,它会需要重新计算好几次。另外需要尽量保持View的层级是扁平化的,这样对提高效率很有帮助。
  • 如果你有一个复杂的UI,你应该考虑写一个自定义的ViewGroup来执行他的layout操作。与内置的view不同,自定义的view可以使得程序仅仅测量这一部分,这避免了遍历整个view的层级结构来计算大小。这个PieChart
  • 例子展示了如何继承ViewGroup作为自定义view的一部分。PieChart有子views,但是它从来不测量它们。而是根据他自身的layout法则,直接设置它们的大小。

05.参考资料

  • 需要列出方案设计过程的文档,包括但不局限于PM需求文档,技术参考文档等。
  • https://blog.csdn.net/u011578734/article/details/109582595
  • https://github.com/Knight-ZXW/BlockCanaryX
  • https://blog.csdn.net/chuyouyinghe/article/details/131576008
  • 优秀:https://juejin.cn/post/6844904066259091469
贡献者: yangchong211
上一篇
06.卡顿监控设计实践
下一篇
08.网络分析与优化实践