Android性能优化笔记(一)——启动优化

应用启动类型

  • 冷启动
  1. 场景:开机后第一次启动应用 或者 应用被杀死后再次启动

  2. 生命周期:Process.start->Application创建->attachBaseContext->onCreate->onStart->onResume->Activity生命周期

  3. 启动速度:在几种启动类型中最慢,也是我们优化启动速度最大的拦路虎

  • 温启动
  1. 场景:应用已经启动,返回键退出

  2. 生命周期:onCreate->onStart->onResume->Activity生命周期

  3. 启动速度:较快

  • 热启动
  1. 场景:Home键最小化应用

  2. 生命周期:onResume->Activity生命周期

  3. 启动速度:快

从上面的总结可以看出,在应用的启动过程中,冷启动是最慢最耗时的,系统以及应用本身都有大量的工作需要处理,所以,冷启动对于应用的启动速度是最具挑战以及最有必要进行优化的。

冷启动流程

冷启动指的是应用程序从进程在系统不存在,到系统创建应用运行进程空间的过程。冷启动通常会发生在一下两种情况:

  • 设备启动以来首次启动应用程序

  • 系统杀死应用程序之后再次启动应用程序

在冷启动的最开始,系统需要负责做三件事:

  • 加载以及启动app

  • app启动之后立刻显示一个空白的预览窗口

  • 创建app进程

一旦系统完成创建app进程后,app进程将要接着负责完成下面的工作:

  • 创建Application对象

  • 创建并且启动主线程ActivityThread

  • 创建启动第一个Activity

  • Inflating views

  • 布局屏幕

  • 执行第一次绘制

一旦app进程完完成了第一次绘制工作,系统进程就会用main activity替换前面显示的预览窗口,这个时候,用户就可以正式开始与app进行交互了。

从冷启动的流程看,我们无法干预app进程创建等系统操作,我们能够干预的有:

  • 预览窗口

  • Application生命周期回调

  • Activity生命周期回调

优化分析测量工具

对研发人员来说,启动速度是我们的“门面”,它清清楚楚可以被所有人看到,我们都希望自己应用的启动速度可以秒杀所有竞争对手。

“工欲善其事必先利其器”,我们需要先找到一款适合做启动优化分析的工具或者方式。

  • adb shell am start -W [packageName]/[ packageName. AppstartActivity]

在统计 app 启动时间时,系统为我们提供了 adb 命令,可以输出启动时间。系统在绘制完成后,ActivityManagerService 会回调该方法,但是能够方便我们通过脚本多次启动测量 TotalTime,对比版本间启动时间差异。但是统计时间不如 Systrace 准确。

  • 代码埋点

通过代码埋点来准确获取记录每个方法的执行时间,知道哪些地方耗时,然后再有针对性地优化。例如通过在 app 启动生命周期中,关键位置加入时间点记录,达到测量目的;又例如可以在 Application 的 attachBaseContext方法中记录开始时间,然后在启动的第一个 Activity 的 onWindowFocusChanged方法记录结束时间。

但是从用户点击 app Icon 到 Application 被创建,再到 Activity 的渲染,中间还是有很多步骤的,比如冷启动的进程创建过程,而这个时间用此版本是没办法统计了,必须得承受这点数据的不准确性。

  • Nanoscope

Nanoscope 非常真实,不过暂时只支持 Nexus 6 和 x86 模拟器。

  • Simpleperf

Simpleperf 的火焰图并不适合做启动流程分析。

  • TraceView

通过 TraceView 主要可以得到两种数据:单次执行耗时的方法 以及 执行次数多的方法。但是TraceView 性能耗损太大,不能比较正确反映真实情况。

  • Systrace

Systrace 能够追踪关键系统调用的耗时情况,如系统的 IO 操作、内核工作队列、CPU 负载、Surface 渲染、GC 事件以及 Android 各个子系统的运行状况等。但是不支持应用程序代码的耗时分析。

