[2.4K Start] 放弃 Dagger 拥抱 Koin

一名优秀的Android开发,需要一份完备的 知识体系,在这里,让我们一起成长为自己所想的那样~。

当我正在反复学习 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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0ww5TaQu-1589549024727)(https://upload-images.jianshu.io/upload_images/13841068-b52c35f79a89c41c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]

如你所见配置 Dagger 需要很多文件 而 Koin 只需要 2 个文件,例如 用 Dagger 注入 1 个 view models 就需要 3 个文件(真的需要用这么多文件吗?)

比较 Dagger 和 Koin 代码行数

我使用 Statistic 工具来统计的,反复对比了项目编译前和编译后,Dagger 和 Koin 生成的代码行数,结果是非常吃惊的

正如你看到的 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

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

Dagger 和 Koin 使用上怎么样呢

如果你想在 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 语法特性

  • get(): 解析 Koin 模块中的实例,调用 get() 函数解析所请求组件需要的实例,这个 get() 函数通常用于构造函数中,注入构造函数值
  • factory:声明这是一个工厂组件,每次请求都为您提供一个新实例
  • single:采用单例设计模式
  • name:用于命名定义,当您希望具有不同类型的同一个类的多个实例时,需要使用它

我们没有创建具有多个注释和多个组件的许多文件,而是为 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:

  • 文件数量:基于 mvvm 架构,分别使用了 Dagger 和 koltin 作为依赖注入框架,初始化 Dagger 时至少需要 9 个文件,而 koltin 只需要 2 个文件,Dagger 文件数量远超过 koltin
  • 代码行数:作者使用了 Statistic 工具,反复对比了项目编译前和编译后,Dagger 和 Koin 生成的代码行数,如下图所示

  • 反复的对比了 Dagger 和 Koin 编译时间,结果如下所示 koin 比 Dagger 快
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 的模块列有所改动,代码如下所示:

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

案例一

//  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 (内联函数) 的作用:提升运行效率,调用被 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 (具体化的类型参数):使用 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 带来性能损失的那些事

思考了很久需不需要写这部分内容,因为在 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,当我看到这段代码的时候惊呆了,居然还可以这么写。

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]
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值