【译】【24K Star】 放弃 Dagger 拥抱 Ko

前言

作者这篇文章到目前为止已经收到了 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
2
3
4
5
6
7
复制代码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

我认为这个结果证明了,如果是在一个更大、更真实的项目中,这个代价是非常昂贵。

Dagger 和 Koin 使用上怎么样呢

如果你想在 MVVM 和 Android Support lib 中使用 Dagger 你必须这么做。

首先在 module gradle 中 添加 Dagger 依赖。

1
2
3
复制代码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)。

1
2
3
4
5
6
7
8
9
10
11
12
复制代码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。

我们来看一下下面的代码

1
2
3
4
5
6
7
复制代码@Module
abstract class ActivityModule {
@ContributesAndroidInjector(modules = [FragmentModule::class])
abstract fun contributeMainActivity(): MainActivity

//Add your other activities here
}

Fragments 如下所示:

1
2
3
4
5
6
7
8
9
10
11
复制代码@Module
abstract class FragmentModule {
@ContributesAndroidInjector
abstract fun contributeLoginFragment(): LoginFragment

@ContributesAndroidInjector
abstract fun contributeRegisterFragment(): RegisterFragment

@ContributesAndroidInjector
abstract fun contributeStartPageFragment(): StartPageFragment
}

ViewModels 如下所以:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码@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 依赖

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
2
复制代码private val generalApi: GeneralApi =  retrofit.create(GeneralApi::class.java)
private val authApi: AuthApi = retrofit.create(AuthApi::class.java)

AuthApi 和 GeneralApi 是 retrofit 接口

1
2
3
4
复制代码val viewModelModule = module {
viewModel { LoginFragmentViewModel(get()) }
viewModel { StartPageViewModel() }
}

在 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
2
3
4
5
6
7
复制代码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
  • 学习成本巨大,如果使用了 Dagger 朋友,应该和作者的感觉是一样的,Dagger 学习的成本是非常高的,如果项目中引入了 Dagger 意味着团队每个人都要学习 Dagger,无疑这个成本是巨大的,而且使用起来非常的复杂

注意:作者在 Application 中调用 startKoin 方法初始化 Koin 的模块列表,是 Koin 1X 的方式,Koin 团队在 2x 的时候做了很多改动(下面会介绍),初始化 Koin 的模块列有所改动,代码如下所示:

1
2
3
4
5
6
7
8
复制代码startKoin {
// Use Koin Android Logger
androidLogger()
// declare Android context
androidContext(this@MainApplication)
// declare modules to use
modules(module1, module2 ...)
}

Koin 为什么可以做到无代码生成、无反射

Koin 作为一个轻量级依赖注入框架,为什么可以做到无代码生成、无反射?因为 kotlin 强大的语法糖(例如 Inline、Reified 等等)和函数式编程,我们先来看一段代码。

koin-projects/koin-core/src/main/kotlin/org/koin/dsl/Module.kt

案例一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码//  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,会等到你需要的时候才会执行。

案例二

1
2
3
4
5
6
7
8
9
复制代码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 (内联函数) 的作用:提升运行效率,调用被 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码inline fun twoPrintTwo() {
print(2)
print(2)
}

inline fun twoTwoPrintTwo() {
twoPrintTwo()
twoPrintTwo()
}

inline fun twoTwoTwoPrintTwo() {
twoTwoPrintTwo()
twoTwoPrintTwo()
}

fun twoTwoTwoTwoPrintTwo() {
twoTwoTwoPrintTwo()
twoTwoTwoPrintTwo()
}

执行完最后一个方法 twoTwoTwoTwoPrintTwo,反编译出来的结果是非常令人吃惊的,结果如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
复制代码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 修饰符时,我们根本不需要创建任何其他类,来看一下下面代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码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
}
}

编译结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码// 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 (具体化的类型参数):使用 reified 修饰符来限定类型参数,结合着 inline 修饰符具体化的类型参数,可以直接在函数内部访问它。

我想分享两个使用 Reified 修饰符很常见的例子 reified-type-parameters,使用 Java 是不可能实现的。

案例一:

1
2
3
4
5
6
复制代码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)

案例二:

1
2
复制代码inline fun <reified T: Activity> Context.startActivity(vararg params: Pair<String, Any?>) =
AnkoInternals.internalStartActivity(this, T::class.java, params)

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
2
3
4
5
6
7
8
9
10
复制代码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 系统源码、逆向分析、算法、翻译相关的文章,目前正在翻译一系列欧美精选文章,不仅仅是翻译,更重要的是翻译背后对每篇文章思考,如果你喜欢这片文章,请帮我点个赞,感谢,期待与你一起成长。

计划建立一个最全、最新的 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,文章都会同步到这个仓库

Android 应用系列

工具系列

逆向系列

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%