依赖注入:在鸿蒙中实现简单的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)接口,实现“全局单例”在不同设备间的状态自动对齐。
更多推荐



所有评论(0)