在现代 Android 应用开发中,依赖注入(Dependency Injection, DI)已成为构建松耦合、可测试代码的重要技术。Koin 作为一个轻量级的Kotlin依赖注入框架,因其简洁的DSL和易用性深受开发者喜爱。最近对其源码进行学习了解,通过手写一个极度精简的 Koin 核心代码,来透彻理解Koin的注册、解析和参数传递机制。
本文代码基于 Koin 源码思想实现,仅用于学习核心原理,并非 Koin官方代码。
核心概念与项目结构
下图是基于 koin 4.1 解析的 主要类UML图,可以比较清晰地看看各个类之间的关系 power by mermaidchart
主要类:
KoinApplication
: Koin启动的入口,负责初始化容器和加载模块。Koin
: 核心容器,持有实例注册表 InstanceRegistry 和作用域注册表 ScopeRegistry。Module
: 定义依赖的地方,存放了所有的 bean 定义 BeanDefinition 与 InstanceFactory。BeanDefinition
: 对一个依赖项的定义,包括其类型、限定符、所属作用域以及创建它的 lambda 表达式。InstanceFactory
: 负责根据BeanDefinition
创建实例的核心工厂,分为SingleFactory
(单例)、FactoryFactory
(工厂模式) 和ScopeFactory
(作用域内单例)。Scope
: 作用域,用于管理特定生命周期内的实例。ParametersHolder
: 参数容器,用于在获取实例时动态传递参数。
手写 koin 代码介绍
基于对源码的理解和参考,实现了 koin 的基本功能,整体分成三部分:简单 single 数据存取、包含 scope 能力、动态参数能力,分成三个文件夹,顺序123是基于前面带代码累加的。
简单 single 数据存取 代码实现在:KoinWithoutScope.kt
这是一份最简单的代码,大概200行不到,基本上包含了 koin 的核心思想:启动时注册组件定义。解析时,先查作用域缓存,命中则直接返回。未命中则递归解析其依赖项,调用工厂函数创建实例,最后返回实例。
从这也能看出来 koin 的缺点:Koin 启动时 (startKoin) 需要将所有模块的定义 (BeanDefinition) 注册到容器中。实例数量过多会显著增加启动注册过程的耗时,影响应用启动速度。由于每个实例都会对应一个 BeanDefinition 以及 Factory ,内存占用会相应地上升。
整个流程简单来讲就是生成一个 map,通过 key 获取对于的数据。
Scope 能力 代码实现在:KoinWithScope.kt
这一份是在之前的能力上进行添加,此前将所有的数据都注册到 "root" 这个容器内,全局通用,但为了将不同作用域分开,需要引入 scope 的概念。
简单理解就是在通过 key 获取的 map 里面的数据的时候,这个 key 是有一定的规则的,核心逻辑在这里:
kotlin
inline fun indexKey(clazz: KClass<*>, typeQualifier: String?, scopeQualifier: String): String {
return buildString {
append(clazz.java.name)
append(':')
append(typeQualifier ?: "")
append(':')
append(scopeQualifier)
}
}
不同的 scope 实际上也就是获取的 key 值的不同。
动态参数能力 代码实现在:KoinWithParameter.kt
最后在 scope 的基础上实现了一个比较重要的能力-动态参数能力,通过这个能力可以让有实例能够在运行的时候根据参数动态创建。这个能力也是像在安卓 Activity/Fragment 里面 viewmodel() 实现依赖注入的必要实现。
简单理解就是在 get() 的时候将参数传入到获取实例的调用链中,在运行时执行注册的 Lambda 函数invoke时候将作为参数传递到构造方法中去。这里单独拎出来实现是因为这个参数传递影响到整个流程的逻辑,为了上上面的两个能力逻辑更简单清晰,单独在这一部分实现。
Koin 的注册流程(Declaration)
注册是DI容器工作的第一步。通过 startKoin
和 module
DSL来声明依赖。
启动 Koin 与模块加载
整个启动加载流程将 kotlin 的语法糖用到了极致,也就使得整个代码看起来是如此的简洁。
kotlin
val myApp = startKoin {
modules(appModule)
}
// 定义一个模块
val appModule = module {
// 注册一个单例,其构造需要一個 Int 参数
single { (data: Int) -> ComponentInt(data) }
// 注册一个工厂(每次获取都是新实例),其构造需要 Int 和 Float 参数
factory { (data1: Int, data2: Float) -> ComponentIntFloat(data1, data2) }
}
流程剖析:
startKoin
这是一个顶级函数,它调用 GlobalContext.startKoin
,创建并初始化一个 KoinApplication
对象。
modules(...)
KoinApplication
的方法,它将传入的 Module
列表交给 Koin
实例的 loadModels
方法处理。
module { ... }
DSL函数,它创建一个 Module
对象,并执行其中的配置lambda。
single/factory/scope
Module
的扩展函数。它们的作用是:
- 使用
_createDefinition
将 lambda 表达式包装成一个BeanDefinition
对象。 - 使用
_InstanceFactory
将BeanDefinition
包装成对应的InstanceFactory
。 - 调用
indexPrimaryType
,生成一个唯一的Key (格式:类名:限定符:作用域
),并将Factory
存入Module.mappings
这个HashMap
中。
最终存储 Koin
的 InstanceRegistry
会遍历所有 Module
,将它们 mappings
中的全部 Factory
都合并到自己的 _instances
(一个 ConcurrentHashMap
)中。
至此,所有依赖的定义都已注册到容器中,静待获取。
Koin的实例获取流程(Retrieval)
kotlin
// 获取无参依赖(普通方式)
val component = get<Component>()
// 通过 scope 作用域限定进行获取
val scope = koin.createScope("scope", scopeQualifier)
val component = scope.get<Component>()
// 通过需要动态参数的获取
val componentWithArgs = get<ComponentInt> { parametersOf(42) }
val componentWithMultiArgs = get<ComponentIntFloat> { parametersOf(101, 3.14f) }
流程剖析
Scope.get<T>
这是 Scope
的一个扩展函数。它首先创建一个 ResolutionContext
,封装了当前作用域、要解析的类型、限定符以及最重要的------参数持有器 ParametersHolder
(由 parametersOf
函数创建)。
解析上下文(ResolutionContext)
这个上下文对象包含了解析一个实例所需的所有信息。
核心解析器(CoreResolver) get
操作会委托给 Koin
的 CoreResolver
进行处理。源码里面对于查找顺序有非常清晰的层次体现:
kotlin
override fun <T> resolveFromContext(scope : Scope, instanceContext: ResolutionContext): T {
return resolveFromContextOrNull(scope,instanceContext) ?: throwNoDefinitionFound(instanceContext)
}
private fun <T> resolveFromContextOrNull(scope : Scope, instanceContext: ResolutionContext, lookupParent : Boolean = true): T? {
return resolveFromInjectedParameters(instanceContext)
?: resolveFromRegistry(scope,instanceContext)
?: resolveFromStackedParameters(scope,instanceContext)
?: resolveFromScopeSource(scope,instanceContext)
?: resolveFromScopeArchetype(scope,instanceContext)
?: if (lookupParent) resolveFromParentScopes(scope,instanceContext) else null
?: resolveInExtensions(scope,instanceContext)
}
查找工厂
Resolver
会调用InstanceRegistry.resolveDefinition
。- 该方法使用和注册时相同的算法 生成Key(类名:限定符:作用域),然后从
_instances
中查找对应的InstanceFactory
。
创建实例
-
找到
Factory
后,调用其get(context: ResolutionContext)
方法。 -
Factory
会调用自己的create
方法。关键一步来了 :在create
方法中,会执行BeanDefinition.definition.invoke(context.scope, parameters)
。这其实就是执行了之前注册的 lambda:{ (data: Int) -> ComponentInt(data) }
。 -
参数传递 :这里的
parameters
就是在get
时传入的ParametersHolder
。Lambda 的参数(data: Int)
会从ParametersHolder
中按顺序(或使用解构)取出值
返回实例
工厂将创建好的实例返回给调用者。
对于 SingleFactory
,它会将第一次创建出来的实例缓存起来,后续调用直接返回缓存实例。FactoryFactory
则每次都会执行 create
方法。
其他技术
@DslMarker 的作用
在实现的过程中发现如下图所示:koin 的代码有颜色分层,能比较清晰地看到各个 block 之间的差异,自己写的代码全部是白色。

