Android 應(yīng)用啟動性能 | 延遲初始化

image

上一篇文章 中荆几,我展示了 content provider (它出現(xiàn)在應(yīng)用合并后的 manifest 文件) 是如何在應(yīng)用啟動的時候自動加載第三方庫以及模塊的储藐。

在這篇文章中,我會介紹如何使用 AndroidX 的 應(yīng)用啟動 (App Startup) 庫來進(jìn)一步控制那些庫該在何時以及以何種方式被加載缭乘。也許沐序,我是說也許,我們也會順便發(fā)現(xiàn)該如何縮短應(yīng)用的啟動時間堕绩。

使用應(yīng)用啟動庫自動初始化

使用應(yīng)用啟動庫 (App Startup) 最簡單的方式是利用它的 content provider 在后臺初始化其他庫策幼。您既可以指定應(yīng)用啟動庫該如何初始化其他的庫,也可以從合并后的 manifest 文件中移除其他庫的 content provider奴紧。避免使用多個 content provider 執(zhí)行啟動任務(wù)特姐,而是將資源用于加載應(yīng)用啟動庫,然后再加載其他內(nèi)容黍氮。

您可以通過如下三步實現(xiàn)上述操作唐含,首先在您工程的 build.gradle 文件中添加應(yīng)用啟動庫作為依賴,其次為每一個需要初始化的庫創(chuàng)建一個 Initializer沫浆,最后在您工程的 Manifest.xml 文件中添加相關(guān)信息捷枯。

讓我們再看一遍我在 第一篇文章 中使用的 WorkManager 示例。為了通過應(yīng)用啟動庫加載 WorkManager专执,我先在應(yīng)用的 build.gradle 文件中添加了應(yīng)用啟動庫:

// 查看最新的版本號 https://developer.android.google.cn/jetpack/androidx/releases/startup
def startup_version = "1.0.0"
implementation “androidx.startup:startup-runtime:$startup_version”

然后淮捆,基于應(yīng)用啟動庫提供的 Initializer 接口,我創(chuàng)建了一個 Initializer:

class MyWorkManagerInitializer : Initializer<WorkManager> {
    override fun create(context: Context): WorkManager {
        val configuration = Configuration.Builder().build()
        WorkManager.initialize(context, configuration)
        return WorkManager.getInstance(context)
    }

    override fun dependencies(): List<Class<out Initializer<*>>> {
        // 沒有其他依賴庫
        return emptyList()
    }
}

每一個 Initializer 有兩個方法需要復(fù)寫: create()dependencies()dependencies() 被用來指定多個依賴庫的初始化順序攀痊。在這個示例中我并不需要這個功能桐腌,因為我只需要處理 WorkManager。如果您需要在應(yīng)用中使用多個庫苟径,請查看 應(yīng)用啟動使用手冊 中關(guān)于使用 dependencies() 的詳情案站。

對于 create() 方法,我模仿了 WorkManager’s content provider 中的實現(xiàn)棘街。

順便說一下蟆盐,其實這個方法在使用應(yīng)用啟動庫的時候很常用。一個庫的 content provider 負(fù)責(zé)了其初始化的實現(xiàn)蹬碧,所以您通常都可以參考那個類中的代碼來手動實現(xiàn)它舱禽。有些庫可能比較麻煩,因為它們使用了隱藏的或者內(nèi)部的 API恩沽,但是好在 WorkManager 并不是誊稚,所以我可以這么做,希望該方法也適用于您的情況罗心。

最后里伯,我在 Manifest.xml 文件的 <application> 代碼塊中添加了兩個 provider 的標(biāo)簽。第一個如下所示:

<provider
    android:name="androidx.work.impl.WorkManagerInitializer"
    android:authorities="${applicationId}.workmanager-init"
    android:exported="false"
    tools:node="remove" />

WorkManagerInitializer 標(biāo)簽很重要渤闷,因為它表示需要 Android Studio 刪除自動生成的 provider疾瓮,而該 provider 是在 build.gradle 文件中添加 WorkManager 后生成的。如果沒有這個特殊的標(biāo)簽飒箭,這個庫仍然會在應(yīng)用啟動的時候自動初始化狼电,繼而在應(yīng)用啟動庫嘗試初始化它的時候報錯,因為它已經(jīng)被初始化了弦蹂。

