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

本文主要是学习了极客时间张绍文老师的 Android开发高手课 以及 谷歌官网文章 的启动优化笔记~java

参考文章: 
https://time.geekbang.org/column/article/73651 https://mp.weixin.qq.com/s/eaArt5Udc4WZ3NoH5RlEkQ https://juejin.im/post/5874bff0128fe1006b443fa0 https://developer.android.google.cn/topic/performance/vitals/launch-time

应用启动类型

  • 冷启动

  1. 场景:开机后第一次启动应用 或者 应用被杀死后再次启动
  2. 生命周期:Process.start->Application建立->attachBaseContext->onCreate->onStart->onResume->Activity生命周期
  3. 启动速度:在几种启动类型中最慢,也是咱们优化启动速度最大的拦路虎

  • 温启动

  1. 场景:应用已经启动,返回键退出
  2. 生命周期:onCreate->onStart->onResume->Activity生命周期
  3. 启动速度:较快

  • 热启动

  1. 场景:Home键最小化应用
  2. 生命周期:onResume->Activity生命周期
  3. 启动速度:快

从上面的总结能够看出,在应用的启动过程当中,冷启动是最慢最耗时的,系统以及应用自己都有大量的工做须要处理,因此,冷启动对于应用的启动速度是最具挑战以及最有必要进行优化的。android


冷启动流程

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

  • 设备启动以来首次启动应用程序
  • 系统杀死应用程序以后再次启动应用程序

在冷启动的最开始,系统须要负责作三件事:github

  • 加载以及启动app
  • app启动以后马上显示一个空白的预览窗口
  • 建立app进程

一旦系统完成建立app进程后,app进程将要接着负责完成下面的工做:算法

  • 建立Application对象
  • 建立而且启动主线程ActivityThread
  • 建立启动第一个Activity
  • Inflating views
  • 布局屏幕
  • 执行第一次绘制

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

           

从冷启动的流程看,咱们没法干预app进程建立等系统操做,咱们可以干预的有:数组

  • 预览窗口
  • Application生命周期回调
  • Activity生命周期回调


优化分析测量工具

对研发人员来讲,启动速度是咱们的“门面”,它清清楚楚能够被全部人看到,咱们都但愿本身应用的启动速度能够秒杀全部竞争对手。缓存

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

  • 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框架修改字节码的方式 分别插入到每一个方法的入口和出口。

class TraceMethod {
    public static void i() {
        Trace.beginSection();
    }

    public static void o() {
        Trace.endSection();
    }
}复制代码

固然这里面有很是多的细节须要考虑,好比怎么样下降插桩对性能的影响、哪些函数须要被排除掉。函数插桩后的效果以下:

class Test {

    public void test() {
        TraceMethod.i();
        // 原来的工做
        TraceMethod.o();
    }
}复制代码

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


启动优化方法

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

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

预览窗口优化

当用户点击应用桌面图标启动应用的时候,利用提早展现出来的 Window,快速展现出一个界面,用户只须要很短的时间就能够看到“预览页”,这种彻底“跟手”的感受在高端机上体验很是好,但对于中低端机,会把总的的闪屏时间变得更长。

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

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

Layout XML file:

<layer-list xmlns:android="http://schemas.android.com/apk/res/android" android:opacity="opaque">
  <!-- The background color, preferably the same as your normal theme -->
  <item android:drawable="@android:color/white"/>
  <!-- Your product logo - 144dp color version of your app icon -->
  <item>
    <bitmap
      android:src="@drawable/product_logo_144dp"
      android:gravity="center"/>
  </item>
</layer-list>复制代码

Manifest file:

<activity ...
android:theme="@style/AppTheme.Launcher" />复制代码

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

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);
    // ...
  }
}复制代码

业务梳理

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

  • 必要且耗时:启动初始化,考虑用线程来初始化。
  • 必要不耗时:首页绘制。
  • 非必要但耗时:数据上报、插件初始化。
  • 非必要不耗时:不用想,这块直接去掉,在须要用的时再加载。

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

               

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

业务优化

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

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

多进程优化

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

可是每一个进程须要初始化的内容确定是不同的,因此,为了防止资源的浪费,咱们须要在Application 中区分进程,对应进程只初始化对应的内容。

线程优化

线程优化分两方面:

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

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

