App Startup一篇就懂
Android 11系統(tǒng)已經(jīng)來了,隨之而來的是,Jetpack家族也引入了許多新的成員。
其實以后Android的更新都會逐漸采用這種模式,即特定系統(tǒng)相關(guān)的API會越來越少,更多的編程API是以Jetpack Library的形式提供給我們的。這樣我們就不需要專門針對不同的系統(tǒng)版本去寫很多的適配邏輯,而是統(tǒng)一用Jetpack提供的接口即可。Android也是在用這種方式去解決長期以來的碎片化問題。
而今年的Jetpack家族當(dāng)中又加入了兩名重磅的新成員,一個是Hilt,另一個是App Startup。
Hilt是一個依賴注入組件庫,功能非常強大,但是由于想把依賴注入講清楚還是一個相對比較困難的工作,我準(zhǔn)備過段時間再好好想想怎樣去寫好一篇關(guān)于Hilt的文章。
本篇文章的主題是App Startup。
App Startup是一個可以用于加速App啟動速度的一個庫。很多人一聽到可以加速App的啟動速度?那這是好東西啊,迫不及待地想要將這個庫引入到自己的項目當(dāng)中,結(jié)果研究了半天,發(fā)現(xiàn)越看越不明白,怎么學(xué)著學(xué)著還和ContentProvider扯上關(guān)系了?
所以,在學(xué)習(xí)App Startup的用法之前,首先我們需要搞清楚的是,App Startup具體是用來解決什么問題的。
關(guān)注我比較久的朋友應(yīng)該都知道,LitePal是由我編寫并長期維護的一個Android數(shù)據(jù)庫框架。這個框架可以幫助大家自動管理表的創(chuàng)建與升級,并提供方便的數(shù)據(jù)庫操作API。
而用過LitePal的朋友一定知道,LitePal有提供一個initialize()接口,在進行所有的數(shù)據(jù)庫操作之前,我們需要在自己的Application當(dāng)中去調(diào)用這個接口進行初始化:
class MyApplication : Application() {
? ?override fun onCreate() {
? ? ? ?super.onCreate()
? ? ? ?LitePal.initialize(this)
? ?}
? ?...}
為什么LitePal要求先進行初始化呢?因為Android的數(shù)據(jù)庫中有需要操作都是需要依賴于Context的,在初始化的時候傳入一次Context,LitePal會在內(nèi)部將其保存下來,這樣所以有其他數(shù)據(jù)庫接口就不需要再傳入Context參數(shù)了,從而讓API變得更加精簡。
這確實是個不錯的主意,但是并不是只有LitePal想到了這一點,許多庫也提供了類似的初始化接口,因此如果你在項目當(dāng)中引入了非常多的第三方庫,那么Application中的代碼就可能會變成這個樣子:
class MyApplication : Application() {
? ?override fun onCreate() {
? ? ? ?super.onCreate()
? ? ? ?LitePal.initialize(this)
? ? ? ?AAA.initialize(this)
? ? ? ?BBB.initialize(this)
? ? ? ?CCC.initialize(this)
DDD.initialize(this)
EEE.initialize(this)
? ?}
? ?...}
這樣的代碼就會顯得有些凌亂了對不對?隨著你引用的第三方庫越來越多,這種情況真的是有可能發(fā)生的。
于是,有些更加聰明的庫設(shè)計者,他們想到了一種非常巧妙的辦法來避免顯示地調(diào)用初始化接口,而是可以自動調(diào)用初始化接口,這種辦法就是借助ContentProvider。
ContentProvider我們都知道是Android四大組件之一,它的主要作用是跨應(yīng)用程序共享數(shù)據(jù)。比如為什么我們可以讀取到電話簿中的聯(lián)系人、相冊中的照片等數(shù)據(jù),借助的都是ContentProvider。
然而這些聰明的庫設(shè)計者們并沒有打算使用ContentProvider來跨應(yīng)用程序共享數(shù)據(jù),只是準(zhǔn)備使用它進行初始化而已。我們來看如下代碼:
class MyProvider : ContentProvider() {
? ?override fun onCreate(): Boolean {
? ? ? ?context?.let {
? ? ? ? ? ?LitePal.initialize(it)
? ? ? ?}
? ? ? ?return true
? ?}
? ?...}
這里我定義了一個MyProvider,并讓它繼承自ContentProvider,然后我們在onCreate()方法中調(diào)用了LitePal的初始化接口。注意在ContentProvider中也是可以獲取到Context的。
當(dāng)然,繼承了ContentProvider之后,我們是要重寫很多個方法的,只不過其他方法在我們這個場景下完全使用不到,所以你可以在那些方法中直接拋出一個異常,或者進行空實現(xiàn)都是可以的。
另外不要忘記,四大組件是需要在AndroidManifest.xml文件中進行注冊才可以使用的,因此記得添加如下內(nèi)容:
<application ...>
? ?<provider
? ? ? ?android:name=".MyProvider"
? ? ? ?android:authorities="${applicationId}.myProvider"
? ? ? ?android:exported="false" /></application>
authorities在這里并沒有固定的要求,填寫什么值都是可以的,但必須保證這個值在整個手機上是唯一的,所以通常會使用${applicationId}作為前綴,以防止和其他應(yīng)用程序沖突。
那么,自定義的這個MyProvider它會在什么時候執(zhí)行呢?我們來看一下這張流程圖:

