指南AI
指南AI

(语音可以玩的互动游戏)抖音互动游戏不做指定动作

幕言助手 2024-04-28 03:21:06 幕言直播助手 444 ℃ 阿比整蛊源头|厂商微信:gogoh6
正文

启动性能是 APP 利用体验的门面,启动过程耗时较长很可能利用户削减利用 APP 的兴趣,抖音通过对启动性能做劣化尝试也验证了其关于营业目标有显著影响。抖音有数亿的日活,启动耗时几百毫秒的增长就可能带来成千上万用户的留存缩减,因而,启动性能的优化成为了抖音 Android 根底手艺团队在体验优化标的目的上的重中之重。

在上一篇抖音 Android 性能优化系列:启动优化之理论和东西篇中,已经从原理、办法论、东西的角度对抖音的启动性能优化停止了介绍,本文将从理论的角度通过详细的案例阐发介绍抖音启动优化的计划与思绪。

媒介

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

启动过程比力复杂,在历程与线程维度,它涉及到屡次跨历程的通信与多个线程之间的切换;在耗时成因维度,它包罗 CPU、CPU 调度、IO、锁期待等多类耗时。固然启动过程比力复杂,但是抖音互动游戏不做指定动做我们最末能够把它笼统成主线程的一个线性过程。因而关于启动性能的优化,就是去缩短主线程的那个线性过程。

接下来,我们将根据主线程间接优化、后台线程间接优化、全局优化的逻辑,介绍团队在启动优化的理论中碰到的一些比力典型的案例,其间关于业界一些比力优良的计划也会停止简要介绍。

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

关于主线程的优化,我们将根据生命周期先后的挨次展开介绍。

1.1 MutilDex 优化

起首我们来看第一个阶段,也就是 Application 的 attachBaseContext 阶段。那个阶段因为 Applicaiton Context 赋值等问题,一般不会有太多的营业代码,预期中也不会有几耗时。但在现实测试过程中,我们发如今某些机型上,应用安拆后的初次启动耗时十分严峻,颠末初步定位,次要耗时在MultiDex.install。

抖音互动游戏不做指定动做

在颠末详细阐发后,我们确定该问题集中在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 文件。详细的优化计划如下:

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

抖音互动游戏不做指定动做

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 后,我们的整个过程则为:

起首继承 FileProvider 实现一个自定义的 FileProvider,并把那个 Provider 在 manifest 中停止注册,为其 FILE_PROVIDER_PATHS 属性联系关系一个 file path 的 xml 文件;利用办法通过 FileProvider 的 getUriForFile 办法将文件途径转化为 Content Uri,然后去挪用 ContentProvider 的 query、openFile 等办法。当 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 逻辑。

抖音互动游戏不做指定动做

优化计划

FileProvider 是 androidx 中的代码,我们无法间接修改,但是它会参与我们的代码编译,我们能够在编译阶段通过修改字节码的体例去修改它的实现,详细的实现计划为:

对 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 阶段,那个阶段是启动使命施行的顶峰阶段,该阶段的优化就是针对各类启动使命的优化,具有极强的营业联系关系性,那里简单介绍一下我们优化的大要思绪。

抖音互动游戏不做指定动做

抖音启动使命优化的核心思惟是代码价值更大化和资本操纵率更大化。此中代码价值更大化次要是确定哪些使命应该在启动阶段施行,它的核心目的是将不该该在启动阶段施行的使命从启动阶段去除掉;资本操纵率更大化则是在启动阶段使命已经确定的情况下,尽可能多的去操纵系统资本以到达削减使命施行耗时的目标。关于单个使命而言,我们需要去优化它的内部实现,削减它自己的资本消耗以供给更多资本给其抖音互动游戏不做指定动做他使命施行,关于多个使命则是通过合理的调度以充实操纵系统的资本。

从落地角度而言我们次要围绕两个工作开展:启动使命重构与使命调度。

启动使命重构

因为营业复杂度较高且前期对启动使命的管控较为宽松,抖音启动阶段的使命有超越 300 个,那种情况下对启动阶段的使命停止调度可以在必然水平上提拔启动速度,但是仍然比力难将启动速度提拔到一个较高的程度,因而启动优化中十分重要的一个标的目的就是削减启动使命。

为此我们将启动使命分红了设置装备摆设使命、预加载使命和功用使命三大类。此中设置装备摆设使命次要用于对各类 sdk 停止初始化,在它没有施行之前相关的 sdk 是无法工做的;预加载使命次要是为了对后续的某些功用停止预热,以提拔后续功用的施行速度;功用使命则是在历程启动那一生命周期施行的与功用相关的使命。关于那三类使命我们接纳了差别的革新体例:

设置装备摆设使命:关于设置装备摆设使命我们最末目的是把它们从启动阶段去除掉,如许做次要有两个原因,起首部门设置装备摆设使命仍然存在必然的耗时,将它们从启动使命移除掉能够提拔我们的启动速度;其次设置装备摆设使命在没有施行前相关 sdk 无法一般利用,那会对我们的功用可用性、不变性以及优化过程中的调度形成影响。为了到达去除设置装备摆设使命的目标,我们对设置装备摆设使命停止了原子化的革新,将本来需要主动挪用向 sdk 中注入 context、callback 等各类参数的实现,通过 spi(办事发现)的体例改为了按需挪用的体例——关于抖音本身的代码我们在需要利用 context、callback 等参数时通过 spi 的体例去恳求应用上层停止获取,关于我们无法修改代码的三方 sdk,我们则对它们停止一个中间层封拆,后续关于三方 sdk 的利用都通过封拆的中间层,在中间层相关接口被挪用时再施行 sdk 的设置装备摆设使命。通过如许的体例我们能够把设置装备摆设使命从启动阶段移除掉,实现利用时再按需施行。预加载使命:关于预加载使命,我们则对它们停止了标准化革新,以确保预加载使命在被降级情况下功用的准确性,同时对过时的预加载使命以及预加载使命中冗余的逻辑停止去除,以提拔预加载使命的价值。功用使命:关于功用性的启动使命,我们则是对它们停止了粒度拆解与瘦身,去除启动阶段非必需的逻辑,同时对功用使命添加了调度与降级才能撑持,以供后续的调度与降级。

使命调度

关于使命调度业界有过比力多的介绍,那里关于使命的依赖阐发、使命排布等不再停止介绍,次要介绍抖音在理论过程中一些可能的立异点:

基于落地页停止调度:抖音启动除了进入首页,还有受权登录、push 拉活等差别的落地页,那些差别的落地页在使命的施行上是有比力大差别的,我们能够在 Application 阶段通过反射主线程动静队列中的动静获取待启动的目的页面,基于落地页停止针对性的使命调度;基于设备性能调度:收罗设备的各类性能数据在后台对设备停止打分与归一化处置,将归一化之后的成果下发到端上,端上按照所在的性能品级停止使命的调度;基于功用活泼度调度:统计用户对各个功用的利用情况,为用户计算出每个功用的一个活泼度数据,并将抖音互动游戏不做指定动做他们下发到端上,端上按照功用活泼度凹凸来停止调度;基于端智能的调度:在端上通过端智能的体例预测用户的后续行为,为后续功用停止预热等;启动功用降级:关于部门性能较差的设备与用户,对启动阶段的使命、功用停止降级,将其延后到启动之后再去施行,以至完全不施行,以包管整体体验。1.4 Activity 阶段优化

之前的几个阶段都属于 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。

抖音互动游戏不做指定动做

多实例问题

1、内部启动多实例的问题

利用 singletop 固然可以处理 home 进来再次进入无法回到之前页面的问题,但是随之而来的是 MainActivity 多实例的问题。在抖音的逻辑中存在一些与 MainActivity 生命周期强联系关系的逻辑,若是 MainActivity 存在多个实例,那部门逻辑将会遭到影响,同时多个 MainActivity 的实现,也会招致我们没必要要的资本开销,与预期是不符的,因而我们希望可以处理那个问题。

针对那个问题我们的处理计划是,关于应用内所有启动 MainActivity 的 Intent 增加 FLAG_ACTIVITY_NEW_TASK 与 FLAG_ACTIVITY_CLEAR_TOP 的 flag,以实现类似于 singletask 的 clear top 的特征。

利用 FLAG_ACTIVITY_NEW_TASK + FLAG_ACTIVITY_CLEAR_TOP 的计划,我们根本可以处理内部启动 MainActivity 多实例的问题,但是现实测试过程中,我们发如今部门系统上,即便实现了 clear top 的特征,仍然存在多实例的问题。

颠末阐发,我们发如今那部门系统上,即便通过 activity-alias+targetActivity 体例将 SplashActivity 指向了 MainActivity,但是在 AMS 侧它仍然认为启动的是 SplashActivity,后续再启动 MainActivity 时会认为之前是不存在 MainActivity 的,因而会再次启动一个 MainActivity。

针对那个问题我们的处理计划是,修改启动 MainActivity Intent 的 Component 信息,将其改从 MainActivity 改为 SplashActivity,如许我们就彻底处理了内部启动 MainActivity 招致的多实例的问题。

抖音互动游戏不做指定动做

为了尽可能少的侵入营业,同时也避免后续迭代再呈现内部启动招致 MainActivity 问题,我们对 Context startActivity 的挪用停止了插桩。关于启动 MainActivity 的挪用,在完成向 Intent 中添加 flag 和替代 Component 信息后再挪用原有实现。之所以选择插桩体例实现,是因为抖音的代码构造比力复杂,存在多个基类 Activity,且部门基类 Activity 无法间接修改到代码。关于没有那方面问题的营业,能够通过重写基类 Activtity 及 Application 的 startActivity 办法的体例实现。

2、外部启动多实例问题

以上处理 MainActivity 多实例的计划,是成立在启动 Activity 之前往修改待启动 Activity 的 Intent 的体例实现的,那种体例关于应用外部启动 MainActivity 招致的 MainActivity 多实例的问题显然是无法处理的。那么针对外部启动 MainActivity 招致的多实例问题,我们能否有其他处理计划呢?

我们先回到处理 MainActivity 多实例问题的起点。之所以要制止 MainActivity 多实例,是为了避免同时呈现多个 MainActivity 对象,呈现不契合预期的 MainActivity 生命周期的施行。因而只要确保不会同时呈现多个 MainActivity 对象,一样能够处理 MainActivity 多实例问题。

要制止同时呈现多个 MainActivity 对象,我们起首需要晓得当前能否已经存在 MainActivity 对象,处理那个问题的思绪比力简单,我们能够去监听 Activity 的生命周期,在 MainActivity 的 onCreate 和 onDestroy 平分别去增加削减 MainActivity 的实例数。若是 MainActivity 实例数为 0 则认为当前不存在 MainActivity 对象。

处理了 MainActivity 对象数统计的问题,接下来我们就需要让 MainActivity 同时存在的对象数永久连结在 1 个以下。要处理那个问题我们需要回忆一下 Activity 的启动流程,启动一个 Activity 起首会颠末 AMS,AMS 会再挪用到 Activity 所在的历程,在 Activity 所在的历程会颠末主线程的 Handler post 到主线程,然后通过 Instrumentation 去创建 Activity 对象,以及施行后续的生命周期。关于外部启动 MainActivity ,我们可以控造的是从 AMS 回到历程之后的部门,那里能够选择以 Instrumentation 的 newActivity 做为入口。

抖音互动游戏不做指定动做

详细来说我们的优化计划如下:

继承 Instrumentation 实现一个自定义的 Instrumentaion 类,以代办署理转发体例重写里面的所有办法;反射获取 ActivityThread 中 Instrumentaion 对象,并以其为参数创建一个自定义的 Instrumentaion 对象,通过反射体例用自定义的 Instrumentaion 对象替代 ActivityThread 原有的 Instrumentaion;在自定义 Instrumentaion 类的 newActivity 办法中,停止判断当前待创建的 Activity 能否为 MainActivity,若是不是 MainActivity 或者当前不存在 MainActivity 对象,则挪用原有实现,不然替代其 className 参数将其指向一个空的 Activity,以创建一个空的 Activity;在那个空的 Activity 的 onCreate 中 finish 掉本身,同时通过一个添加了 FLAG_ACTIVITY_NEW_TASK 和 FLAG_ACTIVITY_CLEAR_TOP flag 的 Intent 去启动一下 SplashActivity。抖音互动游戏不做指定动做

需要留意的是我们那里 hook Instrumentaion 的实现计划,在高版本的 Android 系统上我们也能够以 AppComponentFactory instantiateActivity 的体例替代。

1.4.2 反序列化优化

抖音 Activity 阶段另一个典型的优化是反序列化的优化——在抖音利用过程中会在当地序列化一部门数据,在启动过程中需要对那部门数据停止反序列化,那个过程会对抖音的启动速度形成影响。在之前的优化过程中,我们从营业层面临 block 逻辑停止了异步化、快照化等 case by case 的优化,获得了不错的效果,但是如许的体例维护起来比力费事,迭代过程也经常呈现劣化,因而我们测验考试以正面优化反序列化的体例停止优化。

抖音启动阶段的反序列化问题详细来说就是 Gson 数据解析耗时问题,Gson 是 Google 推出的一个 json 解析库,其具有接入成本低、利用便利、功用扩展性优良等长处,但是其也有一个比力明显的弱点,那就是关于它在停止某个 Model 的初次解析时会比力耗时,而且跟着 Model 复杂水平的增加,其耗时会不竭膨胀。

