抖音 Android 性能优化系列:启动优化实践

本文详细介绍了抖音Android应用在启动优化上的实践,包括ContentProvider优化、启动任务重构与调度、Activity阶段优化以及主线程耗时消息优化。针对ContentProvider的初始化耗时,通过JetPack Startup库进行聚合优化。启动任务重构主要分为配置任务、预加载任务和功能任务三类,通过分类和调度提升启动速度。Activity阶段优化中,将SplashActivity与MainActivity合并以减少启动过程,并解决了多实例问题。此外,UI渲染优化通过异步预加载和处理LayoutParams问题提高效率。主线程消息调度确保核心启动路径消息优先执行,减少主线程耗时。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

  1. 延后异步对非首个 dex 进行 odex 优化。

关于 MutilDex 优化的更多细节可以参照之前的一篇公众号文章,目前该方案已经开源,具体见该项目的 github 地址(https://github.com/bytedance/BoostMultiDex)。

1.2 ContentProvider 优化

接下来介绍的是ContentProvider的相关优化,ContentProvider 作为 Android 四大组件之一,其在生命周期方面有着独特性——Activity、Service、BroadcastReceiver 这三大组件都只有在它们被调用到时,才会进行实例化,并执行它们的生命周期;ContentProvider 即使在没有被调用到,也会在启动阶段被自动实例化并执行相关的生命周期。在进程的初始化阶段调用完 Application 的 attachBaseContext 方法后,会再去执行 installContentProviders 方法,对当前进程的所有 ContentProvider 进行 install。

这个过程将会对当前进程的所有 ContentProvider 通过 for 循环的方式逐一进行实例化、调用它们的 attachInfo 与 onCreate 生命周期方法,最后将这些 ContentProvider 关联的 ContentProviderHolder 一次性 publish 到 AMS 进程。

2745f554d3aa7a8103a035a59c32cbc7.png

ContentProvider 这种在进程初始化阶段自动初始化的特性,使得在其作为跨进程通信组件的同时,也被一些模块用来进行自动初始化,这其中最为典型的就是官方的 Lifecycle 组件,其初始化就是借助了一个叫 ProcessLifecycleOwnerInitializer 的 ContentProvider 进行初始化的。

LifeCycle 的初始化只是进行了 Activity 的 LifecycleCallbacks 的注册耗时不多,我们在逻辑层面上不需要做太多的优化。值得注意的是,如果这类用于进行初始化的 ContentProvider 非常多,ContentProvider 本身的创建、生命周期执行等堆积起来也会非常耗时。针对这个问题,我们可以通过 JetPack 提供的 Startup 将多个初始化的 ContentProvider 聚合成一个来进行优化。

除了这类耗时很少的 ContentProvider,在实际优化过程中我们也发现了一些耗时较长的 ContentProvider,这里大致介绍一下我们的优化思路。

public class ProcessLifecycleOwnerInitializer extends ContentProvider {

@Override

public boolean onCreate() {

LifecycleDispatcher.init(getContext());

ProcessLifecycleOwner.init(getContext());

return true;

}

}

对于我们自己的 ContentProvider,如果初始化耗时我们可以通过重构的方式将自动初始化改为按需初始化。对于一些三方甚至是官方的 ContentProvider,则无法直接通过重构的方式进行优化。这里以官方的 FileProvider 为例,来介绍我们的优化思路。

FileProvider 使用

FileProvider 是 Android7.0 引入的用于进行文件访问权限控制的组件,在引入 FileProvider 之前我们对于拍照等一些跨进程的文件操作,是以直接传递文件 Uri 的方式进行的;在引入 FileProvider 后,我们的整个过程则为:

  1. 首先继承 FileProvider 实现一个自定义的 FileProvider,并把这个 Provider 在 manifest 中进行注册,为其 FILE_PROVIDER_PATHS 属性关联一个 file path 的 xml 文件;

  2. 使用方法通过 FileProvider 的 getUriForFile 方法将文件路径转化为 Content Uri,然后去调用 ContentProvider 的 query、openFile 等方法。

  3. 当 FileProvider 被调用到时,将会首先去进行文件路径的校验,判断其是否在第 1 步定义的 xml 中,文件路径校验通过则继续执行后续的逻辑。

耗时分析

从上面的过程来看,只要我们在启动阶段没有 FileProvider 的调用,是不会有 FileProvider 的相关耗时的。但实际上从启动 trace 来看,我们的启动阶段是存在 FileProvider 相关耗时的,具体的耗时则是在 FileProvider 的生命周期方法 attachInfo 方法中,FileProvider 的 attachInfo 方法除了会去调用我们最为熟悉的 onCreate 方法,同时还会去调用 getPathStrategy 方法,我们的耗时则是集中在这个 getPathStrategy 方法中。

从实现来看, getPathStrategy 方法主要是进行 FileProvider 关联 xml 文件的解析,解析结果将会赋值给 mStrategy 变量。进一步分析我们会发现 mStrategy 会在 FileProvider 的 query、getType、openFile 等接口进行文件路径校验时用到,而我们的 query、getType、openFile 等接口在启动阶段是不会被调用到的,因此 FileProvider attachInfo 方法中的 getPathStrategy 是完全没有必要的,我们完全可以在 query、getType、openFile 等接口被调用到的时候再去执行 getPathStrategy 逻辑。

3a915027ed0ef722621b4cec51bf8599.png

优化方案

FileProvider 是 androidx 中的代码,我们无法直接修改,但是它会参与我们的代码编译,我们可以在编译阶段通过修改字节码的方式去修改它的实现,具体的实现方案为:

  1. 对 ContentProvider 的 attachInfo 方法进行插桩,在执行原有实现前将参数 ProviderInfo 的 grantUriPermissions 设置为 false,然后调用原实现并进行异常捕获,在调用完成后再对 ProviderInfo 的 grantUriPermissions 设置回 true,利用 grantUriPermissions 的检查绕过 getPathStrategy 的执行。(这里之所以没有使用 ProviderInfo 的 exported 异常检测绕过 getPathStrategy 调用是因为在 attachInfo 的 super 方法中会对 ProviderInfo 的 exported 属性进行缓存)

public void attachInfo(@NonNull Context context, @NonNull ProviderInfo info) {

super.attachInfo(context, info);

// Sanity check our security

if (info.exported) {

throw new SecurityException(“Provider must not be exported”);

}

if (!info.grantUriPermissions) {

throw new SecurityException(“Provider must grant uri permissions”);

}

mStrategy = getPathStrategy(context, info.authority);

}

  1. 对 FileProvider 的 query、getType、openFile 等方法进行插桩,在调用原方法之前首先进行 getPathStrategy 的初始化,完成初始化之后再调用原始实现。

单个 FileProvider 的耗时虽然不多,但是对于一些大型的 app,为了模块解耦其可能会有多个 FileProvider,在这种情况下 FileProvider 优化的收益还是比较可观的。与 FileProvider 类似,Google 提供的 WorkManager 也会存在初始化的 ContentProvider,我们可以采用类似的方式进行优化。

1.3 启动任务重构与任务调度

启动的第三个阶段是 Application 的 onCreate 阶段,这个阶段是启动任务执行的高峰阶段,该阶段的优化就是针对各类启动任务的优化,具有极强的业务关联性,这里简单介绍一下我们优化的大概思路。

2db7a29ccacda69cb048f0c537b66218.png

抖音启动任务优化的核心思想是代码价值最大化资源利用率最大化。其中代码价值最大化主要是确定哪些任务应该在启动阶段执行,它的核心目标是将不应该在启动阶段执行的任务从启动阶段去除掉;资源利用率最大化则是在启动阶段任务已经确定的情况下,尽可能多的去利用系统资源以达到减少任务执行耗时的目的。对于单个任务而言,我们需要去优化它的内部实现,减少它本身的资源消耗以提供更多资源给其他任务执行,对于多个任务则是通过合理的调度以充分利用系统的资源。

从落地角度而言我们主要围绕两个事情开展:启动任务重构与任务调度。

启动任务重构

由于业务复杂度较高且前期对启动任务的管控较为宽松,抖音启动阶段的任务有超过 300 个,这种情况下对启动阶段的任务进行调度能够在一定程度上提升启动速度,但是仍然比较难将启动速度提升到一个较高的水平,因此启动优化中非常重要的一个方向就是减少启动任务

为此我们将启动任务分成了配置任务、预加载任务和功能任务三大类。其中配置任务主要用于对各类 sdk 进行初始化,在它没有执行之前相关的 sdk 是无法工作的;预加载任务主要是为了对后续的某些功能进行预热,以提升后续功能的执行速度;功能任务则是在进程启动这一生命周期执行的与功能相关的任务。对于这三类任务我们采用了不同的改造方式:

  • 配置任务:对于配置任务我们最终目标是把它们从启动阶段去除掉,这样做主要有两个原因,首先部分配置任务仍然存在一定的耗时,将它们从启动任务移除掉可以提升我们的启动速度;其次配置任务在没有执行前相关 sdk 无法正常使用,这会对我们的功能可用性、稳定性以及优化过程中的调度造成影响。为了达到去除配置任务的目的,我们对配置任务进行了原子化的改造,将原本需要主动调用向 sdk 中注入 context、callback 等各类参数的实现,通过 spi(服务发现)的方式改

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值