一、前言
阅读 HMRouter、ImageKnife、ImageKnifePro 三个鸿蒙开源库的源码时,我发现单例模式出现的频率远超预期。粗略统计,三个仓库中至少有 6 处显著不同的单例实现,从最基本的懒汉式,到 C++ 静态局部变量,再到构建工具生命周期驱动的自毁式单例,覆盖了 ArkTS、C++、hvigor 三个技术层。
这些实现之间的差异并非随意选择。每一种写法都与它所处的运行环境、线程模型和资源管理需求紧密绑定。把它们放在一起对比,能清晰看到单例模式在不同场景下的工程权衡。

二、ImageKnife.getInstance():经典懒汉式
ImageKnife 的 ArkTS 层实现是最朴素的懒汉式单例。整个类只暴露一个 getInstance() 静态方法,构造函数标记为 private,实例在首次调用时创建。
typescript
// ImageKnife/library/src/main/ets/ImageKnife.ets
export class ImageKnife {
private static instance: ImageKnife;
public static getInstance(): ImageKnife {
if (!ImageKnife.instance) {
ImageKnife.instance = new ImageKnife();
}
return ImageKnife.instance;
}
private constructor() {
}
}
这段代码没有任何线程安全保护。ArkTS 的执行模型是单线程事件循环,getInstance() 不会被并发调用,所以不需要锁。实例一旦创建就常驻内存,ImageKnife 持有内存缓存(MemoryLruCache)、文件缓存(FileCache)和调度器(ImageKnifeDispatcher)三个核心资源,这些资源在整个应用生命周期内都需要存活。
ImageKnifePro 的 ArkTS 层也用了相同的写法。它的 ImageKnife 类同样是 private constructor 加 getInstance(),但实际图片加载逻辑已经下沉到 C++ 层,ArkTS 侧的单例更多扮演一个 API 代理角色,方法内部调用的是 libimageknifepro.so 导出的 native 函数。
三、HMRouterMgr:静态方法集 + 内部委托
HMRouterMgr 的写法与经典单例有明显区别。它本身没有 getInstance() 方法,所有公开 API 都是静态方法。但它并不是一个纯粹的工具类------内部通过静态成员持有了 HMRouterMgrService 的单例引用。
typescript
// HMRouterLibrary/src/main/ets/api/HMRouterMgr.ets
export class HMRouterMgr {
public static isInit: boolean = false;
private static service: HMRouterMgrService = HMRouterMgrService.getInstance();
private static routerStore: IHMRouterStore = hmRouterStore;
private static pageLifecycleMgr: HMPageLifecycleMgr = HMPageLifecycleMgr.getInstance();
static push(pathInfo: HMRouterPathInfo, callback?: HMRouterPathCallback): void {
HMRouterMgr.service.push(pathInfo, callback);
}
}
HMRouterMgrService 才是真正的单例持有者,它使用饿汉式初始化------private static instance 在声明时就 new 了出来:
typescript
// HMRouterLibrary/src/main/ets/router/HMRouterMgrService.ets
export class HMRouterMgrService {
private static instance: HMRouterMgrService = new HMRouterMgrService();
static getInstance(): HMRouterMgrService {
return HMRouterMgrService.instance;
}
private constructor() {
globalEventBus.on('changeNavigationId', (navigationId: string) => {
this.lastNavigationId = navigationId;
});
// ...
}
}
这种两层设计的好处是调用方只需要写 HMRouterMgr.push(),不必关心底层的实例管理。HMRouterMgr 是一个 Facade,HMRouterMgrService 是实际的单例。构造函数里直接订阅了 globalEventBus 的事件,说明它需要在实例化时就开始监听导航栈的变化。饿汉式在这里是合理的------路由服务在应用启动后马上就需要工作。
四、ImageKnifePro:C++ 层 static 局部变量
ImageKnifePro 的 C++ 层使用了 C++11 标准保证线程安全的 static 局部变量。头文件中 ImageKnife 是一个纯虚基类,实际实例化的是 ImageKnifeInternal 子类。
cpp
// imageknifepro/library/src/main/cpp/include/imageknife.h
class ImageKnife {
public:
static ImageKnife &GetInstance();
virtual void Init() = 0;
// ...
};
cpp
// imageknifepro/library/src/main/cpp/imageknife_internal.cpp
ImageKnife &ImageKnife::GetInstance()
{
static ImageKnifeInternal imageKnifeInternal;
return imageKnifeInternal;
}
C++11 规范要求 static 局部变量的初始化必须是线程安全的。编译器会在底层插入等价于 std::once_flag + std::call_once 的保护机制,确保多线程首次调用 GetInstance() 时只有一个线程执行构造函数,其他线程阻塞等待。这种写法被称为 Meyers' Singleton,比手动使用 std::once_flag 更简洁。
ImageKnifeInternal 的构造函数做了较多初始化工作:创建默认的缓存 key 生成器、设置线程池并发上限为 16、构造默认 loader、配置 DNS 规则。同时它通过 delete 禁止了拷贝构造和赋值操作符,并将 ImageKnife::GetInstance() 声明为友元函数。这些措施共同确保全局只存在一个实例。
cpp
ImageKnifeInternal();
ImageKnifeInternal(const ImageKnifeInternal&) = delete;
ImageKnifeInternal &operator = (const ImageKnifeInternal&) = delete;
friend ImageKnife &ImageKnife::GetInstance();
基类定义接口、子类实现逻辑、通过基类引用返回------这让单例的使用者只依赖抽象接口,不需要知道 ImageKnifeInternal 的存在。
五、HMServiceFactory:带初始化时序约束的工厂单例
HMServiceFactory 是一个工厂类,它管理的不是自身的实例,而是 IHMServiceMgr 接口的实现。
typescript
// HMRouterLibrary/src/main/ets/service/HMServiceFactory.ets
export class HMServiceFactory {
private static serviceMgr: IHMServiceMgr;
static getServiceMgr(): IHMServiceMgr {
if (!HMServiceFactory.serviceMgr) {
HMServiceFactory.serviceMgr = new HMServiceMgrImpl();
}
return HMServiceFactory.serviceMgr;
}
static setServiceMgr(mgr: IHMServiceMgr): void {
if (!HMServiceFactory.serviceMgr) {
HMServiceFactory.serviceMgr = mgr;
} else {
HMLogger.warn('The serviceMgr exists. Setting failed. This Parameter be set before HMRouter.init')
}
}
}
与前面几种单例不同,这里存在一个隐式的时序约束。setServiceMgr() 允许外部注入自定义实现来替换默认的 HMServiceMgrImpl,但这个窗口期只存在于 getServiceMgr() 首次被调用之前。一旦 serviceMgr 被创建,setServiceMgr() 就会打印警告并忽略传入的参数。

