koin框架预研文档

 

  • 背景

koin框架简介:

  • Koin框架,适用于使用Kotlin开发 ,是一款轻量级的依赖注入框架,无代理,无代码生成,无反射,相对于dagger 而言更加适合Kotlin语

引入目的:

  • 目前app比较常用的是dagger框架,dagger框架属于一种依赖注入框架,经过证明这种框架有助于帮助代码中各个模块进行解耦,所以我们前提条件是支持引入依赖注入框架的。
  • 另一方面,在依赖注入框架的选择上,我们对比了dagger和koin,发现了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框架最大的优势就是简单上手,因此集成成本非常低,只要四个步骤就能集成:

1)AndroidX依赖库:

// 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 contextandroidContext(this@MyApplication)// modulesmodules(myModule)}} 
} 

4)依赖方注入,项目中只要将原先用dagger注解@Inject修饰的变量换成下面这种形式就行

private val service: BusinessService by inject()

  相比dagger基本无学习成本以及维护成本:

 

6、收益

1)使项目更简洁,相比dagger,koin的优势就是简易上手,dagger会比较复杂一些,同样的module,dagger需要定义Component和Module以及一些注解,koin只要一个module定义就够了

 

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

fun injectAll(component: MainComponent) {component.inject(this)component.inject(testController)component.inject(exposureController as ExposureController)component.inject(reportController as ReportController)component.inject(autoTestController as AutoTestController)
}

3)koin在kotlin上面提供了很多扩展库,比如 by viewmodel()方式注入会将ViewModel绑定当前View的生命周期,这些扩展库更加充分利用了kotlin特性,会比dagger来的强大

4) 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个对象,测试每个框架初始化时间和注入时间,下面是对Koin和dagger测试的主要代码:

//dagger测试private fun daggerTest(): LibraryResult {log("Running Dagger...")lateinit var kotlinComponent: KotlinDaggerComponentlateinit var javaComponent: JavaDaggerComponentreturn 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() },teardown = { stopKoin() }),Variant.JAVA to runTest(setup = {startKoin {modules(koinJavaModule)}},test = { get() },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

Samsung Galaxy S8

samsung dreamlte with Android 8.0.0

Huawei P8 Lite

Huawei hwALE-H with Android 6.0

Xiaomi MI A1

xiaomi tissot_sprout with Android 8.1.0

OnePlus One

oneplus A0001 with Android 5.0.2

OnePlus 5

OnePlus OnePlus5 with Android 8.1.0

Nexus 6

google shamu with Android 7.1.1

 

备注:上面结果都是基于koin 2.0.0-alpha-3版本测试的,目前koin 2.1.5版本性能又提升了很多,最后奉上一张基于Koin 2.1.5版本,在华为低端机上拿上面demo测试的结果:

 

重点:上面的结果可能大家都有个疑问,就是中位数是不能代表实际耗时的,我们往往更关注第一次执行的结果,经过测试的确第一次执行比较耗时,在低端机上注册45个module对象需要30ms左右,注册450个需要180ms左右,所以这个怎么优化呢?

好在Koin给我们提供了注册module的接口,我们可以自己异步注册module,例如下面这样:

startKoin {modules(syncModule)
}
Thread {loadKoinModules(module { asyncModule })
}.start()

和multi dex原理一样,我们可以定义一个syncModule,这个会在主线程注册对象,同时定义一个asyncModule,这个可以在syncModule注册完之后在异步线程注册,我们可以控制主线程注册的module(即MainActivity马上用到的module)数量不超过40个,这样就能最大程度削弱koin的耗时缺点,真正发挥它的优点

 

总结:

  • 从上面可以看出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  single(qualifier: Qualifier? = null,createdAtStart: Boolean = false,override: Boolean = false,noinline definition: Definition
): BeanDefinition {return Definitions.saveSingle(qualifier,definition,rootScope,makeOptions(override, createdAtStart))
}

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

inline fun  saveSingle(qualifier: Qualifier? = null,noinline definition: Definition,scopeDefinition: ScopeDefinition,options: Options
): BeanDefinition {val beanDefinition = createSingle(qualifier, definition, scopeDefinition, options)scopeDefinition.save(beanDefinition)return beanDefinition
}inline fun  createSingle(qualifier: Qualifier? = null,noinline definition: Definition,scopeDefinition: ScopeDefinition,options: Options,secondaryTypes: List> = emptyList()
): BeanDefinition {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  ComponentCallbacks.inject(qualifier: Qualifier? = null,noinline parameters: ParametersDefinition? = null
) = lazy(LazyThreadSafetyMode.NONE) { get(qualifier, parameters) }

上面是by inject源码,可以看出主要调用了懒加载,最终走到get方法,我们接着往下看:

/*** get given dependency for Android koincomponent* @param name - bean name* @param scope* @param parameters - injection parameters*/
inline fun  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  get(qualifier: Qualifier? = null,noinline parameters: ParametersDefinition? = null
): T = _scopeRegistry.rootScope.get(qualifier, parameters)/*** Get a Koin instance* @param qualifier* @param scope* @param parameters*/
@JvmOverloads
inline fun  get(qualifier: Qualifier? = null,noinline parameters: ParametersDefinition? = null
): T {return get(T::class, qualifier, parameters)
}/*** Get a Koin instance* @param clazz* @param qualifier* @param parameters** @return instance of type T*/
fun  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(qualifier, clazz, parameters)}_koin._logger.debug("|- '${clazz.getFullName()}' in $duration ms")return instance} else {resolveInstance(qualifier, clazz, parameters)}
}@Suppress("UNCHECKED_CAST")
private fun  resolveInstance(qualifier: Qualifier?,clazz: KClass<*>,parameters: ParametersDefinition?
): T {if (_closed) {throw ClosedScopeException("Scope '$id' is closed")}//TODO Resolve in Root or linkval indexKey = indexKey(clazz, qualifier)return _instanceRegistry.resolveInstance(indexKey, parameters)?: findInOtherScope(clazz, qualifier, parameters) ?: getFromSource(clazz)?: throwDefinitionNotFound(qualifier, clazz)}

继续看_instanceRegistry.resolveInstance:

@Suppress("UNCHECKED_CAST")
internal fun  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会创建SingleInstanceFactory,factory修饰的会创建FactoryInstanceFactory

 

总结

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

 

 

  • 项目使用

1)AndroidX依赖库:

// 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 contextandroidContext(this@MyApplication)// modulesmodules(myModule)}} 
} 

4)依赖方注入,项目中只要将原先用dagger注解@Inject修饰的变量换成下面这种形式就行

private val service: BusinessService by inject()

 

  • 文档

API文档:

https://doc.insert-koin.io/#/koin-core/modules?id=linking-modules-strategies

 

 

  • 测试评估

主要是对启动初始化耗时的一些影响

 

  • 总结

经过上面的预研分析,相比dagger

koin有以下几个优点:

  • 上手简单,没有学习成本及维护成本
  • 注入简单,不需要像dagger一样定义Module和Component,不需要手动调用inject方法
  • 扩展性高,koin提供了各种扩展库来丰富对依赖注入的各种需求

koin有以下几个缺点

  • 初始化耗时的成本,这个和注册的对象数量成正比
  • 包体积成本,大概增加150KB,包括各种常用扩展库

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

 


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部