Android 11系統已經來了,隨之而來的是,Jetpack家族也引入了許多新的成員。
其實以後Android的更新都會逐漸採用這種模式,即特定系統相關的API會越來越少,更多的程式設計API是以Jetpack Library的形式提供給我們的。這樣我們就不需要專門針對不同的系統版本去寫很多的適配邏輯,而是統一用Jetpack提供的介面即可。Android也是在用這種方式去解決長期以來的碎片化問題。
而今年的Jetpack家族當中又加入了兩名重磅的新成員,一個是Hilt,另一個是App Startup。
Hilt是一個依賴注入元件庫,功能非常強大,但是由於想把依賴注入講清楚還是一個相對比較困難的工作,我準備過段時間再好好想想怎樣去寫好一篇關於Hilt的文章。
本篇文章的主題是App Startup。
App Startup是一個可以用於加速App啟動速度的一個庫。很多人一聽到可以加速App的啟動速度?那這是好東西啊,迫不及待地想要將這個庫引入到自己的專案當中,結果研究了半天,發現越看越不明白,怎麼學著學著還和ContentProvider扯上關係了?
所以,在學習App Startup的用法之前,首先我們需要搞清楚的是,App Startup具體是用來解決什麼問題的。
關注我比較久的朋友應該都知道,LitePal是由我編寫並長期維護的一個Android資料庫框架。這個框架可以幫助大家自動管理表的建立與升級,並提供方便的資料庫操作API。
而用過LitePal的朋友一定知道,LitePal有提供一個initialize()介面,在進行所有的資料庫操作之前,我們需要在自己的Application當中去呼叫這個介面進行初始化:
class MyApplication : Application() { override fun onCreate() { super.onCreate() LitePal.initialize(this) } ...}
為什麼LitePal要求先進行初始化呢?因為Android的資料庫中有需要操作都是需要依賴於Context的,在初始化的時候傳入一次Context,LitePal會在內部將其儲存下來,這樣所以有其他資料庫介面就不需要再傳入Context引數了,從而讓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) } ...}
這樣的程式碼就會顯得有些凌亂了對不對?隨著你引用的第三方庫越來越多,這種情況真的是有可能發生的。
於是,有些更加聰明的庫設計者,他們想到了一種非常巧妙的辦法來避免顯示地呼叫初始化介面,而是可以自動呼叫初始化介面,這種辦法就是藉助ContentProvider。
ContentProvider我們都知道是Android四大元件之一,它的主要作用是跨應用程式共享資料。比如為什麼我們可以讀取到電話簿中的聯絡人、相簿中的照片等資料,藉助的都是ContentProvider。
然而這些聰明的庫設計者們並沒有打算使用ContentProvider來跨應用程式共享資料,只是準備使用它進行初始化而已。我們來看如下程式碼:
class MyProvider : ContentProvider() { override fun onCreate(): Boolean { context?.let { LitePal.initialize(it) } return true } ...}
這裡我定義了一個MyProvider,並讓它繼承自ContentProvider,然後我們在onCreate()方法中呼叫了LitePal的初始化介面。注意在ContentProvider中也是可以獲取到Context的。
當然,繼承了ContentProvider之後,我們是要重寫很多個方法的,只不過其他方法在我們這個場景下完全使用不到,所以你可以在那些方法中直接丟擲一個異常,或者進行空實現都是可以的。
另外不要忘記,四大元件是需要在AndroidManifest.xml檔案中進行註冊才可以使用的,因此記得新增如下內容:
<application ...> <provider android:name=".MyProvider" android:authorities="${applicationId}.myProvider" android:exported="false" /></application>
authorities在這裡並沒有固定的要求,填寫什麼值都是可以的,但必須保證這個值在整個手機上是唯一的,所以通常會使用${applicationId}作為字首,以防止和其他應用程式衝突。
那麼,自定義的這個MyProvider它會在什麼時候執行呢?我們來看一下這張流程圖:
可以看到,一個應用程式的執行順序是這個樣子的。首先呼叫Application的attachBaseContext()方法,然後呼叫ContentProvider的onCreate()方法,接下來呼叫Application的onCreate()方法。
那麼,假如LitePal在自己的庫當中實現了上述的MyProvider,會發生什麼情況呢?
你會發現LitePal.initialize()這個介面可以省略了,因為在MyProvider當中這個介面會被自動呼叫,這樣在進入Application的onCreate()方法時,LitePal其實已經初始化過了。
有沒有覺得這種設計方式很巧妙?它可以將庫的用法進一步簡化,不需要你主動去呼叫初始化介面,而是將這個工作在背後悄悄自動完成了。
那麼有哪些庫使用了這種設計方式呢?這個真的有很多了,比如說Facebook的庫,Firebase的庫,還有我們所熟知的WorkManager,Lifecycles等等。這些庫都沒有提供一個像LitePal那樣的初始化介面,其實就是使用了上述的技巧。
看上去如此巧妙的技術方案,那麼它有沒有什麼缺點呢?
有,缺點就是,ContentProvider會增加許多額外的耗時。
畢竟ContentProvider是Android四大元件之一,這個元件相對來說是比較重量級的。也就是說,本來我的初始化操作可能是一個非常輕量級的操作,依賴於ContentProvider之後就變成了一個重量級的操作了。
關於ContentProvider的耗時,Google官方也有給出一個測試結果:
這是在一臺搭載Android 10系統的Pixel2手機上測試的情況。可以看到,一個空的ContentProvider大約會佔用2ms的耗時,隨著ContentProvider的增加,耗時也會跟著一起增加。如果你的應用程式中使用了50個ContentProvider,那麼將會佔用接近20ms的耗時。
注意這還只是空ContentProvider的耗時,並沒有算上你在ContentProvider中執行邏輯的耗時。
這個測試結果告訴我們,雖然剛才所介紹的使用ContentProvider來進行初始化的設計方式很巧妙,但是如果每個第三方庫都自己建立了一個ContentProvider,那麼最終我們App的啟動速度就會受到比較大的影響。
有沒有辦法解決這個問題呢?
有,就是使用我們今天要介紹的主題:App Startup。
我上面花了很長的篇幅來介紹App Startup具體是用來解決什麼問題的,因為這部分內容才是App Startup庫的核心,只有了解了它是用來解決什麼問題的,才能快速掌握它的用法。不然就會像剛開始說的那樣,學著學著怎麼學到ContentProvider上面去了,一頭霧水。
那麼App Startup是如何解決這個問題的呢?它可以將所有用於初始化的ContentProvider合併成一個,從而使App的啟動速度變得更快。
具體來講,App Startup內部也建立了一個ContentProvider,並提供了一套用於初始化的標準。然後對於其他第三方庫來說,你們就不需要再自己建立ContentProvider了,都按我的這套標準進行實現就行了,我可以保證你們的庫在App啟動之前都成功進行初始化。
了解了App Startup具體是用來解決什麼問題的,以及它的實現原理,接下來我們開始學習它的用法,這部分就非常簡單了。
首先要使用App Startup,我們要將這個庫引入進來:
dependencies { implementation "androidx.startup:startup-runtime:1.0.0-alpha01"}
接下來我們要定義一個用於執行初始化的Initializer,並實現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) }}
實現Initializer介面要求重現兩個方法,在create()方法中,我們去進行之前要進行的初始化操作就可以了,create()方法會把我們需要的Context引數傳遞進來。
dependencies()方法表示,當前的LitePalInitializer是否還依賴於其他的Initializer,如果有的話,就在這裡進行配置,App Startup會保證先初始化依賴的Initializer,然後才會初始化當前的LitePalInitializer。
當然,絕大多數的情況下,我們的初始化操作都是不會依賴於其他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庫的用法就是這麼簡單,基本我將它總結成了三步走的操作。
引入App Startup的庫。自定義一個用於初始化的Initializer。將自定義Initializer配置到AndroidManifest.xml當中。這樣,當App啟動的時候會自動執行App Startup庫中內建的ContentProvider,並在它的ContentProvider中會搜尋所有註冊的Initializer,然後逐個呼叫它們的create()方法來進行初始化操作。
只用一個ContentProvider就可以讓所有庫都正常初始化,Everyone is happy。
其實到這裡為止,App Startup庫的知識就已經講完了,最後再介紹一個不太常用的知識點吧:延遲初始化。
現在我們已經知道,所有的Initializer都會在App啟動的時候自動執行初始化操作。但是如果我作為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>
區別就在於,這裡在LitePalInitializer的meta-data當中加入了一個tools:node="remove"的標記。
這個標記用於告訴manifest merger tool,在最後打包成APK時,將所有android:name是com.example.LitePalInitializer的meta-data節點全部刪除。
這樣,LitePal庫在自己的AndroidManifest.xml中配置的Initializer也會被刪除,既然刪除了,App Startup在啟動的時候肯定就無法初始化它了。
而在之後手動去初始化LitePal的程式碼也極其簡單,如下所示:
AppInitializer.getInstance(this) .initializeComponent(LitePalInitializer::class.java)
將LitePalInitializer傳入到initializeComponent()方法當中即可,App Startup庫會按照同樣的標準去呼叫其create()方法來執行初始化操作。
到這裡為止,App Startup的功能基本就全部講解完了。
最後如果讓我總結一下的話,這個庫的整體用法非常簡單,但是可能並不適合所有人去使用。如果你是一個庫開發者,並且使用了ContentProvider的方式來進行初始化操作,那麼你應該接入App Startup,這樣可以讓接入你的庫的App降低啟動耗時。而如果你是一個App開發者,我認為使用ContentProvider來進行初始化操作的概率很低,所以可能App Startup對你來說用處並不大。
當然,考慮到業務邏輯分離的程式碼結構,App的開發者也可以考慮將一些原來放在Application中的初始化程式碼,移動到一個Initializer中去單獨執行,或許可以讓你的程式碼結構變得更加合理與清晰。