-
背景
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 context
androidContext(this@MyApplication)
// modules
modules(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
测试对象:
测试内容:
测试内容主要是对每个框架注入了450个对象,测试每个框架初始化时间和注入时间,下面是对Koin和dagger测试的主要代码:
//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)机型
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 <reified T> single(
qualifier: Qualifier? = null,
createdAtStart: Boolean = false,
override: Boolean = 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)
/**
* 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::class, qualifier, parameters)
}
/**
* 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会创建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 context
androidContext(this@MyApplication)
// modules
modules(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的搭配使得它有更大的发展前途,相对于它的缺点,感觉优点更明显,所以建议引入项目试用!