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 : 有向无环图

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

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

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

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

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

  • 2.2 : 拓扑排序

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

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

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

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

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

三、技术方案

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: Booleanget() = completed.get()/ ** * 默认运行在主进程 */   override val process: RunProcessget() = 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() {}@CallSuperoverride fun onStart() {completed.set(false)}@CallSuperoverride 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执行完,直接进入页面。

流程如图

这里在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 + needPrivateTasksexecuteTasks(application, currentTaskList)}
}

其中 privateAgreeCondition是配置方法,我们需要在调用start方法之前配置好,当启动时会根据privateCondition的返回值去决定是否去启动那些需要同意协议后才能初始化的task

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

四 、上线效果与总结

在app内部新增启动分析页面 把启动过程中的任务和耗时做了一个简单可视化页面,启动流程一目了然。

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

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

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

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

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

启动优化

内存优化

UI优化

网络优化

Bitmap优化与图片压缩优化https://qr18.cn/FVlo89

多线程并发优化与数据传输效率优化


体积包优化

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

《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 源码分析实战

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/3991.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

libevent实践06:监听TCP服务器

简介 函数evconnlistener_new_bind struct evconnlistener * evconnlistener_new_bind(struct event_base *base, evconnlistener_cb cb,void *ptr, unsigned flags, int backlog, const struct sockaddr *sa,int socklen) 参数解析&#xff1a; base&#xff1a;事件集合 ev…

Llama大模型运行的消费级硬件要求【CPU|GPU|RAM|SSD】

大型语言模型 (LLM) 是强大的工具&#xff0c;可以为各种任务和领域生成自然语言文本。 最先进的LLM之一是 LLaMA&#xff08;大型语言模型 Meta AI&#xff09;&#xff0c;这是由 Facebook 的研究部门 Meta AI 开发的一个包含 650 亿个参数的模型 要在家运行 LLaMA 模型&…

MySQL数据库优化技术一

纵论 对mysql优化时一个综合性的技术&#xff0c;主要包括 表的设计合理化(符合3NF)添加适当索引(index) [ 四种: 普通索引、主键索引、唯一索引unique、全文索引 ]分表技术( 水平分割、垂直分割 ) 水平分割根据一个标准重复定义几个字段值相同&#xff0c;表名称不同的表&…

CSDN-AI小组2023-半年-研发总结

目录 1.丐版「大模型」&#xff0c;Proof of concept2. LLM和AIGC的各种综述3. 基于Embedding的应用&#xff0c;问答&#xff0c;AI编程4. 评论区的AI助手5. 结合AIGC的各种数据自动计算6. 个性化推荐的系统重构7. 基于AIGC的个性化博客创作鼓励8. 博客质量分V5: 可解释性计算…

Windows 下后台启动 jar 包,UTF-8 启动 jar 包

目录 1. Windows 下启动 jar 包2. 设置 cmd 编码3. UTF-8 编码启动 jar 包 1. Windows 下启动 jar 包 小贴士&#xff1a;打包的时候把 application.yml 所有内容都注释掉&#xff0c;然后打包&#xff0c;再把 application.yml 与打好的 jar 包放在同级目录下&#xff0c;如图…

Redis概述及安装、使用和管理

文章目录 一、NoSQL非关系型数据库1.NoSQL概述2.关系型数据库和非关系型数据库区别&#xff08;1&#xff09;数据存储方式不同&#xff08;2&#xff09;扩展方式不同&#xff08;3&#xff09;对事务性的支持不同 3.非关系型数据库使用场景 二、Redis概述1.简介2.优点3.Redis…

go读写文件总结

别人的经验&#xff1a; 如今任何计算机系统每天都会产生大量的日志或数据。随着系统的增长&#xff0c;将调试数据存储到数据库中是不可行的&#xff0c;因为它们是不可变的&#xff0c;主要用于分析和解决故障的目的。因此&#xff0c;企业倾向于将其存储在文件中&#xff0…

使用conda虚拟环境,Jupyter Notebook 链接不上 kernel

1&#xff0c;检查 ipykernel 和 ipython 是否一致 输入pip list 或者conda list检查一下相应库的版本是不一致 不一致的话&#xff0c;可以更新这两个库的版本&#xff1a;pip install --upgrade 库名 2&#xff0c;看控制台的报错&#xff0c;如果是报404&#xff0c;内核找不…

【Linux】软硬链接与动静态库

系列文章 收录于【Linux】文件系统 专栏 关于文件描述符与文件重定向的相关内容可以移步 文件描述符与重定向操作。 可以到 浅谈文件原理与操作 了解文件操作的系统接口。 想进一步理解文件系统还可以看看文件缓冲区和文件系统。 目录 系列文章 软硬链接 软链接 硬链接…

vue(脚手架创建)代理解决跨域问题

目录 为什么会出现跨域问题 什么是跨域 Vue CLI Vue2解决跨域问题 不重写路径 重写路径 vue.config.js代码 Vue3解决跨域问题 ViteVue解决跨域问题 vite.config.ts代码 总结 为什么会出现跨域问题 出于浏览器的同源策略的限制。同源策略是一种约定&#xff0c;它是…

Linux网络环境配置

第一种方式&#xff08;自动获取&#xff09;&#xff1a; 说明&#xff1a;登陆后&#xff0c;通过界面的来设置自动获取IP 特点&#xff1a;Linux启动后会自动获取IP 缺点&#xff1a;是每次自动获取的IP地址可能不一样 第二种方法&#xff08;指定IP)&#xff1a; 1、说明…

科技资讯|2023Q1中国电动汽车销量增长 29%,充电桩市场持续增长

根据市场调查机构公布的 2023 年第 1 季度中国国内电动汽车市场报告&#xff0c;比亚迪继续引领竞争日益激烈的电动汽车市场。 报告称 2023 年第 1 季度中国乘用电动汽车销量同比增长 29%&#xff0c;其中纯电动汽车&#xff08;BEV&#xff09;占销售额的近 70%、插电式混合…