依赖注入:在鸿蒙中实现简单的DI框架(43)
在鸿蒙(HarmonyOS)原生应用开发中,随着业务复杂度的提升,组件间的依赖关系会变得错综复杂。传统的硬编码依赖方式会导致代码耦合度高、难以测试和维护。依赖注入(Dependency Injection, DI)通过控制反转(IoC)机制,将对象的创建和依赖关系的管理交由专门的容器来处理,是解决这些问题的最佳实践。
在鸿蒙 ArkTS 中,我们可以利用装饰器(Decorators)和反射(Reflect)机制,实现一个轻量级的依赖注入框架。
一、 核心概念与架构设计
一个基础的 DI 框架主要包含三个核心部分:
- 服务标识(Service):用于标记哪些类是“可被注入的服务”。
- 注入标识(Inject):用于标记哪些属性需要由容器自动注入。
- IoC 容器(Container):负责管理服务的注册、生命周期(如单例、瞬时)以及自动解析依赖关系。
一个“用户资料展示”的实战场景
1. IoC 容器(Container):全局的“服务管家”
容器是整个 DI 框架的核心,负责记录“谁提供了什么服务”以及“如何创建服务”。
class DIContainer { private services: Map<string, any> = new Map(); // 1. 注册服务(将接口标识与具体的类或实例绑定) register(identifier: string, service: any): void { this.services.set(identifier, service); } // 2. 解析服务(根据标识获取具体的服务实例) resolve<T>(identifier: string): T { const service = this.services.get(identifier); if (!service) { throw new Error(`Service [${identifier}] 未注册!`); } return service as T; } } // 导出一个全局单例容器,供整个应用使用 export const container = new DIContainer();2. 服务标识(Service):标记“可被注入的能力”
通常会定义一个接口或抽象类作为标识,并让具体的服务类去实现它。
// 1. 定义服务标识(接口) interface IStorageService { getData(key: string): string; saveData(key: string, value: string): void; } // 2. 具体的服务实现 class LocalStorageService implements IStorageService { getData(key: string): string { return `从本地获取的 ${key}`; } saveData(key: string, value: string): void { console.log(`已保存到本地: ${key} = ${value}`); } } // 3. 在应用启动时,向容器注册服务 container.register<IStorageService>('IStorageService', new LocalStorageService());3. 注入标识(Inject):在业务层“自动获取依赖”
在 ViewModel 或 UI 组件中,我们不再手动new具体的服务类,而是通过容器获取。
class UserViewModel { // 传统方式(高耦合): // private storage = new LocalStorageService(); // DI 方式(低耦合):通过容器解析获取 private storage: IStorageService = container.resolve<IStorageService>('IStorageService'); loadUserProfile() { // 业务层只关心接口,不关心底层是本地存储还是网络存储 return this.storage.getData('user_profile'); } }完整串联:在鸿蒙 View 层中的使用
@Component struct UserPage { @State profileInfo: string = '加载中...'; private viewModel: UserViewModel = new UserViewModel(); aboutToAppear() { // 调用 ViewModel 的方法,ViewModel 内部自动使用了注入的 Storage 服务 this.profileInfo = this.viewModel.loadUserProfile(); } build() { Column({ space: 20 }) { Text(this.profileInfo).fontSize(20) Button('切换为网络存储') .onClick(() => { // 动态替换服务实现(依赖倒置的威力) container.register<IStorageService>('IStorageService', new NetworkStorageService()); this.profileInfo = this.viewModel.loadUserProfile(); }) } } }二、 轻量级 DI 框架代码实现
// 1. 服务标识装饰器:将类标记为服务,并可选地指定一个唯一标识符 function Service(identifier?: string): ClassDecorator { return function (target: any) { Reflect.defineMetadata('di:service', true, target); if (identifier) { Reflect.defineMetadata('di:identifier', identifier, target); } }; } // 2. 注入装饰器:标记需要注入的属性,并记录其依赖的类型或标识符 function Inject(identifier?: string): PropertyDecorator { return function (target: any, propertyKey: string | symbol) { const serviceIdentifier = identifier || Reflect.getMetadata('design:type', target, propertyKey); Reflect.defineMetadata('di:inject', serviceIdentifier, target, propertyKey); }; } // 3. IoC 容器类:负责注册和解析服务 class DIContainer { private static instance: DIContainer; private services: Map<string, any> = new Map(); // 获取全局单例容器 static getInstance(): DIContainer { if (!DIContainer.instance) { DIContainer.instance = new DIContainer(); } return DIContainer.instance; } // 注册服务实例 register<T>(identifier: string, service: T): void { this.services.set(identifier, service); } // 解析并获取服务实例 resolve<T>(identifier: string): T { const service = this.services.get(identifier); if (!service) { throw new Error(`Service ${identifier} not found`); } return service; } // 自动注册:扫描带有 @Service 装饰器的类并实例化 autoRegister(constructor: any): void { const isService = Reflect.getMetadata('di:service', constructor); if (isService) { const identifier = Reflect.getMetadata('di:identifier', constructor) || constructor.name; this.register(identifier, new constructor()); } } }三、 业务层实战示例
结合之前的 MVVM 架构,DI 框架可以完美地应用于服务层和 ViewModel 层:
// 1. 定义服务层:使用 @Service 标记 @Service('UserService') class UserService { private users: Map<string, any> = new Map(); addUser(user: any): void { this.users.set(user.id, user); } getUser(id: string): any { return this.users.get(id); } } // 2. 定义 ViewModel:使用 @Inject 注入依赖 @Service('UserViewModel') class UserViewModel { // 容器会自动将 UserService 注入到该属性中 @Inject('UserService') private userService!: UserService; loadUser(id: string) { return this.userService.getUser(id); } } // 3. 在 UI 组件中使用 @Component struct UserProfile { @Inject('UserService') private userService!: UserService; @State userInfo: any = null; aboutToAppear(): void { // 从容器中获取服务并加载数据 this.userInfo = this.userService.getUser('user123'); } build() { Column() { Text(this.userInfo ? `用户名: ${this.userInfo.name}` : '加载中...') } } }四、 生命周期与最佳实践
在实际的鸿蒙工程中,DI 框架的使用需要遵循以下规范:
- 分层架构设计:将 UI 组件、ViewModel、数据服务严格分层,通过 DI 容器进行连接,确保各层职责单一。
- 生命周期管理策略:
- Singleton(单例):适用于全局配置、数据库连接、工具类。容器内缓存实例,整个应用生命周期内共享。
- Transient(瞬时):适用于每次请求都需要新实例的场景。每次调用
resolve时创建新实例。 - Scoped(作用域):适用于请求上下文或特定页面。在作用域内共享实例,页面销毁时自动释放资源。
- 配置集中管理:可以将 API 基础 URL、超时时间等配置封装为
AppConfig服务,通过 DI 注入到网络请求服务中,避免硬编码。 - 延迟注入与缓存:避免在应用启动时(如
Application.onCreate())一次性注入所有依赖。采用懒加载策略,并对频繁使用的依赖进行缓存,以提升应用启动性能。
五、 进阶扩展
跨平台与模块化 DI 方案
如果你的鸿蒙项目是基于Flutter for OpenHarmony开发,或者需要处理多 HAP(Feature)模块的复杂工程,可以考虑使用成熟的第三方 DI 库:
- weaver:一个极致轻量的 DI 和服务发现框架。它提出了“注册(Register)- 注入(Inject)”模型,支持局部作用域容器(
weaver.scoped),非常适合在鸿蒙多 HAP 模式下实现模块自治和强弱隔离。 - injector / flutter_simple_dependency_injection:纯 Dart 库,100% 兼容 OpenHarmony。支持通过工厂模式(Factory)和单例模式(Singleton)管理依赖,非常适合用来屏蔽鸿蒙与 Android/iOS 的底层 API 差异,实现一套代码多端运行。
1、 拥抱自动化:使用injectable与inject_generator
在大型工程中,手动维护注册和解析逻辑不仅繁琐,还极易出错。引入代码生成器可以将开发者从繁琐的依赖树组装中解放出来。
- 核心机制:利用
@module、@provide等注解标记依赖,通过build_runner在编译期自动扫描并生成*.inject.dart文件,构建好单例或工厂的实例化逻辑。 - 鸿蒙适配优势:由于生成的代码是标准的 Dart 实例化调用,不涉及运行时反射,因此不会带来额外的性能损耗,且能在编译期提前发现依赖链条断裂或环形依赖的问题。
1. 编写注解:定义模块与依赖
在业务代码中,我们只需通过@module和@provide等注解告诉生成器“谁是谁的依赖”。
// 1. 定义一个鸿蒙模块,提供底层数据库服务 @module class OhosMainModule { @provide OhosDatabase provideDb() => OhosDatabase(); } // 2. 定义业务服务,声明它依赖 OhosDatabase @injectable class OhosUserService { final OhosDatabase _db; // 构造函数中的参数,生成器会自动寻找并注入 OhosUserService(this._db); void fetchUser() => print('通过数据库获取用户'); }2. 运行生成器:触发自动化组装
在终端中执行build_runner命令:
dart run build_runner build此时,生成器会在后台自动分析OhosUserService与OhosMainModule之间的依赖树,并在同级目录下生成一份名为*.inject.dart(或*.g.dart)的文件。在这个生成的文件中,机器已经为你写好了类似如下的标准 Dart 实例化代码:
// 自动生成的代码(开发者无需手写) OhosUserService createOhosUserService() { final db = OhosDatabase(); // 自动实例化底层依赖 return OhosUserService(db); // 自动组装并返回完整对象 }3. 业务层使用:零手动配置的解耦体验
在鸿蒙应用的入口或页面中,你只需要调用自动生成的容器,即可获取组装好的服务:
void main() async { // 1. 初始化鸿蒙注入容器(底层会自动调用生成器组装好的逻辑) final container = await OhosAppComponent.create(OhosMainModule()); // 2. 直接获取服务,无需关心 OhosUserService 是如何被 new 出来的 runApp(MyApp(userService: container.userService)); }2、 多环境隔离与动态适配(Environments)
鸿蒙应用通常面临真机调试、模拟器运行、自动化测试(ohos_test)等多种环境。DI 框架应支持根据环境动态注入不同的实现:
- 环境标签:利用
@Environment('ohos_real')或@Environment('dev')为同一个接口定义多份实现。 - 实战场景:在开发环境下注入
MockStorage(内存缓存),而在鸿蒙真机环境下自动注入NativeOhosStorage(鸿蒙原生持久化存储)。业务层无需修改任何代码,即可实现跨环境的无缝切换。
3、 异步服务的先行预解析(Asynchronous Init)
鸿蒙系统中许多底层 API(如Preferences.getInstance()、设备传感器初始化)都是异步的。如果在 DI 容器就绪前就尝试获取这些服务,会导致运行时异常。
- 解决方案:使用
@preResolve注解。它会强制 DI 容器在初始化阶段等待异步服务完成解析,确保后续业务逻辑在调用getIt<T>()时,服务已完全就绪。
@module abstract class OhosPlatformModule { // 使用 @preResolve 标记异步初始化服务 @preResolve Future<SharedPreferences> get prefs => SharedPreferences.getInstance(); }4、 防御循环依赖与内存泄漏
在复杂的模块化架构中,A 依赖 B、B 又依赖 A 的“炸弹式堆栈溢出”是常见的崩溃诱因。
- 延迟初始化(Lazy Fetch):在业务逻辑内部才调用
df.get()或weaver.get(),不要在构造函数的第一行就触发全量寻找,将依赖解析的时间点向后推移。 - 内存防御:对于持有大体积数据(如鸿蒙媒体缓存、大规模 JSON 字典)的 Service,应谨慎使用全局单例。建议在模块卸载或页面销毁时,手动触发容器的
unregister或配合weaver.scoped局部作用域,确保依赖资源能被及时释放。
5、 多 Isolate 间的“注入真空”处理
由于鸿蒙的隔离空间(Isolate)不共享内存,UI 线程注册的服务在后台计算线程中是拿不到的。
- 独立注入空间:在每一个新创建的 Isolate 起始处,重新执行一次轻量级的注册逻辑。利用现代 DI 框架(如
df_di)的极速启动特性,这种重复注册的性能损耗可以忽略不计。 - 参数序列化透传:如果必须跨线程共享某些服务状态,建议将对象关键参数序列化后通过消息队列(如
tw_queue)进行跨线程传递。
6、 分布式场景下的状态对齐
在鸿蒙“万物互联”的分布式场景下,多个设备副本可能同时运行着 DI 容器。如果某个服务需要在设备间保持状态一致(如全局配置、分布式数据对象),建议在 DI 的实现类内部调用鸿蒙原生的分布式数据对象(Distributed Data Object)接口,实现“全局单例”在不同设备间的状态自动对齐。