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