下面是我添加的第二個 provider 標(biāo)簽:

<provider
    android:name="androidx.startup.InitializationProvider"
    android:authorities="${applicationId}.androidx-startup"
    android:exported="false"
    tools:node="merge">
    <meta-data     android:name="com.example.startuplibtest.MyWorkManagerInitializer"
    android:value="androidx.startup" />
</provider>

InitializationProvider 標(biāo)簽和通過添加應(yīng)用啟動庫到 build.gradle 文件中自動生成的標(biāo)簽基本相同 (您可以通過查看合并后的 manifest 文件來驗證 -- 詳情請查看 第一篇文章)肩碟,但是它們有兩個很重要的不同點:

tools:node="merge"

這個參數(shù)主要用于 Android Studio 所負(fù)責(zé)的 manifest 合并操作。它告訴工具在最終合并的 manifest 文件中合并這個標(biāo)簽的多個實例凸椿。在這個例子中削祈,它會合并由庫依賴自動生成的 <provider> 到這個版本的 provider,這樣在最終合并的 manifest 文件中只會有這一個標(biāo)簽實例脑漫。

另一行包含了這個 meta-data:

<meta-data  android:name="com.example.startuplibtest.MyWorkManagerInitializer"
    android:value="androidx.startup" />

這個 provider 中的 metadata 標(biāo)簽告訴應(yīng)用啟動庫如何找到您的 Initializer 代碼髓抑,這些代碼會在應(yīng)用啟動的時候執(zhí)行來初始化這個庫。請注意這導(dǎo)致的區(qū)別: 如果您沒有使用應(yīng)用啟動庫优幸,就會自動執(zhí)行相關(guān)初始化吨拍,因為 Android 會在那個庫中創(chuàng)建并執(zhí)行 content provider,之后會自動初始化這個庫本身网杆。但是通過應(yīng)用啟動庫指定您的 Initializer羹饰,以及在合并 manifest 文件中去除 WorkManager 的 provider握爷,相當(dāng)于告訴 Android 轉(zhuǎn)而使用應(yīng)用啟動庫的 content provider 來加載 WorkManager 庫。如果通過這個方式初始化多個庫严里,您可以利用應(yīng)用啟動庫的這個單獨的 content provider 有效地管理這些請求,而不是導(dǎo)致每個庫都創(chuàng)建自己的 content provider追城。

偷個懶...如果您想的話

當(dāng)優(yōu)化應(yīng)用啟動性能的時候刹碾,我們不能改變那些無法控制的代碼實現(xiàn)。所以這里的思路并不是加速我們使用庫的初始化座柱,而是控制這些庫什么時候以及如何被初始化迷帜。尤其是我們可以決定任一個庫是否需要在應(yīng)用啟動的時候被初始化 (要么使用庫的默認(rèn)機(jī)制添加 content provider 到合并的 manifest 文件,或者也可以利用應(yīng)用啟動庫的 content provider 來集中管理初始化請求)色洞,還是需要稍候再加載它們戏锹。

舉個例子,或許在您應(yīng)用的一個特殊的流程中需要某一個包含 content provider 初始化的庫火诸,但是這個庫并不需要在應(yīng)用啟動的時候立即被加載锦针,又或者在某些情況下它根本不需要被加載。如果是這樣的話置蜀,為什么要因為只在某個特殊代碼路徑中需要而在應(yīng)用啟動時花時間初始化一個很大的庫呢奈搜?為什么不等到這個庫真正被需要的時候再引入相關(guān)的初始化開銷呢?

這正是應(yīng)用啟動庫高明的地方盯荤,它能幫您從合并的 manifest 文件中和應(yīng)用啟動的過程中移除隱藏的 content provider馋吗,也能幫您延遲或者更有目的地加載這些庫。

使用應(yīng)用啟動庫實現(xiàn)延遲初始化

