Android启动优化实践

这篇具有很好参考价值的文章主要介绍了Android启动优化实践。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

作者:95分技术

启动优化是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 : 有向无环图

Android启动优化实践

  • DAG,有向无环图,能够管理任务之间的依赖关系,并调度这些任务,似乎能够满足本节开始的诉求,那么我们先了解下这种数据结构。

  • 顶点:在DAG中,每个节点(sdk1/sdk2/sdk3…)都是一个顶点;

  • :连接每个节点的连接线;

  • 入度:每个节点依赖的节点数,形象来说就是有几根线连接到该节点,例如sdk2的入度是1,sdk5的入度是2。

  • 我们从图中可以看出,是有方向的,但是没有路径再次回到起点,因此就叫做有向无环图

  • 2.2 : 拓扑排序

  • 拓扑排序用于对节点的依赖关系进行排序,主要分为两种:DFS(深度优先遍历)(这也是我们的方案)、BFS(广度优先遍历),如果了解二叉树的话,对于这两种算法应该比较熟悉。

  • 我们就拿这张图来演示,拓扑排序算法的流程:

  • 1:首先找到图中,入度为0的顶点,那么这张图中入度为0的顶点就是task1,然后删除

Android启动优化实践

2:删除之后,再次找到入度为0的顶点,这个时候有两个入度为0的顶点,task2和task3,所以拓扑排序的结果不是唯一的!

Android启动优化实践

3:依次递归,直到删除全部入度为0的顶点,完成拓扑排序

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执行完,直接进入页面。

流程如图

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代码,很不优雅😄 。

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内部新增启动分析页面 把启动过程中的任务和耗时做了一个简单可视化页面,启动流程一目了然。

Android启动优化实践

同时在数据平台观察最新的上报数据

Android启动优化实践

可以看到启动过程中 Application的onCreate方法耗时下降接近一倍,大幅提升用户启动时的体验,同时方案设计也保留了充分的拓展性,后续新增启动项时也可以快速高效的接入这套框架,保证启动效果不劣化。

启动优化一直都是Android性能优化中最重要的一环,简称APP的门面担当。在app上线后,与用户接触的第一个功能就是APP的启动,它的启动时长直接就决定了用户后续是否会继续使用。在APP性能优化中除了启动优化很重要以外,其他的优化技术也很重要,像内存优化、卡顿优化、网络优化、安全优化……等等

为了帮助到大家更好的全面清晰的掌握好性能优化,准备了相关的学习路线以及核心笔记(还该底层逻辑):https://qr18.cn/FVlo89 大家可以进行参考学习:

性能优化核心笔记:https://qr18.cn/FVlo89

启动优化
Android启动优化实践
内存优化
Android启动优化实践
UI优化
Android启动优化实践
网络优化
Android启动优化实践
Bitmap优化与图片压缩优化https://qr18.cn/FVlo89
Android启动优化实践
多线程并发优化与数据传输效率优化

Android启动优化实践
体积包优化
Android启动优化实践

《Android 性能监控框架》:https://qr18.cn/FVlo89

Android启动优化实践

《Android Framework学习手册》:https://qr18.cn/AQpN4J

  1. 开机Init 进程
  2. 开机启动 Zygote 进程
  3. 开机启动 SystemServer 进程
  4. Binder 驱动
  5. AMS 的启动过程
  6. PMS 的启动过程
  7. Launcher 的启动过程
  8. Android 四大组件
  9. Android 系统服务 - Input 事件的分发过程
  10. Android 底层渲染 - 屏幕刷新机制源码分析
  11. Android 源码分析实战

Android启动优化实践文章来源地址https://www.toymoban.com/news/detail-485612.html