综上所述,这几种方式都各有各的优点以及缺点,我们都要掌握。

但是有没有一种比较折中比较理想的方案呢?有的。

  • “Systrace + 函数插桩”

除了能够看到例如 GC、System Server、CPU 调度等系统调用的耗时,还能够通过 Android 工程编译的过程中,在指定的方法前后,自动化插入插桩函数,统计方法执行时间。通过插桩,我们可以看到应用主线程和其他线程的函数调用流程。它的实现原理非常简单,就是将下面的两个函数 通过用ASM框架修改字节码的方式 分别插入到每个方法的入口和出口。

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="" cid="n109" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">class TraceMethod {
public static void i() {
Trace.beginSection();
}

public static void o() {
Trace.endSection();
}
}</pre>

当然这里面有非常多的细节需要考虑,比如怎么样降低插桩对性能的影响、哪些函数需要被排除掉。函数插桩后的效果如下:

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="" cid="n111" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">class Test {

public void test() {
TraceMethod.i();
// 原来的工作
TraceMethod.o();
}
}</pre>

只有准确的数据评估才能指引优化的方向,这一步是非常重要的。没有充分评估或者评估使用了错误的方法,最终得到了错误的方向,会导致最后发现根本达不到预期的优化效果。

启动优化方法

在拿到整个启动流程的全景图之后,我们可以清楚地看到这段时间内系统、应用各个进程和线程的运行情况,现在我们要开始真正开始“干活”了。

具体的优化方式,我把它们分为预览窗口优化、业务梳理、业务优化、多进程优化、线程优化、GC 优化和系统调用优化。业务梳理、业务优化、线程优化、GC 优化、系统调用优化和布局优化。

预览窗口优化

当用户点击应用桌面图标启动应用的时候,利用提前展示出来的 Window,快速展示出一个界面,用户只需要很短的时间就可以看到“预览页”,这种完全“跟手”的感觉在高端机上体验非常好,但对于中低端机,会把总的的闪屏时间变得更长。

如果点击图标没有响应,用户主观上会认为是手机系统响应比较慢。所以比较推荐的做法是,只在 Android 6.0 或者 Android 7.0 以上才启用“预览窗口”方案,让手机性能好的用户可以有更好的体验。

要实现预览窗口的显示,只需要在利用 activity 的windowBackground主题属性提供一个简单的自定义 drawable 给启动的 activity,如下:

Layout XML file:

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="" cid="n123" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><layer-list xmlns:android="http://schemas.android.com/apk/res/android" android:opacity="opaque">

<item android:drawable="@android:color/white"/>

<item>
<bitmap
android:src="@drawable/product_logo_144dp"
android:gravity="center"/>
</item>
</layer-list></pre>

Manifest file:

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="" cid="n125" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><activity ...
android:theme="@style/AppTheme.Launcher" /></pre>

这样一个 activity 启动的时候,就会先显示一个预览窗口,给用户快速响应的体验。当 activity想要恢复原来 theme,可以通过在调用super.onCreate()setContentView()之前调用 setTheme(R.style.AppTheme),如下:

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="" cid="n127" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">public class MyMainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
// Make sure this is before calling super.onCreate
setTheme(R.style.Theme_MyApp);
super.onCreate(savedInstanceState);
// ...
}
}</pre>

业务梳理

不要一股脑把全部初始化工作放在 Application 中做,需要梳理清楚当前启动过程正在运行的每一个模块,哪些是一定需要的、哪些可以砍掉、哪些可以懒加载。但是需要注意的是,懒加载要防止集中化,否则容易出现首页显示后用户无法操作的情形。总的来说,用以下四个维度分整理启动的各个点:

  • 必要且耗时:启动初始化,考虑用线程来初始化。

  • 必要不耗时:首页绘制。

  • 非必要但耗时:数据上报、插件初始化。

  • 非必要不耗时:不用想,这块直接去掉,在需要用的时再加载。

