乐于分享
好东西不私藏

万字拆解Aether插件系统:5个核心模式看完你也能写一个

万字拆解Aether插件系统:5个核心模式看完你也能写一个

从C++到工业级:Aether项目精讲 · 第3篇

一、理念是虚的,代码是实的

上一篇拆了插件架构的设计理念——为什么用插件、什么时候用、架构怎么搭。

但理念再漂亮,不落地就是空中楼阁。

这篇直接打开 Aether 的 ExtensionSystem 源码,一行行拆给你看:

PluginManager 怎么做到全局唯一?对象池怎么管理?插件的生命周期谁在驱动?依赖解析怎么处理循环引用?

看完这篇,你能自己写一个工业级的插件管理器。


二、PluginManager:全局唯一的"插件总管"

先看入口。整个插件系统的老大是谁?

// pluginmanager.h · 简化核心
class PluginManager : public QObject
{
    Q_OBJECT

public:
static PluginManager *instance();  // ← 全局单例

// 对象池操作
static void addObject(QObject *obj);
static void removeObject(QObject *obj);
static QList<QObject *> allObjects();

// 模板方法:按类型取对象
template <typename T>
static T *getObject()
{
QReadLocker lock(listLock());
const QList<QObject *> all = allObjects();
for (QObject *obj : all) {
if (T *result = qobject_cast<T *>(obj))
return result;
        }
return nullptr;
    }

// 插件操作
static void loadPlugins();
static QList<PluginSpec *> plugins();
void shutdown();
};

注意几个设计细节:

单例不是花哨的单例。 就是最朴素的 static instance(),没有模板,没有 CRTP,没有魔法。因为插件管理器在整个进程里只需要一个,搞复杂了反而难懂。

对象池用了 QReadWriteLock。 读多写少的场景——大部分时间插件在查对象,只在注册/卸载时写。读写锁比互斥锁性能好一个数量级。

getObject 用 qobject_cast。 这比 dynamic_cast 快,前提是你的接口必须继承 QObject。如果某些纯虚接口不想继承 QObject,备选方案是 getInterface() 用 dynamic_cast。

对象池怎么玩?

看一个真实用法——HomePlugin 从对象池里捞主窗口服务:

bool HomePlugin::initialize(QString *errorString)
{
// 从对象池获取主窗口服务(由 CorePlugin 注册)
    IMainWindowService *svc = PluginManager::getObject<IMainWindowService>();
if (!svc) {
if (errorString)
            *errorString = "HomePlugin: IMainWindowService not found.";
return false;
    }

// 注册首页,排在最左边(order=10)
auto *homePage = new SimplePage("page.home.content");
    svc->addPage("home""nav.home", homePage, 10);
return true;
}

插件的通信方式就这么简单: 你往池子里丢对象,我从池子里捞对象。没有头文件依赖,没有复杂的 IPC,就是一个 QList<QObject *>

这就是对象池模式在工业项目里的真实用法。


三、IPlugin:每个插件的"身份证"

打开 iplugin.h,只有 50 行不到。

class IPlugin : public QObject
{
    Q_OBJECT

public:
enum ShutdownFlag {
        SynchronousShutdown,
        AsynchronousShutdown
    };

// 必须实现的两个虚函数
virtual bool initialize(QString *errorString)0;
virtual void extensionsInitialized()0;

// 可选实现的两个虚函数
virtual bool delayedInitialize()return false; }
virtual ShutdownFlag aboutToShutdown()return SynchronousShutdown; }

PluginSpec *pluginSpec() const;

signals:
void asynchronousShutdownFinished();
};

C++ Primer 会告诉你: 虚函数是一种多态机制,运行时根据虚表调用派生类实现。

但工业项目会告诉你: IPlugin 的四个虚函数定义了插件的生死节奏

看这个生命周期:

加载 → initialize → extensionsInitialized → delayedInitialize → ...运行... → aboutToShutdown

每个阶段的用意:

阶段
执行时机
典型操作
initialize
插件刚加载完
注册服务、解析配置
extensionsInitialized
所有插件都 initialize 完了
访问其他插件的服务
delayedInitialize
主循环空闲后分批执行
耗时初始化(如加载大模型)
aboutToShutdown
应用准备关闭
释放资源、持久化状态

为什么要分这么多阶段?

因为初始化顺序在插件系统里是个大坑。

如果插件 A 依赖插件 B 的服务,A 在 initialize 里就直接去拿 B 的对象——问题是 B 可能还没加载完。所以把"注册服务"和"使用服务"拆到两个阶段:先跑所有人的 initialize,再跑所有人的 extensionsInitialized

delayedInitialize 更妙。 它用定时器分批执行,不会阻塞主界面。如果某个插件要加载 500MB 的模型文件,你不会希望它在启动时把界面卡死 10 秒。


四、PluginSpec:插件的"户籍档案"

每个插件都有一个对应的 PluginSpec,相当于身份证加户口本。

class PluginSpec
{
public:
enum State {
        Invalid, Read, Resolved,
        Loaded, Initialized, Running,
        Stopped, Deleted
    };

QString name() const;
QString version() const;
QString description() const;
QString category() const;
bool isRequired() const;
QVector<PluginDependency> dependencies() const;

// 依赖解析结果
QHash<PluginDependency, PluginSpec *> dependencySpecs() const;

State state() const;
bool hasError() const;
QString errorString() const;
};

元数据从哪里来?每个插件目录下有个 JSON 文件:

{
"Name""Home",
"Version""1.0.0",
"Description" "Provides the Home page.",
"Category""UI",
"Required"false,
"Dependencies"[
"Name""Core""Version""1.0.0" }
]
}