proc/[pid]/sched: 
 nr_voluntary_switches:主动上下文切换次数,由于线程没法获取所需资源致使上下文切换,最广泛的是 IO。 
 nr_involuntary_switches:被动上下文切换次数,线程被系统强制调度致使上下文切换,例如大量线程在抢占 CPU。 复制代码

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

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

第四,设置子线程优先级。不重要任务,设置子线程优先级为 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流程。



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

Dex 文件用的到的类和安装包 APK 里面各类资源文件通常都比较小,可是读取很是频繁。咱们能够利用系统这个机制将它们按照读取顺序从新排列,减小真实的磁盘 I/O 次数。

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

类重排

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

不明白能够看这篇文章:Redex 初探与 Interdex:Andorid 冷启动优化

根据interdex官方介绍的原理,咱们能够知道要实现这个优化须要解决三个问题:

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

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

class GetClassLoader extends PathClassLoader {
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // 将类名 name 记录到文件
        writeToFile(name, "coldstart_classes.txt");
        return super.findClass(name);
    }
}复制代码
  • 如何把须要的类放到主dex中?

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

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

开源就是好。Android编译时把.class转换成.dex是依靠dx.bat,这个工具实际执行的是sdk中的dx.jar。咱们能够修改dx的源码,替换这个jar包,就能够执行自定义的dx逻辑了。简单说下具体修改方法:

这里须要对dex的文件格式作必定了解,再也不细说,网上有一篇很好的文章,有兴趣能够了解下 http://blog.csdn.net/jiangwei0910410003/article/details/50668549

资源文件重排

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

类的加载

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

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


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

// Dalvik Globals.h
gDvm.classVerifyMode = VERIFY_MODE_NONE;
// Art runtime.cc
verify_ = verifier::VerifyMode::kNone;复制代码

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

AndroidRuntime runtime = AndroidRuntime.getInstance();
runtime.init(context);
runtime.setVerificationEnabled(false);复制代码

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

黑科技

保活

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

插件化和热修复

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

  • 稳定性。虽然你们都号称兼容 100% 的机型,因为厂商的兼容性、安装失败、dex2oat 失败等缘由,仍是会有那么一些代码和资源的异常。Android P 推出的 non-sdk-interface 调用限制,之后适配只会愈来愈难,成本越来高。
  • 性能。Android Runtime 每一个版本都有不少的优化,由于插件化和热修复用到的一些黑科技,致使底层 Runtime 的优化咱们是享受不到的。Tinker 框架在加载补丁后,应用启动速度会下降 5%~10%。

总的来讲,对于黑科技咱们须要慎重,当你足够了解它们内部的机制之后,能够选择性的使用。


总结

以上就是本人学习过程当中对启动优化相关内容的总结,谢谢你们可以阅读到这里。

启动优化,是一项长期的任务,任重而道远。

开发者要未雨绸缪,在编码过程当中尽可能减小给启动带来性能损耗的工做,主要注意如下几个事项:

  • 尽可能避免启动时在主线程作密集繁重的工做,如:避免 I/O 操做、反序列化、网络操做、锁等待等。
  • 对模块以及第三方库按需加载,采起分步加载、异步加载、延期加载等策略。
  • 利用线程池管理线程,避免建立大量线程,形成 CPU 竞争,致使主线程时间片减小。
  • 启动过程当中,尽可能避免频繁建立的大量对象,减小 GC 给启动性能带来的卡顿影响。
  • 尽可能避免在启动过程当中调用阻塞性的系统调用。

至于“启动优化进阶方法”小节中总结的优化方法要慎重选择使用,由于这些方法或多或少会带来一些很差影响。咱们在使用这些方法以前,要足够了解他们的内部实现机制,作好评估工做,进行选择性使用。

最后附上几篇好文章帮助理解:

预览窗口:显示Activity的启动窗口
Interdex的介绍: Redex 初探与 Interdex:Andorid 冷启动优化
加载类过程 verify class 步骤的介绍: 微信 Android 热补丁实践演进之路
资源文件重排:支付宝 App 构建优化解析:经过安装包重排布优化 Android 端启动性能
插件化与热修复:Android热修复,没你想的那么难
初到掘金,人生地不熟,喜欢的朋友,点个赞鼓励下新手呗~
相关文章
相关标签/搜索