把数据整理出来后,按需实现加载逻辑,采取分步加载、异步加载、延期加载策略,如下图所示:

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="" cid="n140" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">

</pre>

一句话概述,要提高应用的启动速度,核心思想是在启动过程中少做事情,越少越好。

业务优化

通过梳理之后,剩下的都是启动过程一定要用的模块。这个时候,我们只能硬着头皮去做进一步的优化。优化前期需要“抓大放小”,先看看主线程究竟慢在哪里。最理想是通过算法进行优化,例如一个数据解密操作需要 1 秒,通过算法优化之后变成 10 毫秒。退而求其次,我们要考虑这些任务是不是可以通过异步线程预加载实现,但需要注意的是过多的线程预加载会让我们的逻辑变得更加复杂。

业务优化做到后面,会发现一些架构和历史包袱会拖累我们前进的步伐。比较常见的是一些事件会被各个业务模块监听,大量的回调导致很多工作集中执行,部分框架初始化“太厚”,例如一些插件化框架,启动过程各种反射、各种 Hook,整个耗时至少几百毫秒。还有一些历史包袱非常沉重,而且“牵一发动全身”,改动风险比较大。但是我想说,如果有合适的时机,我们依然需要勇敢去偿还这些“历史债务”。

多进程优化

Android app 是支持多进程的,在 Manifest 中只要在组件声明中加入android:process属性就可以让组件在启动时运行在不同的进程中。举个例子: 对于多进程 app ,可能拥有主进程,插件进程以及下载进程,但开发者只能在 Manifest 中声明一个 Application 组件,如果对应不同进程的组件启动时,系统会创建三个进程,创建三个 Application 对象,同时attachBaseContextonCreate等生命周期回调方法也会被调用三次。

但是每个进程需要初始化的内容肯定是不一样的,所以,为了防止资源的浪费,我们需要在Application 中区分进程,对应进程只初始化对应的内容。

线程优化

线程优化分两方面:

第一,耗时任务异步化。子线程处理耗时任务,主线程做的事情越少,越早进入Acitivity绘制阶段,界面越早展现。例如不在主线程做如 IO 、网络等耗时操作。但是要注意,子线程不能阻塞主线程。

第二,线程池管理线程,控制线程的数量。线程数量太多会相互竞争 CPU 资源,导致分给主线程的时间片减少,从而导致启动速度变慢。线程切换的数据我们可以通过卡顿优化中学到的 sched 文件查看,这里特别需要注意 nr_involuntary_switches 被动切换的次数。

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="" cid="n152" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">proc/[pid]/sched:
nr_voluntary_switches:主动上下文切换次数,因为线程无法获取所需资源导致上下文切换,最普遍的是 IO。
nr_involuntary_switches:被动上下文切换次数,线程被系统强制调度导致上下文切换,例如大量线程在抢占 CPU。 </pre>

第三,避免主线程与子线程之间的锁阻塞等待。有一次我们把主线程内的一个耗时任务放到线程中并发执行,但是发现这样做根本没起作用。仔细检查后发现线程内部会持有一个锁,主线程很快就有其他任务因为这个锁而等待。通过 Systrace 可以看到锁等待的事件,我们需要排查这些等待是否可以优化,特别是防止主线程出现长时间的空转。

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="" cid="n154" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">

特别是现在有很多启动框架,会使用 Pipeline 机制,根据业务优先级规定业务初始化时机。比如微信内部使用的 mmkernel 、阿里最近开源的 Alpha 启动框架,它们为各个任务建立依赖关系,最终构成一个有向无环图。对于可以并发的任务,会通过线程池最大程度提升启动速度。如果任务的依赖关系没有配置好,很容易出现下图这种情况,即主线程会一直等待 taskC 结束,空转 2950 毫秒。</pre>

image
第四,设置子线程优先级。不重要任务,设置子线程优先级为 THREAD_PRIORITY_BACKGROUND,这样子线程最多能获取到10%的时间片,优先保证主线程执行。