这种设计在框架中很常见:默认行为能覆盖大部分场景,但需要保留扩展点。时序约束通过日志警告而非异常来提醒开发者,是一种防御性编程策略。调用方需要确保在 HMRouterMgr.init() 之前完成注入,否则自定义实现不会生效。
六、HMRouterPluginManager:hvigor 生命周期驱动的单例
HMRouterPluginManager 是一个运行在 hvigor 构建环境中的单例,它的生命周期与应用运行时无关,而是绑定到一次构建过程。
typescript
// HMRouterPlugin/src/HMRouterPluginManager.ts
export class HMRouterPluginManager {
private static instance: HMRouterPluginManager | null = null;
private constructor() {
registerPluginExtension(new HMRouterDefaultExtension());
hvigor.nodesEvaluated(() => this.handleNodesEvaluated());
hvigor.buildFinished(() => {
this.controllerMap.forEach((controller) => {
controller.complete();
});
HMRouterPluginManager.instance = null;
PluginStore.destroy();
ExtensionManager.destroy();
this.controllerMap.clear();
TsAstUtil.clearProject();
});
}
static getInstance(): HMRouterPluginManager {
if (!HMRouterPluginManager.instance) {
HMRouterPluginManager.instance = new HMRouterPluginManager();
}
return HMRouterPluginManager.instance;
}
}
关键在 buildFinished 回调中的 instance = null。构建完成后,单例主动把自己置为 null,同时销毁 PluginStore、ExtensionManager,清空控制器和 AST 缓存。这是为了防止 hvigor 的增量构建或 watch 模式下,上一次构建的残留状态污染下一次构建。

