Android 11系統(tǒng)已經(jīng)來了闻鉴,隨之而來的是筛武,Jetpack家族也引入了許多新的成員。
其實以后Android的更新都會逐漸采用這種模式盗胀,即特定系統(tǒng)相關(guān)的API會越來越少,更多的編程API是以Jetpack Library的形式提供給我們的锄贼。這樣我們就不需要專門針對不同的系統(tǒng)版本去寫很多的適配邏輯票灰,而是統(tǒng)一用Jetpack提供的接口即可。Android也是在用這種方式去解決長期以來的碎片化問題。
而今年的Jetpack家族當中又加入了兩名重磅的新成員屑迂,一個是Hilt浸策,另一個是App Startup。
Hilt是一個依賴注入組件庫屈糊,功能非常強大的榛,但是由于想把依賴注入講清楚還是一個相對比較困難的工作,我準備過段時間再好好想想怎樣去寫好一篇關(guān)于Hilt的文章逻锐。
本篇文章的主題是App Startup夫晌。
App Startup是一個可以用于加速App啟動速度的一個庫。很多人一聽到可以加速App的啟動速度昧诱?那這是好東西啊晓淀,迫不及待地想要將這個庫引入到自己的項目當中,結(jié)果研究了半天盏档,發(fā)現(xiàn)越看越不明白凶掰,怎么學著學著還和ContentProvider扯上關(guān)系了?
所以蜈亩,在學習App Startup的用法之前懦窘,首先我們需要搞清楚的是,App Startup具體是用來解決什么問題的稚配。
關(guān)注我比較久的朋友應該都知道畅涂,LitePal是由我編寫并長期維護的一個Android數(shù)據(jù)庫框架。這個框架可以幫助大家自動管理表的創(chuàng)建與升級道川,并提供方便的數(shù)據(jù)庫操作API午衰。
而用過LitePal的朋友一定知道,LitePal有提供一個initialize()接口冒萄,在進行所有的數(shù)據(jù)庫操作之前臊岸,我們需要在自己的Application當中去調(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想到了這一點响疚,許多庫也提供了類似的初始化接口鄙信,因此如果你在項目當中引入了非常多的第三方庫,那么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ā)生的。
于是,有些更加聰明的庫設計者鸦采,他們想到了一種非常巧妙的辦法來避免顯示地調(diào)用初始化接口宾巍,而是可以自動調(diào)用初始化接口,這種辦法就是借助ContentProvider渔伯。
ContentProvider我們都知道是Android四大組件之一顶霞,它的主要作用是跨應用程序共享數(shù)據(jù)。比如為什么我們可以讀取到電話簿中的聯(lián)系人锣吼、相冊中的照片等數(shù)據(jù)选浑,借助的都是ContentProvider掺出。
然而這些聰明的庫設計者們并沒有打算使用ContentProvider來跨應用程序共享數(shù)據(jù)匿级,只是準備使用它進行初始化而已溢豆。我們來看如下代碼:
class MyProvider : ContentProvider() {
override fun onCreate(): Boolean {
context?.let {
LitePal.initialize(it)
}
return true
}
...
}
這里我定義了一個MyProvider棍厂,并讓它繼承自ContentProvider,然后我們在onCreate()方法中調(diào)用了LitePal的初始化接口爸吮。注意在ContentProvider中也是可以獲取到Context的钙态。
當然述呐,繼承了ContentProvider之后寺惫,我們是要重寫很多個方法的疹吃,只不過其他方法在我們這個場景下完全使用不到,所以你可以在那些方法中直接拋出一個異常西雀,或者進行空實現(xiàn)都是可以的互墓。
另外不要忘記,四大組件是需要在AndroidManifest.xml文件中進行注冊才可以使用的蒋搜,因此記得添加如下內(nèi)容:
<application ...>
<provider
android:name=".MyProvider"
android:authorities="${applicationId}.myProvider"
android:exported="false" />
</application>
authorities在這里并沒有固定的要求,填寫什么值都是可以的判莉,但必須保證這個值在整個手機上是唯一的豆挽,所以通常會使用${applicationId}作為前綴,以防止和其他應用程序沖突券盅。
那么帮哈,自定義的這個MyProvider它會在什么時候執(zhí)行呢?我們來看一下這張流程圖:
可以看到锰镀,一個應用程序的執(zhí)行順序是這個樣子的娘侍。首先調(diào)用Application的attachBaseContext()方法,然后調(diào)用ContentProvider的onCreate()方法泳炉,接下來調(diào)用Application的onCreate()方法憾筏。
那么,假如LitePal在自己的庫當中實現(xiàn)了上述的MyProvider花鹅,會發(fā)生什么情況呢氧腰?
你會發(fā)現(xiàn)LitePal.initialize()這個接口可以省略了,因為在MyProvider當中這個接口會被自動調(diào)用,這樣在進入Application的onCreate()方法時古拴,LitePal其實已經(jīng)初始化過了箩帚。
有沒有覺得這種設計方式很巧妙?它可以將庫的用法進一步簡化黄痪,不需要你主動去調(diào)用初始化接口紧帕,而是將這個工作在背后悄悄自動完成了。
那么有哪些庫使用了這種設計方式呢桅打?這個真的有很多了是嗜,比如說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的增加,耗時也會跟著一起增加舍杜。如果你的應用程序中使用了50個ContentProvider新娜,那么將會占用接近20ms的耗時。
注意這還只是空ContentProvider的耗時既绩,并沒有算上你在ContentProvider中執(zhí)行邏輯的耗時概龄。
這個測試結(jié)果告訴我們,雖然剛才所介紹的使用ContentProvider來進行初始化的設計方式很巧妙饲握,但是如果每個第三方庫都自己創(chuàng)建了一個ContentProvider旁钧,那么最終我們App的啟動速度就會受到比較大的影響吸重。
有沒有辦法解決這個問題呢?
有歪今,就是使用我們今天要介紹的主題:App Startup嚎幸。
我上面花了很長的篇幅來介紹App Startup具體是用來解決什么問題的,因為這部分內(nèi)容才是App Startup庫的核心寄猩,只有了解了它是用來解決什么問題的嫉晶,才能快速掌握它的用法。不然就會像剛開始說的那樣田篇,學著學著怎么學到ContentProvider上面去了替废,一頭霧水。
那么App Startup是如何解決這個問題的呢泊柬?它可以將所有用于初始化的ContentProvider合并成一個椎镣,從而使App的啟動速度變得更快。
具體來講兽赁,App Startup內(nèi)部也創(chuàng)建了一個ContentProvider状答,并提供了一套用于初始化的標準。然后對于其他第三方庫來說刀崖,你們就不需要再自己創(chuàng)建ContentProvider了惊科,都按我的這套標準進行實現(xiàn)就行了,我可以保證你們的庫在App啟動之前都成功進行初始化亮钦。
了解了App Startup具體是用來解決什么問題的馆截,以及它的實現(xiàn)原理,接下來我們開始學習它的用法蜂莉,這部分就非常簡單了蜡娶。
首先要使用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()方法表示枢赔,當前的LitePalInitializer是否還依賴于其他的Initializer,如果有的話拥知,就在這里進行配置踏拜,App Startup會保證先初始化依賴的Initializer,然后才會初始化當前的LitePalInitializer低剔。
當然速梗,絕大多數(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當中位隶。但是注意拷窜,這里的配置是有比較嚴格的格式要求的,如下所示:
<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當中材义。
這樣,當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的全路徑類名是什么蝠检,比如上述例子當中的com.example.LitePalInitializer(注意這里我只是為了講解這個知識點而舉的例子,實際上LitePal還并沒有接入App Startup)挚瘟。
然后叹谁,在你的項目的AndroidManifest.xml當中加入如下配置:
<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當中加入了一個tools:node="remove"的標記。
這個標記用于告訴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)
將LitePalInitializer傳入到initializeComponent()方法當中即可,App Startup庫會按照同樣的標準去調(diào)用其create()方法來執(zhí)行初始化操作顿乒。
到這里為止议街,App Startup的功能基本就全部講解完了。
最后如果讓我總結(jié)一下的話璧榄,這個庫的整體用法非常簡單特漩,但是可能并不適合所有人去使用。如果你是一個庫開發(fā)者骨杂,并且使用了ContentProvider的方式來進行初始化操作涂身,那么你應該接入App Startup,這樣可以讓接入你的庫的App降低啟動耗時搓蚪。而如果你是一個App開發(fā)者蛤售,我認為使用ContentProvider來進行初始化操作的概率很低,所以可能App Startup對你來說用處并不大妒潭。
當然悴能,考慮到業(yè)務邏輯分離的代碼結(jié)構(gòu),App的開發(fā)者也可以考慮將一些原來放在Application中的初始化代碼雳灾,移動到一個Initializer中去單獨執(zhí)行漠酿,或許可以讓你的代碼結(jié)構(gòu)變得更加合理與清晰。
當前保存作為筆記原文地址