koin框架预研文档

Koin是一款轻量级的Kotlin依赖注入框架,相较于Dagger,它更易于上手,无代理、无代码生成、无反射,降低了集成和维护成本。Koin在项目中的应用包括定义模块、启动初始化和对象注入。虽然在初始化阶段可能稍有性能开销,但可以通过异步注册模块等方式进行优化。考虑到其与Kotlin的契合度和社区活跃度,Koin是值得考虑替代Dagger的选项。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 

  • 背景

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 #koinStackoverflow - #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

 

3、采用度

有很多开源项目在使用,但是还没找到具体哪家企业在使用

 

4、发展趋势

可以看一下下面几篇文章的分析,文章作者都是之前用过dagger,后面转向koin,提到的主要原因就是dagger比较复杂,koin容易上手,特别是针对比较大的项目,dagger接入成本比较高。

 

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的搭配使得它有更大的发展前途,相对于它的缺点,感觉优点更明显,所以建议引入项目试用!

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值