Gson 的初次解析耗时与它的实现计划有关,在 Gson 的数据解析过程中有一个十分重要的角色,那就是 TypeAdapter,关于每一个待解析的对象的 Class,Gson 会起首为其生成一个 TypeAdapter,然后操纵那个 TypeAdapter 停止解析,Gson 默认的解析计划接纳的是 ReflectiveTypeAdapterFactory 创建的 TypeAdapter 的,其创建与解析过程中涉及到大量的反射挪用,详细流程为:

起首通过反射获取待解析对象的所有 Field,并逐个读取去读取它们的注解,生成一个从 serializeName 到 Filed 映射 map;解析过程中,通过读取到的 serializeName,到生成的 map 中找到对应的 Filed 信息,然后按照 Filed 的数据类型接纳特定类型的体例停止解析,然后通过反射体例停止赋值。

因而关于 Gson 解析耗时优化的核心就是削减反射,那里详细介绍一下抖音中利用到的一些优化计划。

自定义 TypeAdapter 优化

通过对 Gson 的源码阐发,我们晓得 Gson 的解析接纳的是责任链的形式,若是在 ReflectiveTypeAdapterFactory 之前已经有 TypeAdapterFactory 可以处置某个 Class,那么它是不会施行到 ReflectiveTypeAdapterFactory 的,而 Gson 框架又是撑持注入自定义的 TypeAdapterFactory 的,因而我们的一种优化计划就是注入一个自定义的 TypeAdapterFactory 去优化那个解析过程。

那个自定义 TypeAdapterFactory 会在编译期为每个待优化的 Class 生成一个自定义的 TypeAdapter,在那个 TypeAdapter 中会为 Class 的每个字段生成相关的解析代码,以到达制止反射的目标。

抖音互动游戏不做指定动做

