背景
前面我们分析了 App 启动流程分析(基于 Android 10) ,这次我们一鼓作气,来撸一撸 App 启动优化,本文主要就一些常规手段做一些梳理,毕竟不同的 App 要优化的目的会有一些不同和侧重。
应用启动类型(冷启动、温启动、热启动)
冷启动
冷启动是指应用从头开始启动,冷启动开始后,系统会做以下事情:
- 加载并启动应用。
- 再启动后立即显示应用的空白启动窗口(不做优化时的白屏现象)。
- 创建应用进程。
创建应用进程可分为以下阶段:
- 创建应用对象。
- 启动主线程。
- 创建主 Activity。
- 扩充视图。
- 布局屏幕。
- 执行初始绘制。
- 把当先显示的后窗口(即前面提到的白屏窗口)替换为主 Activity,回调生命周期方法。
冷启动在几种启动类型中最慢,一般我们做启动优化大部分工作也是消耗在这里。
温启动
温启动比冷启动的效率高一点,比如说用户退出应用后(不是按 Home 键退后台),马上又打开 App,这时候进程大概率会继续运行,即免去了创建进程那一步,而直接创建主 Activity 并回调生命周期。
热启动
热启动在这三种启动类型中开销最低,一般来说就是应用的 Activity 都还驻留在内存中,应用无须再创建 Activity 实例,布局的绘制和呈现。一个比较的场景是用户按 Home 键然后在系统杀死进程前重新进入 App。
优化分析测量工具
我们首先创建一个空项目,如下:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="me.tandeneck.launchtime">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".SplashActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
class SplashActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_splash)
}
}
首先要明确启动优化的目的是使我们的应用启动速度更快,让用户所点即所达,解决问题的前提是找出问题,因此我们要确定启动的时间,在哪里耗时严重,下面就介绍几种常用的方法来帮助我们找到耗时的原因。
1. Logcat 中 筛选 Displayed 关键字
这个 Displayed 的值代表从启动进程到第一个 Activity 完成绘制所经过的时间。如下图:创建一个空项目并运行,发现启动需要 462 ms,当然这个值每一次都会有点差异的:
2. 使用 ADB Shell Activity Manager 命令运行应用测量显示时间,命令如下:
adb shell am start -S -W [packageName]/[ packageName. AppstartActivity]
-S 表示在启动 Activity,强行停止目标应用,-W 表示等待启动完成,更多信息有兴趣的同学可以查看 adb 命令
在 Terminal 运行上面命令可得到以下信息:
可以发现 LaunchState 为 COLD,代表的是冷启动,还有 TotalTime 和 WaitTime,可以发现 TotalTime 和 Diapalyed 打印处理的值是一样的,而 WaitTime 会大点,因为它会把系统初始化的一些工作时间算进去,而这部分的时间我们是比较难进行优化的,因此我们关注 TotalTime 即可。
前面这两种方法可以帮助我们对 App 启动的速度有个概览,但是具体到哪个方法耗时,哪个调用时间长我们是无法得知的。下面就介绍一些检测耗时操作的方法。
3. 代码埋点
乍听起来好高大上的感觉,其实就是打印日志,在方法执行开始前获取当前时间,再在方法结束后获取时间,两个时间相减即可得到方法执行时间,相信大家都有试过这种方法。这样会写很多样板代码,可以引入第三方库比如 JakeWharton 的 Hugo 来减少样板代码,这个库可以通过注解的方式获取执行时间,主要运用了 AOP 技术,不过这个库有点年头了,有兴趣的同学还是可以去了解下的。
4.使用 TraceView、SysTrace 等性能分析工具
TraceView 和 SysTrace 这些都是分析性能的神器,由于篇幅原因,这里不做具体展开,有兴趣的同学可以自行了解相关资料,或者期待下我后续的博文。
优化手段
1. 启动窗口优化
前面也提到过,默认情况下会启动一个空白窗口,如下图:
白屏窗口虽然短暂,但是还是可以明显感知它的存在,这还是在简单的 Demo 情况下,而且测试手机性能也算中规中矩。
为了应对这个问题,有些同学会用透明主题来解决,如下:
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<item name="android:windowFullscreen">true</item>
<item name="android:windowIsTranslucent">true</item>
</style>
我们简单来看下效果:
哎?空白窗口消失啦,不过细心的同学会发现点击后会有点延时,这是因为我们设置的透明主题起作用了,即我们之前看到的白屏窗口现在是透明的,所以会延迟。这时,如果我们可以把透明主题替换为闪屏页图片,比如下面这张图:
style 设置为:
<style name="MyTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="windowNoTitle">true</item>
<item name="android:windowFullscreen">true</item>
<item name="android:windowBackground">@drawable/bg</item>
</style>
然后设置 SplashActivityActivity 的启动 Theme:
<activity android:name=".SplashActivity"
android:theme="@style/SplashTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
效果如下:
可以看到,首先显示的是在主题中设置的背景图,然后是 SplashActivity (下面的那行文本其实是SplashActivity的布局文件),最后是 MainActivity,整个流程是这样:
启动页主题背景图在某些手机会出现拉伸的情况,可以通过使用点 9 图或者使用 layer-list 来解决。
2. Application 优化和主 Activity 优化。
以上的这种优化只是视觉优化,并不能真正减少用户的启动时间,用大白话说就是 Application 和 主 Activity 创建的时间该是多少就是多少。实际项目中并不会像 Demo 这样只是一个空壳 Applicaiton 和 Activity,比如在 Application 中会做许多繁重的初始化操作。这种情况下就要结合业务要进行优化啦,首先通过工具比如 Systrace 获取耗时的函数,然后对耗时的函数进行优化,如果放在子线程中加载不影响业务的情况,则优先选择放在子线程中加载。主 Activity 的优化同理,不过主 Activity 涉及到界面,还可以从页面优化的方向着手,无非就是减少冗余或者嵌套的布局来减少底视图层次结构,用 ViewStub 替代在启动过程红不需要显示的 UI 控件。
3.类重排
类重排的实现通过 ReDex 的 Interdex 调整类在 Dex 中的排列顺序,把启动时需要加载的类按顺序放在主 dex 里。具体实现可以参考 Redex 初探与 Interdex:Andorid 冷启动优化。
4.减少冷启动的次数
从前面得知,冷启动的耗时是最长的,因此我们可以在用户非主动退出应用的情况下不再退出进程。在我们的 Activity 栈底 activity (一般是 MainActivity)加入以下代码:
override fun onBackPressed() {
// super.onBackPressed()
moveTaskToBack(true)
}
moveTaskToBack 的作用是把 Activity 隐藏在后台,相当于触发 Home 键的效果。
5. 其他一些常规优化
- ,通过减少 CPU 调度带来的波动,让启动时间更稳定,如果启动过程中有太多的线程一起启动,会给 CPU 带来非常大的压力。
- ,启动过程中减少 GC 的次数。如避免进行大量的字符串操作,特别是序列化和反序列化;如避免重复创建对象,出现这种情况,可以考虑复用。
- ,启动过程 IO 负载较高,尽量较少网络 IO 和磁盘 IO。
- ,启动的应用如果需要大内存,而这时如果没有足够的内存,那么系统必须要通过杀死其他应用的方式来满足这个 App 内存的需要,这个过程中会对内存进行频繁的操作,导致启动速度变慢。
总结
App 启动优化除了以上的优化手段外,还有许多手段由于篇幅原因未能一一进行详细说明,比如保活、预加载等。