这个 JSON 就是插件的身份证。 PluginManager 启动时扫描插件目录,反序列化 JSON,构建 PluginSpec 列表。

依赖解析的核心逻辑

问题来了:插件 A 依赖 B,B 依赖 C——加载顺序怎么定?

Aether 的做法是拓扑排序。简单说:

1. 先扫一遍所有插件,构建依赖图
2. 找没有依赖(或依赖已满足)的节点作为起点
3. 加载一个,标记"已就绪",解开依赖它的其他节点
4. 循环,直到全部加载或发现环

代码实现藏在 PluginManagerPrivate::loadQueue() 里:

bool PluginManagerPrivate::loadQueue(PluginSpec *spec,
        QList<PluginSpec *> &queue,
        QList<PluginSpec *> &circularityCheckQueue)

{
// 如果已经加入队列,说明有环
if (circularityCheckQueue.contains(spec)) {
// 报错:检测到循环依赖
return false;
    }
    circularityCheckQueue.append(spec);

// 先加载依赖项
for (const auto &dep : spec->dependencies()) {
        PluginSpec *depSpec = pluginByName(dep.name);
if (!depSpec) {
// 依赖缺失,报错
continue;
        }
loadQueue(depSpec, queue, circularityCheckQueue);
    }

if (!queue.contains(spec))
        queue.append(spec);

return true;
}

这就是工业级和 toy project 的区别。 你的小项目可以写死 loadPluginA(); loadPluginB();。但 50 个插件互相依赖的生产项目,一个可靠的有环检测的拓扑排序算法是标配。

版本兼容性检查也是一样的道理。PluginSpec::provides() 会比对 CompatVersion,确保加载的插件版本和依赖方期望的版本兼容。不兼容?直接拒绝加载。


五、从零写一个插件

有了上面的基础,现在手把手写一个完整插件。

第一步:写插件类

// myplugin.h
#pragma once

#include "extensionsystem/iplugin.h"

class MyPlugin : public ExtensionSystem::IPlugin
{
Q_OBJECT
Q_PLUGIN_METADATA(IID EXTENSIONSYSTEM_IID
                      FILE "myplugin.json")


public:
    MyPlugin() = 
default;
    ~MyPlugin() override = default;

bool initialize(QString *errorString) override;
void extensionsInitialized() override{}
};

关键点就两个:

  • Q_PLUGIN_METADATA 宏告诉 Qt "这是一个插件",IID 是全局唯一的接口标识符
  • FILE "myplugin.json" 指向元数据文件路径

第二步:实现初始化

// myplugin.cpp
#include "myplugin.h"

#include "extensionsystem/pluginmanager.h"
#include "uibase/imainwindowservice.h"

using namespace ExtensionSystem;

bool MyPlugin::initialize(QString *errorString)
{
Q_UNUSED(errorString)

// 注册自己的服务到对象池
auto *myService = new MyService();
    PluginManager::addObject(myService);

qDebug() << "[MyPlugin] initialized and service registered.";
return true;
}

记得在 extensionsInitialized 里做跨插件操作。 这里只注册自己的服务,不去碰别人的东西。

第三步:写元数据

// myplugin.json
{
"Name""MyPlugin",
"Version""1.0.0",
"CompatVersion""1.0.0",
"Category""Business",
"Description" "My first Aether plugin",
"Required"false,
"Dependencies"[
"Name""Core""Version""1.0.0" }
]
}

第四步:配置 CMake

# CMakeLists.txt
set(PLUGIN_NAME MyPlugin)

find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core Widgets)

fz2_collect_sources(PLUGIN_SOURCES PLUGIN_HEADERS)

add_library(${PLUGIN_NAME} SHARED
    ${PLUGIN_SOURCES}
    ${PLUGIN_HEADERS}
    myplugin.json          # ← 元数据也要参与编译!
)

target_compile_features(${PLUGIN_NAME} PRIVATE cxx_std_17)

target_link_libraries(${PLUGIN_NAME} PRIVATE
    Qt${QT_VERSION_MAJOR}::Core
    Qt${QT_VERSION_MAJOR}::Widgets
    ${COMMON_TARGET_NAMESPACE}::extensionsystem
)

fz2_setup_plugin(${PLUGIN_NAME} "plugins/myplugin")

编译出来就是一个 DLL/SO。 放到程序插件目录,PluginManager 自动扫描加载。

全流程加起来不到 30 行有效代码。你学会了吗?


六、插件系统核心设计模式总结

回顾整篇,Aether 的插件系统用了三个关键模式:

模式
实现
解决的问题
单例模式
PluginManager::instance()
确保全局只有一个管理器
对象池模式
addObject / getObject / allObjects
插件间零耦合通信
状态机模式
PluginSpec::State 八状态流转
精确控制插件生命周期

这三板斧组合起来,搞出了一个可以在 50+ 插件规模下稳定运行的生产级框架。


评论区话题: 如果用一句话总结插件系统的核心设计模式,你觉得是什么?单例?策略?还是另有高见?评论区说说你的理解,我每条都会看。

下篇预告: 插件系统让模块各自独立,但模块之间怎么通信?用对象池传递指针太原始了——下一篇,IoC 容器,Laravel 风格的依赖注入在 C++ 中完整落地。我写了 5 年 C++ 才彻底看懂的东西,一篇给你讲透。

系列目录

第 1 篇:[从 60 万行 C++ 项目中我学到了什么?](已发布)

第 2 篇:[Aether 插件架构:为什么我放弃了传统的 MVC](已发布)

第 3 篇:万字拆解 Aether 插件系统,看完你也能写一个(本文)

第 4 篇:我写了 5 年 C++,才彻底看懂 IoC 容器(预告)