Koin、Dagger、Hilt 目前都是很是流行的庫,面對這麼多層出不窮的新技術,咱們該作如何選擇,是一直困擾咱們的一個問題,以前我分析過 Koin 和 Dagger 的性能對比,Hilt 與 Dagger 的不一樣之處,能夠點擊下方連接前往查看。java
本文主要來一塊兒分析一下 Hilt 和 Koin 的性能,若是你以前對 Hilt 和 Koin 不瞭解也沒有關係,對閱讀本文沒有什麼影響,接下來將會從如下幾個方面來分析 Hilt 和 Koin 不一樣之處。android
Koin 是爲 Kotlin 開發者提供的一個實用型輕量級依賴注入框架,採用純 Kotlin 語言編寫而成,僅使用功能解析,無代理、無代碼生成、無反射。git
Hilt 是在 Dagger 基礎上進行開發的,減小了在項目中進行手動依賴,Hilt 集成了 Jetpack 庫和 Android 框架類,並刪除了大部分模板代碼,讓開發者只須要關注如何進行綁定,同時 Hilt 也繼承了 Dagger 優勢,編譯時正確性、運行時性能、而且獲得了 Android Studio 的支持。github
Hilt、Dagger、Koin 等等都是依賴注入庫,依賴注入是面向對象設計中最好的架構模式之一,使用依賴注入庫有如下優勢:面試
接下來將從 AndroidStudio 基礎支持、項目結構、代碼行數、編譯時間、使用上的不一樣,這幾個方面對 Hilt 和 Koin 進行全方面的分析。算法
Android Studio >= 4.1 的版本,在編輯器和代碼行號之間,增長了一個新的 "間距圖標",能夠在 Dagger 的關聯代碼間進行導航,包括依賴項的生產者、消費者、組件、子組件以及模塊。數據庫
Hilt 是在 Dagger 基礎上進行開發的,因此 Hilt 天然也擁有了 Dagger 的優勢,在 Android Studio >= 4.1 版本上也支持在 Hilt 的關聯代碼間進行導航,以下圖所示。編程
PS: 我用的版本是 Android Studio 4.1 Canary 10,命名和圖標在不一樣版本上會有差別。數組
有了 Android Studio 支持,在 Android 應用中 Dagger 和 Hilt 在關聯代碼間進行導航是如此簡單。性能優化
這兩個圖標的意思以下:
遺憾的是 Koin 不支持,其實 Koin 並不須要這個功能,Koin 並不像 Hilt 注入代碼那麼分散,並且 Koin 注入關係很明確,能夠很方便的定位到與它相關聯的代碼,而且 Koin 提供的 Debug 工具,能夠打印出其構建過程,幫助咱們分析。
而 Hilt 不同的是 Hilt 採用註解的方式,在使用 Hilt 的項目中,若是想要弄清楚其依賴項來自 @Inject 修飾的構造器、@Binds 或者 @Provides 修飾的方法?仍是限定符?不是一件容易的事,尤爲在一個大型複雜的的項目中,想要弄清楚它們之間的依賴關係是很是困難的,而 Android Studio >= 4.1 的版本,增長的 "間距圖標",幫助咱們解決了這個問題。
爲了可以正確比較這兩種方式,新建了兩個項目 Project-Hilt 和 Project-Koin, 分別用 Hilt 和 Koin 去實現,Project-Hilt 和 Project-Koin 兩個項目的依賴庫版本管理統一用 Composing builds 的方式(關於 Composing builds 請參考這篇文章 再見吧 buildSrc, 擁抱 Composing builds 提高 Android 編譯速度),除了它們自己的依賴庫,其餘的依賴庫都是相同的,以下圖所示:
項目 Project-Hilt 和 Project-Koin 都分別實現了 Room 和 Retrofit 進行數據庫和網絡訪問,統一在 Repository 裏面進行處理,它們的依賴注入都放在了 di 下面,這應該是一個小型 App 基礎架構,以下圖所示:
如上圖所示,這裏須要關注 di 包下的類,Project-Hilt 和 Project-Koin 分別注入了 Room、Retrofit 和 Repository,以 Hilt 注入的方式至少須要三個文件才能完成,可是若是使用 Koin 的方式只須要一個文件就能夠完成,後面我會進行詳細的分析。
項目 Project-Hilt 和 Project-Koin 除了它們自己的依賴以外,其餘的依賴都是相同的。
我使用 Statistic 工具來進行代碼行數的統計,反覆對比了項目編譯前和編譯後,它們的結果以下所示:
代碼行數 | Hilt | Koin |
---|---|---|
編譯以前 | 2414 | 2414 |
編譯以後 | 149608 | 138405 |
正如你所見 Hilt 生成的代碼多於 Koin,隨着項目愈來愈複雜,生成的代碼量會愈來愈多。
爲了保證測試的準確性,每次編譯以前我都會先 clean 而後纔會 rebuild,反覆的進行了三次這樣的操做,它們的結果以下所示。
第一次編譯結果:
Hilt:
BUILD SUCCESSFUL in 28s
27 actionable tasks: 27 executed
Koin:
BUILD SUCCESSFUL in 17s
27 actionable tasks: 27 executed
複製代碼
第二次編譯結果:
Hilt:
BUILD SUCCESSFUL in 22s
27 actionable tasks: 27 executed
Koin:
BUILD SUCCESSFUL in 15s
27 actionable tasks: 27 executed
複製代碼
第三編譯結果:
Hilt:
BUILD SUCCESSFUL in 35s
27 actionable tasks: 27 executed
Koin:
BUILD SUCCESSFUL in 18s
27 actionable tasks: 27 executed
複製代碼
每次的編譯時間確定是不同的,速度取決於你的電腦的環境,無論執行多少次,結果如上所示 Hilt 編譯時間老是大於 Koin,這個結果告訴咱們,若是在一個很是大型的項目,這個代價是很是昂貴。
爲何 Hilt 編譯時間老是大於 Koin
由於在 Koin 中不須要使用註解,也不須要用 kapt,這意味着沒有額外的代碼生成,全部的代碼都是 Kotlin 原始代碼,因此說 Hilt 編譯時間老是大於 Koin,從這個角度上同時也解釋了,爲何會說 Koin 僅使用功能解析,無額外代碼生成。
爲了節省篇幅,這裏只會列出部分代碼,具體詳細使用參考我以前寫的 Hilt 入門三部曲,包含了 Hilt 全部的用法以及實戰案例。
在項目中使用 Hilt
若是咱們須要在項目中使用 Hilt,咱們須要添加 Hilt 插件和依賴庫,首先在 project 的 build.gradle 添加如下依賴。
buildscript {
...
dependencies {
...
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28-alpha'
}
}
複製代碼
而後在 App 模塊中的 build.gradle 文件中添加如下代碼。
...
apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'
android {
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
// For Kotlin projects
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
}
dependencies {
implementation "com.google.dagger:hilt-android:2.28-alpha"
kapt "com.google.dagger:hilt-android-compiler:2.28-alpha"
}
複製代碼
注意: 這裏有一個坑,對於 Kotlin 項目,須要添加 kotlinOptions,這是 Google 文檔 Dependency injection with Hilt 中沒有提到的,不然使用 ViewModel 會編譯不過。
完成以上步驟就能夠在項目中使用 Hilt 了,全部使用 Hilt 的 App 必須包含一個使用 @HiltAndroidApp
註解的 Application,這是依賴注入容器的入口。
@HiltAndroidApp
class HiltApplication : Application() {
/**
* 1. 全部使用 Hilt 的 App 必須包含一個使用 @HiltAndroidApp 註解的 Application
* 2. @HiltAndroidApp 將會觸發 Hilt 代碼的生成,包括用做應用程序依賴項容器的基類
* 3. 生成的 Hilt 組件依附於 Application 的生命週期,它也是 App 的父組件,提供其餘組件訪問的依賴
* 4. 在 Application 中設置好 @HiltAndroidApp 以後,就可使用 Hilt 提供的組件了,
* Hilt 提供的 @AndroidEntryPoint 註解用於提供 Android 類的依賴(Activity、Fragment、View、Service、BroadcastReceiver)等等
* Application 使用 @HiltAndroidApp 註解
*/
}
複製代碼
@HiltAndroidApp
註解將會觸發 Hilt 代碼的生成,用做應用程序依賴項容器的基類,這下咱們就能夠在 di 包下注入 Room、Retrofit 和 Repository,其中 Room 和 Retrofit 比較簡單,這裏咱們看一下 如何注入 Repository, Repository 有一個子類 TasksRepository,代碼以下所示。
class TasksRepository @Inject constructor(
private val localDataSource: DataSource,
private val remoteDataSource: DataSource
) : Repository
複製代碼
TasksRepository 的構造函數包含了 localDataSource 和 remoteDataSource,須要構建這兩個 DataSource 才能完成 TasksRepository 注入,代碼以下所示:
@Module
@InstallIn(ApplicationComponent::class)
object QualifierModule {
// 爲每一個聲明的限定符,提供對應的類型實例,和 @Binds 或者 @Provides 一塊兒使用
@Qualifier
// @Retention 定義了註解的生命週期,對應三個值(SOURCE、BINARY、RUNTIME)
// AnnotationRetention.SOURCE:僅編譯期,不存儲在二進制輸出中。
// AnnotationRetention.BINARY:存儲在二進制輸出中,但對反射不可見。
// AnnotationRetention.RUNTIME:存儲在二進制輸出中,對反射可見。
@Retention(AnnotationRetention.RUNTIME)
annotation class RemoteTasksDataSource // 註解的名字,後面直接使用它
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class LocalTasksDataSource
@Singleton
@RemoteTasksDataSource
@Provides
fun provideTasksRemoteDataSource(): DataSource { // 返回值相同
return RemoteDataSource() // 不一樣的實現
}
@Singleton
@LocalTasksDataSource
@Provides
fun provideTaskLocalDataSource(appDatabase: AppDataBase): DataSource { // 返回值相同
return LocalDataSource(appDatabase.personDao()) // 不一樣的實現
}
@Singleton
@Provides
fun provideTasksRepository(
@LocalTasksDataSource localDataSource: DataSource,
@RemoteTasksDataSource remoteDataSource: DataSource
): Repository {
return TasksRepository(
localDataSource,
remoteDataSource
)
}
}
複製代碼
這只是 Repository 注入代碼,固然這並非所有,還有 Room、Retrofit、Activity、Fragment、ViewModel 等等須要注入,隨着項目愈來愈複雜,多模塊化的拆分,還有更多的事情須要去作。
Hilt 和 Dagger 比起來雖然簡單不少,可是 Hilt 相比於 Koin,其入門的門檻仍是很高的,尤爲是 Hilt 的註解,須要瞭解其每一個註解的含義才能正確的使用,避免資源的浪費,可是對於註解的愛好者來講,可能更偏向於使用 Hilt,接下來咱們來看一下如何在項目中使用 Koin。
在項目中使用 Koin
若是要在項目中使用 Koin,須要在項目中添加 Koin 的依賴,咱們只須要在 App 模塊中的 build.gradle 文件中添加如下代碼。
implementation 「org.koin:koin-core:2.1.5」
implementation 「org.koin:koin-androidx-viewmodel:2.1.5」
複製代碼
若是須要在項目中使用 Koin 進行依賴注入,須要在 Application 或者其餘的地方進行初始化。
class KoinApplication : Application() {
override fun onCreate() {
super.onCreate()
startKoin {
AndroidLogger(Level.DEBUG)
androidContext(this@KoinApplication)
modules(appModule)
}
}
}
複製代碼
當初始化完成以後,就能夠在項目中使用 Koin 了,首先咱們來看一下如何在項目中注入 Repository, Repository 有一個子類 TasksRepository,代碼和上文介紹的同樣,須要在其構造函數構造 localDataSource 和 remoteDataSource 兩個 DataSource。
class TasksRepository @Inject constructor(
private val localDataSource: DataSource,
private val remoteDataSource: DataSource
) : Repository
複製代碼
那麼在 Koin 中如何注入呢,很簡單,只須要幾行代碼就能夠完成。
val repoModule = module {
single { LocalDataSource(get()) }
single { RemoteDataSource() }
single { TasksRepository(get(), get()) }
}
// 添加全部須要在 Application 中進行初始化的 module
val appModule = listOf(repoModule)
複製代碼
和上面 Hilt 長長的代碼比起來,Koin 是否是簡單不少,那麼 Room、Retrofit、ViewModel 如何注入呢,也很簡單,代碼以下所示。
// 注入 ViewModel
val viewModele = module {
viewModel { MainViewModel(get()) }
}
// 注入 Room
val localModule = module {
single { AppDataBase.initDataBase(androidApplication()) }
single { get<AppDataBase>().personDao() }
}
// 注入 Retrofit
val remodeModule = module {
single { GitHubService.createRetrofit() }
single { get<Retrofit>().create(GitHubService::class.java) }
}
// 添加全部須要在 Application 中進行初始化的 module
val appModule = listOf(viewModele, localModule, remodeModule)
複製代碼
上面 Koin 的代碼嚴格意義上講,其實不太規範,在這裏只是爲了和 Hilt 進行更好的對比。
到這裏是否是感受 Hilt 相比於 Koin 是否是簡單不少,在閱讀 Hilt 文檔的時候花了好幾天時間才消化,而 Koin 只須要花很短的時間。
咱們在看一下使用 Hilt 和 Koin 完成 Room、Retrofit、Repository 和 ViewModel 等等所有的依賴注入須要多少行代碼。
依賴注入框架 | Hilt | Koin |
---|---|---|
代碼行數 | 122 | 42 |
正如你所見依賴注入部分的代碼 Hilt 多於 Koin,示例中只是一個基本的項目架構,實際的項目每每比這要複雜的不少,所須要的代碼也更多,也愈來愈複雜。
不只僅如此而已,根據 Koin 文檔介紹,Koin 不須要用到反射,那麼無反射 Koin 是如何實現的呢,由於 Koin 基於 kotlin 基礎上進行開發的,使用了 kotlin 強大的語法糖(例如 Inline、Reified 等等)和函數式編程,來看一個簡單的例子。
inline fun <reified T : ViewModel> Module.viewModel(
qualifier: Qualifier? = null,
override: Boolean = false,
noinline definition: Definition<T>
): BeanDefinition<T> {
val beanDefinition = factory(qualifier, override, definition)
beanDefinition.setIsViewModel()
return beanDefinition
}
複製代碼
內聯函數支持具體化的類型參數,使用 reified 修飾符來限定類型參數,能夠在函數內部訪問它,因爲函數是內聯的,因此不須要反射。
可是在另外一方面 Koin 相比於 Hilt 錯誤提示不夠友好,Hilt 是基於 Dagger 基礎上進行開發的,因此 Hilt 天然也擁有了 Dagger 的優勢,編譯時正確性,對於一個大型項目來講,這是一個很是嚴重的問題,由於咱們更喜歡編譯錯誤而不是運行時錯誤。
咱們總共從如下幾個方面對 Hilt 和 Koin 進行全方面的分析:
AndroidStudio 支持 Hilt 在關聯代碼間進行導航,支持在 @Inject 修飾的構造器、@Binds 或者 @Provides 修飾的方法、限定符之間進行跳轉。
項目結構:完成 Hilt 的依賴注入須要的文件每每多於 Koin。
代碼行數:使用 Statistic 工具來進行代碼統計,反覆對比了項目編譯前和編譯後,Hilt 生成的代碼多於 Koin,隨着項目愈來愈複雜,生成的代碼量會愈來愈多。
代碼行數 | Hilt | Koin |
---|---|---|
編譯以前 | 2414 | 2414 |
編譯以後 | 149608 | 138405 |
編譯時間:Hilt 編譯時間老是大於 Koin,這個結果告訴咱們,若是是在一個很是大型的項目,這個代價是很是昂貴。
Hilt:
BUILD SUCCESSFUL in 35s
27 actionable tasks: 27 executed
Koin:
BUILD SUCCESSFUL in 18s
27 actionable tasks: 27 executed
複製代碼
使用上對比:Hilt 使用起來要比 Koin 麻煩不少,其入門門檻高於 Koin,在閱讀 Hilt 文檔的時候花了好幾天時間才消化,而 Koin 只須要花很短的時間,依賴注入部分的代碼 Hilt 多於 Koin,在一個更大更復雜的項目中所須要的代碼也更多,也愈來愈複雜。
依賴注入框架 | Hilt | Koin |
---|---|---|
代碼行數 | 122 | 42 |
爲何 Hilt 編譯時間老是大於 Koin?
由於在 Koin 中不須要使用註解,也不須要 kapt,這意味着沒有額外的代碼生成,全部的代碼都是 Kotlin 原始代碼,因此說 Hilt 編譯時間老是大於 Koin,從這個角度上同時也解釋了,爲何會說 Koin 僅使用功能解析,無額外代碼生成。
爲何 Koin 不須要用到反射?
由於 Koin 基於 kotlin 基礎上進行開發的,使用了 kotlin 強大的語法糖(例如 Inline、Reified 等等)和函數式編程,來看一個簡單的例子。
inline fun <reified T : ViewModel> Module.viewModel(
qualifier: Qualifier? = null,
override: Boolean = false,
noinline definition: Definition<T>
): BeanDefinition<T> {
val beanDefinition = factory(qualifier, override, definition)
beanDefinition.setIsViewModel()
return beanDefinition
}
複製代碼
內聯函數支持具體化的類型參數,使用 reified 修飾符來限定類型參數,能夠在函數內部訪問它,因爲函數是內聯的,因此不須要反射。
正在創建一個最全、最新的 AndroidX Jetpack 相關組件的實戰項目 以及 相關組件原理分析文章,目前已經包含了 App Startup、Paging三、Hilt 等等,正在逐漸增長其餘 Jetpack 新成員,倉庫持續更新,能夠前去查看:AndroidX-Jetpack-Practice, 若是這個倉庫對你有幫助,請倉庫右上角幫我點個贊。
致力於分享一系列 Android 系統源碼、逆向分析、算法、翻譯、Jetpack 源碼相關的文章,正在努力寫出更好的文章,若是這篇文章對你有幫助給個 star,若是文章中有什麼沒有寫明白的地方,歡迎留言,一塊兒來學習,期待與你一塊兒成長。
因爲 LeetCode 的題庫龐大,每一個分類都能篩選出數百道題,因爲每一個人的精力有限,不可能刷完全部題目,所以我按照經典類型題目去分類、和題目的難易程度去排序。
每道題目都會用 Java 和 kotlin 去實現,而且每道題目都有解題思路、時間複雜度和空間複雜度,若是你同我同樣喜歡算法、LeetCode,能夠關注我 GitHub 上的 LeetCode 題解:Leetcode-Solutions-with-Java-And-Kotlin,一塊兒來學習,期待與你一塊兒成長。
正在寫一系列的 Android 10 源碼分析的文章,瞭解系統源碼,不只有助於分析問題,在面試過程當中,對咱們也是很是有幫助的,若是你同我同樣喜歡研究 Android 源碼,能夠關注我 GitHub 上的 Android10-Source-Analysis,文章都會同步到這個倉庫。
目前正在整理和翻譯一系列精選國外的技術文章,不只僅是翻譯,不少優秀的英文技術文章提供了很好思路和方法,每篇文章都會有譯者思考部分,對原文的更加深刻的解讀,能夠關注我 GitHub 上的 Technical-Article-Translation,文章都會同步到這個倉庫。