在开始正题之前,先说点启动优化的思路
App启动方式
冷启动
在App没启动过或者App进程被杀后,系统不存在该App进程时启动App,称为冷启动。 冷启动过程,需要创建应用进程,启动主线程,加载相关资源, 初始化首屏Activity等。 在肉眼所见的过程中, 屏幕会显示一个空白的窗口,直至首屏Activity完全启动。
这个空白窗口叫做StartingWindow,是个临时窗口,对应的WindowType是TYPE_APPLICATION_STARTING 。应用程序初始化完成加载完第一帧之后,这个窗口就会被移除。StartingWindow显示的是一个空DecorView,它会使用launch activity指定的Theme,如果 LaunchActivity没有指定Theme就用application的。
具体执行位置:
ActivityStack#startActivityLocked --> ActivityRecord#showStartingWindow
温启动
App进程未被杀死,Activity未被回收,比方说两个应用之间连续切换。
热启动
当App进程并未被杀死,但Activity可能因为内存不足被回收,此时启动App系统会从已有进程中启动应用,Activity会重新启动,称为热启动。
启动优化主要是针对冷启动。
从哪下手
整个冷启动过程中,系统方法我们无法进行优化,主要需要优化的是系统暴露出来的一些生命周期方法,从Application
的attachBaseContext
开始,到启动页Activity或者首页Activity的onResume
结束,甚至直到Activity
的界面绘制结束。优化的目的就是使这个过程尽量快,不要出现卡顿。其中最重要的就是Application
中的onCreate
方法了。
怎么做
- 将初始化任务按两个维度区分:是否耗时&是否必要。必要且耗时的,考虑使用其他线程来初始化(比如Tinker初始化),不必要的都延迟启动。
- 对于业务逻辑,一定要梳理清楚,不要做多余的动作,处理掉废弃代码。
- 启动中如果有网络请求,考虑能否缓存,不必每次启动都请求;如果有多个请求,就与后台协商看看能否合并接口。
- 如果首页存在
Fragment
,考虑只加载可见的Fragment
,其余的使用到了再初始化。 - 如果要新建线程,尽量用单个线程,因为毕竟创建线程池的开销比单个线程大。注意工作线程优先级不要太高,会影响到主线程的绘制。
- 注意首页的View层级,建议使用
ConstrainLayout
,或者自定义View,尽量减少动画的使用。 - 减少磁盘读写,注意序列化、反序列化,尽量使用内存缓存,可以使用腾讯的开源框架
MMKV
。 - 还有一点要提醒,多开进程的情况下,Application的onCreate可是会多次启动的喔,这个要记得处理。
以上建议都不绝对,按需使用,欢迎补充
接下来进入主题
关于Anchors
- 最初的代码是在一个项目中使用的,主要实现逻辑在
com.demo.appinit.anchors
中,这里修改了其中的一些BUG,一些会造成误解的命名,整理了代码并加入大量注释,并完成了这个Demo - 经过不断地查找,找到了最初项目开源的地方,Demo是基于这个改造的。
UML类图
这里只画出了关键类、变量、方法,部分是省略的,详情请查看Demo。使用方式
- 要执行一个任务,需要自定义Task(继承
BaseTask
)。 - 使用自定义工厂创建对应的Task,工厂继承自
Project.TaskFactory
。 - 使用
Project.Builder
创建Task图。 - 使用
AnchorsManager
的start
方法启动Task(BaseTask
的start
方法是protected
修饰的) - 在Demo中,在Application的onCreate方法中调用以下代码启动整个过程。
MainProcessStarter.start(checkPermission);
- 更多注释都在代码中
原理解释
BaseTask
BaseTask
是一个任务单元,其中定义了前置任务dependTasks
与后置任务behindTasks
,每个Task会有优先级,锚点任务优先级是最高的,优先级主要用来排序。- 当向taskB中加入一个前置条件taskA时,taskA会被加入taskB的前置任务
dependTasks
,同时taskB也会被加入taskA的后置任务behindTasks
中。 - 当向taskB中加入一个后置条件taskC时,taskC会被加入taskB的后置任务
behindTasks
,同时taskB也会被加入taskC的前置任务dependTasks
中。 - 当一个task执行完成时,会将自己的后置任务
behindTasks
逐个启动,此时后置的任务会判断自己还有没有前置的任务,如果有就不执行,没有才执行。
Project
- Project的存在意义在于他的Builder,他能构造一个不会产生环的task图。
AnchorsManager
BaseTask
是不能直接调用start
方法执行的,必须通过AnchorsManager
的start
才能执行,它定义了如何正确地执行一个task。- 从
start
方法可以看出,锚点任务都会在start
所在方法内执行完(比如说我在onCreate中调用了start,则锚点任务都会在onCreate方法中执行)
AnchorsRuntime
- 会对整个执行过程记录信息,并打印出log
- 管理着一个线程池,用于执行异步任务
LockableTask & LockableAnchor
- 可以通用过这两个类实现整个执行过程的阻塞,当然也可以自定义,比如Demo中的
AwaitPermStartTask
为什么会变快
首先看到每个task的启动方法start
/**
* 调用start启动当前task
*/
protected synchronized void start() {
if (mState != TaskState.IDLE) {
throw new RuntimeException("can no run task " + getId() + " again!");
}
toStart();
setExecuteTime(System.currentTimeMillis());
AnchorsRuntime.executeTask(this);
}
最终都会调用到AnchorsRuntime.executeTask(this);
,如下:
/**
* 同步使用handler发送至主线程,异步使用线程池
* @param task 任务
*/
static void executeTask(BaseTask task) {
if (task.isAsyncTask()) {
S_POOL.executeTask(task);
} else {
if (AnchorsRuntime.hasAnchorTasks()) {
AnchorsRuntime.addRunTasks(task);
} else {
sHandler.post(task);
}
}
}
首先异步执行当然会减少启动时间,那如果全部都是同步执行呢?经过测试,启动过程也是变快了的。最终启动变快的原因就在于这个sHandler.post(task);
(sHandler
的Looper是主线程的Looper)。当每个task执行时,会post到主线程的消息队列的末尾,相当于不是立即执行,而是等待主线程现有的任务执行完了才执行,相当于给App启动“让路”。
systrace分析
在Demo中,START_TASK_3
、START_FIRST_OF_ALL
是锚点任务,START_TASK_3
depend on START_CONFIG_PRELOAD
,任务图是在Application的onCreate中执行的,所以这三个任务会在onCreate中执行完毕,如下图:
继续往后看,START_TASK_2
在启动页的Resume之后执行了,因为它post的时候,ActivityThread.H
(主线程Handler)已经把LaunchActivity、ResumeActivity的message发送给了主线程任务队列。
最后这张图是剩下的任务执行过程,可以参看Demo的代码进行比对。Demo中分了Project1与Project2,前者是不需要申请权限的,后者需要,在他们之间有一个AwaitPermStartTask
,可以阻塞整个任务图的执行,等待用户授权后在继续执行需要授权的任务(从图中的p2_start
开始)。具体原理是使用CountDownLatch
进行阻塞,使用RxBus
进行回调。
从整体来看,本来全在Application的onCreate中执行的任务大部分都后移了,App启动的任务会被稍微“提前”,对于部分任务,更是在MainAcitivty启动之后才执行的,所以达到了优化的效果。但是这种方法也会有缺点,这些后移的任务如果需要在主线程执行,可能会影响到界面的绘制,造成卡顿,可以考虑使用MessageQueue.IdleHandler
,在主线程空闲的时候执行剩下的任务。
用数据说话
最后,经过测试Demo,启动速度从900ms多加速到650ms左右,大约提速30%(从Application的attachBaseContext开始计时,直到MainActivity的onWindowFocusChanged)(ps:这里的耗时都是我自定义的,所以真正的优化效果还要看具体情况)