Android 启动优化

统计启动时间

adb查询

adb shell am start -W packageName/targetActivity

image-20210518110909743

不适合线上,且时间非准备。适合分析竞品

手动打点

定一个辅助类,记录开始和结束的时间,其实时间差就是启动时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class LaunchTimeTester {

companion object {
private var totalTime: Long = 0

private var startTime:Long=0
private var endTime:Long=0

fun startRecode() {
startTime=System.currentTimeMillis()
}

fun endRecord(){
endTime=System.currentTimeMillis()
}

fun getLaunchTime():Long{
if (totalTime==0L){
totalTime=endTime- startTime
}
Log.d("LauncherTime","launcher Time:$totalTime")
return totalTime
}
}

}

在Application的attachBaseContext函数记录启动时间。

1
2
3
4
override fun attachBaseContext(base: Context?) {
super.attachBaseContext(base)
LaunchTimeTester.startRecode()
}

而结束时间,建议是在数据拉取后结束统计,而不是在onWindowFocusChanged首帧结束统计,例如在启动页广告加载出来的时候计算启动的结束时间。

1
2
3
4
5
6
7
8
ivBottomLogo.viewTreeObserver.addOnPreDrawListener(object : OnPreDrawListener1 {
override fun onPreDraw(): Boolean {
ivBottomLogo.viewTreeObserver.removeOnPreDrawListener(this)
LaunchTimeTester.endRecord()
LaunchTimeTester.getLaunchTime()
return true
}
})

LaunchTimeTester.getLaunchTime()就输出了打印时间。

时间准确且适合线上,是比较推荐的统计方式。

工具

1、traceview

traceview手动埋点,精准。但是运行时开销严重,整体变慢,可能带偏优化方向。

图形显示展示执行时间,调用栈等,信息比较全面,包含所有线程。

流程 :

1
2
3
4
//在要统计信息的开始地方
Debug.startMethodTracing()
//在要统计信息的结束地方
Debug.stopMethodTracing()

然后运行程序,会在/sdcard/Android/data/packageName/files目录下,生成.trace后缀的文件。

image-20210518170128311

双击该文件后,Android Studio会展示相关信息

image-20210518170427427

左边展示了线程数和线程名,例如上图有20条线程,有mainbugly等线程。

image-20210518172753517

主要看下右边的Summary,TopDownFlameChartBottomUp。单击左边线程名,会在右边显示单击线程的相关信息,例如上图的线程名main

image-20210518173412136

下图是TopDown,BottomUp与它的区别就子方法的展示是倒过来的。其中要注意的是右上角的WallClockTimeThreadTime概念。ThreadTime表示该方法代码所执行的时间,而WallClockTime表示CPU花在该方法的时间,总是大于ThreadTime,也是我们分析的目标。

image-20210518173858784

FlameChart主要把相同方法的调用统计在一起,即一个方法被调用多次的统计信息。

image-20210518174328226

traceview的代码统计不应该存在线上环境,而适合在分析的时候,由于统计信息很全面,也很耗时,占用资源。

2、systrace

默认情况下,systrace只收集系统信息,所以需要在代码添加下面代码来收集APP的引用信息。

1
2
3
4
//开始的地方
Trace.beginSection("test")
//结束的地方
Trace.endSection()

systrace轻量级,开销小,直观反映CPU的利用率。cputime 代码消耗cpu的时间(重点指标)与walltime代码执行时间

启动耗时分析

通过分析各部分,找出耗时的地方,然后针对性优化。

1、手动埋点

即通过第一步手动打点,记录各部分用时。侵入性比较强,影响原有代码布局;工作量大,需要填写大量的启动时间。

2、AOP切面埋点

优点无侵入性代码,修改方便。Android上使用AspectJ

**Join Points**程序运行时的执行点,可以作为切面的地方:函数调用、执行;获取和设置变量;类初始化。

**PointCut**带条件的Join Points,对Join Points进行筛选。

**Advice**一种Hook,要插入代码的位置。即BeforeAfter注解。

优化

1、优化白屏和黑屏

添加自定义背景,避免直接显示白屏和黑屏

2、异步优化

使用子线程分担主线程任务,并发减少时间。根据CPU核心数量,创建合适的线程数量的线程池。

优化方案

1、延时初始化

延时初始化可以理解为日常上下班高峰期错峰出行的原理。本来大家都挤在ApplicationonCreate函数中,而且是希望越早越好。所以都会竞争资源,造成资源竞争,CPU会切线程等。所以就会出现启动慢的情况。这时如果将一些不紧急的第三方SDK(例如Bugly)通过Handler.postDelay进行延时初始化,能起到一定的优化效果。但缺点也明显,只能管控延时时间,如果刚好在延迟时间到期,APP也很繁忙,同样也会造成卡顿。

2、闲时初始化

所谓闲时,就是相关资源不紧张,例如CPU、内存等。更细的说,当前消息队列没有信息,处于空闲状态。阅读过Handler机制源码应该知道在MessageQueuenext函数,在消息队列没有消息时会调用IdelHandler处理不紧急消息。通过IdelHandler我们不仅可以处理一些第三方SDK初始,也可以初始化我们的一些闲时动作,例如异常信息上传,用户Token上传等。

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class DelayDispatcher {

public interface Task{
void run();
}

private Queue<Task> taskQueue=new LinkedList<>();

private MessageQueue.IdleHandler handler= () -> {
if (taskQueue.size()>0){
Task task=taskQueue.poll();
task.run();
}
return !taskQueue.isEmpty();
};


public DelayDispatcher addTask(Task task){
taskQueue.add(task);
return this;
}

public void start(){
Looper.myQueue().addIdleHandler(handler);
}

}

使用:

1
2
3
4
5
6
DelayDispatcher().addTask {
Log.d(TAG, "bugly init")
val strategy = CrashReport.UserStrategy(this)
strategy.appChannel = DeviceUtils.getChannel()
Bugly.init(applicationContext, "*****", BuildConfig.debug, strategy)
}.start()

通过这个初始化方案,ApplicationonCreate函数大概执行时间大概减少了一半,原先在1113毫秒左右,使用在452毫秒左右。推荐使用。