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控件开放给这个插件。

我的经验:能力声明别写得太宽泛。我曾经把一个插件的能力写成“全功能航线规划”,结果主程序把所有航线相关的UI都塞给它,导致界面卡死。后来改成细粒度声明,每个能力对应一个具体的UI组件,问题就解决了。

3.2 插件加载机制

加载机制解决的是“插件从哪来、怎么进内存”的问题。QGC用的是动态库加载,也就是 .so(Linux)或 .dll(Windows)文件。主程序在启动时扫描指定目录,找到所有合法的插件库,逐个加载。

加载流程我总结成三步:

  1. 扫描目录:主程序遍历 plugins/ 文件夹,找出所有后缀为 .so 或 .dll 的文件。
  2. 校验签名:检查动态库的数字签名或哈希值,防止加载恶意代码。
  3. 实例化插件:通过工厂函数 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;
    }
};
注意:动态库加载有个坑——版本兼容性。我曾经在Ubuntu 18.04上编译的插件,放到20.04上直接崩溃,原因是Qt库版本不一致。解决方案是:要么静态链接Qt,要么在插件内自带Qt运行时。

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 整体架构图

下面这张图把上面讲的四个部分串起来了。你看一眼,基本就能理解插件系统是怎么运转的。

插件系统架构图 插件系统架构总览 QGC 主程序 插件加载器 扫描目录 → 校验签名 → 实例化插件 生命周期管理器 onLoad → onActivate → onDeactivate → onUnload 通信总线 信号槽 + 服务接口 位置更新 / 航线上传 已加载插件实例 航线规划插件 能力: 航点编辑 / 地形跟随 禁飞区检测插件 能力: 地理围栏 / 告警 日志记录插件 能力: 飞行数据 / 事件记录 核心流程 ① 主程序启动 → 插件加载器扫描目录 → 加载动态库 ② 生命周期管理器按顺序调用 onLoad → onActivate ③ 插件通过通信总线与主程序交换数据

嗯,这张图基本把插件系统的骨架画清楚了。你写插件的时候,脑子里要有这张图——你的代码会落在哪个位置?是跟加载器打交道,还是跟通信总线交互?想清楚这个,写起来就顺手多了。