到了这里,关于Android启动优化实践的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Android 性能优化(六):启动优化的详细流程

    书接上文,Android 性能优化(一):闪退、卡顿、耗电、APK 从用户体验角度有四个性能优化方向: 追求稳定,防止崩溃 追求流畅,防止卡顿 追求续航,防止耗损 追求精简,防止臃肿 卡顿的场景通常与用户交互体验最直接,分别为UI、启动、跳转、响应四个方面,如下图所示

    2024年04月17日
    浏览(40)
  • Android 应用启动过程优化

    应用启动流程: 1、startActivity 交给AMS判断处理(Binder通信) 2、AMS匹配到对应的应用信息后通知zygote去fork进程(socket通信) 3、反射调用ActivityThreadd的main函数之后,将匿名binder(ApplicationThread)交由AMS,建立了app的binder通信基础。 4、AMS通知App进程(binder通信)去创建启动Activity(on

    2024年02月07日
    浏览(29)
  • Android启动流程优化 中篇

    本文链接:https://blog.csdn.net/feather_wch/article/details/131587046 1、我们可以优化部分 Application构建到主界面onWindowFocusChanged 2、启动方式(官方) 冷启动 热启动 温启动 3、怎么样算是卡顿? 卡顿:2-5-8原则 2秒以内:流程 2-5秒:可以接受 5-8秒:有些卡顿 8秒以上:非常卡顿,没办法接

    2024年02月12日
    浏览(38)
  • Android启动速度优化

    本节主要内容:了解APP启动流程、启动状态、查看启动时间、CPU Profile定位启动耗时代码、StrictMode严苛模式检测不合理写法、解决启动黑白屏问题。 一、APP启动流程 ①用户点击桌面App图标,Launcher进程采用Binder IPC向system_server进程发起startActivity请求; ②system_server进程接收到

    2024年02月15日
    浏览(27)
  • Android页面渲染效率优化实践

      1.车系页布局渲染现状  车系页是重要的车系信息页面,更新迭代多年,页面布局不断变化,xml布局文件越写越复杂。 获取车系页布局文件耗时: 结果如下:   2.卡顿的原因 2.1 Android绘制原理 ► 1.Android的屏幕刷新中涉及到最重要的三个概念 (1)CPU:执行应用层的measure、layo

    2023年04月15日
    浏览(33)
  • Android启动页的加载优化

    现在市面上的app都有个启动页广告或者闪屏广告的过渡页,启动页的作用无非就是2个: 启动页的作用主要有以下几个方面: 提示应用正在加载 :启动页可以作为一个视觉指示,告诉用户应用正在启动和加载。这样用户在等待应用启动的过程中可以得到一个明确的反馈,避免

    2024年02月12日
    浏览(33)
  • Android 启动页白屏优化

    转自:解决 Android APP 启动页白屏问题及如何实现全屏显示_android 启动白屏-CSDN博客 一、白屏原因分析     其实,白屏现象很容易理解,在冷启动一个 APP 的时候,启动页还没完成布局文件的加载,此时显示的是 Window 窗口背景,我们看到的白屏就是 Window 窗口背景。     Win

    2024年01月20日
    浏览(26)
  • 【Android】APP启动优化学习笔记

    用户体验: 应用的启动速度直接影响用户体验。用户希望应用能够快速启动并迅速响应他们的操作。如果应用启动较慢,用户可能会感到不满,并且有可能选择卸载或切换到竞争对手的应用。通过启动优化,可以提高应用的启动速度,让用户获得更好的使用体验。 竞争优势

    2024年02月14日
    浏览(28)
  • Android复杂UI的性能优化实践 - PTQBookPageView 性能优化记录

    作者:彭泰强 要做性能优化,首先得知道性能怎么度量、怎么表示。因为性能是一个很抽象的词,我们必须把它量化、可视化。那么,因为是UI组件优化,我首先选用了 GPU呈现模式分析 这一工具。 在手机上的开发者模式里可以开启 GPU呈现(渲染)模式分析 这一工具,有的

    2024年02月14日
    浏览(33)
  • 13.108.Spark 优化、Spark优化与hive的区别、SparkSQL启动参数调优、四川任务优化实践:执行效率提升50%以上

    13.108.Spark 优化 1.1.25.Spark优化与hive的区别 1.1.26.SparkSQL启动参数调优 1.1.27.四川任务优化实践:执行效率提升50%以上 1.1.25.Spark优化与hive的区别 先理解spark与mapreduce的本质区别,算子之间(map和reduce之间多了依赖关系判断,即宽依赖和窄依赖。) 优化的思路和hive基本一致,比较

    2024年02月10日
    浏览(35)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包