現(xiàn)在我們已經(jīng)知道該如何使用應(yīng)用啟動庫實現(xiàn)自動加載以及初始化庫秋秤。接下來讓我們更進(jìn)一步地來看看宏粤,如果您不想在啟動的時候初始化,該如何實現(xiàn)延遲初始化灼卢。

其實上面的代碼已經(jīng)很接近了绍哎,在 build.gradle 文件中您需要同樣的啟動依賴和其他您想使用的庫,也還是需要特殊的 "移除" provider 標(biāo)簽來去除每個庫自動生成的 content provider芥玉。我們只需要向 manifest 文件添加多一點信息來告訴它同樣移除應(yīng)用啟動庫的 provider蛇摸。這樣在應(yīng)用啟動的時候就不會有任何 content provider 初始化發(fā)生,而完全由您來決定什么時候應(yīng)該觸發(fā)相關(guān)初始化灿巧。

為了達(dá)到這個目的赶袄,我用下面的代碼替換了前面使用的 InitializationProvider。上面所展示的代碼告訴了系統(tǒng)該如何定位 content provider 中自動初始化您庫的代碼抠藕。因為稍后要手動觸發(fā)初始化饿肺,這一次我要跳過那個部分,而只留下在應(yīng)用啟動的時候去除自動生成的 content provider 的部分盾似。

<provider
    android:name="androidx.startup.InitializationProvider"
    android:authorities="${applicationId}.androidx-startup"
    tools:node="remove" />

在我做了這個改動后敬辣,在合并的 manifest 文件中不再有任何 content provider 了雪标,所以應(yīng)用啟動庫和 WorkManager 都不會在應(yīng)用啟動的時候被自動初始化了。

為了手動初始化這些庫溉跃,我在應(yīng)用的其他地方添加了如下代碼來實現(xiàn)它:

val initializer = AppInitializer.getInstance(context)
initializer.initializeComponent(MyWorkManagerInitializer::class.java)

AppInitializer 是應(yīng)用啟動庫提供的連接所有這些部分的類村刨。您需要使用一個 context 對象來創(chuàng)建 AppInitializer 對象,然后可以向其傳遞一個您為初始化各種不同庫創(chuàng)建的 Initializer 引用撰茎。在這個示例中嵌牺,我使用的是 MyWorkManagerInitializer,然后就搞定了龄糊。

時間就是一切

我做了幾次測試 (使用的是我在 測試應(yīng)用啟動性能 文章中提到的計時方法) 來比較幾種不同的啟動應(yīng)用和初始化庫的方法逆粹。我統(tǒng)計了不帶任何庫、帶 WorkManager (使用默認(rèn)自動生成的 content provider)炫惩、在啟動時使用應(yīng)用啟動庫自動初始化 WorkManager 以及使用 AppInitializer 延遲初始化 WorkManager 和應(yīng)用啟動庫。

需要注意的是他嚷,就像我們在 之前的文章 中討論的蹋绽,所有的這些時間計算都是基于鎖定的 CPU 主頻,所以這些時長都要比在沒有鎖定 CPU 主頻的機(jī)器上大很多筋蓖。它們只在相互之間比較的時候有意義蟋字,而并不能代表真實的情況。下面是我發(fā)現(xiàn)的:

  • 不帶 WorkManager: 1244 ms
  • 帶 WorkManager 并且通過 content provider 加載: 1311 ms
  • 帶 WorkManager 并且通過 App Startup 加載: 1315 ms
  • 帶 WorkManager (延遲加載): 1268 ms

最后扭勉,我統(tǒng)計了利用 AppInitalizer 手動初始化 WorkManager 的耗時:

  • 利用 AppInitializer 初始化 WorkManager: 51 ms

這個數(shù)據(jù)給我們帶來一些啟示鹊奖。首先,在應(yīng)用啟動的時候加載 WorkManager 會給我的應(yīng)用平均增加 67 毫秒 (1311–1244) 的啟動時間涂炎。需要注意的是: 加載這個庫的常規(guī)方式 (使用 content provider) 使用的時間和使用應(yīng)用啟動庫的 (1315 – 1244 = 71 ms) 差不多忠聚。這是因為應(yīng)用啟動庫在單個庫的例子中并不會幫我們節(jié)省時間,我們只不過是轉(zhuǎn)移邏輯到另一個代碼路徑中運(yùn)行唱捣。如果使用應(yīng)用啟動庫加載多個庫两蟀,我們會得到相應(yīng)的優(yōu)化效果,但是針對這里的單個庫的例子震缭,使用這個方法不會有任何節(jié)省時間的優(yōu)勢赂毯。

