Android Startup提供一種在應用啓動時可以更加簡單、高效的方式來初始化組件。開發人員可使用Android Startup
來簡化啓動序列,並顯式地設置初始化順序與組件之間的依賴關係。 與此同時,Android Startup
支持同步與異步等待、手動控制依賴執行時機,並經過有向無環圖拓撲排序的方式來保證內部依賴組件的初始化順序。java
Android Startup
通過幾輪的迭代已經更加完善了,支持的功能場景也更加多樣,若是你要使用Android Startup
的新特性,請將依賴升級到最新版本latest releaseandroid
dependencies { implementation 'com.rousetime.android:android-startup:latest release' }
在以前的我爲什麼棄用Jetpack的App Startup?文章中有提供一張與App Startup的對比圖,如今也有了一點變化git
指標 | App Startup | Android Startup |
---|---|---|
手動配置 | ✅ | ✅ |
自動配置 | ✅ | ✅ |
依賴支持 | ✅ | ✅ |
閉環處理 | ✅ | ✅ |
線程控制 | ❌ | ✅ |
異步等待 | ❌ | ✅ |
依賴回調 | ❌ | ✅ |
手動通知 | ❌ | ✅ |
拓撲優化 | ❌ | ✅ |
核心內容都在這種對比圖中,下面根據這種對比圖來詳細分析一下Android Startup的實現原理。github
手動配置是經過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
文件中進行配置。api
<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
來實現自動配置的,在Android
中ContentProvider
的初始化時機介於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
標籤下所配置的Startup
與Config
。緩存
有關解析的部分都在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) }
核心邏輯是:微信
ComponentName()
獲取指定的StartupProvider
getProviderInfo()
獲取對應StartupProvider
下的meta-data
數據meta-data
數組value
來匹配對應的name
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
: cpu使用頻率高,高速計算;核心線程池的大小與cpu核數相關。ioExecutor
: io操做,網絡處理等;內部使用緩存線程池。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()
的具體調用時機分別在StartupRunnable與StartupManagerDispatcher中執行。
在依賴回調以前,先來認識一個接口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()
方法中。最終調用Startup
的onDependenciesCompleted()
方法。
因此咱們能夠在初始化組件中重寫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
: 0SampleSecondStartup
: 1SampleThirdStartup
: 2SampleFourthStartup
: 3那麼這個入度有什麼用呢?根據由AOV網構造拓撲序列的拓撲排序算法主要是循環執行如下兩步,直到不存在入度爲0的頂點爲止。
循環結束後,若輸出的頂點數小於網中的頂點數,則輸出「有迴路」信息,不然輸出的頂點序列就是一種拓撲序列。
根據上面的步驟,能夠得出上面的四個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補給站】或者掃描下方二維碼進行關注