️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启动过程大致如下:
- 加载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
,一个是背景
1 | <layer-list |
设置windowBackground
1 | <style name="ColdStartTheme" parent="APPTheme"> |
在启动时先加载了伪背景,然后才加载了真正的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的耗时操作
1 | getWindow().getDecorView().post(new Runnable() { |
####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 | SquareUtils.getDispatcher().executorService().execute(new Runnable() { |
最后,你就能比较充分利用你的真八核手机
1 | 主线程: 解析xml ----------addView()--------| → 更新界面 |
###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请求与之后拼装的时间加起来,比动态代理要多的多