首先,我们先提出一个问题,为什么要做启动优化?
随着项目的迭代,App的功能越来越丰富,无可避免的是我们将会引入更多的第三方库及各种SDK,因此App在启动时要做的初始化工作也会更繁重,不当的初始化行为就会拖慢App的启动响应速度,给用户带来糟糕的使用体验。
既然是启动优化,我们就需要先了解应用的启动类型:
冷启动流程图:
冷启动的链路包含热启动和温启动,因此启动优化主要是对冷启动而言。
熟悉Android系统启动流程的小伙伴知道直到我们点击Launcher上的应用启动图标,才真正的进入到App的启动流程,即从此节点之后才是我们的可控范围,由此可以引出第一个启动优化点黑白屏优化 ,此优化方案虽然不能缩短App的启动时间,但从App交互设计上给予了用户及时的操作反馈,提升了一些用户体验。由于之前介绍过相关知识点,大家可点击链接查看,本篇中不再赘述,接下来正式开始介绍启动优化。
测量启动时间的目的有两个:
时间测量方式:
在Android 4.4及以上版本,App启动时会在logcat输出一行日志,会打印出名为Displayed的值,此值代表从进程启动到在屏幕上完成对应Activity的绘制所经过的时间。
#com.example.test : 包名
#MainActivity :App的启动Activity
adb shell am start -W com.example.test/.MainActivity
点home键推到后台,再运行adb命令:
点back键推到后台,再运行adb命令:
在此我们也验证了不同启动方式的差异:冷>温>热
手动添加log日志,打印时间戳。优点:可在线上使用。 缺点:只能记录应用内耗时。
可以用代码统计或AndroidStudio自带的cpu profiler。
public class App extends Application {
@Override
public void onCreate() {
super.onCreate();
//文件名,文件路径:扩展存储路径(sdcard)
Debug.startMethodTracing("spend_time");
//
//各种初始化代码
//
Debug.stopMethodTracing();
}
橙色:系统代码调用
绿色:自己代码调用
蓝色:第三方代码调用
用as自带cpu profiler的话,需提前开启设置
可以查看耗时数据,工具具体使用方法大家可自行学习。 需要注意的是此工具对代码的侵入性较高,会拖慢代码的运行速度,所以真实耗时会稍小一点,不过不影响我们分析问题。
需要python脚本来记录,Android9.0及以上版本的设备可以开启跟踪记录。官方文档
添加跟踪代码,并运行到设备。
public class App extends Application {
@Override
public void onCreate() {
super.onCreate();
//Debug.startMethodTracing("spend_time");
//
//各种初始化代码
//
//Debug.stopMethodTracing();
Trace.beginSection("spend_time1");
//
//各种初始化代码
//
Trace.endSection();
}
}
然后进入到systrace.py文件所在路径,(替换成自己的sdk路径)
/Users/zhoumohan/Library/Android/sdk/platform-tools/systrace
运行命令:
python systrace.py -a 应用包名 -o mynewtrace.html sched freq idle am wm gfx view binder_driver hal dalvik camera input res
可能会报错:
ImportError: No module named six
解决方案
随后重新运行,会生成跟踪记录:mynewtrace.html,可在浏览器查看
通过自定义注解及AspectJ框架,对需要检测的方法的前后进行代码注入,从而统计执行耗时,大家可参考AOP思想实现集中式登录,用户行为统计框架,或自行学习相关知识点,这里就不做展开。
此处针对于多进程App,我们知道进程是相互隔离的,所以有多少个进程,项目中的Application就会被执行多少次,因此我们可以根据不同的进程进行对应的初始话操作。
public class App extends Application {
@RequiresApi(api = Build.VERSION_CODES.P)
@Override
public void onCreate() {
super.onCreate();
String processName = getProcessName();
if (processName != null) {
if (processName.equals("进程名称")) {
//...需要在不同进程下运行的代码
}
}
}
}
示例中获取进程名适用于9.0及以上,适配低版本可参考android 获取当前进程的名称
主要是通过使用多个子线程来初始化,达到并行执行,缩短运行时间的目的。
public class App extends Application {
ExecutorService executorService;
int coreSize;
@Override
public void onCreate() {
super.onCreate();
coreSize = Runtime.getRuntime().availableProcessors();
executorService = Executors.newFixedThreadPool(Math.max(2,Math.min(coreSize-1,4)));
asyncInit(new Runnable() {
@Override
public void run() {
//SDK1.init()
}
});
asyncInit(new Runnable() {
@Override
public void run() {
//SDK2.init()
}
});
//等等初始化操作...
}
private void asyncInit(Runnable runnable){
executorService.submit(runnable);
}
}
需要注意一下的是,放到各个子线程进行初始化的代码有没有先后顺序和调用关系,相关的初始话最好放到一个线程中,来保证代码的逻辑正确性,做异步优化时应考虑清楚一下几点:
即仅初始化立即需要用到的对象,不要创建全局静态对象,而是移动到单例模式,在程序第一次使用它的时候进行初始化。
监听应用空闲时间,在空闲时间进行初始化。这里用到了IdelHandle的特性。
public class DelayInit {
private Queue delayQueue = new LinkedList<>();
public void add(Runnable runnable){
delayQueue.add(runnable);
}
public void start(){
Looper.myQueue().addIdleHandler(() -> {
Runnable poll = delayQueue.poll();
if (poll != null){
poll.run();
}
return !delayQueue.isEmpty();
});
}
}
同时也需要注意初始化对象和使用对象的时序性。