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,文章都会同步到这个仓库。