构造函数里注册了两个 hvigor 生命周期钩子:nodesEvaluated 触发插件的实际执行,buildFinished 触发清理。这意味着单例的创建和销毁完全由构建工具的生命周期驱动,而不是应用代码显式调用。如果下一次构建再调用 getInstance(),会重新创建一个干净的实例。
七、AnnotationAnalyzerRegistry:注册表单例
AnnotationAnalyzerRegistry 管理 HMRouter 插件中所有注解分析器的注册和检索。它的单例结构本身并不特殊,值得关注的是它的两阶段初始化模式。
typescript
// HMRouterPlugin/src/hmrouter_extension/analyzer/AnnotationAnalyzerRegistry.ts
export class AnnotationAnalyzerRegistry {
private static instance: AnnotationAnalyzerRegistry;
private analyzers: Set<AbstractAnnotationAnalyzer> = new Set();
private constantResolver: IConstantResolver | null = null;
private constructor() {}
static getInstance(): AnnotationAnalyzerRegistry {
if (!this.instance) {
this.instance = new AnnotationAnalyzerRegistry();
}
return this.instance;
}
initialize(): void {
if (this.constantResolver) {
return;
}
this.constantResolver = new ConstantResolver();
this.registerDefaultAnalyzers();
}
}
getInstance() 只创建空壳对象,构造函数是空的。真正的初始化逻辑在 initialize() 方法中,它创建 ConstantResolver 并注册默认的 RouterAnalyzer 和 ComponentAnalyzer。getConstantResolver() 方法会检查 constantResolver 是否为 null,如果未初始化则抛出异常。
这种拆分的原因是注册表需要在所有扩展都注册完毕后才能初始化默认分析器。如果把初始化逻辑放在构造函数里,扩展插件就没有机会在默认分析器之前注入自己的实现。initialize() 内部通过 if (this.constantResolver) 做了幂等保护,重复调用不会产生副作用。
八、单例模式的选型建议
以上 6 种实现可以按两个维度分类:线程安全需求和生命周期管理需求。
ArkTS 层的单例不需要考虑线程安全,因为 ArkTS 运行在单线程事件循环中。ImageKnife 的懒汉式和 HMRouterMgrService 的饿汉式之间的选择,取决于实例是否需要在模块加载时就准备好。路由服务需要立即工作,用饿汉式;图片加载可以延迟到首次使用,用懒汉式。
C++ 层需要线程安全保证时,优先使用 Meyers' Singleton(static 局部变量)。它比 std::once_flag 更简洁,编译器提供的线程安全保证与手动加锁等价,且析构顺序由编译器管理。ImageKnifePro 还通过纯虚基类做了接口隔离,调用方不依赖实现类。
当单例需要支持外部注入时,采用 HMServiceFactory 的工厂模式。注意明确窗口期------一旦默认实例创建,注入接口就应该拒绝或警告。
构建工具中的单例必须处理好清理问题。HMRouterPluginManager 在 buildFinished 时主动置 null 并销毁所有关联资源,避免增量构建时状态泄漏。
需要两阶段初始化时,把 getInstance() 和 initialize() 分开。AnnotationAnalyzerRegistry 用这种方式给扩展插件留出了注册窗口,同时通过幂等检查防止重复初始化。