4. Qt配置与裁剪

在嵌入式设备上部署 Qt,第一步往往不是写业务代码,而是把 Qt 本身“配”成适合目标板的样子。桌面发行版预装的 Qt 通常包含大量模块与默认特性,直接搬到资源受限的 ARM 板上,轻则占用几十兆存储,重则因缺少图形后端或编译选项不匹配而无法启动。configure 正是控制这一切的总开关:它决定编译哪些模块、启用哪些特性、链接方式如何,以及最终产物是静态库还是动态库。

一、configure 的三种调用形态

Qt 5 与 Qt 6 的 configure 入口略有差异。Qt 5 使用源码根目录下的 configure 脚本,Qt 6 则推荐使用 configure.bat(Windows)或 ./configure(Linux/macOS),并支持通过 - 前缀传递特性开关。无论哪个版本,核心逻辑一致:解析参数 → 生成 qconfig.h 与 qconfig.pri → 调用 qmake 生成 Makefile。

# Qt 5 典型交叉编译配置
./configure -prefix /opt/qt5-arm \
  -opensource -confirm-license \
  -release -static \
  -xplatform linux-arm-gnueabi-g++ \
  -no-opengl -no-xcb -no-gtk \
  -nomake examples -nomake tests \
  -skip qtwebengine -skip qt3d \
  -qt-zlib -qt-libpng -qt-freetype

# Qt 6 典型配置(特性开关用 -feature/-no-feature)
./configure -prefix /opt/qt6-arm \
  -release -shared \
  -device linux-arm64-g++ \
  -no-feature-opengl -no-feature-xcb \
  -nomake examples -nomake tests

参数大致分为五类:安装与许可(-prefix、-opensource、-confirm-license)、编译模式(-release/-debug、-static/-shared)、平台与工具链(-xplatform、-device、-sysroot)、模块与特性(-skip、-no-xxx、-feature/-no-feature)、构建裁剪(-nomake、-no-compile-examples)。理解这五类,就能快速定位一个参数该放在哪里。

二、模块裁剪:从“全量”到“最小可用”

模块裁剪的目标是只保留业务真正依赖的 Qt 模块。Qt 的模块分为基础模块(QtCore、QtGui、QtWidgets、QtNetwork 等)和扩展模块(QtWebEngine、Qt3D、QtCharts、QtMultimedia 等)。嵌入式设备通常只需要 QtCore + QtGui + QtWidgets,或 QtCore + QtQuick(QML 场景)。

裁剪手段有三种:-skip <module> 直接跳过整个模块的配置与编译;-no-<feature> 关闭模块内的某个特性;-nomake examples -nomake tests 不编译示例与测试。三者叠加,可把 Qt 源码编译时间从数小时压缩到几十分钟,产物体积从数百 MB 降到几十 MB。

建议先做一次“最小配置”编译,确认能跑通目标板的图形界面,再逐步加回需要的模块。反向操作(先全量再删)往往因为依赖关系复杂而反复失败。

三、特性开关:qconfig.h 与条件编译

特性开关最终会写入 qconfig.h,形如 #define QT_NO_OPENGL、#define QT_FEATURE_xcb -1。业务代码中可以用 #ifndef QT_NO_OPENGL 做条件编译,避免在未启用该特性的平台上编译失败。Qt 6 进一步引入 QT_FEATURE_xxx 宏,取值 1 表示启用、-1 表示禁用、0 表示自动检测。

// qconfig.h 片段(Qt 6)
#define QT_FEATURE_opengl -1
#define QT_FEATURE_xcb -1
#define QT_FEATURE_eglfs 1
#define QT_FEATURE_freetype 1

// 业务代码中的条件编译
#if QT_CONFIG(opengl)
    // 使用 OpenGL 渲染路径
#else
    // 回退到软件渲染
#endif

