- 原标题: 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,Koin 不仅节省了我的时间,还提高了效率,将我从复杂 Dagger 给释放出来了。
这篇文章将会告诉你什么是 Koin,与 Dagger 对比有那些优势,以及如何使用 Koin。
Koin 是为 Kotlin 开发者提供的一个实用型轻量级依赖注入框架,采用纯 Kotlin 语言编写而成,仅使用功能解析,无代理、无代码生成、无反射。
为了正确比较这两种方式,我们用 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 个文件(真的需要用这么多文件吗?)
我使用 Statistic 工具来统计的,反复对比了项目编译前和编译后,Dagger 和 Koin 生成的代码行数,结果是非常吃惊的
正如你看到的 Dagger 生成的代码行比 Koin 多两倍
每次编译之前我都会先 clean 然后才会 rebuild,我得到下面这个结果
Koin: BUILD SUCCESSFUL in 17s 88 actionable tasks: 83 executed, 5 up-to-date Dagger: BUILD SUCCESSFUL in 18s 88 actionable tasks: 83 executed, 5 up-to-date 复制代码
我认为这个结果证明了,如果是在一个更大、更真实的项目中,这个代价是非常昂贵。
如果你想在 MVVM 和 Android Support lib 中使用 Dagger 你必须这么做。
首先在 module gradle 中 添加 Dagger 依赖。
kapt "com.google.dagger:dagger-compiler:$dagger_version" kapt "com.google.dagger:dagger-android-processor:$dagger_version" implementation "com.google.dagger:dagger:$dagger_version" 复制代码
然后创建完 modules 和 components 文件之后, 需要在 Application 中 初始化 Dagger(或者其他方式初始化 Dagger)。
Class MyApplication : Application(), HasActivityInjector { @Inject lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Activity> override fun activityInjector() = dispatchingAndroidInjector fun initDagger() { DaggerAppComponent .builder() .application(this) .build() .inject(this) } } 复制代码
所有的 Activity 继承 BaseActivity,我们需要实现 HasSupportFragmentInjector 和 inject DispatchingAndroidInjector。
对于 view models,我们需要在 BaseFragment 中注入 ViewModelFactory,并实现 Injectable。
但这并不是全部。还有更多的事情要做。
对于每一个 ViewModel、Fragment 和 Activity 我们需要告诉 DI 如何注入它们,正如你所见我们有 ActivityModule、FragmentModule、和 ViewModelModule。
我们来看一下下面的代码
@Module abstract class ActivityModule { @ContributesAndroidInjector(modules = [FragmentModule::class]) abstract fun contributeMainActivity(): MainActivity //Add your other activities here } 复制代码
Fragments 如下所示:
@Module abstract class FragmentModule { @ContributesAndroidInjector abstract fun contributeLoginFragment(): LoginFragment @ContributesAndroidInjector abstract fun contributeRegisterFragment(): RegisterFragment @ContributesAndroidInjector abstract fun contributeStartPageFragment(): StartPageFragment } 复制代码
ViewModels 如下所以:
@Module abstract class ViewModelModule { @Binds abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory @Binds @IntoMap @ViewModelKey(loginViewModel::class) abstract fun bindLoginFragmentViewModel(loginViewModel: loginViewModel): ViewModel @Binds @IntoMap @ViewModelKey(StartPageViewModel::class) abstract fun bindStartPageViewModel(startPageViewModel: StartPageViewModel): ViewModel ...... } 复制代码
所以你必须在 DI modules 中添加这些 Fragments、Activities 和 ViewModels。
那么在 Koin 中如何做
你需要在 module gradle 中添加 Koin 依赖
implementation "org.koin:koin-android-viewmodel:$koin_version" 复制代码
然后我们需要创建 module 文件,稍后我告诉你怎么做,实际上我们并不需要像 Dagger 那么多文件。
Dagger 还有其他问题
学习 Dagger 成本是很高的,如果有人加入你的项目或者团队,他/她不得不花很多时间学习 Dagger,我使用 Dagger 两年了,到现在还不是很了解,每次我开始学习 Andorid 新技术的时候,我不得不去搜索和学习如何用 Dagger 实现新技术。
来看一下 Koin 代码
首先我们需要添加 Koin 依赖,如下所示:
implementation "org.koin:koin-android-viewmodel:$koin_version" 复制代码
我们使用的是 koin-android-viewmodel 库,因为我们希望在 MVVM 中使用它,当然还有其他的依赖库。
添加完依赖之后,我们来实现第一个 module 文件,像 Dagger 一样,可以在一个单独的文件中实现每个模块,但是由于代码简单,我决定在一个文件中实现所有模块,你也可以把它们分开。
首先我们需要了解一下 koin 语法特性
我们没有创建具有多个注释和多个组件的许多文件,而是为 DI 注入每个类的时候,提供一个简单、可读的文件。
了解完 koin 语法特性之后,我们来解释下面代码什么意思
private val retrofit: Retrofit = createNetworkClient() 复制代码
createNetworkClient 方法创建 Retrofit 实例,设置 baseUrl,添加 ConverterFactory 和 Interceptor
private val generalApi: GeneralApi = retrofit.create(GeneralApi::class.java) private val authApi: AuthApi = retrofit.create(AuthApi::class.java) 复制代码
AuthApi 和 GeneralApi 是 retrofit 接口
val viewModelModule = module { viewModel { LoginFragmentViewModel(get()) } viewModel { StartPageViewModel() } } 复制代码
在 module 文件中声明为 viewModel, Koin 将会向 ViewModelFactory 提供 viewModel,将其绑定到当前组件。
正如你所见,在 LoginFragmentViewModel 构造函数中有调用了 get() 方法,get() 会解析一个 LoginFragmentViewModel 需要的参数,然后传递给 LoginFragmentViewModel,这个参数就是 AuthRepo。
最后在 Application onCreate 方法中添加如下代码
startKoin(this, listOf(repositoryModule, networkModule, viewModelModule)) 复制代码
这里只是调用 startKoin 方法,传入一个上下文和一个希望用来初始化 Koin 的模块列表。
现在使用 ViewModel 比使用纯 ViewModel 更容易,在 Fragment 和 Activity 视图中添加下面的代码
private val startPageViewModel: StartPageViewModel by viewModel() 复制代码
通过这段代码,koin 为您创建了一个 StartPageViewModel 对象,现在你可以在 Fragment 和 Activity 中使用 view model
作者总共从以下 4 个方面对比了 Dagger 和 Kotlin:
Koin: BUILD SUCCESSFUL in 17s 88 actionable tasks: 83 executed, 5 up-to-date Dagger: BUILD SUCCESSFUL in 18s 88 actionable tasks: 83 executed, 5 up-to-date 复制代码
注意:作者在 Application 中调用 startKoin 方法初始化 Koin 的模块列表,是 Koin 1X 的方式,Koin 团队在 2x 的时候做了很多改动(下面会介绍),初始化 Koin 的模块列有所改动,代码如下所示:
startKoin { // Use Koin Android Logger androidLogger() // declare Android context androidContext(this@MainApplication) // declare modules to use modules(module1, module2 ...) } 复制代码
Koin 作为一个轻量级依赖注入框架,为什么可以做到无代码生成、无反射?因为 kotlin 强大的语法糖(例如 Inline、Reified 等等)和函数式编程,我们先来看一段代码。
koin-projects/koin-core/src/main/kotlin/org/koin/dsl/Module.kt
案例一
// typealias 是用来为已经存在的类型重新定义名字的 typealias ModuleDeclaration = Module.() -> Unit fun module(createdAtStart: Boolean = false, override: Boolean = false, moduleDeclaration: ModuleDeclaration): Module { // 创建 Module val module = Module(createdAtStart, override) // 执行匿扩展函数 moduleDeclaration(module) return module } // 如何使用 val mModule: Module = module { single { ... } factory { ... } } 复制代码
Module 是一个 lambda 表达式,才可以在 “{}” 里面自由定义 single 和 factory,会等到你需要的时候才会执行。
案例二
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 可以做到无代码生成、无反射。建议大家都去看看 Koin 的源码,能够从中学到很多技巧,后面我会花好几篇文章分析 Koin 源码。
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 修饰符的函数,会把里面的代码放到我调用的地方,来看一下下面这段代码。
inline fun twoPrintTwo() { print(2) print(2) } inline fun twoTwoPrintTwo() { twoPrintTwo() twoPrintTwo() } inline fun twoTwoTwoPrintTwo() { twoTwoPrintTwo() twoTwoPrintTwo() } fun twoTwoTwoTwoPrintTwo() { twoTwoTwoPrintTwo() twoTwoTwoPrintTwo() } 复制代码
执行完最后一个方法 twoTwoTwoTwoPrintTwo,反编译出来的结果是非常令人吃惊的,结果如下所示:
public static final void twoTwoTwoTwoPrintTwo() { byte var1 = 2; System.out.print(var1); var1 = 2; System.out.print(var1); var1 = 2; System.out.print(var1); var1 = 2; System.out.print(var1); var1 = 2; System.out.print(var1); var1 = 2; System.out.print(var1); var1 = 2; System.out.print(var1); var1 = 2; System.out.print(var1); var1 = 2; System.out.print(var1); var1 = 2; System.out.print(var1); var1 = 2; System.out.print(var1); var1 = 2; System.out.print(var1); var1 = 2; System.out.print(var1); var1 = 2; System.out.print(var1); var1 = 2; System.out.print(var1); var1 = 2; System.out.print(); } 复制代码
这显示了使用 Inline 修饰符的主要问题,当我们过度使用它们时,代码会快速增长。这就是为什么 IntelliJ 在我们使用它的时候会给出警告。
2. 为什么编译器建议 inline 修饰符需要和 lambda 表达式一起使用呢?
因为 JVM 是不支持 lambda 表达式的,非内联函数中的 Lambda 表达式会被编译为匿名类,这对性能开销是非常巨大的,而且它们的创建和使用都较慢,当我们使用 inline 修饰符时,我们根本不需要创建任何其他类,来看一下下面代码。
fun main(args: Array<String>) { var a = 0 // 带 inline 的 Lambda 表达式 repeat(100_000_000) { a += 1 } var b = 0 // 不带 inline 的 Lambda 表达式 noinlineRepeat(100_000_000) { b += 1 } } 复制代码
编译结果如下:
// Java 代码 public static final void main(@NotNull String[] args) { int a = 0; // 带 inline 的 Lambda 表达式, 会把里面的代码放到我调用的地方 int times$iv = 100000000; int var3 = 0; for(int var4 = times$iv; var3 < var4; ++var3) { ++a; } // 不带 inline 的 Lambda 表达式,会被编译为匿名类 final IntRef b = new IntRef(); b.element = 0; noinlineRepeat(100000000, (Function1)(new Function1() { public Object invoke(Object var1) { ++b.element; return Unit.INSTANCE; } })); } 复制代码
那么我们应该在什么时候使用 inline 修饰符呢?
使用 inline 修饰符时最常见的场景就是把函数作为另一个函数的参数时(高阶函数),例如 filter、map、joinToString 或者一些独立的函数 repeat。
如果没有函数类型作为参数,也没有 reified 实化类型参数时,不应该使用 inline 修饰符了。
从分析 Koin 源码,inline 应该 lambda 表达式或者 reified 修饰符配合在一起使用的,另外 Android Studio 越来越智能了,如果在不正确的地方使用,会有一个大大大的警告。
reified (具体化的类型参数):使用 reified 修饰符来限定类型参数,结合着 inline 修饰符具体化的类型参数,可以直接在函数内部访问它。
我想分享两个使用 Reified 修饰符很常见的例子 reified-type-parameters,使用 Java 是不可能实现的。
案例一:
inline fun <reified T> Gson.fromJson(json: String) = fromJson(json, T::class.java) // 使用 val user: User = Gson().fromJson(json) val user = Gson().fromJson<User>(json) 复制代码
案例二:
inline fun <reified T: Activity> Context.startActivity(vararg params: Pair<String, Any?>) = AnkoInternals.internalStartActivity(this, T::class.java, params) 复制代码
思考了很久需不需要写这部分内容,因为在 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 一样,还有自动拆箱等等,在后面的文章会详细的分析一下。
我想分享一个快速排序算法,这是一个很酷的函数编程的例子 share cool examples,当我看到这段代码的时候惊呆了,居然还可以这么写。
fun <T : Comparable<T>> List<T>.quickSort(): List<T> = if(size < 2) this else { val pivot = first() val (smaller, greater) = drop(1).partition { it <= pivot} smaller.quickSort() + pivot + greater.quickSort() } // 使用 [2,5,1] -> [1,2,5] listOf(2,5,1).quickSort() // [1,2,5] 复制代码
译者基于 Material Design 的响应式框架,撸了一个 "为互联网人而设计 国内国外名站导航" ,收集了国内外热门网址,涵括新闻、体育、生活、娱乐、设计、产品、运营、前端开发、Android开发等等导航网站,如果你有什么好的建议,也可以留言,点击前去浏览 如果对你有帮助,请帮我点个赞,感谢
ps: 网站中的地址如果有原作者不希望展示的,可以留言告诉我,我会立刻删除
国际资讯网址大全
Android 网址大全
致力于分享一系列 Android 系统源码、逆向分析、算法、翻译相关的文章,目前正在翻译一系列欧美精选文章,不仅仅是翻译,更重要的是翻译背后对每篇文章思考,如果你喜欢这片文章,请帮我点个赞,感谢,期待与你一起成长。