Android 启动优化

八秒定律:是在互联网领域存在的一个定律,即指用户访问一个网站时,如果等待网页打开的时间超过 8 秒,会有超过 70% 的用户放弃等待。

启动方式

对于计算机,简单的讲,开机是冷启动,重启是热启动。在 App 中:

  • 冷启动:系统没有该应用的进程,需要创建一个新的进程分配给应用,所以会先创建和初始化Application类,再创建和初始化MainActivity类(包括一系列的测量、布局、绘制),最后显示在界面上。
  • 热启动: 系统中已有该应用的进程(例:按back键、home键,应用虽然会退出,但是该应用的进程还是保留在后台),当再次点开APP马上能够恢复到上次使用的状态,不需要再回到手机的首页打开应用程序。不会创建和初始化Application类,直接创建和初始化MainActivity类(包括一系列的测量、布局、绘制),最后显示在界面上。
  • 温启动

检测 App Activity 的启动时间

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 1.Shell
* ActivityManager -> 执行命令:
* adb shell am start -S -W com.example.app/com.example.app.(启动页)
* ThisTime: 478ms 最后一个Activity的启动耗时
* TotalTime: 478ms 启动一连串Activity的总耗时
* WaitTime: 501ms 应用创建的时间 + TotalTime
* 应用创建时间: WaitTime - TotalTime(501 - 478 = 23ms)
* 2.Log
* Android 4.4 开始,ActivityManager增加了Log TAG = displayed
* 通过日志筛选:displayed(打印的日志等级为:Verbose)
*/

冷启动流程

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 冷启动经过的步骤:
* 1.第一次安装,加载应用程序并且启动
* 2.启动后显示一个空白的窗口 getWindow()
* 3.启动/创建了我们的应用进程
*
* App内部:
* 1.创建App对象/Application对象
* 2.启动主线程(Main/UI Thread)
* 3.创建应用入口/LAUNCHER
* 4.填充ViewGroup中的View
* 5.绘制View measure -> layout -> draw
*/

通过翻阅 Application 启动的源码,当我们点击桌面图标进入我们软件应用的时候,会由 AMS 通过 Socket 给 Zygote 发送一个 fork 子进程的消息,当 Zygote fork 子进程完成之后会通过反射启动 ActivityThread##main 函数,最后又由 AMS 通过 aidl 告诉 ActivityThread##H 来反射启动创建Application 实例,并且依次执行 attachBaseContext 、onCreate 生命周期,由此可见我们不能在这 2 个生命周期里做主线程耗时操作。

总结:Application 构造方法 –> attachBaseContext() –> onCreate() –> Activity 构造方法 –> onCreate() –> 配置主题中背景等属性 –> onStart() –> onResume() –> 测量布局绘制显示在界面上。

  • 创建和初始化Application类、创建MainActivity。

  • inflate布局、当onCreate/onStart/onResume方法都走完。

  • contentView 的 measure/layout/draw 显示在界面上。


优化方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 优化手段:
* 1.视图优化
* 1.设置主题透明
* 2.设置启动图片
* 2.代码优化
* 1.优化Application
* 1.必要的组件在程序主页去初始化
* 2.如果组件一定要在App中初始化,那么尽可能的延时
* 3.非必要的组件,子线程中初始化
* 2.布局的优化,不需要繁琐的布局
* 3.阻塞UI线程的操作
* 4.加载Bitmap/大图
* 5.其他的一个占用主线程的操作
*/

程序在冷启动的时候,会有 1s 左右的白屏闪现,低版本是黑屏的现象。app 启动后,WindowManager 会先加载 app theme 中的 windowBackground。

常见的解决方案如:

  • 通过设置 windowIsTranslucent 透明属性,虽然没有了白屏,但是中间还是有一小段不可见,点击应用图标会有种卡顿的感觉,延迟一秒才打开应用。

    1
    <item name="android:windowIsTranslucent">true</item>
  • 一般应用都会有个启动页,可以在白屏期间,放一张同启动页一样的图片过渡,但注意处理状态栏。

    1
    2
    <item name="android:windowBackground">@drawable/</item>
    <item name="android:windowDrawsSystemBarBackgrounds">false</item>
  • 使用 Splash 的广告页,同时再增加一个倒数的计时器,最后才进入到登录页面或者主页面。广告页可以是第三方,也可以是对这个项目的简介,而且在倒数的时候可以对一些插件和必须或者耗时的初始化做一些准备。

知道了 attachBaseContext 、onCreate 在应用中最先启动,那么我们就可以通过 TreceView 等性能检测工具,来检测具体函数耗时时间,然后来对其做具体的优化。

  • 项目不及时需要的代码通过异步加载。
  • 将对一些使用率不高的初始化,做懒加载。
  • 将对一些耗时任务通过开启一个 IntentService 来处理。
  • 还通过 redex 重排列 class 文件,将启动阶段需要用到的文件在 APK 文件中排布在一起,尽可能的利用 Linux 文件系统的 pagecache 机制,用最少的磁盘 IO 次数,读取尽可能多的启动阶段需要的文件,减少 IO 开销,从而达到提升启动性能的目的。
  • 5.0 低版本可以做 MultiDex 优化,在第一次启动的时候,直接加载没有经过 OPT 优化的原始 DEX,先使得 APP 能够正常启动。然后在后台启动一个单独进程,慢慢地做完 DEX 的 OPT 工作,尽可能避免影响到前台 APP 的正常使用。

Application 启动完之后,AMS 会找出前台栈顶待启动的 Activity , 最后也是通过 AIDL 通知 ActivityThread#H 来进行对 Activity 的实例化并依次执行生命周期 onCreate、onStart、onRemuse 函数,那么这里由于 onCreate 生命周期中如果调用了 setContentView 函数,底层就会通过将 XML2View 那么这个过程肯定是耗时的。

所以要精简 XML 布局代码,尽可能的使用 ViewStub、include 、merge 标签来优化布局。接着在 onResume 声明周期中会请求 JNI 接收 Vsync (垂直同步刷新的信号) 请求,16ms 之后如果接收到了刷新的消息,那么就会对 DecorView 进行 onMeasure->onLayout->onDraw 绘制。最后才是将 Activity 的根布局 DecorView 添加到 Window 并交于 SurfaceFlinger 显示。

所以这一步除了要精简 XML 布局,还有对自定义 View 的测量,布局,绘制等函数不能有耗时和导致 GC 的操作。最后也可以通过 TreaceView 工具来检测这三个声明周期耗时时间,从而进一步优化,达到极限。


备注

参考资料

面试官: 说一下你做过哪些性能优化?

单词音标: