冷启动优化-转摘https://www.jianshu.com/p/d97b390da87b
探究Android的冷启动优化
Cold Startup Perfermance Improvement in Android
本文依据平台如下
- 机型: 魅蓝Note(高通615真八核/2G/1080P/4.4)
- 效果:1.1s -> 0.7s(实际用户看到的假界面时间更短)
- 检测网站: https://nimbledroid.com,是 @程序亦非猿 推荐的哦
1. 启动过程概述
在应用层,普通APP启动过程大致如下:
- 加载Application
- 静态代码段/构造函数
- onCreate方法
- 加载主Activity
- 静态代码段/构造函数
- 消息队列第一次循环: onCreate,通过setContentview解析、加载xml
- 消息队列第二次循环: 被动地调用Choreographerd中的FrameDisplayEventReceiver的run()进行进行实际绘制
为了提高用户感知,我希望在主线程中执行的顺序如下(注意本流程不适用于插件化的App):
- 尽快显示DecoView(Main Thread)(显示Theme中定义的ActionBar、背景等)
- 尽快显示xml中的静态View(Main Thread)(显示xml中的布局)
- 加载第三方黑盒SDK(Main Thread)
- 进行网络、图片等框架的构造(Main Thread)
- 通过框架进行业务请求(Gson/OkHttp等, Worker Thread),并更新View
不建议在Application中初始化耗时任务,它将直接导致白屏
2. 用户感知优化
本部分可以提高上文1,2,3的用户体验
2.1. 加载伪背景(0.1~0.2s)
DecoView的优先级比setContentView
优先级更高,所以可以让DecoView显示一个伪启动背景界面,而不是白屏黑屏或者没界面甩锅给手机厂商,让用户感受到App正在加载是一个好的选择。
绘制一个App启动的草图,如下,一个是Toolbar
,一个是背景
<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
<style name="ColdStartTheme" parent="APPTheme">
<item name="android:windowBackground">@drawable/cold_start_bg</item>
</style>
在启动时先加载了伪背景,然后才加载了真正的View元素
最终可以让用户觉得“提高”了0.1~0.2s的速度
参考文章:
上述方案均不能很好处理状态栏,如果你使用Translucent,慎用
2.2. XML布局优化
此部分适用于解析、处理、绘制静态xml时的优化
xml布局优化是老生常谈的话题了,本质是减少无谓的绘制,网上面试宝典很多,这里就也不介绍了。解决方法如下:
- 使用Include,Merge,viewStub简化布局
- 使用相对布局,layer-list降低树的层级
- 使用gone标签可以跳过绘制
- 被遮挡的view避免重复绘制
参考文章:
3. 延后启动耗时框架
本部分不能压缩总时间,只是将耗时操作移动到后面而已,可以让白屏时间减少0.2~0.3s(取决于框架数量)。
3.1. 实现方法
在onCreate()的最后,加入post操作,即可实现在绘制XmlView完成后再进行非UI的耗时操作
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()
,进行了消息队列的优先独占操作
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
接着调用doTraversal()
释放
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
SyncBarrier拥有消息队列的独占性,当使用SyncBarrier
时,后面的消息将被阻塞,这样在主线程中就有更多的CPU时间可以分给WMS进行绘图了。在View绘制完成后,解除SyncBarrier后才会调用我们在上文Post的耗时框架加载任务,这样就实现了延迟加载。
4. 多线程初始化
此部分真的可以压缩启动时间,但是对SDK线程安全有一定的要求,在黑盒SDK下容易出现问题
下文复用了OkHttp中的单例Worker线程池,节省了0.16s的启动时间
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();
}
});
}
});
最后,你就能比较充分利用你的真八核手机
主线程: 解析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请求与之后拼装的时间加起来,比动态代理要多的多