暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Koin框架

周末随心分享 2021-10-16
1391
背景
Koin框架简介:
  •  Koin框架,适用于使用Kotlin开发 ,是一款轻量级的依赖注入框架,无代理,无代码生成,无反射,相对于dagger 而言更加适合Kotlin
引入目的:
  • 目前依赖注入比较常见的是dagger框架,dagger框架经过证明有助于帮助代码中各个模块进行解耦,所以我们前提条件是支持引入依赖注入框架的。
  • 另一方面,在依赖注入框架的选择上,我们对比了daggerkoin,发现了dagger的一些不足,比如上手难,需要手动注入等问题,同时研究了目前比较流行的koin框架,发现Koin在一些方面会比dagger来的好,同时也更符合项目往kotlin迁移的趋势,所以对这两种框架做了一些对比来看看Koin是否有取代dagger的能力


技术方案对比
一、项目视角
1、开发者
  • github地址:https://github.com/InsertKoinIO/koin
  • 作者:Arnaud Giuliani,来自法国图卢兹
  • koin目前在slack #koin Stackoverflow - #koin tag上面都开设了讨论区,在Twitter@insertkoin_io上面推送最新状态,其中Stackoverflow关于koin的有221个讨论,最近几天都有新增讨论
  • koin项目目前Pull requests总共185个,其中打开状态18个,最近一个打开时间是2020.5.13;关闭状态是167个,最近一个关闭时间是2020.3.23。总体来看活跃度还是比较高的,处理速度也挺快,目前打开的数量并不多
  • 版本迭代情况:第一个版本是0.8.0,目前稳定版本是2.1.5,重大版本发布历史为:0.8.0(2018.1.4)->0.8.2(2018.2.2)->0.9.0(2018.3.3)->0.9.1(2018.3.13)->0.9.2(2018.4.18)->1.0.0 Beta(2018.7.10)->1.0(2018.9.14)->2.0(2019.5.28)->2.1.5,可以看到2018年处于初期更新比较频繁,2019年推出的2.0版本主要对性能等方面进行了非常大的优化,后面就进入了稳定期,主要是小版本的更新迭代
