安卓手机怎么降低抖音版本,安卓手机抖音怎么才能不自动更新

首页 > 经验 > 作者:YD1662022-10-27 15:37:14

启动性能是 APP 使用体验的门面,启动过程耗时较长很可能使用户削减使用 APP 的兴趣,抖音通过对启动性能做劣化实验也验证了其对于业务指标有显著影响。抖音有数亿的日活,启动耗时几百毫秒的增长就可能带来成千上万用户的留存缩减,因此,启动性能的优化成为了抖音 Android 基础技术团队在体验优化方向上的重中之重。

在上一篇中,已经从原理、方法论、工具的角度对抖音的启动性能优化进行了介绍,本文将从实践的角度通过具体的案例分析介绍抖音启动优化的方案与思路。

前言

启动是指用户从点击 icon 到看到页面首帧的整个过程,启动优化的目标就是减少这一过程的耗时。

启动过程比较复杂,在进程与线程维度,它涉及到多次跨进程的通信与多个线程之间的切换;在耗时成因维度,它包括 CPU、CPU 调度、IO、锁等待等多类耗时。虽然启动过程比较复杂,但是我们最终可以把它抽象成主线程的一个线性过程。因此对于启动性能的优化,就是去缩短主线程的这个线性过程。

接下来,我们将按照主线程直接优化、后台线程间接优化、全局优化的逻辑,介绍团队在启动优化的实践中遇到的一些比较典型的案例,其间对于业界一些比较优秀的方案也会进行简要介绍。

优化案例解析1. 主线程直接优化

对于主线程的优化,我们将按照生命周期先后的顺序展开介绍。

1.1 MutilDex 优化

首先我们来看第一个阶段,也就是 Application 的 attachBaseContext 阶段。这个阶段由于 Applicaiton Context 赋值等问题,一般不会有太多的业务代码,预期中也不会有多少耗时。但在实际测试过程中,我们发现在某些机型上,应用安装后的首次启动耗时非常严重,经过初步定位,主要耗时在MultiDex.install

安卓手机怎么降低抖音版本,安卓手机抖音怎么才能不自动更新(1)

在经过详细分析后,我们确定该问题集中在4.x的机型上,其影响首次安装及后续更新后的首次启动。

造成这一问题的原因为:dex 的指令格式设计并不完善,单个 dex 文件中引用的 Java 方法总数不能超过 65536 个,在方法数超过 65536 的情况下,将拆分成多个 dex。一般情况下 Dalvik 虚拟机只能执行经过优化后的 odex 文件,在 4.x 设备上为了提升应用安装速度,其在安装阶段仅会对应用的首个 dex 进行优化。对于非首个 dex 其会在首次运行调用 MultiDex.install 时进行优化,而这个优化是非常耗时的,这就造成了 4.x 设备上首次启动慢的问题。

造成这个问题的必要条件有多个,它们分别是:dex 被拆分成多个 dex 文件、安装过程中仅对首个 dex 进行的优化、启动阶段调用 MultiDex.install 以及 Dalvik 虚拟机需要加载 odex。

很明显前两个条件我们是没办法破坏的——对于抖音来说,我们很难将其优化成只有单 dex,系统的安装过程我们也无法改变。启动阶段调用MultiDex.install这个条件也比较难以破坏——首先,随着业务的膨胀我们很难做到用一个 dex 去承载启动阶段的代码;其次,即使做到了后续也比较难以维护。

因此我们选择破坏“Dalvik 虚拟机需要加载 odex”这一限制,即绕过 Dalvik 的限制直接加载未经优化的 dex。这个方案的核心在 Dalvik_dalvik_system_DexFile_openDexFile_bytearray 这个 native 函数,它支持加载未经优化后的 dex 文件。具体的优化方案如下:

  1. 首先从 APK 中解压获取原始的非首个 dex 文件的字节码;
  2. 调用 Dalvik_dalvik_system_DexFile_openDexFile_bytearray,逐个传入之前从 APK 获取的 DEX 字节码,完成 DEX 加载,得到合法的 DexFile 对象;
  3. 将 DexFile 都添加到 APP 的 PathClassLoader 的 DexPathList 里;
  4. 延后异步对非首个 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 进程。

安卓手机怎么降低抖音版本,安卓手机抖音怎么才能不自动更新(2)

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 逻辑。

安卓手机怎么降低抖音版本,安卓手机抖音怎么才能不自动更新(3)

优化方案

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); }

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

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

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

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

安卓手机怎么降低抖音版本,安卓手机抖音怎么才能不自动更新(4)

首页 12345下一页

栏目热文

文档排行

本站推荐

Copyright © 2018 - 2021 www.yd166.com., All Rights Reserved.