许多应用都出现启动速度缓慢,出现黑屏,白屏问题,这是因为 APP 没有进行启动优化。
Google文档
先看看 Google 官方文档 Launch-Time Performance 对应用启动优化的概述:
应用的启动分为冷启动、热启动、温启动,而启动最慢的就是冷启动。
应用在冷启动之时,要执行三个任务:
- 加载启动 App;
- App 启动时立即展示出一个 Preview Window(预览窗口);
- 创建 App 的进程;
而这三个任务执行完毕之后,马上执行以下任务:
- 创建 Application 对象;
- 创建启动 Activity 对象;
- 加载 View;
- 布置屏幕;
- 进行初始绘制;
而一旦 App 进程完成了第一次绘制,系统进程就会用 Activity 替换已经展示的 Background Window,此时用户就可以使用 App 了。
有上述 APP 启动过程可知,App 进程的创建等环节我们是无法控制的,可以优化的也就是Application、Activity创建以及回调等过程。
Google 也给出了优化启动速度的方式:
- 修改 Launcher Activity 主题,提前展示预览画面;
- 避免在启动时做密集沉重的初始化;
- 定位问题:避免I/O操作、反序列化、网络操作、布局嵌套等。
上面三种方式,第一种是用户体验上的提升,第二三种是真实的优化启动速度。
修改主题
App 一般都会有一个 Splash Activity,也就是 APP 启动时的第一个页面,其作用一般是检查更新,初始化程序,宣传公司品牌,以及活动展示。
我们的优化点,就是修改这个 Activity 的 windowBackground 属性,因为如果不指定,系统默认 windowBackground 要么全白要么全黑,导致刚刚启动 APP 时,总能在一瞬间看到这个窗口,体验很差。
具体操作如下:
首先,在 AndroidManifest.xml 中更改 Splash Activity 的主题
<activity
android:name=".mvp.ui.activity.SplashActivity"
android:theme="@style/SplashTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
然后,在 styles.xml 中指定 windowBackground
<style name="SplashTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:windowBackground">@drawable/splash</item>
</style>
splash.xml
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 背景颜色 -->
<item android:drawable="@color/white"/>
<item>
<!-- 图片 -->
<bitmap
android:gravity="center"
android:src="@drawable/ic_launcher"/>
</item>
</layer-list>
这种方式其实并没有真正的加速启动过程,而是通过交互体验来优化了展示的效果。
启动加速
现在,我们来真正的加速 APP 的启动,这里关注的就是 Application 以及 Splash Activity 中的初始化代码。
在 Application 中,如果使用了 MultiDex ,那么需要进行对应优化,详情请查看底部参考文章。
其次,在 onCreate()
中,第三方 SDK 会要求在此初始化,如果全部放在主线程,这样的方式肯定是过重的。
解决办法是:异步加载、延时加载、懒加载。
将耗时操作放到子线程,争抢资源的操作进行延时加载或者懒加载。
具体使用哪种方式,这还要根据 第三方SDK 自身情况来决定,这里也不易展开。
我们知道,最常见的解决办法是异步加载,启动时,耗时操作发生在主线程,那是不是多开几个子线程同时操作就行呢?不是。
优化启动不能全都靠着异步来解决问题,错误的使用线程不仅不能改善,反而肯能加剧卡顿。是否需要开启线程?需要开几个线程?这些需要根据具体情况分析性能瓶颈后,才能给出对应的解决方式。
开启线程的方式也同样有区别,Thread、ThreadPool、AsyncTask、HandlerThread、IntentService都各有利弊。分析可知,IntentService 运行在子线程,而且还可以与 Activity 绑定,所以一般会使用 IntentService 来进行一些可以异步的初始化工作。
除了异步,还应该多多考虑,一些初始化是否可以使用延时加载,是否能使用懒加载?
还有一些流程性的事物也可以优化,比如 广告页的下载展示,APP 后台下载升级等。
工具的使用
在优化启动时,我们必须掌握一些分析耗时的基础工具。
1. 使用 ADB
最简单的方式是通过 ADB 查看启动耗时
adb shell am start -W packagename/activity
例如:
➜ adb shell am start -W com.medprin.medprin/com.medprin.medprin.mvp.ui.activity.SplashActivity
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.medprin.medprin/.mvp.ui.activity.SplashActivity }
Status: ok
Activity: com.medprin.medprin/.mvp.ui.activity.SplashActivity
ThisTime: 248
TotalTime: 248
WaitTime: 250
Complete
这里解释一下三个值的含义:
- WaitTime 就是总的耗时,包括前一个应用 Activity pause 的时间和新应用启动的时间;
- ThisTime 表示一连串启动 Activity 的最后一个 Activity 的启动耗时;
- TotalTime 表示新应用启动的耗时,包括新进程的启动和 Activity 的启动。
详细解释,请看Android 中如何计算 App 的启动时间?
如果关心系统启动应用耗时,参考WaitTime;
如果关心应用有界面Activity启动耗时,参考ThisTime。
如果只关心某个应用自身启动耗时,参考TotalTime,开发者一般关注这个;
2.TraceView
使用 adb 的方式太简单粗略了,只能看出启动的总耗时,也看不出具体哪些地方耗时多长,这个时候就需要更加专业的工具,TraceView。
使用非常简单,直接在想要监测位置的开头和结尾,各自调用两个方法
Debug.startMethodTracing("Test");//开始
...
Debug.stopMethodTracing();//结束
运行程序,会在 sdcard 上生成名为 Test.trace
的文件。注意,sdcard 的操作需要添加权限。
然后通过 adb pull 命令 将文件导入到本地后,使用 DDMS 打开 trace 文件, 或者 直接使用 AS3.0之后出现的工具 Devices File Exploer ,直接打开 trace 文件。
一般只需要关注两个值:
- Cpu Time / Call:反映调用次数不多,但每次调用却需要花费很长时间的函数
- Calls + Recur Calls / Total:反映自身占用时间不长,但调用却非常频繁的函数
3. Hugo
还有一个是 JakeWharton 的 hugo ,这个库利用 AspectJ 使用注解,可以非常方便的打印方法耗时。
就像这样:
@DebugLog
public String getName(String first, String last) {
SystemClock.sleep(15); // Don't ever really do this!
return first + " " + last;
}
V/Example: ⇢ getName(first="Jake", last="Wharton")
V/Example: ⇠ getName [16ms] = "Jake Wharton"
详细集成配置,请查看 GitHub 上文档。