GC优化

在启动过程,要尽量减少 GC 的次数,避免造成主线程长时间的卡顿,特别是对 Dalvik 来说,我们可以通过 Systrace 单独查看整个启动过程 GC 的时间。

启动过程避免进行大量的字符串操作,特别是序列化跟反序列化过程。一些频繁创建的对象,例如网络库和图片库中的 Byte 数组、Buffer 可以复用。如果一些模块实在需要频繁创建对象,可以考虑移到 Native 实现。

Java 对象的逃逸也很容易引起 GC 问题,我们在写代码的时候比较容易忽略这个点。我们应该保证对象生命周期尽量的短,在栈上就进行销毁。

系统调用优化

部分系统的API使用是阻塞性的,文件很小可能无法感知,当文件过大,或者使用频繁时,可能造成阻塞。例如:SharedPreference.Editor 的提交操作建议使用异步的 apply,而不是阻塞的 commit。

通过 systrace 的 System Service 类型,我们可以看到启动过程 System Server 的CPU 工作情况。在启动过程,我们尽量不要做系统调用,例如 PackageManagerService 操作、Binder 调用等待。

在启动过程也不要过早地拉起应用的其他进程,System Server 和新的进程都会竞争 CPU 资源。特别是系统内存不足的时候,当我们拉起一个新的进程,可能会成为“压死骆驼的最后一根稻草”。它可能会触发系统的 low memorykiller 机制,导致系统杀死和拉起(保活)大量的进程,从而影响前台进程的 CPU。举个例子,之前一个程序在启动过程会拉起下载和视频播放进程,改为按需拉起后,线上启动时间提高了 3%,对于 1GB 以下的低端机优化,整个启动时间可以优化 5%~8%,效果还是非常明显的。

布局优化

布局越复杂,测量布局绘制的时间就越长。主要做到以下几点:

  1. 布局的层级越少,加载速度越快。

  2. 一个控件的属性越少,解析越快,删除控件中的无用属性。

  3. 使用<ViewStub/>标签加载一些不常用的布局,做到使用时在加载。

  4. 使用<merge/>标签减少布局的嵌套层次。

  5. 尽可能少用wrap_content,wrap_content会增加布局measure时的计算成本,已知宽高为固定值时,不用wrap_content。

启动优化进阶方法

还有什么方法可以做进一步优化吗?

数据重排

如果我们在启动的过程中需要读一个文件 test.io 的 1KB 数据,而我们的 buffer 不小心写成 1byte,那么总共要读取 1000 次。系统是否会真的发起 1000 次磁盘 IO 呢?

事实上 1000 次读操作只是我们发起的次数,并不是真正的磁盘 I/O 次数。你可以参考下面 Linux 文件 I/O流程。

image

Linux 文件系统从磁盘读文件的时候,会以 block 为单位去磁盘读取,一般 block 大小是 4KB。也就是说一次磁盘读写大小至少是 4KB,然后会把 4KB 数据放到页缓存 Page Cache 中。如果下次读取文件数据已经在页缓存中,那就不会发生真实的磁盘 I/O,而是直接从页缓存中读取,大大提升了读的速度。所以上面的例子,我们虽然读了 1000 次,但事实上只会发生一次磁盘 I/O,其他的数据都会在页缓存中得到。

Dex 文件用的到的类和安装包 APK 里面各种资源文件一般都比较小,但是读取非常频繁。我们可以利用系统这个机制将它们按照读取顺序重新排列,减少真实的磁盘 I/O 次数。

在启动优化中,数据的重排主要有两方面:类重排 以及 资源文件重排。

类重排

类重排的实现通过 ReDex的 Interdex调整类在 Dex 中的排列顺序。

根据interdex官方介绍的原理,我们可以知道要实现这个优化需要解决三个问题:

  • 如何获取启动时加载类的序列?

redex中的方案是dump出程序启动时的hprof文件,再从中分析出加载的类,比较麻烦。这里我们采用的方案是hook住ClassLoader.findClass方法,在系统加载类时日志打印出类名,这样分析日志就可以得到启动时加载的类序列了。

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="" cid="n196" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">class GetClassLoader extends PathClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 将类名 name 记录到文件
writeToFile(name, "coldstart_classes.txt");
return super.findClass(name);
}
}</pre>

  • 如何把需要的类放到主dex中?

redex的做法应该是解析出所有dex中的类,再按配置的加载类序列,从主dex开始重新生成各个dex,所以会打乱原有的dex分布。而在手q中,分dex规则是编译脚本中维护的,因此我们可以修改分包逻辑,将需要的类放到主dex。

  • 如何调整主dex中类的顺序?

开源就是好。Android编译时把.class转换成.dex是依靠dx.bat,这个工具实际执行的是sdk中的dx.jar。我们可以修改dx的源码,替换这个jar包,就可以执行自定义的dx逻辑了。

资源文件重排

Facebook 在比较早的时候就使用“资源热图”来实现资源文件的重排,最近支付宝在《通过安装包重排布优化 Android 端启动性能》中也详细讲述了资源重排的原理和落地方法。

类的加载

加载类的过程有一个 verify class 的步骤,它需要需要校验方法的每一个指令,是一个比较耗时的操作。

verify步骤可以看这篇文章:微信 Android 热补丁实践演进之路

image

我们可以通过 Hook 来去掉 verify 这个步骤,这对启动速度有几十毫秒的优化。不过我想说,其实最大的优化场景在于首次和覆盖安装时。以 Dalvik 平台为例,一个 2MB 的 Dex 正常需要 350 毫秒,将 classVerifyMode 设为 VERIFY_MODE_NONE 后,只需要150 毫秒,节省超过 50% 的时间。

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="" cid="n212" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">// Dalvik Globals.h
gDvm.classVerifyMode = VERIFY_MODE_NONE;
// Art runtime.cc
verify_ = verifier::VerifyMode::kNone;</pre>

但是 ART 平台要复杂很多,Hook 需要兼容几个版本。而且在安装时大部分 Dex 已经优化好了,去掉 ART 平台的 verify 只会对动态加载的 Dex 带来一些好处。Atlas 中的 dalvik_hack-3.0.0.5.jar可以通过下面的方法去掉 verify,但是当前没有支持 ART 平台。

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="" cid="n214" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">AndroidRuntime runtime = AndroidRuntime.getInstance();
runtime.init(context);
runtime.setVerificationEnabled(false);</pre>

这个黑科技可以大大降低首次启动的速度,代价是对后续运行会产生轻微的影响。同时也要考虑兼容性问题,暂时不建议在 ART 平台使用。

黑科技

保活

讲到黑科技,你可能第一个想到的就是保活。保活可以减少 Application 创建跟初始化的时间,让冷启动变成温启动。不过在 Target 26 之后,保活的确变得越来越难。对于大厂来说,可能需要寻求厂商合作的机会。

插件化和热修复

它们真的那么好吗?事实上大部分的框架在设计上都存在大量的 Hook 和私有 API 调用,带来的缺点主要有两个:

  • 稳定性。虽然大家都号称兼容 100% 的机型,由于厂商的兼容性、安装失败、dex2oat 失败等原因,还是会有那么一些代码和资源的异常。Android P 推出的 non-sdk-interface 调用限制,以后适配只会越来越难,成本越来高。

  • 性能。Android Runtime 每个版本都有很多的优化,因为插件化和热修复用到的一些黑科技,导致底层 Runtime 的优化我们是享受不到的。Tinker 框架在加载补丁后,应用启动速度会降低 5%~10%。

总的来说,对于黑科技我们需要慎重,当你足够了解它们内部的机制以后,可以选择性的使用。

总结

以上就是本人学习过程中对启动优化相关内容的总结,今天的文章就到这里,感谢您的阅读。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容