Android Startup實現分析

1_lDGK7qz9h3xQP_IUoZxGaQ.png

前言

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來實現自動配置的,在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中提供了三種線程分別爲

  1. cpuExecutor: cpu使用頻率高,高速計算;核心線程池的大小與cpu核數相關。
  2. ioExecutor: io操做,網絡處理等;內部使用緩存線程池。
  3. 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爲例:

android_startup_diagram.png

咱們將每個Startup的邊指向目標爲一個入度。根據這個規定很容易算出這四個Startup的入度

  1. SampleFirstStartup: 0
  2. SampleSecondStartup: 1
  3. SampleThirdStartup: 2
  4. SampleFourthStartup: 3

那麼這個入度有什麼用呢?根據由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補給站.jpg

相關文章
相關標籤/搜索