Android Startup实现分析

微信扫一扫,分享到朋友圈

Android Startup实现分析

前言

Android Startup 提供一种在应用启动时能够更加简单、高效的方式来初始化组件。开发人员可以使用 Android Startup 来简化启动序列,并显式地设置初始化顺序与组件之间的依赖关系。 与此同时, Android Startup 支持 同步与异步等待、手动控制依赖执行时机 ,并通过 有向无环图拓扑排序 的方式来保证内部依赖组件的初始化顺序。

Android Startup 经过几轮的迭代已经更加完善了,支持的功能场景也更加多样,如果你要使用 Android Startup 的新特性,请将依赖升级到最新版本 latest release

dependencies {
implementation 'com.rousetime.android:android-startup:latest release'
}

在之前的 我为何弃用Jetpack的App Startup? 文章中有提供一张与App Startup的对比图,现在也有了一点变化

指标 App Startup Android Startup
手动配置 :white_check_mark: :white_check_mark:
自动配置 :white_check_mark: :white_check_mark:
依赖支持 :white_check_mark: :white_check_mark:
闭环处理 :white_check_mark: :white_check_mark:
线程控制 :x: :white_check_mark:
异步等待 :x: :white_check_mark:
依赖回调 :x: :white_check_mark:
手动通知 :x: :white_check_mark:
拓扑优化 :x: :white_check_mark:

核心内容都在这种对比图中,下面根据这种对比图来详细分析一下Android Startup的实现原理。

配置

手动

手动配置是通过 StartupManager.Builder() 来实现的,本质很简单,使用 builder 模式来初始化一些必要的参数,进而来获取 StartupManager 实例,最后再启动 Android Startup

val config = StartupConfig.Builder()
.setLoggerLevel(LoggerLevel.DEBUG)
.setAwaitTimeout(12000L)
.setListener(object : StartupListener {
override fun onCompleted(totalMainThreadCostTime: Long, costTimesModels: List<CostTimesModel>) {
// can to do cost time statistics.
costTimesLiveData.value = costTimesModels
Log.d("StartupTrack", "onCompleted: ${costTimesModels.size}")
}
})
.build()
StartupManager.Builder()
.setConfig(config)
.addStartup(SampleFirstStartup())
.addStartup(SampleSecondStartup())
.addStartup(SampleThirdStartup())
.addStartup(SampleFourthStartup())
.build(this)
.start()
.await()

自动

另一种方式是自动配置,开发者不需要手动调用 StartupManager.Builder() ,只需在 AndroidManifest.xml 文件中进行配置。

<provider
android:name="com.rousetime.android_startup.provider.StartupProvider"
android:authorities="${applicationId}.android_startup"
android:exported="false">
<meta-data
android:name="com.rousetime.sample.startup.SampleStartupProviderConfig"
android:value="android.startup.provider.config" />
<meta-data
android:name="com.rousetime.sample.startup.SampleFourthStartup"
android:value="android.startup" />
</provider>

而实现这种配置的原理是: Android Startup 内部是通过一个 ContentProvider 来实现自动配置的,在 AndroidContentProvider 的初始化时机介于 ApplicationattachBaseContextonCreate 之间。所以 Android Startup 借助这一特性将初始化的逻辑都封装到自定义的 StartupProvider

class StartupProvider : ContentProvider() {
override fun onCreate(): Boolean {
context.takeIf { context -> context != null }?.let {
val store = StartupInitializer.instance.discoverAndInitialize(it)
StartupManager.Builder()
.setConfig(store.config?.getConfig())
.addAllStartup(store.result)
.build(it)
.start()
.await()
} ?: throw StartupException("Context cannot be null.")
return true
}
...
...
}

有了 StartupProvider 之后,下一步需要做的就是解析在 AndroidManife.xmlprovider 标签下所配置的 StartupConfig

有关解析的部分都在 StartupInitializer 类中,通过它的 discoverAndInitialize() 方法就能获取到解析的数据。

internal fun discoverAndInitialize(context: Context): StartupProviderStore {
TraceCompat.beginSection(StartupInitializer::class.java.simpleName)
val result = mutableListOf<AndroidStartup<*>>()
val initialize = mutableListOf<String>()
val initialized = mutableListOf<String>()
var config: StartupProviderConfig? = null
try {
val provider = ComponentName(context.packageName, StartupProvider::class.java.name)
val providerInfo = context.packageManager.getProviderInfo(provider, PackageManager.GET_META_DATA)
val startup = context.getString(R.string.android_startup)
val providerConfig = context.getString(R.string.android_startup_provider_config)
providerInfo.metaData?.let { metaData ->
metaData.keySet().forEach { key ->
val value = metaData[key]
val clazz = Class.forName(key)
if (startup == value) {
if (AndroidStartup::class.java.isAssignableFrom(clazz)) {
doInitialize((clazz.getDeclaredConstructor().newInstance() as AndroidStartup<*>), result, initialize, initialized)
}
} else if (providerConfig == value) {
if (StartupProviderConfig::class.java.isAssignableFrom(clazz)) {
config = clazz.getDeclaredConstructor().newInstance() as? StartupProviderConfig
// save initialized config
StartupCacheManager.instance.saveConfig(config?.getConfig())
}
}
}
}
} catch (t: Throwable) {
throw StartupException(t)
}
TraceCompat.endSection()
return StartupProviderStore(result, config)
}

核心逻辑是:

  1. 通过 ComponentName() 获取指定的 StartupProvider
  2. 通过 getProviderInfo() 获取对应 StartupProvider 下的 meta-data 数据
  3. 遍历 meta-data 数组
  4. 根据事先预定的 value 来匹配对应的 name
  5. 最终通过反射来获取对应 name 的实例

其中在解析 Statup 的过程中,为了减少 Statup 的配置,使用 doInitialize() 方法来自动创建依赖的 Startup ,并且提前对循环依赖进行检查。

依赖支持

/**
* Returns a list of the other [Startup] objects that the initializer depends on.
*/
fun dependencies(): List<Class<out Startup<*>>>?
/**
* Called whenever there is a dependency completion.
*
* @param [startup] dependencies [startup].
* @param [result] of dependencies startup.
*/
fun onDependenciesCompleted(startup: Startup<*>, result: Any?)

某个初始化的组件在初始化之前所依赖的组件都必须通过 dependencies() 进行申明。申明之后会在后续进行解析,保证依赖的组件优先执行完毕;同时依赖的组件执行完毕会回调 onDependenciesCompleted() 方法。执行顺序则是通过有向图拓扑排序决定的。

闭环处理

有关闭环的处理,一方面会在自动配置环节的 doInitialize() 方法中会进行处理

private fun doInitialize(
startup: AndroidStartup<*>,
result: MutableList<AndroidStartup<*>>,
initialize: MutableList<String>,
initialized: MutableList<String>
) {
try {
val uniqueKey = startup::class.java.getUniqueKey()
if (initialize.contains(uniqueKey)) {
throw IllegalStateException("have circle dependencies.")
}
if (!initialized.contains(uniqueKey)) {
initialize.add(uniqueKey)
result.add(startup)
startup.dependencies()?.forEach {
doInitialize(it.getDeclaredConstructor().newInstance() as AndroidStartup<*>, result, initialize, initialized)
}
initialize.remove(uniqueKey)
initialized.add(uniqueKey)
}
} catch (t: Throwable) {
throw StartupException(t)
}
}

将当前 Startup 加入到 initialize 中,同时遍历 dependencies() 依赖数组,递归调用 doInitialize()

在递归的过程中,如果在 initialize 中存在对应的 uniqueKey (这里为Startup的唯一标识)则代表发送的互相依赖,即存在依赖环。

另一方面,再后续的有向图拓扑排序优化也会进行环处理

fun sort(startupList: List<Startup<*>>): StartupSortStore {
...
if (mainResult.size + ioResult.size != startupList.size) {
throw StartupException("lack of dependencies or have circle dependencies.")
}
}

在排序优化过程中会将在主线程执行与非主线程执行的 Startup 进行分类,再分类过程中并不会进行排重处理,只关注当前的 Startup 是否再主线程执行。所以最后只要这两种分类的大小之和不等于 Startup 的总和就代表存在环,即有互相依赖。

线程处理

线程方面,使用的是 StartupExecutor 接口, 在 AndroidStartup 默认实现了它的接口方法 createExecutor()

override fun createExecutor(): Executor = ExecutorManager.instance.ioExecutor

ExecutorManager 中提供了三种线程分别为

cpuExecutor
ioExecutor
mainExecutor

所以如果需要修改默认线程,可以重写 createExecutor() 方法。

异步等待

在上面的依赖支持部分已经提到使用 dependencies() 来设置依赖的组件。每一个初始化组件能够执行的前提是它自身的依赖组件全部已经执行完毕。

如果是同步依赖,自然很简单,只需要按照依赖的顺序依次执行即可。而对于异步依赖任务,则需要保证所有的异步依赖任务完成,当前组件才能正常执行。

Android Startup 借助了 CountDownLatch 来保证异步依赖的执行完成监听。

CountDownLatch 字面意思就是倒计时锁,它是作用于线程中,初始化时会设置一个 count 大小的倒计时,通过 await() 来等待倒计时的结束,只不过倒计时的数值减少是通过手动调用 countDown() 来触发的。

所以在抽象类 AndroidStartup 中,通过 await()countDown() 来保证异步任务的准确执行。

abstract class AndroidStartup<T> : Startup<T> {
private val mWaitCountDown by lazy { CountDownLatch(dependencies()?.size ?: 0) }
private val mObservers by lazy { mutableListOf<Dispatcher>() }
override fun toWait() {
try {
mWaitCountDown.await()
} catch (e: InterruptedException) {
e.printStackTrace()
}
}
override fun toNotify() {
mWaitCountDown.countDown()
}
...
}

我们通过 toWait() 方法来等待依赖组件的执行完毕,而依赖的组件任务执行完毕之后,通过 toNotify() 来通知当前组件,一旦所有的依赖执行完毕之后,就会释放当前的线程,使它继续执行下去。

toWait()toNotify() 的具体调用时机分别在 StartupRunnableStartupManagerDispatcher 中执行。

依赖回调

在依赖回调之前,先来认识一个接口 ManagerDispatcher

interface ManagerDispatcher {
/**
* dispatch prepare
*/
fun prepare()
/**
* dispatch startup to executing.
*/
fun dispatch(startup: Startup<*>, sortStore: StartupSortStore)
/**
* notify children when dependency startup completed.
*/
fun notifyChildren(dependencyParent: Startup<*>, result: Any?, sortStore: StartupSortStore)
}

ManagerDispatcher 中有三个接口方法,分别用来管理 Startup 的执行逻辑,保证执行前的准备工作,执行过程中的分发与执行后的回调。所以依赖回调自然也在其中。

调用逻辑被封装到 notifyChildren() 方法中。最终调用 StartuponDependenciesCompleted() 方法。

所以我们可以在初始化组件中重写 onDependenciesCompleted() 方法,从而拿到所依赖的组件完成后返回的结果。例如 Sample 中的 SampleSyncFourStartup

class SampleSyncFourStartup: AndroidStartup<String>() {
private var mResult: String? = null
override fun create(context: Context): String? {
return "$mResult + sync four"
}
override fun callCreateOnMainThread(): Boolean = true
override fun waitOnMainThread(): Boolean = false
override fun dependencies(): List<Class<out Startup<*>>>? {
return listOf(SampleAsyncTwoStartup::class.java)
}
override fun onDependenciesCompleted(startup: Startup<*>, result: Any?) {
mResult = result as? String?
}
}

当然这是在当前组件中获取依赖组件的返回结果, Android Startup 还提供了在任意时候来查询任意组件的执行状况,并且支持获取任意已经完成的组件的返回结果。

Android Startup 提供 StartupCacheManager 来实现这些功能。具体使用方式可以通过查看 Sample 来获取。

手动通知

上面介绍了依赖回调,它是自动调用依赖完成后的一系列操作。 Android Startup 也提供了手动通知依赖任务的完成。

手动通知的设置是通过 manualDispatch() 方法开启。它将配合 onDispatch() 一起完成。

ManagerDispatcher 接口具体实现类的 notifyChildren() 方法中,如果开启手动通知,就不会走自动通知流程,调用 toNotify() 方法,而是会将当前组件的 Dispatcher 添加到注册表中。等待 onDispatche() 的手动调用去唤醒 toNotify() 的执行。

override fun notifyChildren(dependencyParent: Startup<*>, result: Any?, sortStore: StartupSortStore) {
// immediately notify main thread,Unblock the main thread.
if (dependencyParent.waitOnMainThread()) {
needAwaitCount.incrementAndGet()
awaitCountDownLatch?.countDown()
}
sortStore.startupChildrenMap[dependencyParent::class.java.getUniqueKey()]?.forEach {
sortStore.startupMap[it]?.run {
onDependenciesCompleted(dependencyParent, result)
if (dependencyParent.manualDispatch()) {
dependencyParent.registerDispatcher(this)
} else {
toNotify()
}
}
}
...
}

具体实现示例可以查看 SampleManualDispatchStartup

拓扑优化

Android Startup 中初始化组件与组件间的关系其实就是一张 有向无环拓扑图

Sample 中的一个demo为例:

我们将每一个 Startup 的边指向目标为一个入度。根据这个规定很容易算出这四个 Startup 的入度

SampleFirstStartup
SampleSecondStartup
SampleThirdStartup
SampleFourthStartup

那么这个入度有什么用呢?根据由AOV网构造拓扑序列的拓扑排序算法主要是循环执行以下两步,直到不存在入度为0的顶点为止。

  1. 选择一个入度为0的顶点并输出之;
  2. 从网中删除此顶点及所有出边