同時延遲初始化 WorkManager 讓我可以 "節(jié)省" 大約 51 毫秒的時間。

這個差別是否足夠明顯到您需要擔(dān)心呢拣宰?答案永遠(yuǎn)是 "看情況而定"党涕。

51 毫秒占了 1.3 秒總時長的不到 4%,而對于一個真實應(yīng)用來說巡社,通常都會比我這個簡單的應(yīng)用更復(fù)雜膛堤,這個耗時占總啟動時間的百分比會更低。這種情況下這個時長可能不值得擔(dān)心晌该。但是有時候您可能發(fā)現(xiàn)有些庫需要太長時間來初始化肥荔,更有可能的是绿渣,您可能使用了幾個自帶 content provider 的庫,而它們每一個都會增加一點您應(yīng)用的啟動時間燕耿。如果您可以將上述大部分或者全部工作推遲到一個更為合適的時間點中符,并且從啟動過程中剝離,或許您會發(fā)現(xiàn)應(yīng)用的啟動速度會有顯著的提高誉帅。

像所有的性能優(yōu)化項目舟茶,您可以做的最重要的事情是分析細(xì)節(jié)、測量以及決定:

  • 檢查您項目合并后的 manifest 文件堵第。您可以看到多少 <provider> 標(biāo)簽?
  • 您能否利用應(yīng)用啟動庫從合并的 manifest 文件中移除一些甚至所有這些 content provider隧出,并觀察它如何影響啟動時間踏志?您能否在實現(xiàn)這個的同時不影響運(yùn)行時行為呢?(值得注意的是: 您需要保證在應(yīng)用開始依賴相關(guān)庫的功能之前胀瞪,確保初始化它們针余。)

最后,盡情享受性能測試和優(yōu)化凄诞。我會繼續(xù)找尋更多分析和優(yōu)化應(yīng)用的性能辦法圆雁,如果發(fā)現(xiàn)什么有價值的東西我會發(fā)布相關(guān)的內(nèi)容。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末帆谍,一起剝皮案震驚了整個濱河市伪朽,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌汛蝙,老刑警劉巖烈涮,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異窖剑,居然都是意外死亡坚洽,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進(jìn)店門西土,熙熙樓的掌柜王于貴愁眉苦臉地迎上來讶舰,“玉大人,你說我怎么就攤上這事需了√纾” “怎么了?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵肋乍,是天一觀的道長庐舟。 經(jīng)常有香客問我,道長住拭,這世上最難降的妖魔是什么挪略? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任历帚,我火速辦了婚禮,結(jié)果婚禮上杠娱,老公的妹妹穿的比我還像新娘挽牢。我一直安慰自己,他們只是感情好摊求,可當(dāng)我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布禽拔。 她就那樣靜靜地躺著,像睡著了一般室叉。 火紅的嫁衣襯著肌膚如雪睹栖。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天茧痕,我揣著相機(jī)與錄音野来,去河邊找鬼。 笑死踪旷,一個胖子當(dāng)著我的面吹牛曼氛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播令野,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼舀患,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了气破?” 一聲冷哼從身側(cè)響起聊浅,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎现使,沒想到半個月后狗超,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡朴下,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年努咐,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片殴胧。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡渗稍,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出团滥,到底是詐尸還是另有隱情竿屹,我是刑警寧澤,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布灸姊,位于F島的核電站拱燃,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏力惯。R本人自食惡果不足惜碗誉,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一召嘶、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧哮缺,春花似錦弄跌、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至糠溜,卻和暖如春淳玩,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背非竿。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工蜕着, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人汽馋。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像圈盔,于是被迫代替她去往敵國和親豹芯。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內(nèi)容