代码开头定义了三个注解:@KoinApplicationDslMarker
, @KoinDslMarker
, @OptionDslMarker
。这是Kotlin DSL的安全卫士。
kotlin
@DslMarker
annotation class KoinDslMarker
@KoinDslMarker
class Module {
fun single(...) { ... } // 这个single在DSL里
}
@KoinDslMarker
class KoinApplication {
fun modules(...) { ... } // 这个modules在DSL里
}
fun test() {
startKoin {
modules(...) // 正确:在 KoinApplication 的 lambda 里
single { ... } // 编译错误!@DslMarker 阻止了隐式地使用外部 Receiver (Module)
}
}
@DslMarker
实际的作用是防止在嵌套的DSL Lambda中,意外地调用到外层 Receiver 的方法,从而让DSL书写更加清晰和安全。代码颜色是由 IDE 提供的效果。在代码中加上几个注解之后,效果如图所示:
跟 koin 的颜色不太一致,不过能明显看到代码有分层,应该是由于 koin 对 annotation 也有处理,这里没有再深入研究。
2. 优雅的参数传递与解构
这个逻辑复刻了Koin的动态参数特性。
ParametersHolder
:一个轻量的参数容器,内部用一个List<Any?>
存储参数。parametersOf
:辅助函数,优雅地创建ParametersHolder
。- 解构声明(Destructuring Declaration) :
ParametersHolder
重写了component1()
到component5()
操作符。这使得在定义lambda时,可以直接用(a: A, b: B)
的形式来接收参数,而不是手动调用parameters.get<X>(0)
。
kotlin
// 注册端:看起来就像普通函数
single { (id: Int, name: String) -> User(id, name) }
// 获取端:传递参数非常直观
val user = get<User> { parametersOf(123, "Julius") }
这种设计极大地提升了API的简洁性和可读性。
总结
通过这个手写的迷你Koin,可以深刻地理解到,一个现代DI容器的核心无非是解决两个问题:
- 如何注册(Declaration):通过DSL将依赖的创建方式(Lambda)以键值对的形式保存到一个全局的注册表中。
- 如何获取(Retrieval):根据请求的类型、限定符和作用域生成Key,从注册表中找到对应的创建工厂,并调用它来生成实例。支持通过参数容器实现动态传参。
除此之外,诸如 @DslMarker
保证DSL安全、解构实现参数优雅传递,都是构建一个健壮、易用框架的关键技术。
虽然这个实现省略了Koin的许多高级功能(如完整的Scope生命周期管理、属性注入、Android特定支持等),但它已经囊括了最核心、最精妙的设计思想,再理解其他的模块也会简单很多。
实现的所有源码位于:JProject/source/koin