发动优化是Android优化老生常谈的问题了。众所周知,android的发动是指用户从点击 icon 到看到首帧可交互的流程。

而发动流程 粗略的能够分为以下几个阶段

  1. fork创立出一个新的进程
  2. 创立初始化Application类、创立四大组件等 走Application.onCreate()
  3. 创立launchActivity 走完onCreate、onStart、onResume生命周期

往深往细里面研究 这儿能够有十分多的‘黑科技’能操作,mutilDex优化,message调度优化,json预热之类的计划十分多。

本文只处理一个点,针对Application.onCreate()做优化。

一、技能布景

跟着事务的开展堆叠,application中初始化办法越来越臃肿。代码全都堆在一起,例如:

initARouter(app)
initAutoSize()
initFlipper()
initNetworkConfig()
launch()
configXXX()
ServiceManager.init(app)
initJVerification(app)
initHotFix(app)
initAPM(app)
initBugly(app)
....省掉很多初始化代码

当很多的初始化办法这样累加在一起必然会导致发动变慢。这是第一个问题:发动变慢

跟着项目的组件化逐渐进行,这儿就存在了一个新问题。为了事务解耦,每个事务模块需求不同的功用,例如商品模块需求共享,物流定位模块需求地图等。可是这些功用并非悉数事务组件都用到的东西,放到主工程Application不合适。这是第二个问题:事务上的解耦

所以咱们需求一个发动时,简略、高效的初始化组件的办法,这也是为什么规划这套startup的原因。

二、算法基础

要处理发动变慢的问题,首要有两个思路,推迟加载和异步加载。当然,大部分库都是需求在进入首页之前初始化完结的,否则会产生一些反常。所以咱们这儿首要处理怎样去异步加载的问题。

2.1 : 有向无环图

95分Android启动优化实践

  • DAG,有向无环图,能够办理使命之间的依靠联系,并调度这些使命,似乎能够满足本节开端的诉求,那么咱们先了解下这种数据结构
  • 极点:在DAG中,每个节点(sdk1/sdk2/sdk3……)都是一个极点;
  • :衔接每个节点的衔接线;
  • 入度:每个节点依靠的节点数,形象来说便是有几根线衔接到该节点,例如sdk2的入度是1,sdk5的入度是2。
  • 咱们从图中能够看出,是有方向的,可是没有路径再次回到起点,因而就叫做有向无环图
  • 2.2 : 拓扑排序

  • 拓扑排序用于对节点的依靠联系进行排序,首要分为两种:DFS(深度优先遍历)(这也是咱们的计划)、BFS(广度优先遍历),如果了解二叉树的话,对于这两种算法应该比较了解。
  • 咱们就拿这张图来演示,拓扑排序算法的流程:
  • 1:首要找到图中,入度为0的极点,那么这张图中入度为0的极点便是task1,然后删去

95分Android启动优化实践
2:删去之后,再次找到入度为0的极点,这个时分有两个入度为0的极点,task2和task3,所以拓扑排序的结果不是唯一的!

95分Android启动优化实践
3:依次递归,直到删去悉数入度为0的极点,完结拓扑排序

95分Android启动优化实践

三、技能计划

3.1 : 接口规划

要把咱们发动使命拆分为若干个小task去调度发动,首要规划咱们的task基类。

interface ITask : ITaskCallBack {
    /**
* 使命name
*/
val taskName: String
    /**
* 使命是否完结
*/
val isCompleted: Boolean
    /**
* 是否要block发动
*/
val needAwait: Boolean
    /**
* 使命初始化进程
*/
val process: RunProcess
    /**
* 使命是否可用
*/
val enable: Boolean
    /**
* 是否在主线程履行
*/
val runOnMainThread: Boolean
    /**
* 是否需求赞同隐私协议后再履行
*/
val needPrivateAgree: Boolean
    /**
* 依靠的task
*/
fun dependsTaskList(): List<String>
    /**
* 使命被履行的时分回调
*/
fun run(application: Application)
}

而且供给完成Task.class

