前言
- 原标题: Koin vs Dagger, Say hello to Koin
- 原文地址: blog.usejournal.com/android-koi…
- 原文作者:Farshid ABZ
作者这篇文章到目前为止已经收到了 2.4k+ 的赞,冲上了 Medium 热门,非常好的一篇文章,我也使用 Koin + kotlin + databinding 结合着 inline、reified 强大的特性封装了基础库,包含了 DataBindingActivity、DataBindingFragment、DataBindingDialog、DataBindingListAdapter 等等, 正在陆续添加新的组件。
通过这篇文章你将学习到以下内容,将在译者思考部分会给出相应的答案
- Dagger 和 Koin 优势劣势对比?应该选择 Dagger 还是 Koin?
- koin 语法特性?
- Koin 为什么可以做到无代码生成、无反射?
- Inline 修饰符做什么用的?如何正确使用?带来的性能损失?
- 只是用 Inline 修饰符,为什么编译器会给我们一个警告?
- 为什么编译器建议 inline 修饰符需要和 lambda 表达式一起使用呢?
- 什么时候应该使用 inline 修饰符?
- Reified 修饰符做什么用?如何使用?
- Koin 带来性能损失的那些事?
- Kotlin 用 5 行代码实现快排算法?
这篇文章涉及很多重要的知识点,请耐心读下去,我相信应该会给大家带来很多不一样的东西。
译文
当我正在反复学习 Dagger 的时候,我遇见了 Koin,Koin 不仅节省了我的时间,还提高了效率,将我从复杂 Dagger 给释放出来了。
这篇文章将会告诉你什么是 Koin,与 Dagger 对比有那些优势,以及如何使用 Koin。
是什么 Koin
Koin 是为 Kotlin 开发者提供的一个实用型轻量级依赖注入框架,采用纯 Kotlin 语言编写而成,仅使用功能解析,无代理、无代码生成、无反射。
Dagger vs Koin
为了正确比较这两种方式,我们用 Dagger 和 Koin 去实现了一个项目,项目的架构都是 MVVM,其中用到了 retrofit 和 LiveData,包含了 1 个Activity、4 个 fragments、5 个 view models、1 个 repository 和 1 个 web service 接口, 这应该是一个小型项目的基础架构了
先来看一下 DI 包下的结构,左边是 Dagger,右边是 Koin
如你所见配置 Dagger 需要很多文件 而 Koin 只需要 2 个文件,例如 用 Dagger 注入 1 个 view models 就需要 3 个文件(真的需要用这么多文件吗?)
比较 Dagger 和 Koin 代码行数
我使用 Statistic 工具来统计的,反复对比了项目编译前和编译后,Dagger 和 Koin 生成的代码行数,结果是非常吃惊的
正如你看到的 Dagger 生成的代码行比 Koin 多两倍
Dagger 和 Koin 编译时间怎么样呢
每次编译之前我都会先 clean 然后才会 rebuild,我得到下面这个结果
1 | 复制代码Koin: |
我认为这个结果证明了,如果是在一个更大、更真实的项目中,这个代价是非常昂贵。
Dagger 和 Koin 使用上怎么样呢
如果你想在 MVVM 和 Android Support lib 中使用 Dagger 你必须这么做。
首先在 module gradle 中 添加 Dagger 依赖。
1 | 复制代码kapt "com.google.dagger:dagger-compiler:$dagger_version" |
然后创建完 modules 和 components 文件之后, 需要在 Application 中 初始化 Dagger(或者其他方式初始化 Dagger)。
1 | 复制代码Class MyApplication : Application(), HasActivityInjector { |
所有的 Activity 继承 BaseActivity,我们需要实现 HasSupportFragmentInjector 和 inject DispatchingAndroidInjector。
对于 view models,我们需要在 BaseFragment 中注入 ViewModelFactory,并实现 Injectable。
但这并不是全部。还有更多的事情要做。
对于每一个 ViewModel、Fragment 和 Activity 我们需要告诉 DI 如何注入它们,正如你所见我们有 ActivityModule、FragmentModule、和 ViewModelModule。
我们来看一下下面的代码
1 | 复制代码@Module |
Fragments 如下所示:
1 | 复制代码@Module |
ViewModels 如下所以:
1 | 复制代码@Module |
所以你必须在 DI modules 中添加这些 Fragments、Activities 和 ViewModels。
那么在 Koin 中如何做
你需要在 module gradle 中添加 Koin 依赖
1 | 复制代码implementation "org.koin:koin-android-viewmodel:$koin_version" |
然后我们需要创建 module 文件,稍后我告诉你怎么做,实际上我们并不需要像 Dagger 那么多文件。
Dagger 还有其他问题
学习 Dagger 成本是很高的,如果有人加入你的项目或者团队,他/她不得不花很多时间学习 Dagger,我使用 Dagger 两年了,到现在还不是很了解,每次我开始学习 Andorid 新技术的时候,我不得不去搜索和学习如何用 Dagger 实现新技术。
来看一下 Koin 代码
首先我们需要添加 Koin 依赖,如下所示:
1 | 复制代码implementation "org.koin:koin-android-viewmodel:$koin_version" |
我们使用的是 koin-android-viewmodel 库,因为我们希望在 MVVM 中使用它,当然还有其他的依赖库。
添加完依赖之后,我们来实现第一个 module 文件,像 Dagger 一样,可以在一个单独的文件中实现每个模块,但是由于代码简单,我决定在一个文件中实现所有模块,你也可以把它们分开。
首先我们需要了解一下 koin 语法特性
- get(): 解析 Koin 模块中的实例,调用 get() 函数解析所请求组件需要的实例,这个 get() 函数通常用于构造函数中,注入构造函数值
- factory:声明这是一个工厂组件,每次请求都为您提供一个新实例
- single:采用单例设计模式
- name:用于命名定义,当您希望具有不同类型的同一个类的多个实例时,需要使用它
我们没有创建具有多个注释和多个组件的许多文件,而是为 DI 注入每个类的时候,提供一个简单、可读的文件。
了解完 koin 语法特性之后,我们来解释下面代码什么意思
1 | 复制代码private val retrofit: Retrofit = createNetworkClient() |
createNetworkClient 方法创建 Retrofit 实例,设置 baseUrl,添加 ConverterFactory 和 Interceptor
1 | 复制代码private val generalApi: GeneralApi = retrofit.create(GeneralApi::class.java) |
AuthApi 和 GeneralApi 是 retrofit 接口
1 | 复制代码val viewModelModule = module { |
在 module 文件中声明为 viewModel, Koin 将会向 ViewModelFactory 提供 viewModel,将其绑定到当前组件。
正如你所见,在 LoginFragmentViewModel 构造函数中有调用了 get() 方法,get() 会解析一个 LoginFragmentViewModel 需要的参数,然后传递给 LoginFragmentViewModel,这个参数就是 AuthRepo。
最后在 Application onCreate 方法中添加如下代码
1 | 复制代码startKoin(this, listOf(repositoryModule, networkModule, viewModelModule)) |
这里只是调用 startKoin 方法,传入一个上下文和一个希望用来初始化 Koin 的模块列表。
现在使用 ViewModel 比使用纯 ViewModel 更容易,在 Fragment 和 Activity 视图中添加下面的代码
1 | 复制代码private val startPageViewModel: StartPageViewModel by viewModel() |
通过这段代码,koin 为您创建了一个 StartPageViewModel 对象,现在你可以在 Fragment 和 Activity 中使用 view model
译者思考
作者总共从以下 4 个方面对比了 Dagger 和 Kotlin:
- 文件数量:基于 mvvm 架构,分别使用了 Dagger 和 koltin 作为依赖注入框架,初始化 Dagger 时至少需要 9 个文件,而 koltin 只需要 2 个文件,Dagger 文件数量远超过 koltin
- 代码行数:作者使用了 Statistic 工具,反复对比了项目编译前和编译后,Dagger 和 Koin 生成的代码行数,如下图所示
- 反复的对比了 Dagger 和 Koin 编译时间,结果如下所示 koin 比 Dagger 快
1 | 复制代码Koin: |
- 学习成本巨大,如果使用了 Dagger 朋友,应该和作者的感觉是一样的,Dagger 学习的成本是非常高的,如果项目中引入了 Dagger 意味着团队每个人都要学习 Dagger,无疑这个成本是巨大的,而且使用起来非常的复杂
注意:作者在 Application 中调用 startKoin 方法初始化 Koin 的模块列表,是 Koin 1X 的方式,Koin 团队在 2x 的时候做了很多改动(下面会介绍),初始化 Koin 的模块列有所改动,代码如下所示:
1 | 复制代码startKoin { |
Koin 为什么可以做到无代码生成、无反射
Koin 作为一个轻量级依赖注入框架,为什么可以做到无代码生成、无反射?因为 kotlin 强大的语法糖(例如 Inline、Reified 等等)和函数式编程,我们先来看一段代码。
koin-projects/koin-core/src/main/kotlin/org/koin/dsl/Module.kt
案例一
1 | 复制代码// typealias 是用来为已经存在的类型重新定义名字的 |
Module 是一个 lambda 表达式,才可以在 “{}” 里面自由定义 single 和 factory,会等到你需要的时候才会执行。
案例二
1 | 复制代码inline fun <reified T : ViewModel> Module.viewModel( |
内联函数支持具体化的类型参数,使用 reified 修饰符来限定类型参数,以在函数内部访问它了,由于函数是内联的,不需要反射,通过上面两个案例,说明了为什么 Koin 可以做到无代码生成、无反射。建议大家都去看看 Koin 的源码,能够从中学到很多技巧,后面我会花好几篇文章分析 Koin 源码。
Inline 修饰符带来的性能损失
Inline (内联函数) 的作用:提升运行效率,调用被 inline 修饰符的函数,会把里面的代码放到我调用的地方。
如果阅读过 Koin 源码的朋友,应该会发现 inline 都是和 lambda 表达式和 reified 修饰符配套在一起使用的,如果只使用 inline 修饰符标记函数会怎么样?
只使用 inline 修饰符会有性能问题,在这篇文章 Consider inline modifier for higher-order functions 也分析了只使用 inline 修饰符为什么会带来性能问题,并且 Android Studio 也会给一个大大大的警告。
编译器建议我们在含有 lambda 表达式作为形参的函数中使用内联,既然 Inline 修饰符可以提升运行效率,为什么编译器会给我们一个警告? 为什么编译器建议 inline 修饰符需要和 lambda 表达式一起使用呢?
1. 既然 Inline 修饰符可以提升运行效率,为什么编译器会给我们一个警告?
刚才我们说过调用被 inline 修饰符的函数,会把里面的代码放到我调用的地方,来看一下下面这段代码。
1 | 复制代码inline fun twoPrintTwo() { |
执行完最后一个方法 twoTwoTwoTwoPrintTwo,反编译出来的结果是非常令人吃惊的,结果如下所示:
1 | 复制代码public static final void twoTwoTwoTwoPrintTwo() { |
这显示了使用 Inline 修饰符的主要问题,当我们过度使用它们时,代码会快速增长。这就是为什么 IntelliJ 在我们使用它的时候会给出警告。
2. 为什么编译器建议 inline 修饰符需要和 lambda 表达式一起使用呢?
因为 JVM 是不支持 lambda 表达式的,非内联函数中的 Lambda 表达式会被编译为匿名类,这对性能开销是非常巨大的,而且它们的创建和使用都较慢,当我们使用 inline 修饰符时,我们根本不需要创建任何其他类,来看一下下面代码。
1 | 复制代码fun main(args: Array<String>) { |
编译结果如下:
1 | 复制代码// Java 代码 |
那么我们应该在什么时候使用 inline 修饰符呢?
使用 inline 修饰符时最常见的场景就是把函数作为另一个函数的参数时(高阶函数),例如 filter、map、joinToString 或者一些独立的函数 repeat。
如果没有函数类型作为参数,也没有 reified 实化类型参数时,不应该使用 inline 修饰符了。
从分析 Koin 源码,inline 应该 lambda 表达式或者 reified 修饰符配合在一起使用的,另外 Android Studio 越来越智能了,如果在不正确的地方使用,会有一个大大大的警告。
Reified 修饰符,具体化的类型参数
reified (具体化的类型参数):使用 reified 修饰符来限定类型参数,结合着 inline 修饰符具体化的类型参数,可以直接在函数内部访问它。
我想分享两个使用 Reified 修饰符很常见的例子 reified-type-parameters,使用 Java 是不可能实现的。
案例一:
1 | 复制代码inline fun <reified T> Gson.fromJson(json: String) = |
案例二:
1 | 复制代码inline fun <reified T: Activity> Context.startActivity(vararg params: Pair<String, Any?>) = |
Koin 带来性能损失的那些事
思考了很久需不需要写这部分内容,因为在 Koin 2x 的版本的时候已经修复了,这是官方的链接 News from the trenches — What’s next for Koin?,后来想想还是写写吧,作为自己的一个学习笔记。
这个源于有个人开了一个 Issue(Bad performance in some Android devices) 现在已经被关闭了,他指出了当 Dependency 数量越来越多的时候,Koin 效能会越来越差,而且还做了一个对比如下图所示:
如果使用过 Koin 1x 的朋友应该会感觉到,引入 Koin 1x 冷启动时间边长了,而且在有大量依赖的时候,查找的时间会有点长,后来 Koin 团队也发现确实存在这个问题,到底是怎么回事呢?
他们在 BeanRegistry 类中维护了一个列表,用来存储了 BeanDefinition,然后使用 Kotlin 的 filter 函数找出对应的 BeanDefinition,所以找出一个 Definition 时间复杂度是 O(n),如果平均有 M 层 Dependency,那么时间复杂度会变成 O(m*n)。
Koin 团队的解决方案是用了 HashMap,使用空间换取时间,查找一个 Definition 时间复杂度变成了 O(1),优化之后的结果如下:
Koin 2x 不仅在性能优化上有很大的提升,也拓展了很多新的特性,例如 FragmentFactory 能够依赖注入到 Fragments 中就像 ViewModels 一样,还有自动拆箱等等,在后面的文章会详细的分析一下。
Kotlin 用 5 行代码实现快排算法
我想分享一个快速排序算法,这是一个很酷的函数编程的例子 share cool examples,当我看到这段代码的时候惊呆了,居然还可以这么写。
1 | 复制代码fun <T : Comparable<T>> List<T>.quickSort(): List<T> = |
最后分享一个译者自己撸的导航网站
译者基于 Material Design 的响应式框架,撸了一个 “为互联网人而设计 国内国外名站导航“ ,收集了国内外热门网址,涵括新闻、体育、生活、娱乐、设计、产品、运营、前端开发、Android开发等等导航网站,如果你有什么好的建议,也可以留言,点击前去浏览 如果对你有帮助,请帮我点个赞,感谢
ps: 网站中的地址如果有原作者不希望展示的,可以留言告诉我,我会立刻删除
国际资讯网址大全
Android 网址大全
参考文献
- My favorite examples of functional programming in Kotlin
- koin 官网:https://insert-koin.io/
- Effective Kotlin: Consider inline modifier for higher-order functions
结语
致力于分享一系列 Android 系统源码、逆向分析、算法、翻译相关的文章,目前正在翻译一系列欧美精选文章,不仅仅是翻译,更重要的是翻译背后对每篇文章思考,如果你喜欢这片文章,请帮我点个赞,感谢,期待与你一起成长。
计划建立一个最全、最新的 AndroidX Jetpack 相关组件的实战项目 以及 相关组件原理分析文章,目前已经包含了 App Startup、Paging3、Hilt 等等,正在逐渐增加其他 Jetpack 新成员,仓库持续更新,可以前去查看:AndroidX-Jetpack-Practice, 如果这个仓库对你有帮助,请帮我点个赞,我会陆续完成更多 Jetpack 新成员的项目实践。
算法
由于 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 视图绑定以及体系结构
- 更多
Android 应用系列
- 如何高效获取视频截图
- 如何在项目中封装 Kotlin + Android Databinding
- [译][Google工程师] 刚刚发布了 Fragment 的新特性 “Fragment 间传递数据的新方式” 以及源码分析
工具系列
逆向系列
本文转载自: 掘金