抖音关于Android性能优化启动优化之理论和工具篇转载
前言
启动性能是 APP 使用体验的门面,启动过程耗时较长很可能导致用户使用 APP 的兴趣骤减,抖音通过对启动性能做劣化的 AB 实验也验证了其对于业务指标有影响显著。抖音拥有数亿的用户,启动耗时几百毫秒的增长就可能带来成千上万用户的留存缩减,因此,启动性能的优化成为了抖音 Android 基础技术团队在体验优化方向上的重中之重。
本文基于过往对抖音 Android 客户端做启动性能优化的实战经验总结提炼出普适性的方**,并将该过程中沉淀的工具加以分享,希望能给大家带来一些新的思考。
正文
带着问题出发
假如你要负责优化抖音的启动性能,你会怎样去规划整体的优化方案?你可能会一下子想到很多方面的细节点,比如:要优化主线程耗时、要减少布局层级、要对某些启动任务做按需加载或预加载、要避免主线程 IO、要对线程使用进行优化、还要有分析工具帮助定位性能问题等……
然而,该如何系统性地把这些细碎点组织起来并按照一定的章法来落地启动优化呢?此时,需要我们在具体细节点之上有进一步的问题分解与深入思考,最终形成一套完整的方**,不仅能覆盖所有细节点,还能切实指导在实战中达成启动优化的效果。切实有效的方**必然是从实战中经过千锤百炼才能形成的,而抖音庞大的用户基数又进一步保障了方**的可行性与普适性。那么接下来让我们带着前述问题来看抖音的启动优化方**是怎样的又是如何应用于实战之中的。
启动优化方**
抖音的启动性能优化方**分为五部分,分别是:理论分析、现状分析、启动性能优化、线上验证与防劣化。
图片
这五部分间存在明显的先后顺序,又能闭环达成可持续的启动性能优化,下面将对这五部分做详细阐述:
理论分析
理论分析放在最先是为了从一开始就避免让视野受到限制,很多同学往往一开始接手启动优化就容易陷入对各种现状细节的分析,拘泥于片面的潜在可优化点,这样就难以做到对全局和优先级的把控,所以,我们应该首先跳出现状,从更加全局的视角来思考整体优化的目标和策略。这里可以利用特斯拉创始人——埃隆·马斯克所推崇的“第一性原理”思考法:
“通过第一性原理,把事情升华到最根本的真理,然后从最核心处开始推理。”
基于此,我们在做启动优化的理论分析时可以从更本源的角度出发做到全局思考,比如抖音会做从进程创建到页面展示的全启动路径分阶段耗时分析、还会按照消耗的系统资源类型做耗时成因分析,通过这种极致的耗时分析可以带来极致的优化策略,此外,从全路径出发还能够发现容易忽视的问题、探索优化的极限。
现状分析
在完成理论分析后,我们基本具备了全局的视角,并且也大致清楚了整体的优化目标和策略,接下来就要基于此来做现状分析从而明晰实现目标的具体路径:
首先使用 profile 工具对可优化点进行摸底:其实不合理的高耗时点就是潜在的优化点,并能按照前述的理论分析归入一个或多个耗时成因中;
然后结合线上的指标数据确定最终优化方向:线下摸底的潜在优化点要结合其线上打点确认是否为普遍耗时,再根据耗时成因明确大致的优化思路、实施成本和预估收益。
在这部分需要尤其注意三点:优质的 profile 工具(这里推荐使用同样来自基础技术团队的“新一代全能型性能分析工具”)、线下 trace 结合线上监控综合分析、根据投入产出比评估实施优先级,这三点是保障切实有效取得启动优化收益的关键。
启动优化
在完成了理论和现状分析后,就可以根据规划的路径来实施具体的启动优化项了。在实施过程中,主要考虑主线程优化、后台线程优化和全局优化三个维度:
1、主线程耗时优化需要在启动全路径各阶段中细化具体的耗时成因,如:CPU Time、CPU Schedule、IO wait、Lock wait 等,完成耗时归因后可以使用逐步升级的优化策略来逐个击破:对于首屏所必须的耗时逻辑做正面优化(可使用缩减耗时逻辑、异步并发、延迟加载等手段)、对于非首屏必须的耗时逻辑做按需加载(需要架构优化的基础)、对于优化后仍存在耗时的逻辑尝试做业务降级(大都有损需评估全局收益);
2、后台线程优化策略与主线程类似,在此基础上还可以实施后台任务缩减、线程收敛、开启多进程等优化措施;此外,主线程和后台线程均存在较多启动任务且彼此间可能存在关联,因此,可以对全局的启动任务做依赖关系梳理并实施精细化的任务重排,旨在减少依赖任务间的等待耗时;
3、全局优化主要是指业务无关的通用的全局优化策略,如虚拟机层面或 IO 层面的优化等。
线上验证
在完成了具体的优化项施工后,就来到了线上验证大盘收益的阶段。这个阶段有三点需要注意:
1、线下的优化一定要有线上的指标反馈,线下的优化项因为设备或操作习惯差异往往难以评估是否具备普遍影响,只有当相应的线上指标取得正面反馈后才能验证拿到了有效的优化收益;
2、线上指标需要结合均值与分位值综合来评估,只关注启动耗时的均值往往会掩盖低分位设备的现状,这部分设备可能占比不高,对均值影响有限,但抖音庞大的用户基数乘以该比例仍旧是不小的数量,为了保障该部分用户的启动性能体验,抖音一般会分 50%、70%、90%三个分位值来评估指标;
3、在验证收益时通过 AB 实验达成,这样做不仅能控制变量确保优化项的严格有效,还能借此来观察性能优化所带来的业务指标收益,这些都可以作为规划后续启动优化方向的参考指导。
防劣化
在线上验证优化措施取得切实收益后,并不是万事大吉了,持续保持住优化效果才算完整达成了启动性能优化的目的。其实不仅是启动优化,整个性能优化领域都是围绕着“攻”和“守”来展开的,“攻”即为前述的分析与优化,而“守”则是防止劣化,在防劣化方面大家往往不会像优化的方面那么重视,但实际上能防止劣化是可持续取得优化效果的前提(否则新的优化效果会用于弥补劣化甚至入不敷出),并且防劣化相比于优化是更能持久有益的。
抖音启动性能防劣化的进程分为了三个时期,不同时期有不同的表现与应对手段,这很可能是大多数 APP 优化启动性能都要经历的,这里提炼出来以供参考:
快速下降期:此时一般位于启动优化的初始阶段,优化空间很大,伴随有小幅度的劣化但往往都能被更大幅度的优化抵消且还仍有收益,这时应该抓大放小,按照更高投入产出比的策略重点推进优化,同时也抽出少部分精力治理修复成本低的劣化。
瓶颈期:到了该时期绝大部分优化收益已经拿到,想进一步做到优化往往需要投入更多成本,且优化幅度有限,整体的投入产出比不高,同期还会伴随有中小幅的劣化,此时需要建立完善的线上线下监控体系,及时发现并修复劣化,此外还要通过架构改造从源头上限制劣化的发生,综合保障优化的收益不会被劣化抵消。
劣化期:这个时期往往出现在年关或重要节日期间,这类时间点往往有重要且紧急的活动项目上线,众多关联方面均要为其开绿灯,启动性能指标也不例外,为了保障活动效果可能要加入若干耗时的主线程启动任务,所带来的的劣化幅度往往比较大,此时需要对齐预期并在活动结束后及时修复。
启动优化方**的应用实践
古人云“纸上得来终觉浅,绝知此事要躬行”,前述的方**讲得再详细再透彻也会与实际的落地存在隔阂,为了做到真正的学以致用,下文将细致讲解如何将启动优化方**应用于实践之中。
理论分析的实践
抖音在理论分析部分会对启动流程分别作全路径分析和耗时成因分析,前者用于发现全路径各个阶段的潜在耗时点避免疏漏,后者用于系统性地将各个耗时点归因从而引导我们找寻优化思路,关于这两部分的具体实践如下:
启动性能全路径分析:抖音的启动路径和大多数 APP 类似,整体分为两大阶段和两个间隙,它们按时间顺序排布为:Application 阶段、handle message 间隙、Activity 阶段和数据加载间隙,全路径各部分细分涵盖的内容如下图所示:
APP 进程由 zygote 进程 fork 出来后会执行 ActivityThread 的 main 方法,该方法最终触发执行bindApplication,这也是 Application 阶段的起点;然后是我们在应用中能触达到的attachBaseContext阶段,4.x 的机型在该阶段具有较长的 MultiDex 耗时可以做针对性优化,本阶段也是最早的预加载时机;接下来是installProvider阶段,很多三方 sdk 借助该时机来做初始化操作,很可能导致启动耗时的不可控情形,需要按具体 case 优化;此后就到了 Application 的onCreate阶段,这里有很多三方库和业务的初始化操作,是通过异步、按需、预加载等手段做优化的主要时机,它也是 Application 阶段的末尾。
在Application 阶段和 Activity 阶段之间往往会不可避免地**入很多 post 到主线程的消息及相应待执行任务,这是拉长启动耗时的另一不可控问题点,需要加以监控治理或通过消息调度优化来尽量减小此间隙。
在来到 Activity 阶段后,首先经历的是其onCreate生命周期,这里涵盖了首屏业务优化的主要场景也是开启异步并发的主要时机,在其中有个重要的 setContentView 方法会触发 DecorView 的 install,可尝试对 DecorView 的构建进行预加载;后续自然来到View 构建的阶段,该阶段在抖音上相当耗时,可采用异步 Inflate 配合 X2C(编译期将 xml 布局转代码)并提升相应异步线程优先级的方法综合优化;再来到View 的整体渲染阶段,涵盖 measure、layout、draw 三部分,这里可尝试从层级、布局、渲染上取得优化收益。
最后是首屏数据加载阶段,这部分涵盖非常多数据相关的操作,也需要综合性优化,可尝试预加载、缓存或网络优先级调度等手段。
此外,针对全路径所有阶段还可以实施通用性的优化项,如:启动任务调度框架、类重排、IO 预加载、全局通用性框架优化等。
启动耗时成因分析:所有的耗时均因代码运行时不合理地消耗系统资源产生,而不合理的耗时点正是需要做归因分析之处。抖音按照不合理耗时点消耗的主要系统资源类型划分出五大成因,分别是:CPU Time、CPU Schedule、IO Wait、Lock Wait 和 IPC,下面分别对各成因进行剖析:
1、CPU Time 指占用 CPU 进行计算所花费的时间绝对值,中断、挂起、休眠等行为是不会增加 CPU Time 的,所以因 CPU Time 开销占比高导致的不合理耗时点往往是逻辑本身复杂冗长需要消耗较多 cpu 时间片才能处理完。比较常见的高 CPU 占用是循环,比如抖音启动时遇到过一个 so 加载耗时,最后定位原因是在解压 so 的时候,遍历 ZipEntry 的次数过多导致,一个可行的优化策略就是可以把 so 所在的 ZipEntry 提前,遍历完 so 的 ZipEntry 之后可以提前中止遍历,而不需要遍历剩下的无效 ZipEntry。
除循环之外,反射也是导致 CPU Time 的重要原因,像在序列化/反序列化、View Inflate 时,都有大量的反射操作,反射的耗时主要是字符串去查找 Method 或者 Field,这个优化策略也可以考虑提前查找 Method 和 Field 缓存起来,或者是通过内联来降低 Field 数量等。另外一个常见的 CPU 耗时是类加载,类的加载过程包括:Load,从 Dex 文件里读取类的信息,可通过类重排优化;Verify,验证指令是否合法等,通过关掉 Class Verify 可以优化该过程,同时高版本的 vdex 也是为了优化 verify 过程而设计,在 dex2oat 的时候做 verify,verify 之后的结果保存成 vdex,后续只需要加载 vdex;Link,给 Field、Method 分配内存,按照名字排序以方便后续反射的时候查找 Field、Method 等,这个过程的优化,art 虚拟机采用了 ImageSpace 的方案进行了优化,将 Link 后的内存保存为 image 文件,后续可以直接 load 这个 image 文件,省去了 Link 过程;Init,类的初始化。
2、CPU Schedule 在分析时主要针对主线程,是指主线程处于可执行状态但获取不到 cpu 时间片,这类耗时可能和线程调度等有关,最终导致分配给主线程的 cpu 时间片不足以及时处理完其内任务。由于主线程的线程优先级比其他线程的优先级要高很多,通常影响并不大,事实上抖音做了线上用户的启动耗时统计,这部分的耗时占比也是不大的。不过有一个场景需要关注,就是渲染,渲染是需要 RenderThread 提交 GPU 的渲染命令,而 RenderThread 并没有主线程那么高的优先级,因此比较容易受 CPU 的负载的影响,导致渲染耗时,这个对于启动来说影响并不算大,启动只有一次首页的渲染,占整体时间的比例不算大,但对于流畅度的影响就会比较大。这类耗时的优化主要还是从降低 CPU 的负载的角度考虑,比如业务降级、业务打散等手段。抖音还通过对 RenderThread 优先级的提升优化,拿到了不错的收益。
3、IO Wait 指发生了 IO 操作需要等待 IO 返回结果,这类耗时可能发生在读取资源和文件,类加载,甚至在内存不足时的 PageFault 都会导致 IO Wait。Resources 的相关的操作耗时,主要是需要从 apk 里读取资源文件,优化策略可以有预加载、资源重排、资源异步加载等。类加载的 IO Wait 和 Resources 类似,也可以通过类的重排、预加载等优化方案。文件读写导致的 IO Wait 又分为业务文件和系统文件,业务文件指业务逻辑的读写文件,一般都可以通过异步来解决,而系统文件的例子是 dex 的读写,抖音的 IO Wait 很大一块是它贡献的,目前的思路还是做 dex 的重排和 IO 的预读来尝试优化。
4、Lock Wait 也是主要针对主线程,指其处于等锁状态,等待被其他线程唤醒或自己超时唤醒,导致这类耗时的问题种类多样,大体也是可以分为业务锁和系统锁,业务锁主要是被主线程等待的业务逻辑未能及时处理完,优化思路一般是移除主线程的锁等待逻辑或者加快被等待的业务逻辑的执行速度。系统锁主要有:String InternTable Lock,ClassLinker Lock,GC Wait Lock 等,目前抖音正在尝试优化这几类的锁耗时。
5、IPC 指进程间通信,操作系统大都含有相应的机制,Android 中所特有的 IPC 机制是 Binder,由于进行 IPC 调用往往需要等待通信结果本质上这也算是一种 Lock Wait,但 Android 特有 Binder 机制所以单独列出,这类耗时可采用减少或替代 Binder 调用等手段来优化。
综合前述的五大耗时成因,这里举一个分析启动阶段 UI 耗时成因的例子作为实践参考,根据 UI 界面的生命周期(一般划分)——UI 构建、数据绑定、View 显示三个阶段分别进行分析:
在UI 构建阶段中首先要对界面布局的 xml 文件进行解析,这会导致 IO Wait 耗时,在接下来要解析 xml 文件中的 TagName 从而获取对应 View 的 class 会用到反射、创建各子 View 实例并生成 View 树又会用到循环递归,两部分都会增加 CPU Time 的开销。
然后是数据绑定阶段,该阶段主要分两部分,一部分是对数据做请求、解析、适配,另一是部分是将适配好的数据填充进 UI 中,前一部分往往会涉及到 Json 解析成 Data Class 实例,这里就可能涉及反射、循环遍历嵌套的数据类结构等增加 CPU Time 的操作。
最后是View 显示阶段,常见的 measure、layout、draw 三大渲染 View 的步骤就在其中,它们同样会产生递归遍历父子 View 的耗时,此外这里还涉及将应用层计算好的渲染 View 的数据传递给系统层做最终的像素点排布,那么必然又会产生 IPC 耗时。
从这个例子可见即使再复杂的场景只要我们进行细粒度的分析,都能将耗时点归入前述某一成因中。
现状分析的实践
如前文方**所述,现状分析包括线下 Profile 数据与线上监控数据的对照分析,综合这两部分可以明确切实影响大盘启动性能的普遍耗时点,从而确保要做的优化项是行之有效的。下面分别讲述这两部分数据的分析实践:
线下 Profile 数据分析:Profile 主要是指使用性能探测工具抓取应用启动路径各阶段的耗时和系统资源消耗情况,常见的开源 Profile 工具有 TraceView、Systrace、Android Profiler 等,这些工具各有优势但均不能完全满足抖音做线下 Profile 的需求(详见后文“启动性能优化工具”部分的讲解),为此,抖音自研了“新一代全能型性能分析工具 RheaTrace”满足了需求。通过该工具我们可以在线下抓取整个启动路径的 Trace 文件,其整体样式与 Systrace 一致,但是涵盖了更多的信息点,一个样例 Trace 文件如下图所示:
这里需要注意抓取 Trace 一定要基于 release 包,debug 包中往往涵盖诸多调试逻辑可能影响启动性能,导致 profile 数据与实际使用情形存在偏差。在查看 profile 数据时,首要观察主线程,寻找其中不符合预期的耗时方法,抖音将主线程耗时在 5ms 以上的方法均认定为不符合预期;然后在所有不符合预期的方法中寻找 Top n 的耗时点,逐个分析耗时原因、寻找突破口;耗时原因需要结合方法实现逻辑以及诸多运行时信息综合分析(这里可以参考 Google 官方文档“浏览 Systrace 报告”),需要关注的运行时信息有方法执行时段对应的 CPU 负载、线程状态的颜色标识值、锁信息、IO 耗时、Binder 调用耗时等,根据这些信息判定引起方法耗时的主要原因,再结合理论分析中不同阶段、不同系统资源类型探寻优化手段。
线上监控数据分析:这部分数据的分析主要是用作参照和补充,参照是指线下 Profile 数据分析出的耗时点要对照线上数据确认其在大盘中存在普遍耗时,补充是指线下 Profile 数据未能复现的耗时点可能存在于线上大盘中,这部分漏掉的耗时点需要在线下尝试复现、归因后实施优化。这里有个很重要的点是:该如何对线上的启动性能指标做监控,这是保障线上数据能真实反映用户体验并且与 QA(做竞品测试等)和业务方(判断业务需求是否影响启动性能等)达成一致的前提,下面将对这部分做详细阐述,分为启动性能指标的定义、统计和校准三部分:
启动性能指标定义:启动指标定义主要在于如何确定启动路径的起点与终点。起点的备选项有下图中的三个点以及 Application 的 attachBaseContext 方法:
- 下图中“点击图标”后的/proc/self/stats starttime 是内核中记录的 App 启动时间点,该数据的 Android 版本兼容性良好也比较贴合真实情况,可以作为备选;
- 接下来的 Process.getStartElapsedRealTime 是 Framework 中记录的 APP 进程创建起点,该 API 是 Android N 起才提供的兼容性较差;
- 再往后是 Application 的构造函数,按照 Android 官方生命周期式的开发模式通常不会往 Application 的构造函数中加逻辑,所以不建议在这里记录起点;
- 最后是大家熟悉的 attachBaseContext 方法也是 Application 生命周期中非常早的一个点,可以在这里记录启动点,虽然可能和真实情况有小幅差距,但能够起到基本的对照效应并且处于 APP 逻辑可以干预的范围内,抖音的启动路径起点选定的正是此处,此外也可以结合前述的内核中启动时刻综合观测。
- 而关于终点的定义同样有几个备选项:Activity-onResume、Activity-onWindowFocusChanged、View-dispatchDraw 和 DecorView-post:
- 少数 APP 在选定冷启阶段终点时可能会选择首页 Activity 的 onResume 时机,但此时整个 Activity 还不是完全可见,与用户感受到的冷启阶段结束时刻有一定的差距;
- 基于上述原因,更多 APP 会选择首页 Activity 的 onWindowFocusChanged 时机,抖音也是选择的此时机作为冷启过程的终点,此时首页 Activity 已经可见但其内部的 view 还不可见,对于用户侧已经可以看见首页背景,即认为冷启阶段到此结束,后续的首页内 View 绘制归入首刷过程中;
- dispatchDraw 从 View 可见这个角度讲应该是比较接近用户感受的,但其受业务改动影响较大,不利于把控冷启时间及维护;
- 最后是通过 DecorView 在 attachToWindow 前 post 一个 runnable 来打点的方式,该方式可以保障在业务 View 完成渲染后做打点,但该方式可能会受业务同学做懒加载在打点前插入逻辑的影响,因此抖音的冷启终点也未选用该时机。
启动性能指标统计:在统计性能指标时有个关键点往往被大家忽略,就是分位值的概念,由于平均值相对更通俗易懂且对大盘突发问题敏感往往作为首要统计指标被关注,但其存在波动较大不利于大盘监控以及难以体现不同分位机型启动性能差异的问题,而分位值更有利于全面监控且各分位波动相对较小,此外对于低端机的性能问题能够更好地显现出来,有助于做专项优化,在优化抖音的启动性能时我们会重点关注 50 分位和 90 分的性能指标,不过分位值也存在一些缺点,比如:概念理解起来相对复杂、个别 bad case 分散到各分位不容易体现出来等,因此比较好的实践是:日常优化主要统计分位值,平均值作为辅助完善监控体系。
启动性能指标校准:由于启动路径往往比较复杂,因此添加了启动性能埋点后还需要额外的校准,总的原则是需要保障指标数据能切实反映大盘用户情形。在添加客户端埋点时最好是先梳理再分主路径和重点 case 分别打点,此外还要对若干异常 case 的数据进行剔除或分类避免污染打点数据,比如抖音在添加启动时间打点时就会对开屏广告、保活进程、push 拉起、deeplink 拉起、启动期间退后台、新用户启动等场景进行过滤或分开统计。
启动优化的实践
在做完理论分析与现状分析后,我们基本对全局待优化点及其大致优化方向会产生整体的认知,在开始落地各个优化措施之前还有很重要但往往会被忽略的一步——按优先级排布优化项、制定整体优化方案,这一步在很大程度上制约着后续启动优化的收益预期与进展把控,这两点对于按时达成启动优化的终极目标都至关重要。前述中提及了对“优先级”的把控,这点是制定整体优化方案的重中之重。
从抖音启动优化实践总结来看比较好的优先级策略是按照“投入产出比”来排布优化项,顾名思义:投入人力越少但优化幅度越大的优化项越应该排在前期,因为所有的性能优化历程都势必会经历从高收益到低收益的变化,那么相应的在排布优化项的前后顺序时也需顺应此规律,最终呈现的态势即为:前期以小成本快速降低大盘启动耗时,后期逐步提高投入突破各个瓶颈型耗时点(更后期大规模重构仅能减少几十毫秒启动时间的情形也应在预期之内),全过程同期加强防劣化机制,最终做到可持续优化。
在完成前述的全局优先级排布及方案制定后,才算真正来到了实施优化的阶段,在这个阶段所要用到的各类优化策略及配合方法在前文方**部分已有详细讲述,在实战部分首先要补充一下前述几类优化策略按照“性能无损”、“业务无损”的区别划分,整体如上图所示,此外,我们会结合抖音启动优化实战经验列举各优化策略下可实施的优化项,以供参考:
- 正面优化:删减非必要的启动逻辑、开屏页与首页 Activity 合并、获取进程名从 IPC 转反射方式等;
- 按需优化:ContentProvider 中过早初始化逻辑转为使用时初始化、多进程由启动时加载转为使用时或特定场景触发加载等;
- 延迟优化:4.x 机型延迟执行 Multidex.install 中的 Odex 操作、主线程消息队列中非启动必要消息延迟执行、启动路径非高优业务逻辑延迟初始化等;
- 运行时优化:CPU 提频、语言层面优化(内联、替换反射、避免用 Kotlin 的 Range 循环)、关闭 Verify Class、4.x 机型抑制 GC、主动触发 AOT 编译、资源重排、类重排、dex 重排等;
- 异步优化:异步预加载(ShardPreference、实例化对象)、异步 inflate view、线程收敛等;
- 降级优化:极速版、组件化降级、非必要耗时逻辑按人群/地区降级等;
- 综合优化:启动任务调度框架、启动路径重构、前后台启动任务精细化重排、后台负载优化等,这些优化项属于前述优化思想的综合应用,一般不局限于单方面的优化。
通过上述列举的各策略优化项你可能会发现,这其中有的优化项其实会对个别业务性能或功能有损,但最终对于启动性能是有显著提升的,那么此时需要按照“全局收益最大”的策略来综合评估这些优化项的可落地性,并不是只看单点的得失,这种全局性的思维在性能优化中非常重要。
线上验证的实践
这部分在前述的方**中已针对三个关键点阐述得比较细致,这里仅针对三个关键点在落地时的技巧或注意事项加以补充:
线下的优化一定要有线上的指标反馈:由于线上设备的固有硬件性能各异,所以需要有足够量级的用户启动打点数据才能相对准确地判定线下的优化是否在线上产生了效果,这个量级从抖音启动优化中摸索的经验来看一般达到 100 万即可;此外,观测启动性能数据的时间点也需要把控好,这是由于每次发布升级版 APP 后,大都是性能相对好的手机会先升级,这个现象会等导致发版初期的启动性能数据整体偏好,不能反映真实大盘情形,因此,抖音一般会选取每个版本发版后 4-5 天(可能随 APP 升级覆盖安装的速度不同而不同)的数据判定大盘情形。
线上指标需要结合均值与分位值综合来评估:在抖音启动优化实践中,启动耗时均值会更多用于大盘情形评估或线上监控中,而作为性能优化的同学最主要关注的是 50 分位机型的数据,这是能代表过半数用户启动性能水准的指标,此外 90 分位以上的机型也需要我们额外关注,这类机型非常容易放大启动性能问题,从实际来看,90 以上相对 50 的绝对分位数差了不到一倍,但冷启耗时却可能差到 2 倍左右(如下图所示抖音在某段时期的各分位冷启耗时情形),这说明低端机的用户启动体验是明显可感知的差,基于我们曾经做过的劣化实验结果来看,这些机型的启动性能如果不能有效提升,将有很大概率减少其留存。
在验证收益时通过 AB 实验达成:AB 实验相对于观测不同版本的大盘数据来看更具有严谨性,因此在产出实验结论前同样需要保障数据量和时间跨度,抖音在开启性能的 AB 实验后,一般会让对照组及实验组进组用户各达到 100 万并保持至少 5 天后才进行实验的数据分析并产出结论,这样可以基本保障所有相关指标的稳定及置信。
防劣化的实践
防劣化的体系建设是个比较复杂的工程,要做好是有非常大的挑战的。抖音从最早的线下手动的分版本测试开始,经过了逐步的摸索优化,演变到当前涵盖了代码提交时静态检测、线下自动化劣化测试和归因、灰度劣化发现和归因、线上常态化的劣化监控和归因。防劣化是一个漏斗,从代码提交阶段到线下测试阶段,再到灰度发布阶段,再到线上版本发布阶段,我们希望劣化能够更前置的发现,每个环节都尽可能的发现解决更多的劣化,保证更少的劣化被带到线上。
防劣化的有几个难点:
一是劣化检测的准确率和召回率,为了更多更准确的发现劣化;
二是劣化的准确归因,发现劣化之后,如果不能精准的指出劣化的原因,需要投入比较多的人力资源和时间定位劣化原因,影响劣化解决的效率;
三是劣化的修复,如果是比较严重的劣化,可以采用阻塞发版限期解决的方式,是比较容易推进解决的。但是从抖音的实践来看,当启动优化到了深水区之后,优化的速度已经比较缓慢,需要关注几十毫秒级别的劣化了,假设我们解决了一二两个难点,发现了这些轻微的劣化,但是如何推进业务去解决这些小劣化也同样是一个难题。我们需要能够量化出这些劣化对业务的影响,针对不同的劣化量级,和业务对齐优先级,确定标准的劣化修复流程,才能够保证劣化不会被带到线上影响大盘用户。
防劣化是一个长期的工作,抖音投入已经有一年多了,目前整体效果还不错,在这个过程中也积累了比较多的经验,之后会专门写一个抖音的防劣化系列文章来给大家介绍我们的技术成果。
启动优化工具
古人云“工欲善其事必先利其器”,在启动性能优化领域也是一样,我们不仅需要趁手的工具来定位优化耗时问题,还需要尽量自动化的工具来持续发现劣化问题,也就是说整个启动优化在“攻”和“守”的两大方面均需要工具的辅助。那么下面将针对这两部分的工具分别进行介绍及分享抖音在启动优化工具方面的探索:
线下分析工具
这部分主要针对业界常见的 APP 性能探测工具进行基本原理解析及优缺点对比,具体包含的工具有:TraceView、CPU Profiler、Systrace,此外还将提及抖音自研的“新一代全能型性能分析工具 RheaTrace”:
- TraceView:Instrumentation 模式下采用 AddListener 的方式注册 MethodError、MethodExited、MethodUnwind 的回调来采集方法起止时间;Sampling 模式下使用一个 SamplingThread 定时主权线程堆栈,通过对此的堆栈对比近似确定函数的进入和退出时间;虽然是官方提供的工具,但两种模式本身都存在比较大的性能损耗,可能带偏优化方向;
- CPU Profiler:整体通过 JVM Agent 实现,具有完成方法调用栈输出,且支持 Java、C/C++方法的耗时检测,上手比较简单,但其同样存在性能损耗较大的问题,且一般仅用于 debug 包,release 包需要额外添加 debuggable 的配置;
- Systrace:基于 Android 系统层的 Atrace 实现,Atrace 又基于 Linux kernal 层的 ftrace 实现,ftrace 在内核中通过函数插桩获取耗时;其自身性能损耗比较低、数据源丰富且具有较好的可视化页面,但其默认监控点较少,在 APP 自有代码中的监控点需要手动加入,比较麻烦;
- RheaTrace:这是抖音基于字节码插桩结合 Systrace 及 Atrace 自研的工具,其具有自动加入监控点、各类耗时信息全面、性能损耗低等特点,是抖音日常在线下实施性能优化时首选的工具,其细节详见前述公众号文章,这里不再赘述。
RheaTrace 目前是抖音性能优化同学的主要工具,它不仅仅是一个工具,也是一个平台。除了 Systrace 自带的性能数据之外,我们增加了业务的函数耗时插桩的数据,可以更全面地对耗时进行分析。但是这些数据还不够,我们支持以插件的形式,增加自己定制的数据,比如为了优化 IO 的耗时,我们通过 hook 增加了更精细化的 IO 的信息,辅助定位 IO 的耗时问题;抖音的类加载耗时也是有些严重,我们也 hook 了类加载,增加了类加载的性能数据。我们要极致地优化抖音启动时间,以上这些数据是不够的,还有锁、View 耗时信息等相关数据补充,给性能优化的同学提供全方位的性能分析工具。
除了 RheaTrace 之外,还有一些特定场景的小工具,比如线程分析工具、内存分析工具、高频函数分析等。由于篇幅有限,就不在这里一一介绍,后面会有专门的系列文章来介绍。
线上监控工具
上面介绍启动优化方**的时候我们提到了,不能只是看线下的性能分析,线下的分析结果并不能完全代表线上大盘用户的情况。我们分析线上的性能数据,一方面能够验证我们的线上优化效果,另一方面能够从线上多个维度的数据里指导后续的优化方向。
线上监控工具和线下的差异点主要在低性能损耗和兼容性,我们将 RheaTrace 做了改造,使其能够满足线上的监控要求。性能损耗上,我们将监控的性能损耗控制在 1%以内,包大小控制在 200KB 以内,基本实现了线上全量用户的启动耗时监控。通过启动路径的全量插桩,可以针对启动路径的各个阶段进行监控,一是可以发现线上用户哪些任务比较耗时,可以针对性的优化,让更多用户受益;二是可以监控线上的启动任务,如果发生了耗时增加,那么说明有劣化,这比监控到启动时间的劣化,要更容易定位到原因。除了线上的全量慢函数监控之外,我们的线上启动监控还会细化IO、锁、GC等多种维度的耗时数据,帮助定位线上为什么耗时慢,提供新的优化方向。
总结一下线上启动监控工具的思路就是:将线下的性能分析数据,低损耗的移植到线上,观察线上用户的性能数据,线上线下相结合的分析启动耗时,为启动优化提供优化方向指导。
启动性能优化之路去向何方
看了上文关于启动性能优化如此多的理论与实践,想必你已经意识到启动优化之路注定是不会平凡的,抖音在这条路上探索了 2 年之久且仍未到达尽头。在这条路上势必会经历前期的坦途、中期的迷茫与后期的瓶颈,但无论如何都要一直坚定地走下去,因为只要业务还有一天在迭代那么启动性能就有一天存在挑战的可能,所以启动优化之路的未来必然是无尽头的。
既然如此,那么我们的重点就应该从何时才能走完这条路转移到如何走得更精彩之上,甚至到最后能够做到把控这条路的走向,这或许也能算作另一种意义上的走完启动优化之路,那么什么才算走得更精彩以及把控路的走向呢?
迷茫时慢下步子再分析全局的耗时点寻找到新的优化策略、遇到瓶颈时先暂时放缓追赶指标尝试从代码重构上挖掘深层的收益、不断开拓跨领域(如端上智能降级)结合的优化方向……这些或许都能称作是一种精彩,并且会因人而异,最终,当这种精彩累计得足够多之时我们很可能会发现启动优化之路上已知的所有岔路口全被走了个遍,同期 APP 的启动性能也很可能已经达到了再优化也没什么明显业务收益的地步,并且出现的任何劣化点都能及时被解决掉,那么这时不出意外的话,启动优化之路走向的把控权已经尽在你手中了。
更多思考
关于Android的性能调优有很多,大家可以阅读以下文章来进阶学习!
抖音 Android 性能优化系列:启动优化实践