abstract class Task(override val taskName: String) : ITask {
    private var completed: AtomicBoolean = AtomicBoolean(false)
    override val isCompleted: Boolean
        get() = completed.get()
    / ** * 默许运转在主进程 */   override val process: RunProcess
        get() = RunProcess.MAIN
    / ** * 默许堵塞发动 */   override val needAwait: Boolean = true
    / ** * 默许运转 */   override val enable: Boolean = true
    / ** * 默许运转在子线程 */   override val runOnMainThread: Boolean = false
    / ** * 默许需求赞同隐私协议后初始化 */   override val needPrivateAgree: Boolean = true
    / ** * 用来在前置使命完结之前堵塞当时task */   private val countDownLatch: CountDownLatch by lazy {  CountDownLatch(dependsTaskList().size)
    } 
  override fun dependsTaskList() = emptyList< String>()
    override fun runProcessName(): List<String> = emptyList( )
    / ** * 当时使命开端等候 直至依靠项悉数完结再开端履行 */   internal fun await() {
        if (dependsTaskList().isNotEmpty( ))
            countDownLatch.await()
    }
    / ** * 告诉某个依靠项完结 */   internal fun countdown() {
        if (dependsTaskList().isNotEmpty( ))
            countDownLatch.countDown()
    }
    override fun onAdd() {
    }
    @CallSuper
    override fun onStart() {
        completed.set(false)
    }
    @CallSuper
    override fun onFinish() {
        completed.set(true)
    }
    override fun toString(): String {
        return "$taskName(enable=$enable, runOnMainThread=$runOnMainThread, needPrivateAgree=$needPrivateAgree ,dependsTaskList=${dependsTaskList()})"
    }
}

咱们供给完成Task类去界说发动使命,留意界说各种参数。

发动装备

 Startup.debug(BuildConfig.DEBUG)
        .privateAgreeCondition { Storage.APP_FIRST_PRIVATE_DIALOG }
        .start(app)

3.2 : 线程办理规划

首要,咱们的使命分为两种模式,运转在主线程和运转在子线程。

  • 已然不能确保每个使命都在主线程中履行,那么就需求对使命做装备
interface ITask {
    /**
* 是否在主线程履行
*/
val runOnMainThread: Boolean
}

3.2.1 : 线程池

已然要在子线程初始化一些使命,那么咱们有必要保护一个线程池。

CPU密集型也是指核算密集型,大部分时刻用来做核算逻辑判别等CPU动作的程序称为CPU密集型使命。该类型的使命需求进行很多的核算,首要消耗CPU资源。这种核算密集型使命虽然也能够用多使命完结,可是使命越多,花在使命切换的时刻就越多,CPU履行使命的功率就越低,所以,要最高效地利用CPU,核算密集型使命一起进行的数量应当等于CPU的核心数。占据CPU的时刻片过多的话会影响性能,所以这儿操控了最大 并发 ,防止主线程的时刻片削减

IO密集型使命指使命需求履行很多的IO操作,涉及到网络、磁盘IO操作,对CPU消耗较少。有好多使命其实占用的CPU time十分少,所以运用缓存线程池,基本上来者不拒

这儿咱们选用的是CPU****密集型使命的线程池。

threadList.forEach {
if (it.isCompleted) {
      setNotifyChildren(it)
  } else {
      threadPoolExecutor.execute(TaskRunnable(application, task = it))
  }
}
mainList.forEach {
if (it.isCompleted) {
      setNotifyChildren(it)
  } else {
      TaskRunnable(application, task = it).run()
  }
} 

3.2.2 : 使命分发

有一些task是依靠于其他task的,需求在其他task初始化完结后,才干初始化自己。比方预取使命有必要要在网络初始化完结后再履行。

而往往这些使命可能是运转在不同的线程里的,那就有一个大问题,使命之间的履行次序,或者说分发。比方sdk4是耗时使命,能够放在子线程中履行,可是又依靠sdk2的初始化,这种状况下,咱们其实不能确保每个使命都是在主线程中履行的,需求等候某个线程履行完结之后,再履行下个线程,咱们先看一个简略的问题:假如有两个线程 AB ,A线程需求三步完结,当履行到第二步的时分,开端履行B线程,这种状况下该怎样处理?

答案是 CountDownLatch。

信任咱们对CountDownLatch并不陌生。它的原理便是会堵塞当时并等候一切的线程都履行完结之后,再履行下一个使命。

  • 咱们先看task的装备
interface ITask : ITaskCallBack {
    /**
* 依靠的task
*/
fun dependsTaskList(): List<String>
}

dependsTaskList表示该task要等候这些task初始化完结后再完结,string是依靠task的taskName。经过字符串解耦。

这儿简略看一下发动的流程,只看一些关键代码:

step1
private fun executeTasks(application: Application, list: List<Task>) {
    //。。。
    //这儿是子线程使命
threadList.forEach {
        threadPoolExecutor.execute(TaskRunnable(application, task = it))
    }
    //这儿是主线程使命
mainList.forEach {
        TaskRunnable(application, task = it).run()
    }
}
step2
class TaskRunnable(
    private val application: Application,
    private val task: Task
) : Runnable {
    override fun run() {
            //  前置使命没有履行完毕的话,等候,履行完毕的话,往下走
            task.await()
            //......
            // 履行使命
            task.run(application)
            //.......
            // 告诉子使命,当时使命履行完毕了,相应的计数器要减一。
            Startup.notify(task)
    }
}
step3
class Task{
    /**
    * 用来在前置使命完结之前堵塞当时task
    */
    private val countDownLatch: CountDownLatch by lazy {
    CountDownLatch(dependsTaskList().size)
    }
    /**
    * 当时使命开端等候 直至依靠项悉数完结再开端履行
    */
    internal fun await() {
        if (dependsTaskList().isNotEmpty())
            countDownLatch.await()
    }
    /**
    * 告诉某个依靠项完结
    */
    internal fun countdown() {
        if (dependsTaskList().isNotEmpty())
            countDownLatch.countDown()
    }
}

当咱们Startup发动的时分,首要会对一切的task实例进行拓扑排序,那些被其他Task所依靠且自身不依靠于其他Task的Task必然会先进队列履行,这儿确保了咱们的task不会被互相堵塞。

一起,咱们有一个childrenMap,key是一切被其他task所依靠的task,value是一切依靠于key的task的list。这个map是当被依靠的task履行完结的用于唤醒被堵塞的task。

当咱们的task被履行的时分,首要咱们会履行Task的await()。如果该task存在依靠task,会堵塞。直到一切的依靠task都履行完毕。而咱们是怎样去判别依靠的task都履行完毕的呢? 这儿就用到了上面说的childrenMap了。

当每个task履行完毕的时分,咱们会调用Startup的setNotifyChildren办法,然后去childrenMap中去查找依靠于此task的其他task,调用其conutdown办法。使其计数器countDownLatch减1,而countDownLatch的count便是其依靠的task的size。当其每个依靠的task都履行完发出notifyChildren信号后,堵塞铺开,开端履行。

一起上面也说了,经过拓扑排序后,被依靠的task必定先进队列,这样也避免了cpu线程池中被堵塞的线程塞满的状况,也便是互相堵塞,一直等候的状况。

3.2.3 : 提早释放

application初始化中的场景十分复杂,这儿存在一种场景,咱们的application不需求等候某个task履行完后再完毕。也便是某些必要task履行完了,不等候其他task履行完,直接进入页面。

流程如图

95分Android启动优化实践
这儿在task中也有装备

interface ITask{
    /**
    * 是否要block发动
    */
    val needAwait: Boolean
}

当然 这个task必定要是运转在子线程的啊。一个使命不能即运转在主线程又不堵塞主线程。

这儿需求留意,当你的task的needAwait为false且runOnMainThread为true的时分,会直接报错, 太扯了。

  • 而详细完成看代码
private fun executeTasks(application: Application, list: List<Task>) {
    if (list.isEmpty()) throw StartupException("tasks不能为空")
    taskMap.clear()
    taskChildMap.clear()
    val sortResult = TaskSortUtil.getSortResult(list, taskMap, taskChildMap)
    sortResult.forEach {
if (it.runOnMainThread) {
            mainList.add(it)
        } else {
            threadList.add(it)
        }
    }
 countDownLatch = CountDownLatch(1)
    executeMonitor.recordProjectStart()
    listeners.forEach {
it.onProjectStart()
    }
threadList.forEach {
if (it.isCompleted) {
            notifyChildren(it)
        } else {
            threadPoolExecutor.execute(TaskRunnable(application, task = it))
        }
    }
mainList.forEach {
if (it.isCompleted) {
            notifyChildren(it)
        } else {
            TaskRunnable(application, task = it).run()
        }
    }
countDownLatch?.await()
}
internal fun notifyChildren(task: Task) {
    taskChildMap[task.taskName]?.forEach {
taskMap[it.taskName]?.countdown()
    }
if (task.needAwait) {
        finishTask.incrementAndGet()
    }
    val taskSize = if (isPrivateAgree) {
        totalAwaitTaskSize.get()
    } else {
        noPrivateTask.sumBy { if (it.needAwait) 1 else 0 }
}
    if (finishTask.get() == taskSize) {
        countDownLatch?.countDown()
        executeMonitor.recordProjectFinish()
        onGetMonitorRecordCallback?.onGetProjectExecuteTime(executeMonitor.projectCostTime)
        onGetMonitorRecordCallback?.onGetTaskExecuteRecord(executeMonitor.executeTimeMap)
        listeners.forEach {
it.onProjectFinish()
        }
}
}