常见误区是“关了特性却仍在代码里调用”。例如关闭 OpenGL 后,若 QML 场景仍使用 ShaderEffect,运行时会直接崩溃。因此特性开关必须与业务代码的渲染路径同步设计。

四、静态编译与动态编译的权衡

静态编译把 Qt 库直接链接进可执行文件,产物是一个独立二进制;动态编译则生成 .so 库,运行时通过 LD_LIBRARY_PATH 加载。两者在嵌入式场景下的取舍非常关键。

维度静态编译动态编译
部署复杂度低,单文件即可运行高,需同步部署 .so 与插件
可执行文件体积大(几十 MB)小(几百 KB)
内存占用较高,无法共享库页较低,多进程可共享
升级维护需重新编译整个程序可单独替换 .so
插件加载需静态导入插件运行时动态加载
许可合规LGPL 下需提供目标文件LGPL 下相对宽松

选择建议:如果设备存储紧张、且只有单一 Qt 应用,优先动态编译;如果设备存储充足、追求“拷贝即运行”的部署体验,或需要避免库版本冲突,则选静态编译。注意静态编译下,平台插件(如 eglfs、linuxfb)必须通过 Q_IMPORT_PLUGIN 显式导入,否则运行时会提示“This application failed to start because no Qt platform plugin could be initialized”。

五、案例:一块 256MB 存储的 ARM 板

目标板为 Cortex-A7、256MB 存储、128MB 内存,运行 Linux + framebuffer。业务是一个 QWidget 配置界面,无网络、无多媒体。配置策略如下:

  • 编译模式:-release -static,减少运行时依赖。
  • 平台插件:-no-xcb -no-opengl,使用 linuxfb 或 eglfs。
  • 模块裁剪:-skip qtwebengine -skip qt3d -skip qtmultimedia -skip qtcharts。
  • 构建裁剪:-nomake examples -nomake tests。
  • 第三方库:-qt-zlib -qt-libpng -qt-freetype,避免交叉编译外部依赖。

最终产物约 18MB,启动时间 1.2 秒,满足需求。若改为动态编译,需额外部署约 25MB 的 .so 与插件目录,且启动脚本要设置 QT_QPA_PLATFORM=linuxfb 与 LD_LIBRARY_PATH。

静态编译时若忘记 -static 与 -no-shared 同时使用,可能生成“半静态”产物:Qt 库静态链接,但插件仍以动态方式查找,导致运行时找不到插件。务必检查 configure 摘要中的 “Build type: static” 与 “Shared libraries: no”。

六、复盘:配置阶段最容易踩的坑

第一,工具链不匹配。交叉编译器的 sysroot 必须包含目标板的头文件与库,否则 configure 会在检测阶段报 “cannot find -lstdc++”。第二,特性依赖未处理。关闭 OpenGL 后,QtQuick 的某些模块仍会尝试编译 OpenGL 相关代码,需同时 -skip qtquick3d。第三,静态插件遗漏。使用 Q_IMPORT_PLUGIN(QLinuxFbIntegrationPlugin) 并确保对应插件被编译进静态库。第四,许可合规。LGPL 下静态链接需提供可重新链接的目标文件,商业项目应提前评估。

Qt configure 决策路径:从需求到产物 目标板需求 存储 / 内存 / 图形后端 编译模式 -static / -shared 平台与工具链 -xplatform / -device 模块裁剪 -skip / -nomake 特性开关 -no-feature / -no-xxx configure 生成 qconfig.h + qconfig.pri 检查摘要:Build type / Shared libraries / Features 静态产物 单二进制 + 静态插件 动态产物 .so + 插件目录 验证 目标板运行 + 插件加载

总结来说,configure 不是“一次性配好就完事”的步骤,而是与业务需求、目标板资源、许可策略紧密耦合的工程决策。先明确“最小可用集合”,再用 -skip、-no-feature、-nomake 逐层裁剪,最后在静态与动态之间做一次有依据的权衡,才能让 Qt 在嵌入式设备上既跑得动,又好维护。