可以看到,一個應(yīng)用程序的執(zhí)行順序是這個樣子的。首先調(diào)用Application的attachBaseContext()方法,然后調(diào)用ContentProvider的onCreate()方法,接下來調(diào)用Application的onCreate()方法。
那么,假如LitePal在自己的庫當(dāng)中實現(xiàn)了上述的MyProvider,會發(fā)生什么情況呢?
你會發(fā)現(xiàn)LitePal.initialize()這個接口可以省略了,因為在MyProvider當(dāng)中這個接口會被自動調(diào)用,這樣在進入Application的onCreate()方法時,LitePal其實已經(jīng)初始化過了。
有沒有覺得這種設(shè)計方式很巧妙?它可以將庫的用法進一步簡化,不需要你主動去調(diào)用初始化接口,而是將這個工作在背后悄悄自動完成了。
那么有哪些庫使用了這種設(shè)計方式呢?這個真的有很多了,比如說Facebook的庫,F(xiàn)irebase的庫,還有我們所熟知的WorkManager,Lifecycles等等。這些庫都沒有提供一個像LitePal那樣的初始化接口,其實就是使用了上述的技巧。
看上去如此巧妙的技術(shù)方案,那么它有沒有什么缺點呢?
有,缺點就是,ContentProvider會增加許多額外的耗時。
畢竟ContentProvider是Android四大組件之一,這個組件相對來說是比較重量級的。也就是說,本來我的初始化操作可能是一個非常輕量級的操作,依賴于ContentProvider之后就變成了一個重量級的操作了。
關(guān)于ContentProvider的耗時,Google官方也有給出一個測試結(jié)果:

這是在一臺搭載Android 10系統(tǒng)的Pixel2手機上測試的情況。可以看到,一個空的ContentProvider大約會占用2ms的耗時,隨著ContentProvider的增加,耗時也會跟著一起增加。如果你的應(yīng)用程序中使用了50個ContentProvider,那么將會占用接近20ms的耗時。
注意這還只是空ContentProvider的耗時,并沒有算上你在ContentProvider中執(zhí)行邏輯的耗時。
這個測試結(jié)果告訴我們,雖然剛才所介紹的使用ContentProvider來進行初始化的設(shè)計方式很巧妙,但是如果每個第三方庫都自己創(chuàng)建了一個ContentProvider,那么最終我們App的啟動速度就會受到比較大的影響。
有沒有辦法解決這個問題呢?
有,就是使用我們今天要介紹的主題:App Startup。
我上面花了很長的篇幅來介紹App Startup具體是用來解決什么問題的,因為這部分內(nèi)容才是App Startup庫的核心,只有了解了它是用來解決什么問題的,才能快速掌握它的用法。不然就會像剛開始說的那樣,學(xué)著學(xué)著怎么學(xué)到ContentProvider上面去了,一頭霧水。
那么App Startup是如何解決這個問題的呢?它可以將所有用于初始化的ContentProvider合并成一個,從而使App的啟動速度變得更快。
具體來講,App Startup內(nèi)部也創(chuàng)建了一個ContentProvider,并提供了一套用于初始化的標(biāo)準(zhǔn)。然后對于其他第三方庫來說,你們就不需要再自己創(chuàng)建ContentProvider了,都按我的這套標(biāo)準(zhǔn)進行實現(xiàn)就行了,我可以保證你們的庫在App啟動之前都成功進行初始化。
了解了App Startup具體是用來解決什么問題的,以及它的實現(xiàn)原理,接下來我們開始學(xué)習(xí)它的用法,這部分就非常簡單了。
首先要使用App Startup,我們要將這個庫引入進來:
dependencies {
? ?implementation "androidx.startup:startup-runtime:1.0.0-alpha01"}
接下來我們要定義一個用于執(zhí)行初始化的Initializer,并實現(xiàn)App Startup庫的Initializer接口,如下所示:
class LitePalInitializer : Initializer<Unit> {
? ?override fun create(context: Context) {
? ? ? ?LitePal.initialize(context)
? ?}
? ?override fun dependencies(): List<Class<out Initializer<*>>> {
? ? ? ?return listOf(OtherInitializer::class.java)
? ?}}
實現(xiàn)Initializer接口要求重現(xiàn)兩個方法,在create()方法中,我們?nèi)ミM行之前要進行的初始化操作就可以了,create()方法會把我們需要的Context參數(shù)傳遞進來。
dependencies()方法表示,當(dāng)前的LitePalInitializer是否還依賴于其他的Initializer,如果有的話,就在這里進行配置,App Startup會保證先初始化依賴的Initializer,然后才會初始化當(dāng)前的LitePalInitializer。
當(dāng)然,絕大多數(shù)的情況下,我們的初始化操作都是不會依賴于其他Initializer的,所以通常直接返回一個emptyList()就可以了,如下所示:
class LitePalInitializer : Initializer<Unit> {
? ?override fun create(context: Context) {
? ? ? ?LitePal.initialize(context)
? ?}
? ?override fun dependencies(): List<Class<out Initializer<*>>> {
? ? ? ?return emptyList()
? ?}}
定義好了Initializer之后,接下來還剩最后一步,將它配置到AndroidManifest.xml當(dāng)中。但是注意,這里的配置是有比較嚴格的格式要求的,如下所示:
<application ...>
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data
android:name="com.example.LitePalInitializer"
android:value="androidx.startup" />
</provider>
</application>
上述配置,我們能修改的地方并不多,只有meta-data中的android:name部分我們需要指定成我們自定義的Initializer的全路徑類名,其他部分都是不能修改的,否則App Startup庫可能會無法正常工作。
沒錯,App Startup庫的用法就是這么簡單,基本我將它總結(jié)成了三步走的操作。
引入App Startup的庫。
自定義一個用于初始化的Initializer。
將自定義Initializer配置到AndroidManifest.xml當(dāng)中。
這樣,當(dāng)App啟動的時候會自動執(zhí)行App Startup庫中內(nèi)置的ContentProvider,并在它的ContentProvider中會搜尋所有注冊的Initializer,然后逐個調(diào)用它們的create()方法來進行初始化操作。
只用一個ContentProvider就可以讓所有庫都正常初始化,Everyone is happy。
其實到這里為止,App Startup庫的知識就已經(jīng)講完了,最后再介紹一個不太常用的知識點吧:延遲初始化。
現(xiàn)在我們已經(jīng)知道,所有的Initializer都會在App啟動的時候自動執(zhí)行初始化操作。但是如果我作為LitePal庫的用戶,就是不希望它在啟動的時候自動初始化,而是想要在特定的時機手動初始化,這要怎么辦呢?
首先,你得通過分析LitePal源碼的方式,找到LitePal用于初始化的Initializer的全路徑類名是什么,比如上述例子當(dāng)中的com.example.LitePalInitializer(注意這里我只是為了講解這個知識點而舉的例子,實際上LitePal還并沒有接入App Startup)。
然后,在你的項目的AndroidManifest.xml當(dāng)中加入如下配置:
<application ...>
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data
android:name="com.example.LitePalInitializer"
tools:node="remove" />
</provider>
</application>
區(qū)別就在于,這里在LitePalInitializer的meta-data當(dāng)中加入了一個tools:node="remove"的標(biāo)記。
這個標(biāo)記用于告訴manifest merger tool,在最后打包成APK時,將所有android:name是com.example.LitePalInitializer的meta-data節(jié)點全部刪除。
這樣,LitePal庫在自己的AndroidManifest.xml中配置的Initializer也會被刪除,既然刪除了,App Startup在啟動的時候肯定就無法初始化它了。
而在之后手動去初始化LitePal的代碼也極其簡單,如下所示:
AppInitializer.getInstance(this)
? ?.initializeComponent(LitePalInitializer::class.java)
1
2
將LitePalInitializer傳入到initializeComponent()方法當(dāng)中即可,App Startup庫會按照同樣的標(biāo)準(zhǔn)去調(diào)用其create()方法來執(zhí)行初始化操作。
到這里為止,App Startup的功能基本就全部講解完了。
最后如果讓我總結(jié)一下的話,這個庫的整體用法非常簡單,但是可能并不適合所有人去使用。如果你是一個庫開發(fā)者,并且使用了ContentProvider的方式來進行初始化操作,那么你應(yīng)該接入App Startup,這樣可以讓接入你的庫的App降低啟動耗時。而如果你是一個App開發(fā)者,我認為使用ContentProvider來進行初始化操作的概率很低,所以可能App Startup對你來說用處并不大。
當(dāng)然,考慮到業(yè)務(wù)邏輯分離的代碼結(jié)構(gòu),App的開發(fā)者也可以考慮將一些原來放在Application中的初始化代碼,移動到一個Initializer中去單獨執(zhí)行,或許可以讓你的代碼結(jié)構(gòu)變得更加合理與清晰。