原理很简略,发动的时分会开启一个countLatch去堵塞住主线程,并当一切需求堵塞主线程的使命完结后铺开,并视为发动完毕。

3.3 : 事务模块主动注册

伴跟着项目的逐渐组件化,各个模块之间充分化耦。当咱们在各个module去界说好自己的初始化task后,存在一个严峻的问题。咱们需求在主application里面去感知收集到这些task,而且对之进行拓扑排序。

当然,咱们能够去一一依靠并手动创立new出来这些task并add到咱们的容器里,可是这样有一些严峻的耦合问题,而且会导致一些重复依靠bug。而且这样极不高雅且代码侵入性极强,当task一多,咱们要手写几十行的addTask代码,很不高雅 。

所以这儿参考了Arouter的处理计划。AutoRegister

AutoRegister很好很强壮,咱们想了解的能够去github上阅读源码,简略直白来说便是五个字 字节码插桩

运用autoRegister办法 ,自界说了一个AutoRegister接口

interface AutoRegister

然后将咱们自界说的发动task去完成AutoRegister接口,即可完结主动注册。

  • 3.4 : 进程办理规划

  • 不同发动使命运转的进程可能不一致,这儿是经过task的process字段操控。
interface ITask : ITaskCallBack {
    /**
* 初始化进程
*/
val process: RunProcess
}
sealed class RunProcess(val processNames: List<String>) {
    abstract fun check(application: Application, processName: String?): Boolean
    //仅主进程初始化
    object MAIN : RunProcess(emptyList( )) {
        override fun check(application: Application, processName: String?): Boolean {
            return application.packageName == processName
        }
    }
    //一切进程都初始化
    object ALL : RunProcess(emptyList( )) {
        override fun check(application: Application, processName: String?): Boolean {
            return true
        }
    }
    //非进程初始化
    object OTHER : RunProcess(emptyList( )) {
        override fun check(application: Application, processName: String?): Boolean {
            return application.packageName ! = processName
        }
    }
    //指定进程初始化
    class SPECIAL(processNames: List<String>) : RunProcess(processNames) {
        override fun check(application: Application, processName: String?): Boolean {
            return processName in processNames
        }
    }
}

望文生义 发动进程mode有四种,仅主进程初始化,仅非主进程初始化,一切进程都初始化,仅特定进程初始化。

当引进进程概念的时分又新增了一个问题,当时task和依靠的task不在同一个进程初始化,可能会导致反常。这儿在主动注册的时分现已判别好了,如果进程有反常会主动抛反常,咱们界说task的时分留意就好了。

3.5 : 非主动使命的处理

当时app大都有隐私合规的需求,当咱们初度冷发动app的时分不能一股脑悉数初始化,有些task需求用户赞同了隐私协议后才干初始化。

为了处理隐私合规的问题,在task中咱们供给了装备项

interface ITask {
    / ** * 是否需求赞同隐私协议后再履行 */   val needPrivateAgree: Boolean
}

Startup类中一起也供给了两个办法

object Startup {
    /**
    * 判别当时是否赞同隐私协议
    *  @param  condition 返回是否赞同隐私协议
    */
    fun privateAgreeCondition(condition: () -> Boolean) = apply {
    privateCondition = condition
    }
    /**
    * 当用户赞同隐私协议后 调用办法进行下一步sdk初始化
    */
    fun notifyPrivateAgree(application: Application) {
        val currentTaskList = noPrivateTask + needPrivateTasks
        executeTasks(application, currentTaskList)
    }
}

其中 privateAgreeCondition是装备办法,咱们需求在调用start办法之前装备好,当发动时会依据privateCondition的返回值去决定是否去发动那些需求赞同协议后才干初始化的task

notifyPrivateAgree是当用户赞同协议后去手动调用,去继续初始化下一步需求赞同协议的task

四 、上线作用与总结

在app内部新增发动剖析页面 把发动过程中的使命和耗时做了一个简略可视化页面,发动流程一目了然。

95分Android启动优化实践

一起在数据平台调查最新的上报数据

95分Android启动优化实践

能够看到发动过程中 Application的onCreate办法耗时下降挨近一倍,大幅提高用户发动时的体会,一起计划规划也保留了充分的拓展性,后续新增发动项时也能够快速高效的接入这套框架,确保发动作用不劣化。