生成自定义 TypeAdapter 过程中的字节码处置,我们接纳了抖音团队开源的字节码处置框架 Bytex(https://github.com/bytedance/ByteX/blob/master/README_zh.md),详细的实现过程如下:

设置装备摆设待优化 Class:在开发阶段,通过注解、设置装备摆设文件的体例对我们需要优化的 Class 停止加白;搜集待优化 Class 信息:起头编译后,我们从设置装备摆设文件中读取通过设置装备摆设文件设置装备摆设 Class;在遍历工程中所有的 class 的 traverse 阶段,我们通过 ASM 供给的 ClassVisitor 去读取通过注解设置装备摆设的 Class。关于所有需要优化的 Class,我们操纵 ClassVisitor 的 visitField 办法搜集当前 Class 的所有 Filed 信息;生成自定义 TypeAdapter 和 TypeAdapterFactory:在 trasform 阶段,我们操纵搜集到的 Class 和 Field 信息生成自定义的 TypeAdapter 类,同时生成创建那些 TypeAdapter 的自定义 TypeAdapterFactory;public class GsonOptTypeAdapterFactory extends BaseAdapterFactory { protected BaseAdapter createTypeAdapter(String var1) { switch(var1.hashCode()) { case -1939156288: if (var1.equals("xxx/xxx/gsonopt/model/Model1")) { return new TypeAdapterForModel1(this.gson); } break; case -1914731121: if (var1.equals("xxx/xxx/gsonopt/model/Model2")) { return new TypeAdapterForModel2(this.gson); } break; return null; }}public abstract class TypeAdapterForModel1 extends BaseTypeAdapter { protected void setFieldValue(String var1, Object var2, JsonReader var3) { Object var4; switch(var1.hashCode()) { case 110371416: if (var1.equals("field1")) { var4 = this.gson.getAdapter(String.class).read(var3); ((Model1)var2).field1 = (String)var4; return true; } break; case 1223751172: if (var1.equals("filed2")) { var4 = this.gson.getAdapter(String.class).read(var3); ((Model1)var2).field2 = (String)var4; return true; } } return false;}}

优化 ReflectiveTypeAdapterFactory 实现

上面那种自定义 TypeAdapter 的体例能够对 Gson 的初次解析耗时优化 70%摆布,但是那个计划需要在编译期增加解析代码,会增加包体积,具有必然的局限性,为此我们也测验考试了对 Gson 框架的实现停止了优化,为了降低接入成本我们通过修改字节码的体例去修改 ReflectiveTypeAdapterFactory 的实现。

原始的 ReflectiveTypeAdapterFactory 在停止现实数据解析之前,会起首去反射 Class 的所有字段信息,再停止解析,而在现实解析过程中并非所有的字段都是会利用到的,以下面的 Person 类为例,在停止 Person 解析之前,会对 Person、Hometown、Job 那三个类都停止解析,但是现实输入可能只是简单的 name,那种情况下关于 Hometown、Job 的解析就是完全没有需要的,若是 Hometown、Job 类的实现比力复杂,那将招致较多没必要要的时间开销。

class Person { @SerializedName(value = "name",alternate = {"nickname"}) private String name; private Hometown hometown; private Job job;}class Hometown { private String name; private int code;}class Job { private String company; private int type;}//现实输入{ "name":"张三"}

针对那类情况我们的处理计划就是“按需解析”,以上面的 Person 为例我们在解析 Person 的 Class 构造时,关于根本数据类型的 name 字段会停止一般的解析,关于复杂类型的 hometown 和 job 字段,会去记录它们的 Class 类型,而且返回一个封拆的 TypeAdapter;在现实停止数据解析时,若是确实包罗 hometown 和 job 节点,我们再去停止 Hometown 与 Job 的 Class 构造解析。那种优化计划关于 Class 构造复杂但是现实数据节点缺失较多情况下效果尤为明显,在抖音的理论过程中某些场景优化幅度接近 80%。

其他优化计划

上面介绍了两种比力典型的优化计划,在抖音的现实优化过程中还测验考试了其他的优化计划,在特定的场景也获得了不错的优化效果,各人能够参考:

同一 Gson 对象:Gson 会对解析过的 Class 停止 TypeAdapter 的缓存,但是那个缓存是 Gson 对象级此外,差别 Gson 对象之间不会停止复用,通过同一 Gson 对象能够实现 TypeAdapter 的复用;预创建 TypeAdapter:关于有足够的并发空间场景,我们在异步线程提早创建相关 Class 的 TypeAdapter,后续则能够间接利用预创建的 TypeAdapter 停止数据解析;利用其他协议:关于当地数据的序列化与反序列化我们测验考试利用了二进造挨次化的存储体例,将反序化耗时削减了 95%。在详细实现上我们接纳的是 Android 原生供给的 Parcel 计划,关于跨版本数据不兼容的情况,我们通过版本控造的体例回滚为版本兼容的 Gson 解析体例。1.5 UI 衬着优化

介绍完 Activity 阶段的优化我们再来看一下 UI 衬着阶段的相关优化,那个阶段我们将介绍 View 加载的相关优化。

抖音互动游戏不做指定动做

一般来说创建 View 有两种体例,第一种体例就是间接通过代码构建 View,第二种体例就是 LayoutInflate 去加载 xml 文件,那里将重点介绍LayoutInflate 加载 xml 的优化。LayoutInflate 停止 xml 加载包罗三个步调:

将 xml 文件解析到内存中 XmlResourceParser 的 IO 过程;按照 XmlResourceParser 的 Tag name 获取 Class 的 Java 反射过程;创建 View 实例,最末生成 View 树。

那 3 个步调整体上是比力耗时的。在营业层面上,我们能够通过优化 xml 层级、利用 ViewStub 体例停止按需加载等体例停止优化,那些优化能够在必然水平上优化 xml 的加载时长。

那里我们介绍另一种比力通用优化计划——异步预加载计划,以下图中 fragment 的 rootview 为例,它是在 UI 衬着的 measure 阶段被 inflate 出来的,而从应用启动到 measure 是有必然的时间 gap 的,我们完全能够操纵那段时间在后台线程提早将那些 view 加载到内存,在 measure 阶段再间接从内存中停止读取。

抖音互动游戏不做指定动做

x2c 处理锁的问题

在 androidx 中已经有供给了 AsyncLayoutInflater 用于停止 xml 的异步加载,但是现实利用下来会发现间接利用 AsyncLayoutInflater 很容易呈现锁的问题,以至招致了更多的耗时。

通过火析我们发现,那是因为在 LayoutInflate 中存在着对象锁,而且即便通过构建差别的 LayoutInflate 对象绕过那个对象锁,在 AssetManager 层、Native 层仍然会有其他锁。我们的处理计划就是 xml2code,在编译期为添加了注解的 xml 文件生成创建 View 的代码,然后异步停止 View 的预创建,通过 x2c 计划在处理了多线程锁的问题的同时,也提拔了 View 的预创建效率。目前该计划正在打磨中,后续在打磨完毕后将会停止详细介绍。

LayoutParams 的问题

异步 Inflate 除了多线程锁的问题,另一个问题就是 LayoutParams 问题。

LayoutInflater 对 View LayoutParam 处置次要依赖于 root 参数,关于 root 不为 null 的情况,在 inflate 的时候将会为 View 构造一个 root 联系关系类型的 LayoutParams,而且为其设置 LayoutParams,但是我们在停止异步 Inflate 的时候是拿不到根规划的,若是传入的 root 为 null,那么被 Inflate 的 View 的 LayoutParams 将会为 null,在那个 View 被添加到父规划时会接纳默认值,那会招致被 Inflate view 的属性丧失,处理那个问题的法子就是在停止预加载时候 new 一个响应类型的 root,以实现看待 inflate view 属性的准确解析。

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) { // 省略其他逻辑 if (root != null) { // Create layout params that match root, if supplied params = root.generateLayoutParams(attrs); if (!attachToRoot) { // Set the layout params for temp if we are not // attaching. (If we are, we use addView, below) root.setLayoutParams(params); } }}public void addView(View child, int index) { LayoutParams params = child.getLayoutParams(); if (params == null) { params = generateDefaultLayoutParams(); if (params == null) { throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null"); } } addView(child, index, params);}

其他问题

除了上面提到的多线程锁的问题和 LayoutParams 的问题,在停止预加载过程中还碰到了一些其他的问题,那些问题详细如下:

inflate 线程优先级的问题:一般情况下后台线程的优先级会比力低,在停止异步 inflate 时可能会因为 inflate 线程优先级过低招致来不及预加载以至比不停止预加载更耗时的情况,在那种情况下建议恰当提拔异步 inflate 线程的优先级。对 Handler 问题:存在一些自定义 View 在创建的时候会去创建 handler,那种情况下我们需要去修改创建 Handler 的代码,为其指定主线程的 Looper。对线程有要求:典型的就是自定义 View 里利用了动画,动画在 start 时会校验能否是 UI 线程主线程,那种情况我们需要去修改营业代码,将相关逻辑挪动到后续实正添加到 View tree 时。需要利用 Activity context 的场景:一种处理法子就是在 Activity 启动之后再停止异步预加载,那种体例无需专门处置 View 的 context 问题,但是预加载的并发空间可能会被压缩;另一种体例就是在 Application 阶段操纵 Applicaiton 的 context 停止预加载,但是在 add 到 view tree 之前将预加载 View 的 context 替代为 Activity 的 context,以满足 Dialog 显示、LiveData 利用等场景对 Activity context 的需求。1.6 主线程耗时动静优化

以上我们根本介绍了主线程各大生命周期的相关优化,在抖音的现实优化过程中我们发现一些被 post 在那些生命周期之间的主线程耗时动静也会对启动速度形成影响。好比 Application 和 Activity 之间、Activity 和 UI 衬着之间。那些主线程动静会招致我们后续的生命周期被延后施行,影响启动速度,我们需要对它们停止优化。

1.6.1 主线程动静调度

关于本身工程中的代码,我们能够比力便利的优化;但是有些是第三方 SDK 内部的逻辑,我们比力难以停止优化;即便是便利优化掉的动静后期的避免劣化成本也十分高。我们测验考试从别的一个角度处理那个问题,在优化部门往主线程 post 动静的同时,对主线程动静队列停止调整,让启动相关的动静优先施行。

抖音互动游戏不做指定动做

我们的核心原理是按照 App 启动流程确定核心启动途径,操纵动静队列调整来包管冷启动场景涉及相关动静优先调度,进而进步启动速度,详细来说包罗如下:

创建自定义的 Printer 通过 Looper 的 setMessageLogging 接口替代原有的 Printer,并对原始的 Printer 停止转发;在 Application 的 onCreate、MainActivity 的 onResume 中更新下一个待调度的动静,Application 的 onCreate 之后预期的目的动静是 Launch Activity,MainActivity 的 onResume 之后的预期动静则是衬着相关的 doFrame 动静。为了缩小影响范畴,在启动完成或者施行了非一般途径后则会对 disable 掉动静调度;动静调度的详细施行则是在自定义 Printer 的 println 办法中停止的,在 println 办法中遍历主线程动静队列,按照 message.what 和 message.getTarget()判断在动静队列中能否存在目的动静,若是存在则将其挪动到头部优先施行;1.6.2 主线程耗时动静优化

通过主线程动静调度,我们能够在必然水平上处理主线程动静对启动速度的影响,但是其也存在必然的局限性:

只能调整已经在动静队列中的动静,好比在 MainActivity onResme 之后存在一个耗时的主线程动静,而此时 doFrame 的动静还没有进入主线程的动静队列,那我们则需要施行完我们的耗时动静才气施行 doFrame 动静,其仍然会对启动速度有所影响;治本不治标,固然我们将主线程耗时动静从启动阶段移走,但是在启动后仍然会有卡顿存在。

基于那两个原因我们需要对启动阶段主线程的耗时动静停止优化。

一般来说主线程耗时动静大部门是营业强相关的,能够间接通过 trace 东西输出的主线程的仓库发现问题逻辑并停止针对性的优化,那里次要介绍一个其他产物也可能会碰到的 case 的优化——WebView 初始化形成的主线程耗时。

在我们的优化过程中发现一个主线程较大的耗时,其挪用仓库第一层为 WebViewChromiumAwInit.startChromiumLocked,是系统 Webview 中的代码,通过火析 WebView 代码发现其是在 WebViewChromiumAwInit 的 ensureChromiumStartedLocked 中 post 到主线程的,在每个历程周期初次利用 Webview 城市施行一次,无论是在主线程仍是子线程挪用最末城市被 post 到主线程形成耗时,因而我们无法通过修改挪用线程处理主线程卡顿的问题;同时因为是系统代码我们也无法通过修改代码实现的体例去停止处理,因而我们只能从营业层从利用的角度测验考试能否能够停止优化。

抖音互动游戏不做指定动做

void ensureChromiumStartedLocked(boolean onMainThread) { //省略其他逻辑 // We must post to the UI thread to cover the case that the user has invoked Chromium // startup by using the (thread-safe) CookieManager rather than creating a WebView. PostTask.postTask(UiThreadTaskTraits.DEFAULT, new Runnable() { @Override public void run() { synchronized (mLock) { startChromiumLocked(); } } }); while (!mStarted) { try { // Important: wait() releases |mLock| the UI thread can take it :-) mLock.wait(); } catch (InterruptedException e) { } } }

问题定位

从营业角度优化我们起首需要找到营业的利用点,固然我们通过火析代码定位到耗时动静是 Webview 相关的,但是我们仍然无法定位到最末的挪用点。要定位最末的挪用点,我们需要对WebView 相关挪用流程有所领会。系统的 WebView 是一个独立的 App,其他应用关于 Webview 的利用都需要颠末一个叫 WebViewFactory 的 framework 类,在那个类中起首会通过 Webview 的包名获取到 Webview 应用的 Context,然后通过获取到的 Context 获得 Webview 应用的 Classloader,最初通过 ClassLoader 去加载 Webview 的相关 so,反射加载 Webview 中的 WebViewFactoryProvider 的实现类并停止实例化,后续关于 WebiView 的相关挪用都是通过 WebViewFactoryProvider 接口停止的。

通事后续阐发发现关于 WebViewFactoryProvider 接口的 getStatics、 getGeolocationPermission、createWebView 等多个办法的初次挪用城市触发 WebViewChromiumAwInit 的 ensureChromiumStartedLocked 往主线程 post 一个耗时动静,因而我们的问题就酿成了关于WebViewFactoryProvider 相关办法的挪用定位。

一种定位法子就是通过插桩的体例实现,因为 WebViewFactoryProvider 并非应用可以间接拜候到的类,因而我们关于 WebViewFactoryProvider 的挪用一定是通过挪用 framework 其他代码实现的,那种情况下我们需要去阐发 framework 中所有关于 WebViewFactoryProvider 的挪用点,然后把应用中所有关于那些挪用点的挪用都停止插桩,停止日记输出以停止定位。很显然那种体例成本是比力高的,比力容易呈现漏掉的情况。

事实上关于 WebViewFactoryProvider 的情况我们能够接纳一个更便利的体例。在前面的阐发中我们晓得 WebViewFactoryProvider 是一个接口,我们是通过反射的体例获得其在 Webview 应用中实现的体例获得的,因而我们完全能够通过动态代办署理体例生成一个 WebViewFactoryProvider 对象,去替代 WebViewFactory 中的 WebViewFactoryProvider,在生成的 WebViewFactoryProvider 类的 invoke 办法中通过办法名过滤,关于我们的白名双方法输出其挪用栈。通过如许的体例我们最末定位到触发主线程耗时逻辑的是我们的 WebView UA 的获取。

抖音互动游戏不做指定动做

处理计划

确认到我们的耗时是由获取 WebView UA 引起的,我们能够接纳当地缓存的体例处理:考虑到 WebView UA 记录的是 Webview 的版本等信息,其在绝大部门情况下是不会发作变革的,因而我们完全能够把 Webview UA 缓存在当地,后续间接从当地停止读取,而且在每次应用切到后台时,去获取一次 WebView UA 更新到当地缓存,以制止形成利用过程中的卡顿。

缓存的计划在 Webview 晋级等形成 Webview UA 发作变革的情况下可能会呈现更新不及时的情况,若是对 WebView 的实时性要求十分高,我们也能够通过挪用子历程 ContentProvider 的体例在子历程去获取 WebView UA,如许固然会影响到子历程的的主线程但是不会影响到我们的前台历程。当然那种体例因为需要启动一个子历程同时需要走完好的 Webview UA 读取,相对当地缓存的体例在读取速度方面是有明显的优势的,关于一些对读取速度有要求的场景是不太合适的,我们能够按照现实需要接纳响应的计划。

2. 后台使命优化

前面的案例根本都是主线程相关耗时的优化,事实上除了主线程间接的耗时,后台使命的耗时也是会影响到我们的启动速度的,因为它们会抢占我们前台使命的 cpu、io 等资本,招致前台使命的施行时间变长,因而我们在优化前台耗时的同时也需要优化我们的后台使命。一般来说后台使命的优化与详细的营业有很强的联系关系性,不外我们也能够整理出来一些共性的优化原则:

削减后台线程没必要要的使命的施行,出格是一些重 CPU、IO 的使命;对启动阶段线程数停止收敛,避免过多的并发使命抢占主线程资本,同时也能够制止频繁的线程间调度降低并发效率。

除了那些通用的原则,那里也介绍两个抖音中比力典型的后台使命优化的案例。

2.1 历程启动优化

我们优化过程中除了需要存眷当前历程后台线程的运行情况,也需要存眷后台历程的运行情况。目前绝大部门应用城市有 push 功用,为了削减后台耗电、制止因为占用过多内存招致历程被杀,一般情况下会把 push 相关功用放在独立的历程。若是在启动阶段去启动 push 历程,其也会对我们的启动速度形成比力大的影响,我们尽量对 push 历程的启动去停止恰当延迟,制止在启动阶段启动。

在线下情况下我们能够通过对 logcat 中“Start proc”等关键字停止过滤,去发现能否存在启动阶段启动子历程的情况,以及获得触发子历程启动的组件信息。关于一些复杂的工程或者是三方 sdk,我们即便晓得了启动历程的组件,也比力难定位到详细的启动逻辑,我们能够通过对 startService、bindService 等启动Service、Recevier、ContentProvider组件挪用停止插桩,输入挪用仓库的体例,连系“Start proc”中组件的去精准定位我们的触发点。除了在 manifest 中生命的历程可能还存在一些 fork 出 native 历程的情况,那种历程我们能够通过adb shell ps的体例去停止发现。

抖音互动游戏不做指定动做

2.2 GC 按捺

后台使命影响启动速度中还有还有另一个比力典型的 case 就是 GC,触发 GC 后可能会抢占我们的 cpu 资本以至招致我们的线程被挂起,若是启动过程中存在大量的 GC,那么我们的启动速度将会遭到比力大的影响。

处理那个问题的一个办法就是削减我们启动阶段代码的施行,削减内存资本的申请与占用,那个计划需要我们去革新我们的代码实现,是处理 gc 影响启动速度的最底子法子。同时我们也能够通过 GC 按捺的通用法子去削减 GC 对启动速度的影响,详细来说就是在启动阶段去按捺部门类型的 GC,以到达削减 GC 的目标。

近期公司的 Client Infrastructure-App Health 团队调研出了 ART 虚拟机上的 GC 按捺计划,在公司的部门产物上测验考试对应用的启动速度有不错的优化效果,详细的手艺细节在后续打磨完成后将会在“字节跳动末端手艺”公家号分享出来。

3. 全局优化

前面介绍的案例根本都是针对某个阶段一些比力耗时点的优化,现实上我们还存在一些单次耗时不那么明显,但是频次很高可能会影响到全局的点,好比我们营业中的高频函数、好比我们的类加载、办法施行效率等,那里我们将对抖音在那些方面的优化测验考试做一些介绍。

3.1 类加载优化3.1.1 ClassLoader 优化

起首我们来看一下抖音在类加载方面的一个优化案例。谈到类加载我们就离不开类加载的双亲委派机造,我们简单回忆一下那种机造下的类加载过程:

起首从已加载类中查找,若是可以找到则间接返回,找不到则挪用 parent classloader 的 loadClass 停止查找;若是 parent clasloader 能找到相关类则间接返回,不然挪用 findClass 去停止类加载;protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{ Class<?> c = findLoadedClass(name); if (c == null) { try { if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { } if (c == null) { c = findClass(name); } } return c;}

Android 中的 ClassLoader

双亲委派机造中很重要的一个点就是 ClassLoader 的父子关系,我们再来看一下 Android 中 ClassLoader 情况。一般情况下 Android 中有两个 ClassLoader,别离是 BootClassLoader 和 PathClassLoader,BootClassLoaderart 负责加载 android sdk 的类,像我们的 Activity、TextView 等都由 BootClassLoader 加载。PathClassLoader 则负责加载 App 中的类,好比我们的自定义的 Activity、support 包中的 FragmentActivity 那些会被打进 app 中的类则由 PathClassLoader 停止加载。BootClassLoader 是 PathClassLoader 的 parent。

ART 虚拟机对类加载的优化

ART 虚拟机在类加载方面仍然遵照双亲委派的原则,不外在实现上做了必然的优化。一般情况下它的大致流程如下:

起首挪用 PathClassLoader 的 findLoadedClass 办法去查找已加载的类中查找,那个办法将会通过 jni 挪用到 ClassLinker 的 LookupClass 办法,若是可以找到则间接返回;在已加载类中找不到的情况下,不会立即返回到 java 层,其会在 native 层去挪用 ClassLinker 的 FindClassInBaseDexClasLoader 停止类查找;在 FindClassInBaseDexClasLoader 中,起首会去判断当前 ClassLoader 能否为 BootClassLoader,若是为 BootClasLoader 则测验考试从当前 ClassLoader 的已加载类中查找,若是可以找到则间接返回,若是找不到则测验考试利用当前 ClassLodaer 停止加载,无论能否加载到都返回;若是当前 ClassLoader 不是 BootClassLoader,则会判断能否为 PathClasLoader,若是不是 PathClassLoader 则间接返回;若是当前 ClassLoader 为 PathClassLoader,则会去判断当前 PathClassLoader 能否存在 parent,若是存在 parent 则将 parent 传入递归挪用 FindClassInBaseDexClasLoader 办法,若是可以找到则间接返回;若是找不到或者当前 PathClassLoader 没有 parent 则间接在 native 层通过 DexFile 间接停止类加载。抖音互动游戏不做指定动做

能够看到当 PathClassLoader 到 BootClassLoader 的 ClassLoadeer 链路上只要 PathClassLoader 时,java 层的 findLoadedClass 办法挪用后,其实不行如其字面含义的去已加载的类中查找,其还会在 native 层间接通过 DexFile 去加载类,那种体例相关于回到 java 层挪用 findClass 再调回 native 层通过 DexFile 加载能够削减一次没必要要的 jni 挪用,在运行效率上是更高的,那是 art 虚拟机对类加载效率的一个优化。

抖音中 ClassLoader 模子

在前面我们介绍了 Android 中的类加载相关机造,那么我们事实在类加载方面做了哪些优化,要解答那个问题我们需要领会一下抖音中的ClassLoader 模子。在抖音中为了削减包体积,一些非核心功用我们通过插件化的体例停止了动态下发。在接入插件化框架后抖音中的 ClassLoader 模子如下:

除了原有的 BootClassLoader 和 PathClassLoader 别的引入了 DelegateClassLoader 和 PluginClasLoader;DelegateClassloader 全局 1 个,它是 PathClassLoader 的 parent,它的 parent 为 BootClassLoader;PluginClassLoader 每个插件一个,它的 parent 为 BootClassLoader;DelegateClassLoader 会持有 PluginClassLoader 的引用,PluginClassLoader 则会持有 PathClasloader 的引用;抖音互动游戏不做指定动做

那种 ClassLoader 模子有一个十分明显的长处,那就是它可以十分便利的同时撑持类的隔离、复用以及插件化与组件化的切换;

类的隔离:若是在宿主和多个插件中存在同名类,在宿主中利用某个类则会起首从宿主 apk 加载,在插件中利用某个类,则会优先从当前插件的 apk 中加载,那种加载机造单 ClassLoader 模子的插件框架是无法撑持的;类的复用:在宿主中利用某个插件中特有的类时,我们能够在 DelegateClassLoader 中检测到类加载失败,进而利用 PluginClassLoader 去插件中加载,实现宿主复用插件中的类;在插件中利用某个宿主特有的类时,能够在 PluginClassLoader 中检测到类加载失败,进而利用 PathClassLoader 去停止加载,实现插件复用宿主中的类,那种复用机造其他多 ClassLoader 模子的插件框是无法撑持的;插件化与组件化自在切换:那种 ClassLoader 模子下,我们加载宿主/插件中的类时无需任何显示的 ClassLoader 的指定,我们能够很便利的在间接依赖的组件化体例以及 compileonly+插件化的体例之间切换;

ART 类加载优化机造被毁坏

上面介绍了抖音的 ClassLoader 模子的长处,但是其也有一个比力隐蔽的不敷,那就是它会毁坏 ART 虚拟机对类加载的优化机造。

通过前面的介绍我们领会,当 PathClassLoader 到 BootClassLoader 的 ClassLoader 链路上只要 PathClassLoader 时,则能够在 native 层停止类的加载,以削减一次 jni 的挪用。在抖音的 ClassLoader 模子中,PathClassLoader 与 BootClassLoader 之间存在一个 DelegateClassLoader,它的存在会招致“PathClassloader 到 BootClassLoader 的 ClassLoader 链路上只要 PathClassLoader”那一前提被毁坏,那招致我们 app 中所有类的初次加载都需要多一次 jni 的挪用。一般情况下多一次 jni 的挪用不会带来几消耗,但是关于启动阶段大量类加载的场景,那个影响也是比力大的,会对我们的启动速度形成必然的影响。

非侵入式优化计划:延迟注入

领会插件化对类加载形成负向的原因,优化思绪也就比力明晰了——将 DelegateClassLoader 从 PathCLasLoader 和 BootClassLoader 之间移除掉。

通过前面的阐发,我们晓得引入 DelegateClassLoader 是为了在利用 PathClassLoader loadClass 失败时,能够利用 PluginClassloader 去插件中加载,因而关于不利用插件的场景,DelegateClassloader 是完全没有需要的,我们完全能够在需要用到插件功用时再停止 DelegateClassloader 的注入。

但在现实施行过程中,那种完全停止按需注入会比力困难,因为我们无法切确掌握插件加载时机,好比我们的可能通过是通过 compileonly 的体例隐式的依赖、加载插件的类,也可能在 xml 中利用某个插件的 view 的体例触发插件的加载,若是要停止适配会对营业开发带来比力大的侵入。

那里测验考试换一个思绪停止优化——我们固然没法切确地晓得插件加载时机,但却能够晓得哪里没有插件加载。好比 Application 阶段是没有插件加载的,那么完全能够等 Applicaiton 阶段施行完成再停止 DelegateClassloader 的注入。事实上在启动过程中,类的加载次要集中在 Application 阶段,通过在 Applicaiton 施行完成再去停止 DelegateClassloader 停止注入,能够极大地削减插件化计划对启动速度的影响,同时也能够制止对营业的侵入。

侵入式优化计划:革新 ClassLoader 模子

上面的计划无需侵入营业革新成本很小,但是它只是优化了 Application 阶段的类加载,后续阶段 ART 对类加载的优化仍然无法享遭到,从极致性能的角度我们做了进一步的优化。我们优化的核心思惟就是把 DelegateClassloader 从 PathClassLoader 和 BootClassLoader 之间彻底去除掉,通过其他体例来处理宿主加载插件类的问题。通过火析我们能够晓得宿主加载插件的类次要有几种体例:

通过 Class.forName 的体例去反射加载插件的类;通过 compileOnly 隐式依赖插件的类,运行时间接加载插件的类;启动插件的四大组件时加载插件的组件类;在 xml 中利用插件的类;

因而我们的问题就酿成了在不注入 ClassLoader 的情况下,若何实现宿主加载插件的那四大类。

起首是Class.forName 的体例,处理那种体例下插件类加载的问题最间接的处理法子就是挪用 Class.forName 时显示的去指定 ClassLoader 为 DelegateClassloader,不外如许的体例对营业开发不敷友好,且存在一些三方 sdk 中代码我们无法修改的问题。我们最末的处理法子就是对 Class.forName 挪用停止字节码插桩,在类加载失败时再测验考试利用 DelegateClassloader 去停止加载。

接下来是compileOnly 的隐式依赖,那种体例比力难停止通用途理,因为我们无法找到一个适宜的时机去对类加载失败停止兜底。针对那个问题我们的处理法子就是停止营业的革新,将 compileOnly 的隐式依赖挪用的体例改成通过 Class.forName 的体例,之所以停止如许的革新次要是基于几下几点考虑:

起首抖音中 compileOnly 隐式依赖挪用的体例十分少,革新成底细对可控;其次 compileOnly 的体例在插件的利用上固然便利,但是它在入口上不敷收敛,在插件加载管控、问题排查、插件宿主版本间兼容上都存在必然的问题,通过 Class.forName + 接口化的体例能够较好的处理那些问题。

插件四大组件类的加载和 xml 中利用插件类的问题都能够通过统一个计划来处理——将 LoadedApk 中的 ClassLoader 替代为DelegateClassLoader,如许无论是四大组件 class 的加载仍是 LayoutInflate 加载 xml 时的 class 加载城市利用 DelegateClassLoader 加载,关于那部门的原理各人能够参考 DroidPlugin、Replugin 等相关插件化原理解析,那里就不展开介绍了。

抖音互动游戏不做指定动做

3.1.2 Class verify 优化

关于 ClassLoader 的优化,优化的是类加载过程中的 load 阶段,关于类加载的其他阶段也能够停止必然的优化,比力典型的一个案例就是classverify的优化,classverify 过程次要是校验 class 能否契合 java 标准,若是不契合标准则会在 verify 阶段抛出 verify 相关的异常。

一般情况下 Android 中的 class 在应用安拆或插件加载时就会停止 verify,但是存在一些特定 case,好比 Android10 之后的插件、插件编译接纳 extract filter 类型、宿主与插件彼此依赖招致静态 verify 失败等情况,则需要在运行时停止 verify。运行 verify 的过程除了会校验 class,还会触发它所依赖 class 的 load,从而形成耗时。

抖音互动游戏不做指定动做

事实上 classverify 次要是针对收集下发的字节码停止校验,关于我们的插件代码其在编译的过程中就会去校验 class 的合法性,并且即便实的呈现了不法的 class,最多也是将 verify 阶段抛出的异常转移到 class 利用的时候。

因而我们能够认为,运行时的 classverify 是没有需要的,能够通过封闭 classverrify来优化那些类的加载。关于封闭 classverify 目前业界已经有一些比力优良的计划,好比运行时在内存中定位出 verify_所在内存地址,然后将其设置成跳过 verify 形式以实现跳过 classverify。

// If kNone, verification is disabled. kEnable by default. verifier::VerifyMode verify_; // If true, the runtime may use dex files directly with the interpreter if an oat file is not available/usable. bool allow_dex_file_fallback_; // List of supported cpu abis. std::vector<std::string> cpu_abilist_; // Specifies target SDK version to allow workarounds for certain API levels. int32_t target_sdk_version_;

当然封闭 classverify 的优化计划其实不必然对所有的应用都有价值,在停止优化之前能够通过 oatdump 号令输出一下宿主、插件中在运行时停止 classverify 的类信息,关于存在大量类在运行时 verify 的情况能够接纳上面介绍的计划停止优化。

oatdump --oat-file=xxx.odex > dump.txtcat dump.txt | grep -i "verified at runtime" |wc -l3.2 其他全局优化

在全局优化方面,还有一些其他比力通用的优化计划,那里也停止一些简单的介绍,以供各人参考:

高频办法优化:对办事发现(spi)、尝试开关读取等高频挪用办法停止优化,将本来在运行时的注解读取、反射等操做前置到编译阶段,通过编译阶段间接生成目的代码替代原有挪用实现施行速度的提拔;IO 优化:通过削减启动阶段没必要要的 IO、对关键链路上的 IO 停止预读以及其他通用的 IO 优化计划提拔 IO 效率;binder 优化:对启动阶段一些会屡次挪用的 binder 停止成果缓存以削减 IPC 的次数,好比我们应用本身的 packageinfo 的获取、收集形态获取等;锁优化:通过去除没必要要的锁、降低锁粒度、削减持锁时间以及其他通用的计划削减锁问题对启动的影响字节码施行优化:通过办法挪用内联的体例,削减一些没必要要的字节码的施行,目前已经以插件的体例集成在抖音的字节码开源框架 Bytex 中(详见 Bytex 介绍);预加载优化:充实操纵系统的并发才能,通过用户画像、端智能预测等体例在异步线程对各类资本停止精准精准预加载,以到达消弭或者削减关键节点耗时的目标,可供预加载的内容包罗 sp、resource、view、class 等;线程调度优化:通过使命的动态优先级调整以及在差别 CPU 核心上的负载平衡等手段,降低 Sleeping 形态和 Uninterrupible Sleeping 耗时,在不进步 CPU 频次的情况下,进步 CPU 时间片的操纵率(由 Client Infrastructure-App Health 团队供给处理计划);厂商合做:与厂商合做通过 CPU 绑核、提频等体例获取到更多的系统资本,以到达提拔启动速度的目标;总结与瞻望

至此,我们已经对抖音启动优化中比力典型、通用的案例停止了介绍,希望那些案列可以为各人的启动优化供给一些参考。回忆抖音以往的所有启动相关的优化,通用的优化只是占了此中一小部门,更多的是与营业相关的优化,那部门优化有着极强的营业联系关系性,其他营业无法间接停止迁徙,针对那部门优化我们总结了一些优化的办法论,详细能够拜见抖音 Android 性能优化系列:启动优化之理论和东西篇。最初从理论的角度对我们的启动优化做一些总结与瞻望, 希望能对各人有所帮忙。

持续迭代

启动优化是一个需要持续迭代与打磨的的过程,一般来说最起头的是“快、猛”的快速优化阶段,那个阶段优化空间会比力大,优化粒度会相对较粗,在投入不多的人力情况下就能获得不错的收益;第二个阶段难点攻坚阶段,那个阶段需要的投入相对第一个阶段要大,最末的提拔效果也取决于难点的攻坚情况;第三个阶段是防劣化与持续的精细化优化过程,那个过程是最为耐久的一个过程,关于快速迭代的产物,那个阶段也十分重要,是我们通向极致化启动性能的必经之路。

场景泛化

启动优化也需要停止必然扩展与泛化的,一般情况下我们存眷的是用户点击 icon 到首页首帧的时间,但是跟着贸易化开屏、push 点击等场景的增加,我们也需要扩展到那些场景。别的良多时候固然页面的首帧出来了,但用户仍是无法看到想看的内容,因为用户存眷的可能不是页面首帧的时间,而是有效内容加载出来的时间。以抖音为例,我们在存眷启动速度的同时,也会去存眷视频首帧的时间,从 AB 尝试来看那个目标以至比启动速度更重要,其他产物也能够连系本身的营业,去定义一些对应的目标,验证对用户体验的影响,决定能否需要停止优化。

全局意识

一般来说,我们以启动速度来权衡启动性能。为了提拔启动速度,我们可能会把一些本来在启动阶段施行的使命停止延后或者按需,那种体例可以有效优化启动速度,但同时也可能损害后续的利用体验。好比,若是将某个启动阶段的后台使命延后到后续利用时,若是初次利用是在主线程,则可能会形成利用卡顿。因而,我们在存眷启动性能的同时,也需要存眷其他可能影响的目标。

性能上我们需要有一个能表现全局性能的宏不雅目标,以避免部分更优效应。营业上我们需要成立启动性能与营业的关系,详细来说就是在优化过程中尽可能对一些较大的启动优化撑持 AB 才能,如许做一方面能够实现对优化的定性阐发,避免一些有部分性能收益但是对全局体验有损害的负优化被带到线上去;另一方面也能够操纵尝试的定性阐发才能,量化各个优化对营业的效果,从而为后续的优化标的目的供给指点。同时也能够对一些可能形成不变性或者功用异常的改动,供给回滚才能以及时行损。

目前,字节跳动旗下的企业级手艺办事平台火山引擎已经对外开放了 AB 尝试才能,感兴趣的同窗能够到火山引擎官网停止领会。

全笼盖与精细化运营

将来抖音的启动优化有两个大的目的,第一个目的是启动优化的笼盖率做到更大化:架构方面我们希望启动阶段的代码可以做到依赖简单、明晰,模块粒度尽可能的小,后续优化与迭代成本低;体验方面在做好性能优化的同时做好交互、内容量量等功用优化,提拔功用的触达效率与品量;场景方面做到冷启动、温启动、热启动等各类启动体例、落地页的全面笼盖;优化标的目的上笼盖 CPU、IO、内存、锁、UI 衬着等各类优化标的目的。第二个目的是实现启动优化精细化运营,做到千人千时千面,关于差别的用户、差别的设备性能与情况、差别的启动场景等接纳差别的启动战略,实现体验优化的更大化。

参加我们

抖音 Android 根底手艺团队是一个深度逃求极致的团队,我们专注于性能、架构、包大小、不变性、根底库、编译构建等标的目的的深耕,保障超大规模团队的研发效率和数亿用户的利用体验。目前北京、上海、杭州、深圳都有大量人才需要,欢送有志之士与我们配合建立亿级用户的 APP!

对抖音工做时机感兴趣的同窗,能够进入字节跳动雇用官网查询「抖音根底手艺 Android」相关职位,也能够邮件联络:fengda.1@bytedance.com 征询相关信息或者间接发送简历内推!

本文TAG:

指南AI

幕言互游在线咨询

上班时间:9:00-22:00
周六、周日:14:00-22:00
wechat
打开微信扫一扫,加我好友!

无限流量卡免费领取

点击预约
免费领取 先到先得