
前言
Android Startup提供一種在應(yīng)用啟動時能夠更加簡單、高效的方式來初始化組件。開發(fā)人員可以使用Android Startup來簡化啟動序列,并顯式地設(shè)置初始化順序與組件之間的依賴關(guān)系。 與此同時,Android Startup支持同步與異步等待、手動控制依賴執(zhí)行時機(jī),并通過有向無環(huán)圖拓?fù)渑判?/strong>的方式來保證內(nèi)部依賴組件的初始化順序。
Android Startup經(jīng)過幾輪的迭代已經(jīng)更加完善了,支持的功能場景也更加多樣,如果你要使用Android Startup的新特性,請將依賴升級到最新版本latest release
dependencies {
implementation 'com.rousetime.android:android-startup:latest release'
}
在之前的我為何棄用Jetpack的App Startup?文章中有提供一張與App Startup的對比圖,現(xiàn)在也有了一點變化
| 指標(biāo) | App Startup | Android Startup |
|---|---|---|
| 手動配置 | ? | ? |
| 自動配置 | ? | ? |
| 依賴支持 | ? | ? |
| 閉環(huán)處理 | ? | ? |
| 線程控制 | ? | ? |
| 異步等待 | ? | ? |
| 依賴回調(diào) | ? | ? |
| 手動通知 | ? | ? |
| 拓?fù)鋬?yōu)化 | ? | ? |
核心內(nèi)容都在這種對比圖中,下面根據(jù)這種對比圖來詳細(xì)分析一下Android Startup的實現(xiàn)原理。
配置
手動
手動配置是通過StartupManager.Builder()來實現(xiàn)的,本質(zhì)很簡單,使用builder模式來初始化一些必要的參數(shù),進(jìn)而來獲取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()
自動
另一種方式是自動配置,開發(fā)者不需要手動調(diào)用StartupManager.Builder(),只需在AndroidManifest.xml文件中進(jìn)行配置。
<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>
而實現(xiàn)這種配置的原理是:Android Startup內(nèi)部是通過一個ContentProvider來實現(xiàn)自動配置的,在Android中ContentProvider的初始化時機(jī)介于Application的attachBaseContext與onCreate之間。所以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.xml的provider標(biāo)簽下所配置的Startup與Config。
有關(guān)解析的部分都在StartupInitializer類中,通過它的discoverAndInitialize()方法就能獲取到解析的數(shù)據(jù)。
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)
}
核心邏輯是:
- 通過
ComponentName()獲取指定的StartupProvider - 通過
getProviderInfo()獲取對應(yīng)StartupProvider下的meta-data數(shù)據(jù) - 遍歷
meta-data數(shù)組 - 根據(jù)事先預(yù)定的
value來匹配對應(yīng)的name - 最終通過反射來獲取對應(yīng)
name的實例
其中在解析Statup的過程中,為了減少Statup的配置,使用doInitialize()方法來自動創(chuàng)建依賴的Startup,并且提前對循環(huán)依賴進(jìn)行檢查。
依賴支持
/**
* 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()進(jìn)行申明。申明之后會在后續(xù)進(jìn)行解析,保證依賴的組件優(yōu)先執(zhí)行完畢;同時依賴的組件執(zhí)行完畢會回調(diào)onDependenciesCompleted()方法。執(zhí)行順序則是通過有向圖拓?fù)渑判驔Q定的。
閉環(huán)處理
有關(guān)閉環(huán)的處理,一方面會在自動配置環(huán)節(jié)的doInitialize()方法中會進(jìn)行處理
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)
}
}
將當(dāng)前Startup加入到initialize中,同時遍歷dependencies()依賴數(shù)組,遞歸調(diào)用doInitialize()。
在遞歸的過程中,如果在initialize中存在對應(yīng)的uniqueKey(這里為Startup的唯一標(biāo)識)則代表發(fā)送的互相依賴,即存在依賴環(huán)。
另一方面,再后續(xù)的有向圖拓?fù)渑判騼?yōu)化也會進(jìn)行環(huán)處理
fun sort(startupList: List<Startup<*>>): StartupSortStore {
...
if (mainResult.size + ioResult.size != startupList.size) {
throw StartupException("lack of dependencies or have circle dependencies.")
}
}
在排序優(yōu)化過程中會將在主線程執(zhí)行與非主線程執(zhí)行的Startup進(jìn)行分類,再分類過程中并不會進(jìn)行排重處理,只關(guān)注當(dāng)前的Startup是否再主線程執(zhí)行。所以最后只要這兩種分類的大小之和不等于Startup的總和就代表存在環(huán),即有互相依賴。
線程處理
線程方面,使用的是StartupExecutor接口, 在AndroidStartup默認(rèn)實現(xiàn)了它的接口方法createExecutor()
override fun createExecutor(): Executor = ExecutorManager.instance.ioExecutor
在ExecutorManager中提供了三種線程分別為
-
cpuExecutor: cpu使用頻率高,高速計算;核心線程池的大小與cpu核數(shù)相關(guān)。 -
ioExecutor: io操作,網(wǎng)絡(luò)處理等;內(nèi)部使用緩存線程池。 -
mainExecutor: 主線程。
所以如果需要修改默認(rèn)線程,可以重寫createExecutor()方法。
異步等待
在上面的依賴支持部分已經(jīng)提到使用dependencies()來設(shè)置依賴的組件。每一個初始化組件能夠執(zhí)行的前提是它自身的依賴組件全部已經(jīng)執(zhí)行完畢。
如果是同步依賴,自然很簡單,只需要按照依賴的順序依次執(zhí)行即可。而對于異步依賴任務(wù),則需要保證所有的異步依賴任務(wù)完成,當(dāng)前組件才能正常執(zhí)行。
在Android Startup借助了CountDownLatch來保證異步依賴的執(zhí)行完成監(jiān)聽。
CountDownLatch字面意思就是倒計時鎖,它是作用于線程中,初始化時會設(shè)置一個count大小的倒計時,通過await()來等待倒計時的結(jié)束,只不過倒計時的數(shù)值減少是通過手動調(diào)用countDown()來觸發(fā)的。
所以在抽象類AndroidStartup中,通過await()與countDown()來保證異步任務(wù)的準(zhǔn)確執(zhí)行。
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()方法來等待依賴組件的執(zhí)行完畢,而依賴的組件任務(wù)執(zhí)行完畢之后,通過toNotify()來通知當(dāng)前組件,一旦所有的依賴執(zhí)行完畢之后,就會釋放當(dāng)前的線程,使它繼續(xù)執(zhí)行下去。
而toWait()與toNotify()的具體調(diào)用時機(jī)分別在StartupRunnable與StartupManagerDispatcher中執(zhí)行。
依賴回調(diào)
在依賴回調(diào)之前,先來認(rèn)識一個接口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的執(zhí)行邏輯,保證執(zhí)行前的準(zhǔn)備工作,執(zhí)行過程中的分發(fā)與執(zhí)行后的回調(diào)。所以依賴回調(diào)自然也在其中。
調(diào)用邏輯被封裝到notifyChildren()方法中。最終調(diào)用Startup的onDependenciesCompleted()方法。
所以我們可以在初始化組件中重寫onDependenciesCompleted()方法,從而拿到所依賴的組件完成后返回的結(jié)果。例如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?
}
}
當(dāng)然這是在當(dāng)前組件中獲取依賴組件的返回結(jié)果,Android Startup還提供了在任意時候來查詢?nèi)我饨M件的執(zhí)行狀況,并且支持獲取任意已經(jīng)完成的組件的返回結(jié)果。
Android Startup提供StartupCacheManager來實現(xiàn)這些功能。具體使用方式可以通過查看Sample來獲取。
手動通知
上面介紹了依賴回調(diào),它是自動調(diào)用依賴完成后的一系列操作。Android Startup也提供了手動通知依賴任務(wù)的完成。
手動通知的設(shè)置是通過manualDispatch()方法開啟。它將配合onDispatch()一起完成。
在ManagerDispatcher接口具體實現(xiàn)類的notifyChildren()方法中,如果開啟手動通知,就不會走自動通知流程,調(diào)用toNotify()方法,而是會將當(dāng)前組件的Dispatcher添加到注冊表中。等待onDispatche()的手動調(diào)用去喚醒toNotify()的執(zhí)行。
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()
}
}
}
...
}
具體實現(xiàn)示例可以查看SampleManualDispatchStartup
拓?fù)鋬?yōu)化
Android Startup中初始化組件與組件間的關(guān)系其實就是一張有向無環(huán)拓?fù)鋱D。
以Sample中的一個demo為例:

我們將每一個Startup的邊指向目標(biāo)為一個入度。根據(jù)這個規(guī)定很容易算出這四個Startup的入度
-
SampleFirstStartup: 0 -
SampleSecondStartup: 1 -
SampleThirdStartup: 2 -
SampleFourthStartup: 3
那么這個入度有什么用呢?根據(jù)由AOV網(wǎng)構(gòu)造拓?fù)湫蛄械耐負(fù)渑判蛩惴ㄖ饕茄h(huán)執(zhí)行以下兩步,直到不存在入度為0的頂點為止。
- 選擇一個入度為0的頂點并輸出之;
- 從網(wǎng)中刪除此頂點及所有出邊
循環(huán)結(jié)束后,若輸出的頂點數(shù)小于網(wǎng)中的頂點數(shù),則輸出“有回路”信息,否則輸出的頂點序列就是一種拓?fù)湫蛄小?/p>
根據(jù)上面的步驟,可以得出上面的四個Startup的輸出順序為
SampleFirstStartup -> SampleSecondStartup -> SampleThirdStartup -> SampleFourthStartup
以上的輸出順序也是初始化組件間的執(zhí)行順序。這樣即保證了依賴組件間的正常執(zhí)行,也保證了初始化組件的執(zhí)行順序的最優(yōu)解,即依賴組件間的等候時間最短,同時也檢查了依賴組件間是否存在環(huán)。
既然已經(jīng)有了方案與實現(xiàn)步驟,下面要做的就是用代碼實現(xiàn)出來。
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插樁,為用戶提供系統(tǒng)分析初始化的耗時詳細(xì)過程;初始化組件的準(zhǔn)確耗時收集統(tǒng)計,方便用戶下載與上傳到指定服務(wù)器等等。
Android Startup的核心功能分析暫時就到這里結(jié)束了,希望能夠?qū)δ阌兴鶐椭?/p>
當(dāng)然,本人真誠的邀請你加入Android Startup的建設(shè)中,如果你有什么好的建議也請不吝賜教。
項目
android_startup: 提供一種在應(yīng)用啟動時能夠更加簡單、高效的方式來初始化組件,優(yōu)化啟動速度。
AwesomeGithub: 基于Github的客戶端,純練習(xí)項目,支持組件化開發(fā),支持賬戶密碼與認(rèn)證登陸。使用Kotlin語言進(jìn)行開發(fā),項目架構(gòu)是基于JetPack&DataBinding的MVVM;項目中使用了Arouter、Retrofit、Coroutine、Glide、Dagger與Hilt等流行開源技術(shù)。
flutter_github: 基于Flutter的跨平臺版本Github客戶端。
android-api-analysis: 結(jié)合詳細(xì)的Demo來全面解析Android相關(guān)的知識點, 幫助讀者能夠更快的掌握與理解所闡述的要點。
daily_algorithm: 每日一算法,由淺入深,歡迎加入一起共勉。
為自己代言
WeChat Official Account:【Android補(bǔ)給站】