探究Android的冷启动优化
2016-09-23 / modified at 2022-04-04 / 1.6k words / 6 mins
️This article has been over 2 years since the last update.

Cold Startup Perfermance Improvement in Android

本文依据平台如下

  • 机型: 魅蓝Note(高通615真八核/2G/1080P/4.4)
  • 效果:1.1s -> 0.7s(实际用户看到的假界面时间更短)
  • 检测网站: https://nimbledroid.com,是 @程序亦非猿 推荐的哦

###1. 启动过程概述
在应用层,普通APP启动过程大致如下:

  1. 加载Application
    • 静态代码段/构造函数
    • onCreate方法
  2. 加载主Activity
    • 静态代码段/构造函数
    • 消息队列第一次循环: onCreate,通过setContentview解析、加载xml
    • 消息队列第二次循环: 被动地调用Choreographerd中的FrameDisplayEventReceiver的run()进行进行实际绘制

为了提高用户感知,我希望在主线程中执行的顺序如下(注意本流程不适用于插件化的App):

  1. 尽快显示DecoView(Main Thread)(显示Theme中定义的ActionBar、背景等)
  2. 尽快显示xml中的静态View(Main Thread)(显示xml中的布局)
  3. 加载第三方黑盒SDK(Main Thread)
  4. 进行网络、图片等框架的构造(Main Thread)
  5. 通过框架进行业务请求(Gson/OkHttp等, Worker Thread),并更新View

不建议在Application中初始化耗时任务,它将直接导致白屏

###2. 用户感知优化
本部分可以提高上文1,2,3的用户体验

####2.1. 加载伪背景(0.1~0.2s)
DecoView的优先级比setContentView优先级更高,所以可以让DecoView显示一个伪启动背景界面,而不是白屏黑屏或者没界面甩锅给手机厂商,让用户感受到App正在加载是一个好的选择。

绘制一个App启动的草图,如下,一个是Toolbar,一个是背景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<layer-list
xmlns:android="http://schemas.android.com/apk/res/android"
android:opacity="opaque"
>

<item android:gravity="top">
<shape android:shape="rectangle">
<solid android:color="#c8ececec"/>
</shape>
</item>

<item
android:top="75dp"
android:gravity="top">
<shape android:shape="rectangle">
<solid android:color="@color/primary"/>
</shape>
</item>
</layer-list>

设置windowBackground

1
2
3
<style name="ColdStartTheme" parent="APPTheme">
<item name="android:windowBackground">@drawable/cold_start_bg</item>
</style>

在启动时先加载了伪背景,然后才加载了真正的View元素

伪背景加载 → 绘制完成View

最终可以让用户觉得“提高”了0.1~0.2s的速度

参考文章:

  1. avoding-android-cold-starts
  2. Android冷启动时间优化 - Wayne’s blog
  3. GitHub - MaterialColdStart

上述方案均不能很好处理状态栏,如果你使用Translucent,慎用

####2.2. XML布局优化
此部分适用于解析、处理、绘制静态xml时的优化

xml布局优化是老生常谈的话题了,本质是减少无谓的绘制,网上面试宝典很多,这里就也不介绍了。解决方法如下:

  1. 使用Include,Merge,viewStub简化布局
  2. 使用相对布局,layer-list降低树的层级
  3. 使用gone标签可以跳过绘制
  4. 被遮挡的view避免重复绘制

参考文章:

  1. http://stormzhang.com/android/2014/04/10/android-optimize-layout/
  2. http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0125/2356.html

###3. 延后启动耗时框架
本部分不能压缩总时间,只是将耗时操作移动到后面而已,可以让白屏时间减少0.2~0.3s(取决于框架数量)。

####3.1. 实现方法
在onCreate()的最后,加入post操作,即可实现在绘制XmlView完成后再进行非UI的耗时操作

1
2
3
4
5
6
7
8
9
10
getWindow().getDecorView().post(new Runnable() {
@Override public void run() {
//加载Applicaiton中的框架 40+ms
GlobalContext.startThirdFrameWork();
//构建网络框架 120ms
repo = SquareUtils.getRetrofit(URL).create(GithubService.class);
//进行ssl库的初始化请求 40+ms
onRefresh();
}
});

####3.2. 实现原理

在XML被inflate后,需要通过mDecoView.addView(xmlView)进行添加。

addview最终调用ViewRootImpl的方法scheduleTraversals(),进行了消息队列的优先独占操作

1
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();

接着调用doTraversal()释放

1
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

SyncBarrier拥有消息队列的独占性,当使用SyncBarrier时,后面的消息将被阻塞,这样在主线程中就有更多的CPU时间可以分给WMS进行绘图了。在View绘制完成后,解除SyncBarrier后才会调用我们在上文Post的耗时框架加载任务,这样就实现了延迟加载。

###4. 多线程初始化

此部分真的可以压缩启动时间,但是对SDK线程安全有一定的要求,在黑盒SDK下容易出现问题

下文复用了OkHttp中的单例Worker线程池,节省了0.16s的启动时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
SquareUtils.getDispatcher().executorService().execute(new Runnable() {
@Override public void run() {
Log.d(TAG, "run: " + System.currentTimeMillis());
//42ms
GlobalContext.startThirdFrameWork();
//120ms
repo = SquareUtils.getRetrofit(DanbooruAPI.KONACHAN).create(DanbooruAPI.class);
runOnUiThread(new Runnable() {
@Override public void run() {
//40ms
onRefresh();
}
});
}
});

最后,你就能比较充分利用你的真八核手机

1
2
主线程: 解析xml ----------addView()--------| → 更新界面
线程池: 初始化框架 --post(请求网络)---wait()--|

###5. 混淆
经过测试,混淆在一定程度上可以提高速度,属于免费的性能提升,但是不是非常明显,大概只有100ms

混淆后要记得测试

###6. 总结
通过上述方法,可以压榨0.3~0.6s的时间,让用户能够更快的启动APP

本文例子: Github - AnimeWallpaper,目前启动速度0.7s,求各位star!

###附录. Retrofit框架加载时间分析
Retrofit 在知乎上有人这样回答的,大意是动态代理 == 反射 == 慢,这就是典型的半桶水,不懂装懂。

通过对每个方法进行统计后,结果却是这样的:

retrofit构造(128ms)

  • 构造OkHttp:121ms, 其中javax.ssl构建耗时117ms,调用的是一个SSL遍历native操作,这个基本无法避免;缓存文件初始化1ms
  • 构造GsonFactory 4ms: 主要是classloader加载的时间
  • 其他 3ms

retrofit访问网络前接口的拼装(42ms)

  • RxJava框架: 12ms
  • 动态代理: 1ms
  • Gson库: 27ms,主要进行反射操作
  • 其他: 2ms

随着SSL的普及,javax.ssl必然会被加载,这个100ms的时间在native中黑盒执行,很难避免,只能等手机ROM去优化喽;剩下的就是Gson的时间比较久,这个时间还是可以接受的。

从上面也可以看出,与动态代理相关的时间,并没有想象中那么慢,不要看到反射就觉得慢,网络I/O请求与之后拼装的时间加起来,比动态代理要多的多