2、成熟度
下面是koin更新记录,目前最新稳定版本是2.1.5
• Ready for Koin 2.0
• News from the trenches, What's next for Koin?
• Koin 1.0.0 Unleashed
• Opening Koin 1.0.0 Beta
• On the road to Koin 1.0
• Koin 0.9.2 — Maintenance fixes, new branding, roadmap for 1.0.0 & some other nice announces
• Koin 0.9.1 - Bug fixes & Improvments
• Koin 0.9.0 - Getting close to stable
• Unlock your Android ViewModel power with Koin
• koin 0.8.2 Improvements bugfixes and crash fix
• Koin release 0.8.0
3、采用度
有很多开源项目在使用,但是还没找到具体哪家企业在使用
4、发展趋势
可以看一下下面几篇文章的分析,文章作者都是之前用过dagger,后面转向koin,提到的主要原因就是dagger比较复杂,koin容易上手,特别是针对比较大的项目,dagger接入成本比较高。
• Dagger is dead. Long live Koin
• Testing a Koin application with KotlinTest
• Ready for Koin 2.0
• Migration from Dagger2 to Koin
• From Dagger to Koin, a step by step migration guide
• Koin in Feature Modules Project
• A brief look at Koin on Android
• Bye bye Dagger
• Testing with Koin
• Painless Android testing with Room & Koin
• Unlock your Android ViewModel power with Koin
• Using dependency injection with Koin
• Koin + Spark = ❤️
• Push SparkJava to the next level (Kotlin Weekly issue 73, DZone.com )
• When Koin met Ktor ... (Kotlin Weekly issue 72)
• Android Dependency Injection – Why we moved from Dagger 2 to Koin?
• Moving from Dagger to Koin - Simplify your Android development - (Kotlin Weekly issue 66 & Android Weekly issue 282)
• Kotlin Weekly #64
• Insert Koin for dependency injection
• Better dependency injection for Android
5、成本
接入成本:
koin框架最大的优势就是简单上手,因此集成成本非常低,只要四个步骤就能集成:
1AndroidX依赖库:
    // Koin for Android
    implementation 'org.koin:koin-android:2.1.5'
    // or Koin for Lifecycle scoping
    implementation 'org.koin:koin-androidx-scope:2.1.5'
    // or Koin for Android Architecture ViewModel
    implementation 'org.koin:koin-androidx-viewmodel:2.1.5'
    2)定义module,项目中dagger注解@Singleton修饰的Module转换成下面的对象定义的Module
      // Given some classes
      class Controller(val service : BusinessService)
      class BusinessService()
      // just declare it
      val myModule = module {
        single { Controller(get()) }
        single { BusinessService() }
      }
      3)启动初始化:
        class MyApplication : Application() {
          override fun onCreate(){
        super.onCreate()
        // start Koin!
        startKoin {
        // Android context
        androidContext(this@MyApplication)
        // modules
        modules(myModule)
        }
        }
        }
        4)依赖方注入,项目中只要将原先用dagger注解@Inject修饰的变量换成下面这种形式就行
          private val service: BusinessService by inject()
          相比dagger基本无学习成本以及维护成本
          6、收益
          1. 使项目更简洁,相比daggerkoin的优势就是简易上手,dagger会比较复杂一些,同样的moduledagger需要定义ComponentModule以及一些注解,koin只要一个module定义就够了

          2. 注入更简单,koin注入只需要在注入类声明一个变量,而dagger需要一个个手动注入,使用dagger注入单例需要初始化时一个个调用,这样不仅麻烦还可能因为集中初始化导致ANR出现,特别在大型项目中引入dagger更加痛苦,因为你要处理每一个注入对象,下面是使用dagger代码:

              fun injectAll(component: MainComponent) {
              component.inject(this)
              component.inject(mainController)
              ...
              }
            • koinkotlin上面提供了很多扩展库,比如 by viewmodel()方式注入会将ViewModel绑定当前View的生命周期,这些扩展库更加充分利用了kotlin特性,会比dagger来的强大

            • koin增加了启动耗时,不过相对的也省去了dagger编译的耗时,而且koin启动耗时还是可以优化的,但是dagger编译耗时却是不可优化的

            7、与dagger兼容性
            目前轻颜项目里面使用的dagger注入主要是对单例对象的注入,这点对应Koin就是single定义,single也是创建一个全局对象,其他地方依赖会先判断是否存在这个对象,存在的话就不会重复创建
            8、安全隐患
            koin采用Apache 2.0协议,可以直接使用
            koin代码主要是利用Kotlin的扩展特性,将依赖注入逻辑封装到扩展库里,并不存在后台下发资源等隐患,基本属于一个工具包类型

            二、技术视角
            1、性能对比,采用官方提供的性能测试demo:
            https://github.com/Sloy/android-dependency-injection-performance
            测试对象:
            • Koin - 2.0.0-alpha-3
            • Kodein - 5.3.0
            • Dagger 2 - 2.16
            • Katana - 1.2.2
            测试内容:
            测试内容主要是对每个框架注入了450个对象,测试每个框架初始化时间和注入时间,下面是对Koindagger测试的主要代码:
              //dagger测试
              private fun daggerTest(): LibraryResult {
              log("Running Dagger...")
              lateinit var kotlinComponent: KotlinDaggerComponent
              lateinit var javaComponent: JavaDaggerComponent
              return LibraryResult("Dagger", mapOf(
              Variant.KOTLIN to runTest(
              setup = { kotlinComponent = DaggerKotlinDaggerComponent.create() },
              test = { kotlinComponent.inject(kotlinDaggerTest) }
              ),
              Variant.JAVA to runTest(
              setup = { javaComponent = DaggerJavaDaggerComponent.create() },
              test = { javaComponent.inject(javaDaggerTest) }
              )
              ))
              }


              //koin测试
              private fun koinTest(): LibraryResult {
              log("Running Koin...")
              return LibraryResult("Koin", mapOf(
              Variant.KOTLIN to runTest(
              setup = {
              startKoin {
              modules(koinKotlinModule)
              }
              },
              test = { get<Fib8>() },
              teardown = { stopKoin() }
              ),
              Variant.JAVA to runTest(
              setup = {
              startKoin {
              modules(koinJavaModule)
              }
              },
              test = { get<FibonacciJava.Fib8>() },
              teardown = { stopKoin() }
              )
              ))
              }
              测试机型及结果:
              1)机型
              • Samsung Galaxy J5
              • Samsung Galaxy S8
              • Huawei P8 Lite
              • Xiaomi MI A1
              • OnePlus One
              • OnePlus 5
              • Nexus 6
              2)结果(下面的时间是测试100轮取中位数结果):
              Samsung Galaxy J5
              samsung j5nlte with Android 6.0.1
               
              Library
              Setup Kotlin
              Setup Java
              Inject Kotlin
              Inject Java
              Koin
              51.47 ms
              53.65 ms
              2.47 ms
              2.52 ms
              Kodein
              73.36 ms
              75.21 ms
              9.89 ms
              9.58 ms
              Katana
              12.34 ms
              12.30 ms
              2.00 ms
              1.94 ms
              Custom
              4.85 ms
              4.81 ms
              0.73 ms
              0.84 ms
              Dagger
              0.02 ms
              0.02 ms
              0.27 ms
              0.23 ms
              Samsung Galaxy S8
              samsung dreamlte with Android 8.0.0
               
              Library
              Setup Kotlin
              Setup Java
              Inject Kotlin
              Inject Java
              Koin
              5.68 ms
              6.04 ms
              0.13 ms
              0.21 ms
              Kodein
              7.13 ms
              7.38 ms
              0.20 ms
              0.21 ms
              Katana
              0.64 ms
              0.68 ms
              0.21 ms
              0.16 ms
              Custom
              0.15 ms
              0.16 ms
              0.11 ms
              0.11 ms
              Dagger
              0.01 ms
              0.01 ms
              0.10 ms
              0.10 ms
              Huawei P8 Lite
              Huawei hwALE-H with Android 6.0
               
              Library
              Setup Kotlin
              Setup Java
              Inject Kotlin
              Inject Java
              Koin
              12.12 ms
              12.34 ms
              0.26 ms
              0.26 ms
              Kodein
              14.46 ms
              14.52 ms
              0.80 ms
              0.79 ms
              Katana
              2.01 ms
              1.99 ms
              0.18 ms
              0.18 ms
              Custom
              0.51 ms
              0.50 ms
              0.08 ms
              0.09 ms
              Dagger
              0.00 ms
              0.00 ms
              0.03 ms
              0.02 ms
              Xiaomi MI A1
              xiaomi tissot_sprout with Android 8.1.0
               
              Library
              Setup Kotlin
              Setup Java
              Inject Kotlin
              Inject Java
              Koin
              9.17 ms
              11.10 ms
              0.25 ms
              0.54 ms
              Kodein
              16.64 ms
              16.22 ms
              0.82 ms
              0.32 ms
              Katana
              1.42 ms
              1.28 ms
              0.31 ms
              0.31 ms
              Custom
              0.28 ms
              0.28 ms
              0.19 ms
              0.23 ms
              Dagger
              0.02 ms
              0.02 ms
              0.28 ms
              0.21 ms
              OnePlus One
              oneplus A0001 with Android 5.0.2
               
              Library
              Setup Kotlin
              Setup Java
              Inject Kotlin
              Inject Java
              Koin
              11.84 ms
              12.00 ms
              0.27 ms
              0.30 ms
              Kodein
              22.52 ms
              23.05 ms
              1.11 ms
              1.28 ms
              Katana
              1.87 ms
              1.58 ms
              0.27 ms
              0.18 ms
              Custom
              0.43 ms
              0.35 ms
              0.09 ms
              0.10 ms
              Dagger
              0.00 ms
              0.00 ms
              0.04 ms
              0.04 ms
              OnePlus 5
              OnePlus OnePlus5 with Android 8.1.0
               
              Library
              Setup Kotlin
              Setup Java
              Inject Kotlin
              Inject Java
              Koin
              2.27 ms
              2.46 ms
              0.05 ms
              0.05 ms
              Kodein
              4.81 ms
              4.43 ms
              0.09 ms
              0.08 ms
              Katana
              0.34 ms
              0.32 ms
              0.04 ms
              0.04 ms
              Custom
              0.07 ms
              0.08 ms
              0.02 ms
              0.03 ms
              Dagger
              0.00 ms
              0.00 ms
              0.04 ms
              0.02 ms
              Nexus 6
              google shamu with Android 7.1.1
               
              Library
              Setup Kotlin
              Setup Java
              Inject Kotlin
              Inject Java
              Koin
              19.80 ms
              20.43 ms
              0.45 ms
              0.35 ms
              Kodein
              21.48 ms
              21.02 ms
              0.74 ms
              0.62 ms
              Katana
              1.23 ms
              1.16 ms
              0.31 ms
              0.27 ms
              Custom
              0.29 ms
              0.29 ms
              0.20 ms
              0.31 ms
              Dagger
              0.03 ms
              0.03 ms
              0.22 ms
              0.15 ms
              备注:上面结果都是基于koin 2.0.0-alpha-3版本测试的,目前koin 2.1.5版本性能又提升了很多,最后奉上一张基于Koin 2.1.5版本,在华为低端机上拿上面demo测试的结果:


              重点:上面的结果可能大家都有个疑问,就是中位数是不能代表实际耗时的,我们往往更关注第一次执行的结果,经过测试的确第一次执行比较耗时,在低端机上注册45module对象需要30ms左右,注册450个需要180ms左右,所以这个怎么优化呢?
              好在Koin给我们提供了注册module的接口,我们可以自己异步注册module,例如下面这样:
                startKoin {
                modules(syncModule)
                }
                Thread {
                loadKoinModules(module { asyncModule })
                }.start()
                multi dex原理一样,我们可以定义一个syncModule,这个会在主线程注册对象,同时定义一个asyncModule,这个可以在syncModule注册完之后在异步线程注册,我们可以控制主线程注册的module(即MainActivity马上用到的module)数量不超过40个,这样就能最大程度削弱koin的耗时缺点,真正发挥它的优点

                3) 总结:
                • 从上面可以看出koin框架在setup阶段会比dagger耗时,因为koin注册Moudle是通过记录每个module的类定义来实现的,因此当注册的module越多越耗时,而dagger是编译时生成注册对象的,不会占用运行时间。注入阶段由于只获取一个对象,因此两个框架相差不多

                • 另一方面koin框架在这方面也在不断优化,之前1.0版本效果更差,2.0改进了不少,相信后面版本会对setup耗时做进一步的优化


                2、安装后包体积影响
                Koin增加 154KB (包括一些koin扩展库)
                Dagger增加 15KB
                方案原理解析
                koin定义moudle解析
                下面是koin定义single类型的module源码,可以看出是先调用单例保存方法
                  /**
                   * Declare a Single definition
                   * @param qualifier
                   * @param createdAtStart
                   * @param override
                   * @param definition - definition function
                   */
                  inline fun <reified T> single(
                          qualifier: Qualifier? = null,
                          createdAtStart: Boolean = false,
                          overrideBoolean = false,
                          noinline definition: Definition<T>
                  ): BeanDefinition<T> {
                      return Definitions.saveSingle(
                              qualifier,
                              definition,
                              rootScope,
                              makeOptions(override, createdAtStart)
                      )
                  }

                  再来看一下Definitions.saveSingle这个方法:

                    inline fun <reified T> saveSingle(
                        qualifier: Qualifier? = null,
                        noinline definition: Definition<T>,
                        scopeDefinition: ScopeDefinition,
                        options: Options
                    ): BeanDefinition<T> {
                        val beanDefinition = createSingle(qualifier, definition, scopeDefinition, options)
                        scopeDefinition.save(beanDefinition)
                        return beanDefinition
                    }


                    inline fun <reified T> createSingle(
                        qualifier: Qualifier? = null,
                        noinline definition: Definition<T>,
                        scopeDefinition: ScopeDefinition,
                        options: Options,
                        secondaryTypes: List<KClass<*>> = emptyList()
                    ): BeanDefinition<T> {
                        return BeanDefinition(
                            scopeDefinition,
                            T::class,
                            qualifier,
                            definition,
                            Kind.Single,
                            options = options,
                            secondaryTypes = secondaryTypes
                        )
                    }
                    可以看出其实就是将module对象的定义保存在一个Set列表里,所以定义的module对象越多越耗时
                    koin注入对象解析
                      /**
                       * inject lazily given dependency for Android koincomponent
                       * @param qualifier - bean qualifier / optional
                       * @param scope
                       * @param parameters - injection parameters
                       */
                      inline fun <reified T : Any> ComponentCallbacks.inject(
                              qualifier: Qualifier? = null,
                              noinline parameters: ParametersDefinition? = null
                      ) = lazy(LazyThreadSafetyMode.NONE) { get<T>(qualifier, parameters) }
                      上面是by inject源码,可以看出主要调用了懒加载,最终走到get<T>方法,我们接着往下看:
                        /**
                         * get given dependency for Android koincomponent
                         * @param name - bean name
                         * @param scope
                         * @param parameters - injection parameters
                         */
                        inline fun <reified T : Any> ComponentCallbacks.get(
                                qualifier: Qualifier? = null,
                                noinline parameters: ParametersDefinition? = null
                        ): T = getKoin().get(qualifier, parameters)
                        继续走到getKoin().get()方法:
                          /**
                           * Get a Koin instance
                           * @param qualifier
                           * @param scope
                           * @param parameters
                           */
                          @JvmOverloads
                          inline fun <reified T> get(
                              qualifier: Qualifier? = null,
                              noinline parameters: ParametersDefinition? = null
                          ): T = _scopeRegistry.rootScope.get(qualifier, parameters)


                          接着rootScope.get():

                            /**
                             * Get a Koin instance
                             * @param qualifier
                             * @param scope
                             * @param parameters
                             */
                            @JvmOverloads
                            inline fun <reified T> get(
                                qualifier: Qualifier? = null,
                                noinline parameters: ParametersDefinition? = null
                            ): T {
                                return get(T::classqualifierparameters)
                            }
                            /**
                             * Get a Koin instance
                             * @param clazz
                             * @param qualifier
                             * @param parameters
                             * @return instance of type T
                             */
                            fun <T> get(
                                clazz: KClass<*>,
                                qualifier: Qualifier? = null,
                                parameters: ParametersDefinition? = null
                            ): T {
                                return if (_koin._logger.isAt(Level.DEBUG)) {
                                    val qualifierString = qualifier?.let { " with qualifier '$qualifier'" } ?: ""
                                    _koin._logger.debug("+- '${clazz.getFullName()}'$qualifierString")
                                    val (instance: T, duration: Double) = measureDurationForResult {
                                        resolveInstance<T>(qualifier, clazz, parameters)
                                    }
                                    _koin._logger.debug("|- '${clazz.getFullName()}' in $duration ms")
                                    return instance
                                } else {
                                    resolveInstance(qualifier, clazz, parameters)
                                }
                            }


                            @Suppress("UNCHECKED_CAST")
                            private fun <T> resolveInstance(
                                qualifier: Qualifier?,
                                clazz: KClass<*>,
                                parameters: ParametersDefinition?
                            ): T {
                                if (_closed) {
                                    throw ClosedScopeException("Scope '$id' is closed")
                                }
                                //TODO Resolve in Root or link
                                val indexKey = indexKey(clazz, qualifier)
                                return _instanceRegistry.resolveInstance(indexKey, parameters)
                                    ?: findInOtherScope<T>(clazz, qualifier, parameters) ?: getFromSource(clazz)
                                    ?: throwDefinitionNotFound(qualifier, clazz)
                            }
                            继续看_instanceRegistry.resolveInstance
                              @Suppress("UNCHECKED_CAST")
                              internal fun <T> resolveInstance(indexKey: IndexKey, parameters: ParametersDefinition?): T? {
                                  return _instances[indexKey]?.get(defaultInstanceContext(parameters)) as? T
                              }
                              到这边差不多了,_instances[indexKey]指向的是这个module对应的factory,我们最后看一下这个factory的创建:
                                private fun createInstanceFactory(
                                        _koin: Koin,
                                        definition: BeanDefinition<*>
                                ): InstanceFactory<*> {
                                    return when (definition.kind) {
                                        Kind.Single -> SingleInstanceFactory(_koin, definition)
                                        Kind.Factory -> FactoryInstanceFactory(_koin, definition)
                                    }
                                }
                                到这里就清楚了,single修饰的module会创建SingleInstanceFactoryfactory修饰的会创建FactoryInstanceFactory

                                3、总结:
                                koin框架会在调用startKoin的时候根据你定义的module文件创建每个module对应的factory,然后在注入的时候会获取每个moudle对应的factory返回module对象,源码还是比较容易阅读的



                                文档
                                API文档:
                                https://doc.insert-koin.io/#/koin-core/modules?id=linking-modules-strategies
                                测试评估
                                主要是对启动初始化耗时的一些影响


                                总结
                                经过上面的预研分析,相比dagger
                                koin有以下几个优点:
                                • 上手简单,没有学习成本及维护成本

                                • 注入简单,不需要像dagger一样定义ModuleComponent,不需要手动调用inject方法

                                •  扩展性高,koin提供了各种扩展库来丰富对依赖注入的各种需求

                                koin有以下几个缺点:
                                • 初始化耗时的成本,这个和注册的对象数量成正比,不过提供了异步初始化API

                                • 包体积成本,大概增加150KB,包括各种常用扩展库


                                经过权衡,发现koin对工程整体的贡献以及后期的维护上面提供了极大的优势,再加上其和kotlin的搭配使得它有更大的发展前途,相对于它的缺点,感觉优点更明显,所以建议引入项目试用!

                                好了,今天的分享就到这了,下周继续^_^

                                文章转载自周末随心分享,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                                评论