抖音启动任务优化的核心思想是代码价值最大化和资源利用率最大化。其中代码价值最大化主要是确定哪些任务应该在启动阶段执行,它的核心目标是将不应该在启动阶段执行的任务从启动阶段去除掉;资源利用率最大化则是在启动阶段任务已经确定的情况下,尽可能多的去利用系统资源以达到减少任务执行耗时的目的。对于单个任务而言,我们需要去优化它的内部实现,减少它本身的资源消耗以提供更多资源给其他任务执行,对于多个任务则是通过合理的调度以充分利用系统的资源。
从落地角度而言我们主要围绕两个事情开展:启动任务重构与任务调度。
启动任务重构
由于业务复杂度较高且前期对启动任务的管控较为宽松,抖音启动阶段的任务有超过 300 个,这种情况下对启动阶段的任务进行调度能够在一定程度上提升启动速度,但是仍然比较难将启动速度提升到一个较高的水平,因此启动优化中非常重要的一个方向就是减少启动任务。
为此我们将启动任务分成了配置任务、预加载任务和功能任务三大类。其中配置任务主要用于对各类 sdk 进行初始化,在它没有执行之前相关的 sdk 是无法工作的;预加载任务主要是为了对后续的某些功能进行预热,以提升后续功能的执行速度;功能任务则是在进程启动这一生命周期执行的与功能相关的任务。对于这三类任务我们采用了不同的改造方式:
- 配置任务:对于配置任务我们最终目标是把它们从启动阶段去除掉,这样做主要有两个原因,首先部分配置任务仍然存在一定的耗时,将它们从启动任务移除掉可以提升我们的启动速度;其次配置任务在没有执行前相关 sdk 无法正常使用,这会对我们的功能可用性、稳定性以及优化过程中的调度造成影响。为了达到去除配置任务的目的,我们对配置任务进行了原子化的改造,将原本需要主动调用向 sdk 中注入 context、callback 等各类参数的实现,通过 spi(服务发现)的方式改为了按需调用的方式——对于抖音自己的代码我们在需要使用 context、callback 等参数时通过 spi 的方式去请求应用上层进行获取,对于我们无法修改代码的三方 sdk,我们则对它们进行一个中间层封装,后续对于三方 sdk 的使用都通过封装的中间层,在中间层相关接口被调用时再执行 sdk 的配置任务。通过这样的方式我们可以把配置任务从启动阶段移除掉,实现使用时再按需执行。
- 预加载任务:对于预加载任务,我们则对它们进行了规范化改造,以确保预加载任务在被降级情况下功能的正确性,同时对过期的预加载任务以及预加载任务中冗余的逻辑进行去除,以提升预加载任务的价值。
- 功能任务:对于功能性的启动任务,我们则是对它们进行了粒度拆解与瘦身,去除启动阶段非必须的逻辑,同时对功能任务添加了调度与降级能力支持,以供后续的调度与降级。
任务调度
关于任务调度业界有过比较多的介绍,这里对于任务的依赖分析、任务排布等不再进行介绍,主要介绍抖音在实践过程中一些可能的创新点:
- 基于落地页进行调度:抖音启动除了进入首页,还有授权登录、push 拉活等不同的落地页,这些不同的落地页在任务的执行上是有比较大差异的,我们可以在 Application 阶段通过反射主线程消息队列中的消息获取待启动的目标页面,基于落地页进行针对性的任务调度;
- 基于设备性能调度:采集设备的各类性能数据在后台对设备进行打分与归一化处理,将归一化之后的结果下发到端上,端上根据所在的性能等级进行任务的调度;
- 基于功能活跃度调度:统计用户对各个功能的使用情况,为用户计算出每个功能的一个活跃度数据,并将他们下发到端上,端上根据功能活跃度高低来进行调度;
- 基于端智能的调度:在端上通过端智能的方式预测用户的后续行为,为后续功能进行预热等;
- 启动功能降级:对于部分性能较差的设备与用户,对启动阶段的任务、功能进行降级,将其延后到启动之后再去执行,甚至完全不执行,以保证整体体验。
之前的几个阶段都属于 Application 阶段,接下来看一下 Activity 阶段的相关优化,这个阶段我们将介绍 Splash 与 Main 合并、反序列化优化两个典型例子。
1.4.1 Splash 与 Main 合并首先来看一下 SplashActivity 与 MainActivity 的合并,在之前的版本中抖音的 launcher activity 是 SplashActivity,它主要承载着广告、活动等开屏相关逻辑。一般情况下我们的启动流程为:
- 进入 SplashActivity,在 SplashActivity 中判断当前是否有待展示的开屏;
- 如果有待展示的开屏则展示开屏,等待开屏展示结束再跳转到 MainActivity,如果没有开屏则直接跳转到 MainActivity。
在这个流程下,我们的启动需要经历两个 Activity 的启动,如果把这两个 Activity 进行合并,我们可以取得两方面的收益:
- 减少一次 Activity 的启动过程;
- 利用读取开屏信息的时间,做一些与 Activity 强关联的并发任务,比如异步 View 预加载等。
要实现 Splash 与 Main 合并,我们需要解决的问题主要有 2 个:
- 合并后如何解决外部通过 Activity 名称跳转的问题;
- 如果解决 LaunchMode 与多实例的问题。
第 1 个问题比较容易解决,我们可以通过 activity-alias targetActivity 将 SplashActivity 指向 MainActivity 解决。接下来我们来看一下第二个问题。
launchMode 问题
在 Splash 与 Main 合并之前,SplashActivity 与 MainActivity 的 LaunchMode 分别是 standard 和 sinngletask,这种情况下我们能够确保 MainActivity 只有一个 实例,并且在我们从应用 home 出去再次进入时,能够重新回到之前的页面。
将 SplashActivity 与 MainActivity 合并以后,我们的 launcher Activity 变成了 MainActivity,如果继续使用 singletask 这个 launchMode,当我们从二级页面 home 出去再次点击 icon 进入时,我们将无法回到二级页面,而会回到 Main 页面,因此合并后 MainActivity 的 launch mode 将不再能够使用 singletask。经过调研,我们最终选择了使用 singletop 作为我们的 launchMode。