3. 插件系统架构设计
插件系统,说白了就是给QGC开了一扇门。让第三方开发者能往里塞自己的代码,又不把主程序搞乱。我刚开始接触这个设计时,也觉得不就是写几个接口嘛,后来踩了坑才明白——接口定义、加载机制、生命周期、通信协议,这四个东西少一个,插件就跑不起来。
这一章,我把这四块掰开揉碎了讲。你跟着走一遍,基本就能搭出一个可用的插件框架。
3.1 插件接口定义
接口是插件和主程序之间的契约。我习惯先定义一组纯虚类,作为所有插件的基类。这样主程序不用关心插件内部怎么实现,只管调用这些接口就行。
// 插件基类 - 所有插件必须继承这个
class QGCCorePlugin {
public:
virtual ~QGCCorePlugin() {}
// 插件基本信息
virtual QString name() const = 0;
virtual QString version() const = 0;
virtual QString description() const = 0;
// 插件能力声明
virtual QList<PluginCapability> capabilities() const = 0;
// 生命周期回调
virtual bool onLoad() = 0;
virtual bool onUnload() = 0;
virtual void onActivate() = 0;
virtual void onDeactivate() = 0;
};
这里有个细节——capabilities() 返回的是插件的能力列表。比如你的航线规划插件,可以声明自己支持“航点编辑”、“地形跟随”、“禁飞区检测”等能力。主程序拿到这个列表,就知道该把哪些UI控件开放给这个插件。
3.2 插件加载机制
加载机制解决的是“插件从哪来、怎么进内存”的问题。QGC用的是动态库加载,也就是 .so(Linux)或 .dll(Windows)文件。主程序在启动时扫描指定目录,找到所有合法的插件库,逐个加载。
加载流程我总结成三步:
- 扫描目录:主程序遍历
plugins/文件夹,找出所有后缀为.so或.dll的文件。 - 校验签名:检查动态库的数字签名或哈希值,防止加载恶意代码。
- 实例化插件:通过工厂函数
createPlugin()创建插件对象,并调用onLoad()。
// 插件加载器核心逻辑
class PluginLoader {
public:
bool loadPlugin(const QString& filePath) {
QLibrary* lib = new QLibrary(filePath);
if (!lib->load()) {
logError("加载失败: " + filePath);
return false;
}
// 获取工厂函数
typedef QGCCorePlugin* (*CreatePluginFunc)();
CreatePluginFunc createFunc =
(CreatePluginFunc)lib->resolve("createPlugin");
if (!createFunc) {
logError("未找到工厂函数");
lib->unload();
return false;
}
// 创建插件实例
QGCCorePlugin* plugin = createFunc();
if (!plugin->onLoad()) {
logError("插件 onLoad 失败");
delete plugin;
lib->unload();
return false;
}
m_loadedPlugins.append({plugin, lib});
return true;
}
};
3.3 生命周期管理
每个插件都有明确的生死节点。我把它分成四个阶段:
| 阶段 | 回调函数 | 触发时机 | 典型操作 |
|---|---|---|---|
| 加载 | onLoad() |
插件库被加载后立即调用 | 初始化资源、注册服务 |
| 激活 | onActivate() |
用户启用插件或主程序启动完成 | 创建UI、连接信号槽 |
| 停用 | onDeactivate() |
用户禁用插件或主程序准备退出 | 断开连接、释放UI资源 |
| 卸载 | onUnload() |
插件被移除前最后一步 | 清理内存、保存配置 |
这里有个容易忽略的点——加载和激活是分开的。加载只是把插件代码放进内存,激活才是真正让它干活。为什么这么设计?因为主程序启动时可能同时加载十几个插件,如果每个插件都在加载阶段创建UI,界面会卡成PPT。我建议把重量级操作放到 onActivate() 里,等主程序界面渲染完成后再执行。
避坑指南:我曾经在 onLoad() 里开了数据库连接,结果用户禁用了插件再启用,连接已经断了。后来我把数据库连接的打开/关闭放到了 onActivate() / onDeactivate() 里,每次激活都重新连接,问题解决。
3.4 与QGC主程序通信
插件不能活在真空里。它需要跟主程序交换数据——比如获取当前无人机的位置、发送航点指令、监听飞行状态变化。通信方式我推荐两种:
- 信号槽(Signal-Slot):Qt原生机制,适合一对多的广播场景。比如主程序发射
vehiclePositionChanged()信号,所有订阅了这个信号的插件都能收到。 - 服务接口(Service API):通过注册中心,插件主动调用主程序提供的服务。比如
MissionService::uploadWaypoints()。
下面是一个信号槽通信的例子:
// 主程序端 - 发射位置更新信号
class VehicleManager : public QObject {
Q_OBJECT
signals:
void positionUpdated(double lat, double lon, double alt);
public:
void updatePosition(double lat, double lon, double alt) {
emit positionUpdated(lat, lon, alt);
}
};
// 插件端 - 接收位置更新
class MyMissionPlugin : public QGCCorePlugin {
public:
bool onActivate() override {
// 连接到主程序的信号
VehicleManager* vm = getVehicleManager();
connect(vm, &VehicleManager::positionUpdated,
this, &MyMissionPlugin::onPositionChanged);
return true;
}
private slots:
void onPositionChanged(double lat, double lon, double alt) {
// 处理位置更新
qDebug() << "当前位置:" << lat << lon << alt;
}
};
服务接口的方式更直接。主程序暴露一个 ServiceRegistry,插件通过它获取各种服务:
// 插件获取航线服务并上传任务
void MyMissionPlugin::uploadMission() {
MissionService* service =
ServiceRegistry::instance()->getService<MissionService>();
if (service) {
Mission mission = buildMission();
service->uploadMission(mission);
}
}
3.5 整体架构图
下面这张图把上面讲的四个部分串起来了。你看一眼,基本就能理解插件系统是怎么运转的。
嗯,这张图基本把插件系统的骨架画清楚了。你写插件的时候,脑子里要有这张图——你的代码会落在哪个位置?是跟加载器打交道,还是跟通信总线交互?想清楚这个,写起来就顺手多了。