95分Android启动优化实践 |
您所在的位置:网站首页 › io密集型和cpu密集型线程池的选用 › 95分Android启动优化实践 |
启动优化是Android优化老生常谈的问题了。众所周知,android的启动是指用户从点击 icon 到看到首帧可交互的流程。 而启动流程 粗略的可以分为以下几个阶段 fork创建出一个新的进程 创建初始化Application类、创建四大组件等 走Application.onCreate() 创建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 : 有向无环图
要把我们启动任务拆分为若干个小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 /** * 任务被执行的时候回调 */ 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 = 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 }dependsTaskList表示该task要等待这些task初始化完成后再完成,string是依赖task的taskName。通过字符串解耦。 这里简单看一下启动的流程,只看一些关键代码: step1 private fun executeTasks(application: Application, list: List) { //。。。 //这里是子线程任务 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执行完,直接进入页面。 流程如图
当然 这个task一定要是运行在子线程的啊。一个任务不能即运行在主线程又不阻塞主线程。 这里需要注意,当你的task的needAwait为false且runOnMainThread为true的时候,会直接报错, 太扯了。 而具体实现看代码 private fun executeTasks(application: Application, list: List) { 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) { 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) : 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内部新增启动分析页面 把启动过程中的任务和耗时做了一个简单可视化页面,启动流程一目了然。 同时在数据平台观察最新的上报数据 可以看到启动过程中 Application的onCreate方法耗时下降接近一倍,大幅提升用户启动时的体验,同时方案设计也保留了充分的拓展性,后续新增启动项时也可以快速高效的接入这套框架,保证启动效果不劣化。 |
今日新闻 |
推荐新闻 |
CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3 |