前言
某次在开发阶段,发现App启动过程中既然有3-4s的白屏时间,瞬间慌了,到底干了些什么???
分析
启动时间统计
# 完整命令
adb [-d|-e|-s <serialNumber>] shell am start -S -W
com.example.app/.MainActivity
-c android.intent.category.LAUNCHER
-a android.intent.action.MAIN
# 常用命令
adb shell am start -W packageName/Activity类全路径(启动Activity)
# -c和-a参数是可选的,允许您为intent指定<category>和<action>。
启动时间如下(小米5s)-Debug环境:
ThisTime | TotalTime | WaitTime |
---|---|---|
4357 | 4357 | 4384 |
看到数据还是还是很惊讶的,感觉下载了个线上版本,用上述命令统计了下启动时间 -Release环境:
ThisTime | TotalTime | WaitTime |
---|---|---|
1279 | 1279 | 1304 |
心中终于松了一口气,既然是这个版本出的问题,那就开始查原因。
ThisTime:最后一个启动的Activity的启动耗时;
TotalTime:自己的所有Activity的启动耗时(多个Activity,如果只启动一个Activity的时候TotalTime=ThisTime);
WaitTime: ActivityManagerService启动App的Activity时的总时间
如果只关心某个应用自身启动耗时,参考TotalTime;如果关心系统启动应用耗时,参考WaitTime;如果关心应用有界面Activity启动耗时,参考ThisTime。
详情计算说明可参考:https://www.zhihu.com/question/35487841
官方指导
官方指导:launch-time
官方译文:应用启动时长
从指导文章中可以看出,App启动时长过长,基本就是做了很多耗时的操作,比如在Application的onCreate中做了很多初始化导致。
找问题瓶颈
我们需要知道到底哪些操作引起了耗时,我们需要去分析Application中都做了哪些耗时操作,于是找了以下一些工具:
- Method Tracing
- Android Profiler
- Systrace(待研究)
- BlockCanary
- 自己手动统计
经过一些尝试后发现,只有Systrace和自己手动统计能满足自己需求,后面细说。
自己手动统计
没有好的方法只能用笨方法,虽然要改造原有代码,但好在灵活性较高,快速出结果:
public static void traceMethod(String source, Action0 func) {
if (PropertyUtils.isProduct()) {
func.call();
} else {
long startTime = System.currentTimeMillis();
func.call();
long endTime = System.currentTimeMillis();
LogUtils.w("Method Measure: source:[" + source + "][" + (endTime - startTime) + "]");
}
}
将你需要统计时长的方法,经过traceMethod
进行一层包装,在log中进行参看。这个方式是否笨拙,但是有效简单。后续如有其他方式可以再替换。
时长数据
[getComponent().inject]--[278]
[RouterManager.init]--[0]
[EnvironmentController.getInstance().init(provider)]--[1]
[UIManager.init(this)]--[0]
[UtilsManager.init(this)]--[7]
[DRImageLoader.init(this)]--[242]
[initLibs]--[252]
[initViewBinding]--[61]
[initRouter]--[923] —>37
[TinkerHelper.initTinker()]--[2]
[initAppComponent()]--[0]
[initARouter()]--[1]
[initCrashHandler()]--[2]
[initLibs()]--[654]
[SDKInitializer.initialize(this)]--[60]
[initPushSdk()]--[47]
[initStatisticsTools()]--[256]
[initOnAppProcess()]--[11][Application onCreate]—[2405]
时长分析
超过100的都应该优化
initRouter—923ms->37ms
if (PropertyUtils.isDebugOpen()) { // 这两行必须写在init之前,否则这些配置在init过程中将无效
ARouter.openLog(); // 打印日志
ARouter.openDebug(); // 开启调试模式(如果在InstantRun模式下运行,必须开启调试模式!线上版本需要关闭,否则有安全风险)
}
ARouter.init(this);
罪魁祸首就是下面两行代码,也不知道干了啥,去掉后时间降到37ms
ARouter.openLog(); // 打印日志
ARouter.openDebug();
initLibs()— 654ms
这个方法里面做了很多依赖库的初始化,再进行细分看下各个依赖库的init耗时
[DRCustomerNetController.getInstance().init()]--[78]
[initUBT()]--[215]
[initCustomerId()]--[0]
[initResourceConfig()]--[3]
[RongYunManager.init()]--[281]
[PlatformManager.init()]--[101]
[getRouterService.init()]--[1]
需要优化的几个地方:
- initUBT
- RongYunManager.init
- PlatformManager.init
initUBT—215ms
牵扯到UBT组件,后续再优化
RongYunManager.init—281ms
移至子线程
PlatformManager.init—101ms
移至子线程
getComponent().inject—278ms
牵扯到组件实例生成,暂不处理
DRImageLoader.init--242ms
不可移至子线程,初始化移至第一次调用
必须在Application中进行调用,底层fresco的imageView在实例化的时候就会去检测
initStatisticsTools--256ms
移至子线程
优化后结果
ThisTime | TotalTime | WaitTime |
---|---|---|
2838 | 2838 | 2850 |
[getComponent().inject]--[230]
[RouterManager.init]--[0]
[EnvironmentController.getInstance().init(provider)]--[0]
[UIManager.init(this)]--[1]
[UtilsManager.init(this)]--[6]
[DRImageLoader.init(this)]--[78]
[initLibs]--[89]
[initViewBinding]--[63]
[initRouter]--[33]
[TinkerHelper.initTinker()]--[1]
[initAppComponent()]--[0]
[initCrashHandler()]--[1]
[ISPMUBTProtocol.init()]--[252]
[initUBT()]--[260]
[initCustomerId()]--[0]
[initResourceConfig()]--[6]
[initAppEnv()]--[269]
[getRouterService.init()]--[2]
[initLibs()]--[273]
[SDKInitializer.initialize(this)]--[44]
[initPushSdk()]--[42]
[initOnAppProcess()]--[12]
Method Measure:Application onCreate耗时:805
Method Measure:initOnWorkThread耗时:653
优化后发现Application的onCreate还耗时800多毫秒,主要是由于UBT、Component、Image的初始化导致,暂无较好优化方案,后续观察;
还有个很奇怪的现象就是App启动要2838,减去Application的805,还剩2000+的时间,这部分时间是谁消耗的,暂无定论,猜测是一些static和单例导致的。由于没有办法去监控,先优化到这样子。
优化方案
- 异步初始化(比如一些非必须的组件可以放在子线程进行初始化)
- 延迟初始化(一般的launchActivity会有几秒的启动屏,可以在里面做一些初始化操作,比如ImageLoader的init)
- 懒加载,用到才初始化(有很多组件并不是应用启动时候就要使用,这个可以参考Dagger)
实现代码如下:
/**
* 在App线程进行初始化
*/
private void initOnAppProcess() {
TinkerHelper.initTinker();
initRouter();
DRCustomerNetController.getInstance().init();
initAppComponent();
initCrashHandler();
initComponentService();
//资源下发配置
initResourceConfig();
SDKInitializer.initialize(this);
DRPushManager.initPushSdk();
initGrowingio();
registerActivityLifecycleCallbacks(new PageLifecycleCallbacks());
}
/**
* 非阻塞的,在子线程进行初始化
*/
private void initOnWorkThread() {
new Thread() {
@Override
public void run() {
long startTime = System.currentTimeMillis();
//不与主线程争抢资源
android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND);
//self lib
initPlatform();
//third lib
RongYunManager.getInStance().init(innerInstance, AppConstans.RONG_CLOUD_APP_KEY);
initBugly();
long endTime = System.currentTimeMillis();
LogUtils.d("Method Measure:initOnWorkThread耗时:" + (endTime - startTime));
}
}.start();
}
后记
发现小米设备的启动要2s+之后,又用其他设备尝试了下基本在1s以内,通过systrace捕获发现小米中bindApplication就用了1.5s左右。后续有编译了个release版本重新运行了结果如下:
ThisTime | TotalTime | WaitTime |
---|---|---|
835 | 835 | 849 |
看到这个结果还是很欣慰的(提速了35%),看来release版本还是做了一些优化,小米系统坑啊!
网友评论
这个预加载框架可以很方便地做到这一点:https://github.com/luckybilly/PreLoader
这个预加载框架可以很方便地做到这一点:https://github.com/luckybilly/PreLoader
这个自己得去衡量,因为异步就会牵扯到初始化执行时机的非确定性。