序、最近太难了 ,MD 。
目录
不知道咋回事 ,最近突然感觉到自己的项目启动有点慢 。要知道在我们程序界,能拿得出手的除了流畅还有启动这一项 。
在超市排队结账,扫码支付启动十几秒都还没完成,只能换一个工具支付?
想买本书充实一下,页面刷出来时候十几秒都不能操作,那就换一个应用购买?
对研发人员来说,启动速度是我们的“门面”,它清清楚楚可以被所有人看到,我们都希望自己应用的启动速度可以秒杀所有竞争对手。
既然发现了自己的程序启动慢 ,那肯定有很多用户也感觉到了 ,可能人家只是默默的埋怨是自己的手机该换了 。(在这里提醒一下,有时候手机应用卡顿真不是你手机问题 ,不信你换个手机试试 。)
启动过程分析
以微信为例
- T1 预览窗口显示。系统在拉起微信进程之前,会先根据微信的 Theme 属性创建预览窗口。当然如果我们禁用预览窗口或者将预览窗口指定为透明,用户在这段时间依然看到的是桌面。
- T2 闪屏显示。在微信进程和闪屏窗口页面创建完毕,并且完成一系列 inflate view、onmeasure、onlayout 等准备工作后,用户终于可以看到熟悉的“小地球”。
- T3 主页显示。在完成主窗口创建和页面显示的准备工作后,用户可以看到微信的主界面。
- T4 界面可操作。在启动完成后,微信会有比较多的工作需要继续执行,例如聊天和朋友圈界面的预加载、小程序框架和进程的准备等。在这些工作完成后,用户才可以真正开始愉快地聊天。
启动问题分析
-
问题 1:点击图标很久都不响应如果我们禁用了预览窗口或者指定了透明的皮肤,那用户点击了图标之后,需要 T2 时间才能真正看到应用闪屏。对于用户体验来说,点击了图标,过了几秒还是停留在桌面,看起来就像没有点击成功,这在中低端机中更加明显。
- 问题 2:首页显示太慢现在应用启动流程越来越复杂,闪屏广告、热修复框架、插件化框架、大前端框架,所有准备工作都需要集中在启动阶段完成。上面说的 T3 首页显示时间对于中低端机来说简直就是噩梦,经常会达到十几秒的时间。
- 问题 3:首页显示后无法操作。既然首页显示那么慢,那我能不能把尽量多的工作都通过异步化延后执行呢?很多应用的确就是这么做的,但这会造成两种后果:要么首页会出现白屏,要么首页出来后用户根本无法操作。很多应用把启动结束时间的统计放到首页刚出现的时候,这对用户是不负责任的。看到一个首页,但是停住十几秒都不能滑动,这对用户来说完全没有意义。启动优化不能过于 KPI 化,要从用户的真实体验出发,要着眼从点击图标到用户可操作的整个过程。
启动时间
想要做启动优化 ,首先的去检测一下自己项目的启动时间 ,看一下跟业内的差距哈 。
冷启动耗时统计
adb命令行
adb shell am start -S -W 包名/启动类的全路径
yinzihandeMacBook-Pro:EduSohoAndroidClient yinzh$ adb shell am start -S -W com.super.test/com.super.test.module.start.ui.StartActivity
Stopping: com.super.test
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.super.test/com.super.test.module.start.ui.StartActivity }
Status: ok
Activity: com.super.test/com.super.test.module.main.ui.MainActivity
ThisTime: 840
TotalTime: 2775
WaitTime: 2801
Complete
- ThisTime : 最后一个 Activity 的启动耗时(例如从 LaunchActivity - >MainActivity「adb命令输入的Activity」 , 只统计 MainActivity 的启动耗时)
- TotalTime : 启动一连串的 Activity 总耗时 (有几个Activity 就统计几个)
- WaitTime : 应用进程的创建过程 + TotalTime
系统日志统计
另外也可以根据系统日志来统计启动耗时,在Android Studio中查找已用时间,必须在logcat视图中禁用过滤器(No Filters)。因为这个是系统的日志输出,而不是应用程序的。你也可以查看其它应用程序的启动耗时。
过滤 displayed 日志
2020-04-01 14:17:52.863 1439-1503/? I/ActivityManager: Displayed com.super.test/com.super.test.module.main.ui.MainActivity: +847ms (total +2s844ms)
2020-04-01 14:19:57.283 1439-1503/? I/ActivityManager: Displayed com.netease.cloudmusic/.activity.LoadingActivity: +801ms
2020-04-01 14:20:02.557 1439-1503/? I/ActivityManager: Displayed com.netease.cloudmusic/.activity.MainActivity: +873ms
2020-04-01 14:20:16.965 1439-1503/? I/ActivityManager: Displayed com.tencent.mobileqq/.activity.SplashActivity: +484ms
2020-04-01 14:20:17.440 1439-1503/? I/ActivityManager: Displayed com.tencent.mobileqq/.activity.LoginActivity: +259ms
2020-04-01 14:20:22.418 1439-1503/? I/ActivityManager: Displayed com.tencent.mobileqq/.activity.SplashActivity: +373ms
2020-04-01 14:20:27.899 1439-1503/? I/ActivityManager: Displayed com.tencent.mobileqq/.activity.SplashActivity: +190ms
2020-04-01 14:20:35.022 1439-1503/? I/ActivityManager: Displayed air.tv.douyu.android/com.douyu.module.home.pages.main.MainActivity: +403ms (total +1s124ms)(斗鱼)
中间的是网易和QQ的启动时间 ,最后一个是斗鱼的。
启动优化
具体的优化方式,我把它们分为闪屏优化、业务梳理、业务优化、线程优化 。
闪屏优化
今日头条把预览窗口实现成闪屏的效果,这样用户只需要很短的时间就可以看到“预览闪屏”。这种完全“跟手”的感觉在高端机上体验非常好,但对于中低端机,会把总的的闪屏时间变得更长。如果点击图标没有响应,用户主观上会认为是手机系统响应比较慢。所以我比较推荐的做法是,只在 Android 6.0 或者 Android 7.0 以上才启用“预览闪屏”方案,让手机性能好的用户可以有更好的体验。
微信做的另外一个优化是合并闪屏和主页面的 Activity,减少一个 Activity 会给线上带来 100 毫秒左右的优化。但是如果这样做的话,管理时会非常复杂,特别是有很多例如 PWA、扫一扫这样的第三方启动流程的时候。业务梳理我们首先需要梳理清楚当前启动过程正在运行的每一个模块,哪些是一定需要的、哪些可以砍掉、哪些可以懒加载。
我们也可以根据业务场景来决定不同的启动模式,例如通过扫一扫启动只需要加载需要的几个模块即可。对于中低端机器,我们要学会降级,学会推动产品经理做一些功能取舍。但是需要注意的是,懒加载要防止集中化,否则容易出现首页显示后用户无法操作的情形。
业务优化
通过梳理之后,剩下的都是启动过程一定要用的模块。这个时候,我们只能硬着头皮去做进一步的优化。优化前期需要“抓大放小”,先看看主线程究竟慢在哪里。最理想是通过算法进行优化,例如一个数据解密操作需要 1 秒,通过算法优化之后变成 10 毫秒。退而求其次,我们要考虑这些任务是不是可以通过异步线程预加载实现,但需要注意的是过多的线程预加载会让我们的逻辑变得更加复杂。
业务优化做到后面,会发现一些架构和历史包袱会拖累我们前进的步伐。比较常见的是一些事件会被各个业务模块监听,大量的回调导致很多工作集中执行,部分框架初始化“太厚”,例如一些插件化框架,启动过程各种反射、各种 Hook,整个耗时至少几百毫秒。还有一些历史包袱又非常沉重,而且“牵一发动全身”,改动风险比较大。但是我想说,如果有合适的时机,我们依然需要勇敢去偿还这些“历史债务”。
线程优化
线程优化就像做填空题和解锁题,我们希望能把所有的时间片都利用上,因此主线程和各个线程都是一直满载的。当然我们也希望每个线程都开足马力向前跑,而不是作为接力棒。所以线程的优化主要在于减少 CPU 调度带来的波动,让应用的启动时间更加稳定。从具体的做法来看,线程的优化一方面是控制线程数量,线程数量太多会相互竞争 CPU 资源,因此要有统一的线程池,并且根据机器性能来控制数量。
实践代码
异步初始化 。
@Override
public void onCreate() {
super.onCreate();
initRouter();
initRealm();
new Thread(new Runnable() {
@Override
public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
Looper.prepare();
//设置线程的优先级,不与主线程抢资源
//子线程初始化第三方组件
initEasemob();
initUMeng();
initGensee();
initBugly();
initFeedback();
initVodsite();
initOAID();
initBaiDuMob();
Looper.loop();
Looper.myLooper().quit();
}
}).start();
}
PS :有些是不能异步初始化的 。还有就是 Looper 问题 。(提示 :一定要把所有异步初始化的功能验收一遍) 。
PS:补充一下
1. 异步初始化能缩减 300 ~ 500 毫秒
2. 闪屏页跟启动页合在一起能减少 100 ~ 200 毫秒
3.不加固,据说能减少 1 秒