前言
Hilt 系列文章
Koin、Dagger、Hilt 目前都是非常流行的库,面对这么多层出不穷的新技术,我们该做如何选择,是一直困扰我们的一个问题,之前我分析过 Koin 和 Dagger 的性能对比,Hilt 与 Dagger 的不同之处,可以点击下方链接前往查看。
这是 Hilt 系列的第四篇,主要来分析 Hilt 和 Koin 的性能,如果你之前对 Hilt 和 Koin 不了解也没有关系,对阅读本文没有什么影响,接下来将会从以下几个方面来分析 Hilt 和 Koin 不同之处。
- 依赖注入的优点?
- Koin 为什么可以做到无代码生成、无反射?
- AndroidStudio 支持 Hilt 和 Koin 在关联代码间进行导航吗?
- Hilt 和 Koin 谁的编译速度更快?以及为什么?
- 比较 Hilt 和 Koin 代码行数?
- Hilt 和 Koin 在使用上的区别,谁上手最快?
依赖注入的优点
Koin 是为 Kotlin 开发者提供的一个实用型轻量级依赖注入框架,采用纯 Kotlin 语言编写而成,仅使用功能解析,无代理、无代码生成、无反射。
Hilt 是在 Dagger 基础上进行开发的,减少了在项目中进行手动依赖,Hilt 集成了 Jetpack 库和 Android 框架类,并删除了大部分模板代码,让开发者只需要关注如何进行绑定,同时 Hilt 也继承了 Dagger 优点,编译时正确性、运行时性能、并且得到了 Android Studio 的支持。
Hilt、Dagger、Koin 等等都是依赖注入库,依赖注入是面向对象设计中最好的架构模式之一,使用依赖注入库有以下优点:
- 依赖注入库会自动释放不再使用的对象,减少资源的过度使用。
- 在指定范围内,可重用依赖项和创建的实例,提高代码的可重用性,减少了很多模板代码。
- 代码变得更具可读性。
- 易于构建对象。
- 编写低耦合代码,更容易测试。
Hilt VS Koin
接下来将从 AndroidStudio 基础支持、项目结构、代码行数、编译时间、使用上的不同,这几个方面对 Hilt 和 Koin 进行全方面的分析。
Android Studio 强大的基础支持
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 的版本,增加的 "间距图标",帮助我们解决了这个问题。
比较 Hilt 和 Koin 项目结构
为了能够正确比较这两种方式,新建了两个项目 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 的方式只需要一个文件就可以完成,后面我会进行详细的分析。
比较 Koin 和 Hilt 代码行数
项目 Project-Hilt 和 Project-Koin 除了它们本身的依赖之外,其他的依赖都是相同的。
我使用 Statistic 工具来进行代码行数的统计,反复对比了项目编译前和编译后,它们的结果如下所示:
代码行数 | Hilt | Koin |
编译之前 | 2414 | 2414 |
编译之后 | 149608 | 138405 |
正如你所见 Hilt 生成的代码多于 Koin,随着项目越来越复杂,生成的代码量会越来越多。
比较 Koin 和 Hilt 编译时间
为了保证测试的准确性,每次编译之前我都会先 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 仅使用功能解析,无额外代码生成。
Koin 和 Hilt 使用上的不同
为了节省篇幅,这里只会列出部分代码,具体详细使用参考我之前写的 Hilt 入门三部曲,包含了 Hilt 所有的用法以及实战案例。
- Jetpack 新成员 Hilt 实践(一)启程过坑记:介绍了 Hilt 的常用注解、以及在实践过程中遇到的一些坑,Hilt 如何与 Android 框架类进行绑定,以及他们的生命周期。
- Jetpack 新成员 Hilt 实践之 App Startup(二)进阶篇:分析注解的区别、限定符和作用域注解的使用,以及如何在 ViewModel、App Startup、ContentProvider 中使用等等。
- Jetpack 新成员 Hilt 与 Dagger 大不同(三)落地篇:Hilt 与 Dagger 不同之处,以及在多模块中局限性以及使用。
在项目中使用 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、Paging3、Hilt 等等,正在逐渐增加其他 Jetpack 新成员,仓库持续更新,可以前去查看:AndroidX-Jetpack-Practice, 如果这个仓库对你有帮助,请仓库右上角帮我点个赞。
结语
公众号开通了:ByteCode , 欢迎小伙伴们前去查看 Jetpack ,Kotlin ,Android 10 系列源码,译文,LeetCode / 剑指 Offer / 国内外大厂算法题 等等一系列文章,如果对你有帮助,请帮我点个 star,感谢!!!,欢迎一起来学习,在技术的道路上一起前进。
算法
由于 LeetCode 的题库庞大,每个分类都能筛选出数百道题,由于每个人的精力有限,不可能刷完所有题目,因此我按照经典类型题目去分类、和题目的难易程度去排序。
- 数据结构: 数组、栈、队列、字符串、链表、树……
- 算法: 查找算法、搜索算法、位运算、排序、数学、……
每道题目都会用 Java 和 kotlin 去实现,并且每道题目都有解题思路、时间复杂度和空间复杂度,如果你同我一样喜欢算法、LeetCode,可以关注我 GitHub 上的 LeetCode 题解:Leetcode-Solutions-with-Java-And-Kotlin,一起来学习,期待与你一起成长。
Android 10 源码系列
正在写一系列的 Android 10 源码分析的文章,了解系统源码,不仅有助于分析问题,在面试过程中,对我们也是非常有帮助的,如果你同我一样喜欢研究 Android 源码,可以关注我 GitHub 上的 Android10-Source-Analysis,文章都会同步到这个仓库。
- 0xA01 Android 10 源码分析:APK 是如何生成的
- 0xA02 Android 10 源码分析:APK 的安装流程
- 0xA03 Android 10 源码分析:APK 加载流程之资源加载
- 0xA04 Android 10 源码分析:APK 加载流程之资源加载(二)
- 0xA05 Android 10 源码分析:Dialog 加载绘制流程以及在 Kotlin、DataBinding 中的使用
- 0xA06 Android 10 源码分析:WindowManager 视图绑定以及体系结构
- 0xA07 Android 10 源码分析:Window 的类型 以及 三维视图层级分析
- 更多......
Android 应用系列
- 如何在项目中封装 Kotlin + Android Databinding
- 再见吧 buildSrc, 拥抱 Composing builds 提升 Android 编译速度
- 为数不多的人知道的 Kotlin 技巧以及 原理解析
- Jetpack 最新成员 AndroidX App Startup 实践以及原理分析
- Jetpack 成员 Paging3 实践以及源码分析(一)
- Jetpack 新成员 Paging3 网络实践及原理分析(二)
- Jetpack 新成员 Hilt 实践(一)启程过坑记
- Jetpack 新成员 Hilt 实践之 App Startup(二)进阶篇
- Jetpack 新成员 Hilt 与 Dagger 大不同(三)落地篇
精选译文
目前正在整理和翻译一系列精选国外的技术文章,不仅仅是翻译,很多优秀的英文技术文章提供了很好思路和方法,每篇文章都会有译者思考部分,对原文的更加深入的解读,可以关注我 GitHub 上的 Technical-Article-Translation,文章都会同步到这个仓库。
- [译][Google工程师] 刚刚发布了 Fragment 的新特性 “Fragment 间传递数据的新方式” 以及源码分析
- [译][Google工程师] 详解 FragmentFactory 如何优雅使用 Koin 以及部分源码分析
- [译][2.4K Start] 放弃 Dagger 拥抱 Koin
- [译][5k+] Kotlin 的性能优化那些事
- [译] 解密 RxJava 的异常处理机制
- [译][1.4K+ Star] Kotlin 新秀 Coil VS Glide and Picasso
- 更多......
工具系列
- 为数不多的人知道的 AndroidStudio 快捷键(一)
- 为数不多的人知道的 AndroidStudio 快捷键(二)
- 关于 adb 命令你所需要知道的
- 10分钟入门 Shell 脚本编程
- 基于 Smali 文件 Android Studio 动态调试 APP
- 解决在 Android Studio 3.2 找不到 Android Device Monitor 工具