启动性能是 APP 使用体验的门面,启动过程耗时较长很可能导致用户使用 APP 的兴趣骤减,抖音通过对启动性能做劣化的 AB 实验也验证了其对于业务指标有影响显著。抖音拥有数亿的用户,启动耗时几百毫秒的增长就可能带来成千上万用户的留存缩减,因此,启动性能的优化成为了抖音 Android 基础技术团队在体验优化方向上的重中之重。
本文基于过往对抖音 Android 客户端做启动性能优化的实战经验总结提炼出普适性的方法论,并将该过程中沉淀的工具加以分享,希望能给大家带来一些新的思考。
抖音 Android 性能优化系列往期文章回顾:[新一代全能型性能分析工具 Rhea]
假如你要负责优化抖音的启动性能,你会怎样去规划整体的优化方案?你可能会一下子想到很多方面的细节点,比如:要优化主线程耗时、要减少布局层级、要对某些启动任务做按需加载或预加载、要避免主线程 IO、要对线程使用进行优化、还要有分析工具帮助定位性能问题等……
然而,该如何系统性地把这些细碎点组织起来并按照一定的章法来落地启动优化呢?此时,需要我们在具体细节点之上有进一步的问题分解与深入思考,最终形成一套完整的方法论,不仅能覆盖所有细节点,还能切实指导在实战中达成启动优化的效果。切实有效的方法论必然是从实战中经过千锤百炼才能形成的,而抖音庞大的用户基数又进一步保障了方法论的可行性与普适性。那么接下来让我们带着前述问题来看抖音的启动优化方法论是怎样的又是如何应用于实战之中的。
抖音的启动性能优化方法论分为五部分,分别是:理论分析、现状分析、启动性能优化、线上验证与防劣化。
这五部分间存在明显的先后顺序,又能闭环达成可持续的启动性能优化,下面将对这五部分做详细阐述:
理论分析放在最先是为了从一开始就避免让视野受到限制,很多同学往往一开始接手启动优化就容易陷入对各种现状细节的分析,拘泥于片面的潜在可优化点,这样就难以做到对全局和优先级的把控,所以,我们应该首先跳出现状,从更加全局的视角来思考整体优化的目标和策略。这里可以利用特斯拉创始人——埃隆·马斯克所推崇的“第一性原理”思考法:
“通过第一性原理,把事情升华到最根本的真理,然后从最核心处开始推理。”
基于此,我们在做启动优化的理论分析时可以从更本源的角度出发做到全局思考,比如抖音会做从进程创建到页面展示的全启动路径分阶段耗时分析、还会按照消耗的系统资源类型做耗时成因分析,通过这种极致的耗时分析可以带来极致的优化策略,此外,从全路径出发还能够发现容易忽视的问题、探索优化的极限。
在完成理论分析后,我们基本具备了全局的视角,并且也大致清楚了整体的优化目标和策略,接下来就要基于此来做现状分析从而明晰实现目标的具体路径:
在这部分需要尤其注意三点:优质的 profile 工具(这里推荐使用同样来自基础技术团队的“[新一代全能型性能分析工具] ”)、线下 trace 结合线上监控综合分析、根据投入产出比评估实施优先级,这三点是保障切实有效取得启动优化收益的关键。
在完成了理论和现状分析后,就可以根据规划的路径来实施具体的启动优化项了。在实施过程中,主要考虑主线程优化、后台线程优化和全局优化三个维度:
在完成了具体的优化项施工后,就来到了线上验证大盘收益的阶段。这个阶段有三点需要注意:
在线上验证优化措施取得切实收益后,并不是万事大吉了,持续保持住优化效果才算完整达成了启动性能优化的目的。其实不仅是启动优化,整个性能优化领域都是围绕着“攻”和“守”来展开的,“攻”即为前述的分析与优化,而“守”则是防止劣化,在防劣化方面大家往往不会像优化的方面那么重视,但实际上能防止劣化是可持续取得优化效果的前提(否则新的优化效果会用于弥补劣化甚至入不敷出),并且防劣化相比于优化是更能持久有益的。
抖音启动性能防劣化的进程分为了三个时期,不同时期有不同的表现与应对手段,这很可能是大多数 APP 优化启动性能都要经历的,这里提炼出来以供参考:
古人云“纸上得来终觉浅,绝知此事要躬行”,前述的方法论讲得再详细再透彻也会与实际的落地存在隔阂,为了做到真正的学以致用,下文将细致讲解如何将启动优化方法论应用于实践之中。
抖音在理论分析部分会对启动流程分别作全路径分析和耗时成因分析,前者用于发现全路径各个阶段的潜在耗时点避免疏漏,后者用于系统性地将各个耗时点归因从而引导我们找寻优化思路,关于这两部分的具体实践如下:
启动性能全路径分析:抖音的启动路径和大多数 APP 类似,整体分为两大阶段和两个间隙,它们按时间顺序排布为:Application 阶段、handle message 间隙、Activity 阶段和数据加载间隙,全路径各部分细分涵盖的内容如下图所示:
APP 进程由 zygote 进程 fork 出来后会执行 ActivityThread 的 main 方法,该方法最终触发执行bindApplication,这也是 Application 阶段的起点;然后是我们在应用中能触达到的attachBaseContext阶段,4.x 的机型在该阶段具有较长的 MultiDex 耗时可以做针对性优化(可参考“[抖音 BoostMultiDex 优化实践] ”),本阶段也是最早的预加载时机;接下来是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,下面分别对各成因进行剖析:
综合前述的五大耗时成因,这里举一个分析启动阶段 UI 耗时成因的例子作为实践参考,根据 UI 界面的生命周期(一般划分)——UI 构建、数据绑定、View 显示三个阶段分别进行分析:
从这个例子可见即使再复杂的场景只要我们进行细粒度的分析,都能将耗时点归入前述某一成因中。
如前文方法论所述,现状分析包括线下 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(做竞品测试等)和业务方(判断业务需求是否影响启动性能等)达成一致的前提,下面将对这部分做详细阐述,分为启动性能指标的定义、统计和校准三部分:
最后是通过 DecorView 在 attachToWindow 前 post 一个 runnable 来打点的方式,该方式可以保障在业务 View 完成渲染后做打点,但该方式可能会受业务同学做懒加载在打点前插入逻辑的影响,因此抖音的冷启终点也未选用该时机。
在做完理论分析与现状分析后,我们基本对全局待优化点及其大致优化方向会产生整体的认知,在开始落地各个优化措施之前还有很重要但往往会被忽略的一步——按优先级排布优化项、制定整体优化方案,这一步在很大程度上制约着后续启动优化的收益预期与进展把控,这两点对于按时达成启动优化的终极目标都至关重要。前述中提及了对“优先级”的把控,这点是制定整体优化方案的重中之重。
从抖音启动优化实践总结来看比较好的优先级策略是按照“投入产出比”来排布优化项,顾名思义:投入人力越少但优化幅度越大的优化项越应该排在前期,因为所有的性能优化历程都势必会经历从高收益到低收益的变化,那么相应的在排布优化项的前后顺序时也需顺应此规律,最终呈现的态势即为:前期以小成本快速降低大盘启动耗时,后期逐步提高投入突破各个瓶颈型耗时点(更后期大规模重构仅能减少几十毫秒启动时间的情形也应在预期之内),全过程同期加强防劣化机制,最终做到可持续优化。
在完成前述的全局优先级排布及方案制定后,才算真正来到了实施优化的阶段,在这个阶段所要用到的各类优化策略及配合方法在前文方法论部分已有详细讲述,在实战部分首先要补充一下前述几类优化策略按照“性能无损”、“业务无损”的区别划分,整体如上图所示,此外,我们会结合抖音启动优化实战经验列举各优化策略下可实施的优化项,以供参考:
通过上述列举的各策略优化项你可能会发现,这其中有的优化项其实会对个别业务性能或功能有损,但最终对于启动性能是有显著提升的,那么此时需要按照“全局收益最大”的策略来综合评估这些优化项的可落地性,并不是只看单点的得失,这种全局性的思维在性能优化中非常重要。
这部分在前述的方法论中已针对三个关键点阐述得比较细致,这里仅针对三个关键点在落地时的技巧或注意事项加以补充:
防劣化的体系建设是个比较复杂的工程,要做好是有非常大的挑战的。抖音从最早的线下手动的分版本测试开始,经过了逐步的摸索优化,演变到当前涵盖了代码提交时静态检测、线下自动化劣化测试和归因、灰度劣化发现和归因、线上常态化的劣化监控和归因。防劣化是一个漏斗,从代码提交阶段到线下测试阶段,再到灰度发布阶段,再到线上版本发布阶段,我们希望劣化能够更前置的发现,每个环节都尽可能的发现解决更多的劣化,保证更少的劣化被带到线上。
防劣化的有几个难点:一是劣化检测的准确率和召回率,为了更多更准确的发现劣化;二是劣化的准确归因,发现劣化之后,如果不能精准的指出劣化的原因,需要投入比较多的人力资源和时间定位劣化原因,影响劣化解决的效率;三是劣化的修复,如果是比较严重的劣化,可以采用阻塞发版限期解决的方式,是比较容易推进解决的。但是从抖音的实践来看,当启动优化到了深水区之后,优化的速度已经比较缓慢,需要关注几十毫秒级别的劣化了,假设我们解决了一二两个难点,发现了这些轻微的劣化,但是如何推进业务去解决这些小劣化也同样是一个难题。我们需要能够量化出这些劣化对业务的影响,针对不同的劣化量级,和业务对齐优先级,确定标准的劣化修复流程,才能够保证劣化不会被带到线上影响大盘用户。
防劣化是一个长期的工作,抖音投入已经有一年多了,目前整体效果还不错,在这个过程中也积累了比较多的经验,之后会专门写一个抖音的防劣化系列文章来给大家介绍我们的技术成果。
古人云“工欲善其事必先利其器”,在启动性能优化领域也是一样,我们不仅需要趁手的工具来定位优化耗时问题,还需要尽量自动化的工具来持续发现劣化问题,也就是说整个启动优化在“攻”和“守”的两大方面均需要工具的辅助。那么下面将针对这两部分的工具分别进行介绍及分享抖音在启动优化工具方面的探索:
这部分主要针对业界常见的 APP 性能探测工具进行基本原理解析及优缺点对比,具体包含的工具有:TraceView、CPU Profiler、Systrace,此外还将提及抖音自研的“[新一代全能型性能分析工具 RheaTrace] ”:
RheaTrace 目前是抖音性能优化同学的主要工具,它不仅仅是一个工具,也是一个平台。除了 Systrace 自带的性能数据之外,我们增加了业务的函数耗时插桩的数据,可以更全面地对耗时进行分析。但是这些数据还不够,我们支持以插件的形式,增加自己定制的数据,比如为了优化 IO 的耗时,我们通过 hook 增加了更精细化的 IO 的信息,辅助定位 IO 的耗时问题;抖音的类加载耗时也是有些严重,我们也 hook 了类加载,增加了类加载的性能数据。我们要极致地优化抖音启动时间,以上这些数据是不够的,还有锁、View 耗时信息等相关数据补充,给性能优化的同学提供全方位的性能分析工具。
除了 RheaTrace 之外,还有一些特定场景的小工具,比如线程分析工具、内存分析工具、高频函数分析等。由于篇幅有限,就不在这里一一介绍,后面会有专门的系列文章来介绍。
上面介绍启动优化方法论的时候我们提到了,不能只是看线下的性能分析,线下的分析结果并不能完全代表线上大盘用户的情况。我们分析线上的性能数据,一方面能够验证我们的线上优化效果,另一方面能够从线上多个维度的数据里指导后续的优化方向。
线上监控工具和线下的差异点主要在低性能损耗和兼容性,我们将 RheaTrace 做了改造,使其能够满足线上的监控要求。性能损耗上,我们将监控的性能损耗控制在 1%以内,包大小控制在 200KB 以内,基本实现了线上全量用户的启动耗时监控。通过启动路径的全量插桩,可以针对启动路径的各个阶段进行监控,一是可以发现线上用户哪些任务比较耗时,可以针对性的优化,让更多用户受益;二是可以监控线上的启动任务,如果发生了耗时增加,那么说明有劣化,这比监控到启动时间的劣化,要更容易定位到原因。除了线上的全量慢函数监控之外,我们的线上启动监控还会细化IO、锁、GC等多种维度的耗时数据,帮助定位线上为什么耗时慢,提供新的优化方向。
总结一下线上启动监控工具的思路就是:将线下的性能分析数据,低损耗的移植到线上,观察线上用户的性能数据,线上线下相结合的分析启动耗时,为启动优化提供优化方向指导。
看了上文关于启动性能优化如此多的理论与实践,想必你已经意识到启动优化之路注定是不会平凡的,抖音在这条路上探索了 2 年之久且仍未到达尽头。在这条路上势必会经历前期的坦途、中期的迷茫与后期的瓶颈,但无论如何都要一直坚定地走下去,因为只要业务还有一天在迭代那么启动性能就有一天存在挑战的可能,所以启动优化之路的未来必然是无尽头的。
既然如此,那么我们的重点就应该从何时才能走完这条路转移到如何走得更精彩之上,甚至到最后能够做到把控这条路的走向,这或许也能算作另一种意义上的走完启动优化之路,那么什么才算走得更精彩以及把控路的走向呢?
迷茫时慢下步子再分析全局的耗时点寻找到新的优化策略、遇到瓶颈时先暂时放缓追赶指标尝试从代码重构上挖掘深层的收益、不断开拓跨领域(如端上智能降级)结合的优化方向……这些或许都能称作是一种精彩,并且会因人而异,最终,当这种精彩累计得足够多之时我们很可能会发现启动优化之路上已知的所有岔路口全被走了个遍,同期 APP 的启动性能也很可能已经达到了再优化也没什么明显业务收益的地步,并且出现的任何劣化点都能及时被解决掉,那么这时不出意外的话,启动优化之路走向的把控权已经尽在你手中了。
扫一扫
在手机上阅读