手写一个精简版Koin:深入理解依赖注入核心原理

在现代 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容器工作的第一步。通过 startKoinmodule 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对象。
  • 使用 _InstanceFactoryBeanDefinition 包装成对应的 InstanceFactory
  • 调用 indexPrimaryType,生成一个唯一的Key (格式:类名:限定符:作用域),并将 Factory 存入 Module.mappings 这个 HashMap 中。

最终存储 KoinInstanceRegistry 会遍历所有 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 操作会委托给 KoinCoreResolver进行处理。源码里面对于查找顺序有非常清晰的层次体现:

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容器的核心无非是解决两个问题:

  1. 如何注册(Declaration):通过DSL将依赖的创建方式(Lambda)以键值对的形式保存到一个全局的注册表中。
  2. 如何获取(Retrieval):根据请求的类型、限定符和作用域生成Key,从注册表中找到对应的创建工厂,并调用它来生成实例。支持通过参数容器实现动态传参。

除此之外,诸如 @DslMarker 保证DSL安全、解构实现参数优雅传递,都是构建一个健壮、易用框架的关键技术。

虽然这个实现省略了Koin的许多高级功能(如完整的Scope生命周期管理、属性注入、Android特定支持等),但它已经囊括了最核心、最精妙的设计思想,再理解其他的模块也会简单很多。

实现的所有源码位于:JProject/source/koin

原文:#手写一个精简版Koin:深入理解依赖注入核心原理

相关推荐
Andy_GF11 分钟前
鸿蒙Next在蒲公英平台分发测试包
android·ios·harmonyos
恋猫de小郭1 小时前
iOS 26 正式版即将发布,Flutter 完成全新 devicectl + lldb 的 Debug JIT 运行支持
android·前端·flutter
幻雨様2 小时前
UE5多人MOBA+GAS 54、用户登录和会话创建请求
android·ue5
Just_Paranoid2 小时前
【SystemUI】锁屏来通知默认亮屏Wake模式
android·framework·systemui·keyguard·aod
没有了遇见2 小时前
Android +,++,+= 的区别
android·kotlin
_无_妄_3 小时前
Android 使用 WebView 直接加载 PDF 文件,通过 JS 实现
android
IT乐手4 小时前
Java 编写查看调用栈信息
android
Digitally5 小时前
如何轻松永久删除 Android 手机上的短信
android·智能手机
JulyYu5 小时前
Flutter混合栈适配安卓ActivityResult
android·flutter