这是我参与「第四届青训营 」笔记创作活动的第10天
初识性能优化及工具
课程回顾
看 PPT 好像回顾的是架构的课程,这里就写点自己的理解了
完美的架构是否还需要性能优化? 个人觉得没有 "完美" 的架构,架构是需要不断演进的,自然需要做优化。
为什么要做性能优化
首先性能优化能带来体验的改善,进而帮助业务指标的提升
另外硬件提升缓慢,而 ARM 平台受益于稳定迭代的架构,引入了很多优化方法
多核带来的性能提升取决于可以真正并行执行的部分,并行任务之间需要串行执行和同步执行的时间比 10%,提升幅度差距大。
优化还可以减少电量等资源消耗。
性能优化是什么?
-
性能优化的主要目标是什么
- 快:最快显示效率、最快网络速率、最快UI响应
- 稳:最佳用户体验,减少强打断
- 省:最低存储、最低功耗、最低流量消耗、最低计算资源
广义的性能优化分类
流畅度优化:快-极致的响应与流畅的体验
资源优化:省-最小负载带来最大的收益
稳定性优化:稳-稳定的实现,减少不必要打断
系统级优化:拓展-底层booster
流畅性优化基础介绍
Google 定义:界面呈现是指从应用生成帧并将其显示在屏幕上的动作。要确保用户能够流畅地与应用互动,应用呈现每帧的时间不应超过 16ms,以达到每秒 60 帧的呈现速度(为什么是 60fps?)。 如果应用存在界面呈现缓慢的问题,系统会不得不跳过一些帧,这会导致用户感觉应用不流畅,我们将这种情况称为卡顿。
思考:为什么是 60fps 或 16ms?
16ms 意味着 1000/60hz,相当于 60fps。这是因为人眼与大脑之间的协作无法感知超过 60fps 的画面更新。12fps 大概类似手动快速翻动书籍的帧率, 这明显是可以感知到不够顺滑的。24fps 使得人眼感知的是连续线性的运动,这其实是归功于运动模糊的效果。 24fps 是电影胶圈通常使用的帧率,因为这个帧率已经足够支撑大部分电影画面需要表达的内容,同时能够最大的减少费用支出。 但是低于 30fps 是 无法顺畅表现绚丽的画面内容的,此时就需要用到 60fps 来达到想要的效果,超过 60fps 就没有必要了。如果我们的应用没有在 16ms 内完成屏幕刷新的全部逻辑操作,就会发生卡顿。
资源优化基础介绍
- 最大化资源调度:最大资源调度分为两个方向,第一个方向将一些有限资源尽量向最影响体验的方向靠拢,首先要满足快,我们需要在网络、渲染等领域提供更多的资源。第二个方向,探寻更多的资源供给,首先要满足多,深度剖析系统资源使用,需要从系统层榨取更多的可用资源,也需要从用户敏感角度榨取更多资源,最终实现最大化资源调度。
- 最小化资源使用:需要对持续性的指标影响降到最低,首先要满足省,我们对功耗、存储、流量等指标在一些用户敏感度大于实际获得的体验的环境下,保障核心功能的体验。本质的原则就是保证业务基本面的前提下,通过降级或者优化等手段,将占用的资源做到最小。
稳定性优化基础介绍
- 当性能劣化累积到一定程度,就变成了稳定性问题,直接强制打断应用的使用体验。
ANR: Application Not Responding 应用程序无响应
系统级优化
主要是了解系统底层模块的特性,以及作用到程序上的机制,利用这些能力来帮助提升性能。
最佳工具选型
性能监控工具的价值
App的性能问题包括崩溃、网络请求错误或超时、响应速度慢、列表滚动卡顿、流量大、耗电等等。而导致App性能低下的原因有很多,除去设备硬件和软件的外部因素,其中大部分是开发者错误地使用线程、锁、系统函数、编程范式、数据结构等导致的。即便是最有经验的程序员,也很难在开发时就能避免所有导致性能低下的“坑”,因此解决性能问题的关键是在于能不能尽早地发现和定位这些“坑”。
Gpu呈现模式
原理:系统通过记录每一帧的相关数据,然后通过图形的形式呈现
优点:无需二次开发,简单易用
缺点:并不完全准确,且无法明确指出造成卡顿问题的具体原因
下面是有关输出的几点注意事项:
- 对于每个可见应用,该工具将显示一个图形。
- 沿水平轴的每个竖条代表一个帧,每个竖条的高度表示渲染该帧所花的时间(以毫秒为单位)。
- 水平绿线表示 16 毫秒。要实现每秒 60 帧,代表每个帧的竖条需要保持在此线以下。当竖条超出此线时,可能会使动画出现暂停。
- 该工具通过加宽对应的竖条并降低透明度来突出显示超出 16 毫秒阈值的帧。
- 每个竖条都有与渲染管道中某个阶段对应的彩色区段。区段数因设备的 API 级别不同而异。
下表介绍了使用运行 Android 6.0 及更高版本的设备时分析器输出中某个竖条的每个区段。
Layertool
抖音自研工具,快速识别图层及过度绘制、绘制区域过大等问题,提升整体渲染效率。
原理 : 通过遍历 ViewTree 信息,输出View层继关系
优点 :清楚明了,可以宏观感知 ViewTree 现状,也可以定制帮助分析overdraw
缺点:还不能够清楚明确的分析出UI的性能瓶颈
CPU Profiler
如需打开 CPU 性能剖析器,请按以下步骤操作:
- 依次选择 View > Tool Windows > Profiler 或点击工具栏中的 Profile 图标
- 如果 Select Deployment Target 对话框显示提示,请选择需将您的应用部署到哪个设备上以进行性能剖析。如果您已通过 USB 连接设备但系统未列出该设备,请确保您已启用 USB 调试。
- 点击 CPU 时间轴上的任意位置以打开 CPU 性能剖析器。
- 当打开 CPU 性能分析器时,它会立即开始显示应用的 CPU 使用率和线程活动。
如图 2 所示,CPU 性能分析器的默认视图包括以下时间轴:
-
事件时间轴:显示应用中的 activity 在其生命周期内不断转换经历各种不同状态的过程,并指示用户与设备的交互,包括屏幕旋转事件。如需了解如何在搭载 Android 7.1(API 级别 25)及更低版本的设备上启用事件时间轴,请参阅启用高级性能分析功能。
-
CPU 时间轴:显示应用的实时 CPU 使用率(以占总可用 CPU 时间的百分比表示)以及应用当前使用的线程总数。此时间轴还会显示其他进程(如系统进程或其他应用)的 CPU 使用率,以便您可以将其与您应用的 CPU 使用率进行对比。您可以通过沿时间轴的横轴方向移动鼠标来检查历史 CPU 使用率数据。
-
线程活动时间轴:列出属于应用进程的每个线程,并使用下面列出的颜色在时间轴上指示它们的活动。记录轨迹后,您可以从此时间轴上选择一个线程,以在轨迹窗格中检查其数据。
- 绿色:表示线程处于活动状态或准备使用 CPU。也就是说,线程处于正在运行或可运行状态。
- 黄色:表示线程处于活动状态,但它正在等待一项 I/O 操作(如磁盘或网络 I/O),然后才能完成它的工作。
- 灰色:表示线程正在休眠且没有消耗任何 CPU 时间。 当线程需要访问尚不可用的资源时,就会出现这种情况。在这种情况下,要么线程主动进入休眠状态,要么内核将线程置于休眠状态,直到所需的资源可用。
-
CPU 性能分析器还会报告 Android Studio 和 Android 平台添加到应用进程的线程的 CPU 使用率,这些线程包括
JDWP、Profile Saver、Studio:VMStats、Studio:Perfa和Studio:Heartbeat等(不过,它们在线程活动时间轴上显示的确切名称可能有所不同)。Android Studio 报告此数据是为了方便您确定线程活动和 CPU 使用率什么时候是由应用的代码实际引发的。
TraceView
如需通过 Android Studio 中的 Traceview 打开跟踪日志,请按以下步骤操作:
- 启动 Android Device Monitor。
- 在 Android Device Monitor 中,依次选择 File > Open File。
- 转到您要检查的
.trace文件。 - 点击 Open。
打开跟踪日志后,Traceview 会使用以下两个窗格显示日志数据:
Systrace
systrace 命令会调用 Systrace 工具,您可以借助该工具收集和检查设备上在系统一级运行的所有进程的时间信息。
本文档说明了如何通过命令行生成 Systrace 报告。在搭载 Android 9(API 级别 28)或更高版本的设备上,您还可以使用“系统跟踪”系统应用生成 Systrace 报告。
如需运行 systrace,请完成以下步骤:
- 从 Android Studio 下载并安装最新的 Android SDK 工具。
- 安装 Python 并将其添加到工作站的
PATH环境变量中。 - 将
android-sdk/platform-tools/添加到PATH环境变量。此目录包含由systrace程序调用的 Android 调试桥二进制文件 (adb)。 - 使用 USB 调试连接将搭载 Android 4.3(API 级别 18)或更高版本的设备连接到开发系统。
systrace 命令在 Android SDK 工具软件包中提供,并且可以在 android-sdk/platform-tools/systrace/ 中找到。
语法
如需为应用生成 HTML 报告,您需要使用以下语法通过命令行运行 systrace:
python systrace.py [options] [categories]
例如,以下命令会调用 systrace 来记录设备活动,并生成一个名为 mynewtrace.html 的 HTML 报告。此类别列表是大多数设备的合理默认列表。
$ python systrace.py -o mynewtrace.html sched freq idle am wm gfx view \
binder_driver hal dalvik camera input res
提示:如果要在跟踪输出中查看任务名称,必须在命令参数中添加 sched 类别。
如需查看已连接设备支持的类别列表,请运行以下命令:
$ python systrace.py --list-categories
如果您未指定任何类别或选项,systrace 会生成包含所有可用类别的报告,并使用默认设置。可用类别取决于您所使用的已连接设备。
命令和命令选项
| 命令和选项 | 说明 | |
|---|---|---|
| -o file | 将 HTML 跟踪报告写入指定的文件。如果您未指定此选项,systrace 会将报告保存到 systrace.py 所在的目录中,并将其命名为 trace.html。 | |
| -t N | --time=N | 跟踪设备活动 N 秒。如果您未指定此选项,systrace 会提示您在命令行中按 Enter 键结束跟踪。 |
| -b N | --buf-size=N | 使用 N KB 的跟踪缓冲区大小。使用此选项,您可以限制跟踪期间收集到的数据的总大小。 |
| -k functions | --ktrace=functions | 跟踪逗号分隔列表中指定的特定内核函数的活动。 |
| -a app-name | --app=app-name | 启用对应用的跟踪,指定为包含进程名称的逗号分隔列表。这些应用必须包含 Trace 类中的跟踪检测调用。您应在分析应用时指定此选项。很多库(例如 RecyclerView)都包括跟踪检测调用,这些调用可在您启用应用级跟踪时提供有用的信息。如需了解详情,请参阅定义自定义事件。如需跟踪搭载 Android 9(API 级别 28)或更高版本的设备上的所有应用,请传递用添加引号的通配符字符 "*"。 |
| --from-file=file-path | 根据文件(例如包含原始跟踪数据的 TXT 文件)创建交互式 HTML 报告,而不是运行实时跟踪。 | |
| -e device-serial | --serial=device-serial | 在已连接的特定设备(由对应的设备序列号标识)上进行跟踪。 |
| categories | 包含您指定的系统进程的跟踪信息,如 gfx 表示用于渲染图形的系统进程。您可以使用 -l 命令运行 systrace,以查看已连接设备可用的服务列表。 |
btrace-进阶
rhea-systrace:全函数插桩,自动生成 Trace 代码,对层数做限制,性能损耗 50%
rhea-mtrace:全函数插桩,抛弃 systrace,自己统计函数耗时,最后数据展现同 systrace
rhea-atrace:优化systace性能,聚合更多性能数据
Battery Historian
如需使用 Batterystats 从您的设备收集数据并在 Battery Historian 中打开该数据,请执行以下操作:
- 将移动设备连接到计算机。
- 在终端窗口中,关闭正在运行的 adb 服务器。
adb kill-server
- 重启 adb 并检查是否有已连接的设备。
adb devices
系统应该会列出您的设备,类似于下面的示例输出。
List of devices attached
048d023709ef2323 device
如果您没有看到任何设备,请确保您的手机已连接,且 USB 调试功能已开启,然后终止并重启 adb。
- 重置电池数据收集。
adb shell dumpsys batterystats --reset
设备始终会在后台收集 Batterystats 和其他调试信息。重置操作会清除旧的电池收集数据。如果不重置,输出内容会非常大。
- 断开设备与计算机的连接,以便仅消耗设备电池的电量。
- 使用您的应用并执行您想要获取数据的操作;例如,断开 WLAN 连接并将数据发送到云端。
- 重新连接手机。
- 确保系统已识别您的手机:
adb devices
- 转储所有电池数据。此过程可能需要一段时间:
adb shell dumpsys batterystats > [path/]batterystats.txt
系统会使用可选路径参数在您指定的目录中创建 batterystats.txt 文件。如果您没有指定路径,该文件会在您的主目录中创建。
- 使用原始数据生成报告。
对于搭载 Android 7.0 及更高版本的设备:
adb bugreport [path/]bugreport.zip
对于搭载 Android 6.0 及更低版本的设备:
adb bugreport [path/]bugreport.txt
错误报告可能需要几分钟才能完成。在完成之前,请勿断开设备连接或取消该进程。
与上面的 batterystats.txt 一样,这些文件是系统使用可选的 path 参数在您指定的目录中创建的。如果您没有指定路径,系统将在您的主目录中创建这些文件。
如果 Battery Historian 尚未运行,请使用以下命令运行:
docker --run -p port_number:9999 gcr.io/android-battery-historian:2.1 --port 9999
- 如需在 Battery Historian 中查看数据,请在浏览器中打开 Battery Historian。(对于 Mac 和 Linux,Battery Historian 在
http://localhost:port_number运行。对于 Windows,Battery Historian 在http://your_IP_address:port_number运行。) - 点击 Browse,然后选择您在上面创建的错误报告文件。
- 点击 Submit。Battery Historian 将打开您根据 Batterystats 数据创建的图表。
补充说明
可以使用两个工具查看systrace和btrace的生成文件,请使用chrome浏览器打开:
chrome://tracing/
perfetto.dev/
- Perfetto 是 Android 10 中引入的全新平台级跟踪工具。这是适用于 Android、Linux 和 Chrome 的更加通用和复杂的开源跟踪项目。与 Systrace 不同,它提供数据源超集,可让您以 protobuf 编码的二进制流形式记录任意长度的跟踪记录。您可以在 Perfetto 界面中打开这些跟踪记录。
这两份报告都提供了 Android 设备在给定时间段内的系统进程的总体情况。该报告还检查了捕获到的跟踪信息,以突出显示发现的问题(例如界面卡顿或耗电量高)。
Perfetto 和 Systrace 可交互使用:
- 在 Perfetto 界面中打开 Perfetto 文件和 Systrace 文件。在 Perfetto 界面中使用旧版 Systrace 查看器打开 Systrace 文件(使用 Open with legacy UI 链接)。
- 使用
traceconv工具将 Perfetto 跟踪记录转换为旧版 Systrace 文本格式。
性能优化技术案例
现状分析
耗时拆解
- CPU Time 指占用CPU进行计算所花费的时间绝对值,中断、挂起、休眠等行为是不会增加CPU Time的,所以因CPU Time开销占比高导致的不合理耗时点往往是逻辑本身复杂冗长需要消耗较多cpu时间片才能处理完。比较常见的高CPU占用是循环,比如抖音启动时遇到过一个so加载耗时,最后定位原因是在解压so的时候,遍历ZipEntry的次数过多导致,一个可行的优化策略就是可以把so所在的ZipEntry提前,遍历完so的ZipEntry之后可以提前中止遍历,而不需要遍历剩下的无效ZipEntry。
- CPU Schedule 在分析时主要针对主线程,是指主线程处于可执行状态但获取不到cpu时间片,这类耗时可能和线程调度等有关,最终导致分配给主线程的cpu时间片不足以及时处理完其内任务。由于主线程的线程优先级比其他线程的优先级要高很多,通常影响并不大,事实上抖音做了线上用户的启动耗时统计,这部分的耗时占比也是不大的。不过有一个场景需要关注,就是渲染,渲染是需要RenderThread提交GPU的渲染命令,而RenderThread并没有主线程那么高的优先级,因此比较容易受CPU的负载的影响,导致渲染耗时。
- IO Wait 指发生了IO操作需要等待IO返回结果,这类耗时可能发生在读取资源和文件,类加载,甚至在内存不足时的PageFault都会导致IO Wait。
- Lock Wait 也是主要针对主线程,指其处于等锁状态,等待被其他线程唤醒或自己超时唤醒,导致这类耗时的问题种类多样,大体也是可以分为业务锁和系统锁,业务锁主要是被主线程等待的业务逻辑未能及时处理完,优化思路一般是移除主线程的锁等待逻辑或者加快被等待的业务逻辑的执行速度。系统锁主要有:String InternTable Lock,ClassLinker Lock,GC Wait Lock等
- IPC 指进程间通信,操作系统大都含有相应的机制,Android中所特有的IPC机制是Binder,由于进行IPC调用往往需要等待通信结果本质上这也算是一种Lock Wait,但Android特有Binder机制所以单独列出,这类耗时可采用减少或替代Binder调用等手段来优化。
运行环境归因
- 前台运行
- 后台运行
抖音启动分析
列举了抖音的整体的启动耗时拆解及优化策略
渲染分析及耗时归因
从任务角度和耗时角度拆解一帧的耗时
优化案例分享
-
拆解View显示流程中的耗时成因
一个布局从 xml 变成 view 对象的耗时点:xml IO、class 反射、创建 view、Asset资源大锁
-
UI 构建
- 在UI构建阶段中首先要对界面布局的xml文件进行解析,这会导致IO Wait耗时,在接下来要解析xml文件中的TagName从而获取对应View的class会用到反射、创建各子View实例并生成View树又会用到循环递归,两部分都会增加CPU Time的开销。
-
数据绑定
- 然后是数据绑定阶段,该阶段主要分两部分,一部分是对数据做请求、解析、适配,另一是部分是将适配好的数据填充进UI中,前一部分往往会涉及到Json解析成Data Class实例,这里就可能涉及反射、循环遍历嵌套的数据类结构等增加CPU Time的操作。
-
View显示
- 最后是View显示阶段,常见的measure、layout、draw三大渲染View的步骤就在其中,它们同样会产生递归遍历父子View的耗时,此外这里还涉及将应用层计算好的渲染View的数据传递给系统层做最终的像素点排布,那么必然又会产生IPC耗时
UI构建的优化方案
数据绑定的优化方案
渲染和布局优化方案
系统工具查看具体的overdraw情况,但并不能细化到具体的布局和位置,我们开发了LayerTool工具可以根据自定义需求过滤各个布局,然后在界面上提示对应的位置,可以有效的查找导致overdraw的布局。我们通过LayerTool工具查看哪些布局导致了过度绘制。然后我们可以逐个开始优化:
- 移除不必要的背景图 :去除冗余背景
- 修改不合理布局:精细化布局显示
- 写高效合理的布局:随着状态的变换,及时调整布局的绘制背景
- 移除默认的 Window 背景 :Activity其实是有一层默认背景windowBackground。如果需要做到极致的优化,可以考虑将windowBackground设置为空,但是同时需要处理好随之而来的各种问题。主要包括:把windowBackground设置为空后,如果有某些View没有设置背景,就会出现花屏/重影的现象。
QA
- 性能优化书籍和资料推荐
比较喜欢源码,具体问题具体分析
- Battery Historian 抛出耗电原因为未知,该如何分析
Battery Historian 只能归因分析,不能直接确定成因,需要其他工具辅助,抖音内部有工具,外部有无开源不了解。