循环结束后,若输出的顶点数小于网中的顶点数,则输出“有回路”信息,否则输出的顶点序列就是一种拓扑序列。

根据上面的步骤,可以得出上面的四个 Startup 的输出顺序为

SampleFirstStartup -> SampleSecondStartup -> SampleThirdStartup -> SampleFourthStartup

以上的输出顺序也是初始化组件间的执行顺序。这样即保证了依赖组件间的正常执行,也保证了初始化组件的执行顺序的最优解,即依赖组件间的等候时间最短,同时也检查了依赖组件间是否存在环。

既然已经有了方案与实现步骤,下面要做的就是用代码实现出来。

fun sort(startupList: List<Startup<*>>): StartupSortStore {
TraceCompat.beginSection(TopologySort::class.java.simpleName)
val mainResult = mutableListOf<Startup<*>>()
val ioResult = mutableListOf<Startup<*>>()
val temp = mutableListOf<Startup<*>>()
val startupMap = hashMapOf<String, Startup<*>>()
val zeroDeque = ArrayDeque<String>()
val startupChildrenMap = hashMapOf<String, MutableList<String>>()
val inDegreeMap = hashMapOf<String, Int>()
startupList.forEach {
val uniqueKey = it::class.java.getUniqueKey()
if (!startupMap.containsKey(uniqueKey)) {
startupMap[uniqueKey] = it
// save in-degree
inDegreeMap[uniqueKey] = it.dependencies()?.size ?: 0
if (it.dependencies().isNullOrEmpty()) {
zeroDeque.offer(uniqueKey)
} else {
// add key parent, value list children
it.dependencies()?.forEach { parent ->
val parentUniqueKey = parent.getUniqueKey()
if (startupChildrenMap[parentUniqueKey] == null) {
startupChildrenMap[parentUniqueKey] = arrayListOf()
}
startupChildrenMap[parentUniqueKey]?.add(uniqueKey)
}
}
} else {
throw StartupException("$it multiple add.")
}
}
while (!zeroDeque.isEmpty()) {
zeroDeque.poll()?.let {
startupMap[it]?.let { androidStartup ->
temp.add(androidStartup)
// add zero in-degree to result list
if (androidStartup.callCreateOnMainThread()) {
mainResult.add(androidStartup)
} else {
ioResult.add(androidStartup)
}
}
startupChildrenMap[it]?.forEach { children ->
inDegreeMap[children] = inDegreeMap[children]?.minus(1) ?: 0
// add zero in-degree to deque
if (inDegreeMap[children] == 0) {
zeroDeque.offer(children)
}
}
}
}
if (mainResult.size + ioResult.size != startupList.size) {
throw StartupException("lack of dependencies or have circle dependencies.")
}
val result = mutableListOf<Startup<*>>().apply {
addAll(ioResult)
addAll(mainResult)
}
printResult(temp)
TraceCompat.endSection()
return StartupSortStore(
result,
startupMap,
startupChildrenMap
)
}

有了上面的步骤,相信这段代码都能够理解。

除了上面所介绍的功能, Android Startup 还支持 Systrace 插桩,为用户提供系统分析初始化的耗时详细过程;初始化组件的准确耗时收集统计,方便用户下载与上传到指定服务器等等。

Android Startup 的核心功能分析暂时就到这里结束了,希望能够对你有所帮助。

当然,本人真诚的邀请你加入 Android Startup 的建设中,如果你有什么好的建议也请不吝赐教。

项目

android_startup : 提供一种在应用启动时能够更加简单、高效的方式来初始化组件,优化启动速度。

AwesomeGithub : 基于Github的客户端,纯练习项目,支持组件化开发,支持账户密码与认证登陆。使用Kotlin语言进行开发,项目架构是基于JetPack&DataBinding的MVVM;项目中使用了Arouter、Retrofit、Coroutine、Glide、Dagger与Hilt等流行开源技术。

flutter_github : 基于Flutter的跨平台版本Github客户端。

android-api-analysis : 结合详细的Demo来全面解析Android相关的知识点, 帮助读者能够更快的掌握与理解所阐述的要点。

daily_algorithm : 每日一算法,由浅入深,欢迎加入一起共勉。

为自己代言

微信公众号:【Android补给站】或者扫描下方二维码进行关注

微信扫一扫,分享到朋友圈

Android Startup实现分析

科学家设计出FIT系统:能通过足迹照追踪黑犀牛

上一篇

WSL2 支持从Windows 挂载物理磁盘,Windows 可直接访问 ext4

下一篇

你也可能喜欢

Android Startup实现分析

长按储存图像,分享给朋友