做嵌入式产品的人大概都遇到过这样的场面:板子上跑的程序在开发机上一测就崩,提示找不到 libQt5Core.so.5;或者现场设备要升级,脚本里写死了库路径,结果一换文件系统整个界面就起不来。更头疼的是有些方案为了省空间,把 Qt 库裁得七零八落,删掉一个看起来没用到的插件 so,程序就在某个特定操作下直接退出。说到底,这些都是动态链接带来的耦合。Qt5.14.2 的 aarch64 静态交叉编译,本质上要解决的就是这件事:让一个可执行文件自己扛下所有依赖,拷过去就能跑,不用在目标板上维护一套 Qt 运行库。这篇手册写给已经会写 Qt 程序、但还没系统做过交叉静态编译的人,也写给被动态库折磨过、想一次性把部署问题解决掉的同行。从工具链搭建到 configure 参数取舍,再到静态插件导入和字体这些静态编译特有的坑,我会按我实际趟过的路径讲一遍。
1. 为什么选静态交叉编译:先想清楚收益和代价
1.1 动态部署踩过的那些坑
动态链接的部署方式,逻辑上是优雅的:目标板存一份 Qt 运行库,多个程序共享,升级库就能统一修 bug。但落到实际产品里,这份优雅经常变成麻烦。最常见的是库版本漂移——开发机装的是某个发行版自带的 Qt5.14.x,目标板是另一套裁剪过的运行时,.so的 soname 对得上,符号版本却对不上,运行时报一句莫名其妙的 "version `Qt_5' not found"。
其次是插件路径问题。Qt 的插件默认从可执行文件目录的platforms/、imageformats/等子目录加载,打包时少拷一个libqjpeg.so,用户上传 jpg 头像就失败;少拷platforms/libqlinuxfb.so,程序直接报 "This application failed to start because no Qt platform plugin could be initialized"。这类问题在实验室里很难复现,因为它只在特定文件系统布局下出现。
再有一个更隐蔽的:目标板 rootfs 是只读的,程序想在运行时动态加载库,但库被放在了一个靠后的挂载点上,启动顺序稍有偏差就加载不到。你临时改个环境变量能绕过,可产品不会给你改环境变量的机会。
静态编译把这些不确定性一刀切掉。所有用到的 Qt 代码、图片格式解析、字体渲染、平台抽象,全部编译进最终的可执行文件,运行时不再依赖外部 Qt 库和插件目录。代价也明确:单个文件体积从几 MB 涨到几十 MB,多个程序无法共享代码,改一个 bug 就得整体重编。这些代价在嵌入式场景里往往是能接受的,尤其是设备功能单一、更新频率低的时候。
1.2 方案选型的几个关键决策
决定做静态交叉编译之后,还有几个岔路口要选。
第一,宿主机用哪个发行版。我不是在开玩笑,这是真会踩坑的地方。Qt5.14.2 的源码对 glibc 版本和 GCC 版本有一定要求,太新的发行版(比如默认 GCC 13+ 的)编译时会撞上一堆老代码的告警甚至报错。我一般倾向选一个稍保守的构建环境,GCC 版本落在 9 到 11 之间比较省心。实在只能用新环境,就准备几组补丁,后面对应问题我会展开。
第二,用不用目标板的 sysroot。sysroot 是目标系统头文件和库的集合。静态编译理论上只需要-static链接 Qt 的库,但因为 Qt 会依赖 zlib、libpng、freetype 这些第三方库,如果没有 sysroot,就得让 Qt 用自己源码树里带的版本。Qt 提供了一批-qt-*开关(-qt-zlib、-qt-libpng等),作用就是用自带的第三方库编译,省去准备 sysroot 的麻烦。我的建议是:能在 sysroot 里准备好的依赖尽量用 sysroot,用得顺手且版本可控;不好准备的(比如 freetype、harfbuzz)就用-qt-前缀让 Qt 自带,减少对目标系统的假设。
第三,平台插件怎么选。aarch64 嵌入式设备常见的显示后端有 linuxfb、eglfs 两种。纯 framebuffer、没 GPU 的方案用 linuxfb;有 GPU、跑 OpenGL ES 的用 eglfs。这个选择会直接影响 configure 里对 opengl 的处理,也决定后面要不要静态导入QLinuxFbIntegrationPlugin还是QEglFSIntegrationPlugin。
提示:平台插件选错不会在编译期报错,而是编译成功后一运行就提示找不到 QPA 插件。先确认目标板有没有 /dev/fb0、有没有 GLES 驱动,再定插件,别凭感觉。
2. 编译环境与工具链的落地准备
2.1 宿主机依赖包清单
交叉编译 Qt 本身对宿主机的依赖不算多,但少了任何一样都会在 configure 或 make 阶段卡住。以常见的 Debian/Ubuntu 系为例,需要准备的包大致是这些:
sudo apt-get install -y \ build-essential perl python3 \ bison flex gperf \ libxkbcommon-dev libxkbcommon-x11-dev \ libglib2.0-dev libpixman-1-dev \ libfontconfig1-dev libfreetype6-dev \ zlib1g-dev libpng-dev libjpeg-dev \ pkg-config这里面有几个值得说明。perl和python3是 Qt 构建系统自己的脚本依赖,具体点说,Qt 有部分模块(比如 qtvirtualkeyboard 的词库处理)用到了 Python;bison、flex、gperf用于生成解析器;libxkbcommon-dev是 X11 和 Wayland 下键盘映射需要的。
需要特别强调的是,这些宿主机的开发包主要服务于 Qt 的 host 工具(比如 moc、uic、rcc、host 版 qmake),它们的编译走的是宿主机本地编译器,和交叉编译目标程序是两条线。这一点很容易混淆:你以为装了libfreetype6-dev就能给目标板用,其实那只是让 host 侧不报错。目标侧真正用到的 freetype,要么在 sysroot 里,要么用-qt-freetype走 Qt 自带源码。
2.2 aarch64 交叉工具链与 sysroot 的取得
工具链我一般用 Linaro 或者 ARM 官方发布的 GNU toolchain,版本挑比 sysroot 里 glibc 稍新一点但不要跨度太大的。判断标准很简单:工具链的 glibc 版本不能高于目标板 rootfs 的 glibc,否则静态链接出来的程序在运行时可能因为符号版本不兼容而直接段错误。
拿到工具链后,先做两件事。
第一,把它解压到固定路径,比如/opt/toolchain/gcc-aarch64,然后把bin加进PATH:
export PATH=/opt/toolchain/gcc-aarch64/bin:$PATH export CROSS_COMPILE=aarch64-linux-gnu-验证:
aarch64-linux-gnu-gcc -v能打印出版本信息就说明路径没问题。
第二,准备 sysroot。如果你的工具链自带 sysroot(很多发行版工具链内置了),可以直接用它的路径;如果没有,就从目标板 rootfs 里把/usr/include、/usr/lib、/lib拷贝出来,整理成一份干净的 sysroot 目录:
mkdir -p /opt/sysroot # 从目标板或 SDK 中同步头文件和库 rsync -a rootfs/usr/include/ /opt/sysroot/usr/include/ rsync -a rootfs/usr/lib/ /opt/sysroot/usr/lib/ rsync -a rootfs/lib/ /opt/sysroot/lib/这里要留意软链接。rootfs 里的/lib经常是/usr/lib的软链接,直接 rsync 会把链接原样带过去甚至报错。用cp -a或者加-L参数把符号链接解引用成实体文件更稳。
注意:sysroot 不需要完整包含目标板所有内容,只保留编译需要的头文件、静态库和必要的动态库即可。多余的脚本和配置文件反而会干扰 configure 的探测。整理完后,general 建议是
du -sh看一眼,控制在几百 MB 到 1GB 之间比较合理。
2.3 Qt5.14.2 源码的获取与目录规划
Qt5.14.2 的源码官方发布为qt-everywhere-src-5.14.2.tar.xz,体积在 500MB 上下,解压后接近 3GB。很多人在搜"qt5.14.2离线安装包下载",其实那类在线安装器主要是给桌面开发用的,交叉编译要的是这份 everywhere-src 源码包。源码里包含了 qtbase、qtdeclarative、qtsvg 等所有子模块,一次编译就能出一整套。
解压和目录规划:
mkdir -p /opt/qt-build cd /opt/qt-build tar -xf qt-everywhere-src-5.14.2.tar.xz mv qt-everywhere-src-5.14.2 src mkdir -p build-static install-static我习惯把源码、构建目录、安装目录三处分开放:源码保持干净、可复用;构建目录放 configure 生成的 Makefile 和中间产物;安装目录就是最终make install的落点。这样即使一次编译失败,删掉 build 目录重来,源码和已安装内容都不受影响。install 目录最终会被打包进 SDK 或直接拷到开发机固定位置,路径记得设成相对稳定、不带用户名的绝对路径,否则 qmake 生成的路径里会写进你的 HOME,团队其他人用起来就麻烦了。
3. configure 参数逐条拆解与 mkspecs 定制
3.1 核心配置参数的含义与取舍
Qt 的 configure 脚本参数多到吓人,但真正影响静态交叉编译成败的其实就那么十来个。下面这条命令是我在 linuxfb 方案下比较常用的一套:
cd /opt/qt-build/build-static /opt/qt-build/src/configure \ -prefix /opt/qt-build/install-static \ -hostprefix /opt/qt-build/install-static-host \ -opensource -confirm-license \ -release -static -optimize-size \ -xplatform linux-aarch64-gnu-g++ \ -device-option CROSS_COMPILE=aarch64-linux-gnu- \ -sysroot /opt/sysroot \ -nomake examples -nomake tests \ -no-opengl \ -no-icu -no-iconv \ -qt-zlib -qt-libpng -qt-libjpeg \ -qt-freetype -qt-harfbuzz -qt-pcre \ -no-dbus -no-cups -no-feature-printdialog \ -skip qtwebengine -skip qtquick3d -skip qtmultimedia逐条说。
-static是核心,让 Qt 编译出静态库并在链接时把依赖打进去。-release关闭调试信息,静态库本来体积就大,debug 版会再翻几倍。-optimize-size让编译器偏向体积优化,对嵌入式很实用;如果你更在意运行速度,可以换成-optimize-speed。
-xplatform linux-aarch64-gnu-g++指定目标平台描述文件。Qt5.14.2 的qtbase/mkspecs/下一般已经有linux-aarch64-gnu-g++。如果没有(某些裁剪过的子版本会缺),就从linux-arm-gnueabi-g++复制一份改名,再改里面的qmake.conf。这个文件后面单独讲。
-device-option CROSS_COMPILE=aarch64-linux-gnu-把交叉前缀传给 mkspecs,配合上面那个 xplatform 使用。它有讲究:如果你的工具链前缀不是标准的aarch64-linux-gnu-(比如带厂商名的aarch64-xx-linux-gnu-),这里必须写全,否则 configure 探测编译器时会失败。
-sysroot /opt/sysroot告诉编译器到哪找目标系统的头文件和库。这个路径会同时影响 host 和 target 两套工具的编译,务必确认它存在且结构正确,写错了 configure 不会立刻报错,而是在某个模块的 make 阶段冒出一堆找不到头文件的错误。
-no-opengl是 linuxfb 方案的关键。没有 GPU 的设备完全不需要 OpenGL,关掉能省一大块体积,也避免因为找不到 GLES 库而编译失败。如果你的设备跑 OpenGL ES,改成-opengl es2,并且确认 sysroot 里有libGLESv2和libEGL的静态库——注意,必须静态库,动态库链接不进去。
-no-icu -no-iconv是为了避开 ICU 这个大依赖。ICU 是国际化库,提供排序、正则、日期格式化。Qt5 里 QtCore 在某些配置下会依赖它。交叉静态编译时 ICU 的静态库很难搞到,用-no-icu直接去掉,代价是部分国际化功能变为 Qt 自带的简化实现,正则表达式仍能用(走 Qt 自带的 QRegularExpression 或 PCRE),对绝大多数设备界面没影响。
-qt-zlib -qt-libpng -qt-libjpeg -qt-freetype -qt-harfbuzz -qt-pcre这一串,意思是这些第三方库全部用 Qt 源码树里自带的版本编译成静态库。这样就不必在 sysroot 里准备它们的目标端版本,极大降低了准备工作的复杂度。代价是版本稍旧,但 Qt 官方选定的版本和 Qt 配合是验证过的,稳定性反而更好。
-nomake examples -nomake tests跳过示例和测试,能省下大量编译时间。-skip qtwebengine这一条尤其重要:qtwebengine 内部自带一个 Chromium,交叉编译它是个独立的大工程,和静态编译无关的时候一定要跳过,否则会卡在拉取和编译 Chromium 上几个小时。
3.2 mkspecs 的定制细节
多数情况下linux-aarch64-gnu-g++直接可用,但你要理解它的结构,出问题时才知道改哪。这个目录下通常只有一个qmake.conf:
# # qmake configuration for linux-aarch64-gnu-g++ # MAKEFILE_GENERATOR = UNIX CONFIG += incremental QMAKE_INCREMENTAL_STYLE = sublib QT_QPA_DEFAULT_PLATFORM = linuxfb include(../common/linux.conf) include(../common/gcc-base-unix.conf) include(../common/g++-unix.conf) QMAKE_CC = $${CROSS_COMPILE}gcc QMAKE_CXX = $${CROSS_COMPILE}g++ QMAKE_LINK = $${CROSS_COMPILE}g++ QMAKE_LINK_SHLIB = $${CROSS_COMPILE}g++ QMAKE_AR = $${CROSS_COMPILE}ar cqs QMAKE_OBJCOPY = $${CROSS_COMPILE}objcopy QMAKE_NM = $${CROSS_COMPILE}nm -P load(qt_config)其中最值得注意的是QT_QPA_DEFAULT_PLATFORM = linuxfb。这一行决定了 Qt 编译时默认启用哪个平台抽象层,也决定了最终链接进程序的是哪个平台的默认逻辑。如果你的设备走 eglfs,这里要改成eglfs,否则程序启动会去初始化 framebuffer 而非 EGL。这一项和 configure 里的 opengl 设置要对应:linuxfb 配-no-opengl,eglfs 配-opengl es2。
另外,QMAKE_CC那几行用了$${CROSS_COMPILE}变量。这个变量要么来自环境变量,要么来自 configure 的-device-option。你如果不想每次 configure 都传,也可以直接把这几行里的$${CROSS_COMPILE}换成字面量aarch64-linux-gnu-。但我不建议写死,因为换个工具链就要改文件,容易忘,反倒不如传参来得清楚。
如果你的工具链没有处理好 sysroot,还可能需要在这里追加QMAKE_CFLAGS和QMAKE_LFLAGS,把--sysroot=/opt/sysroot显式加进去。正常情况下-sysroot参数已经能覆盖,但如果遇到链接阶段找不到-lc、-lpthread这类基础库,八成是这里没配好。
3.3 configure 执行与结果核对
configure 会跑几分钟,逐个探测编译器能力、第三方库存在性、平台特性。跑完之后,重点看两处输出。
第一,看它是用什么编译器探测的。输出里应该能找到aarch64-linux-gnu-g++的字样,如果全是宿主机的g++,说明-xplatform或CROSS_COMPILE没生效,赶紧停下来检查,不然编译出来的“目标程序”其实是 x86 的,白忙一场。
第二,看 configure 结尾的 summary。这个摘要会列出哪些功能启用了、哪些禁用了。重点确认这几点:Build type: release、Static build: yes、-no-opengl对应的 OpenGL 是 disabled、libpng/libjpeg/zlib 用的是 bundled(自带)版本、ICU 是 no。任何一项不对,就回到参数里找原因,别带着错误的配置往下 make,那样只会在几十分钟后以报错收场。
提示:configure 的参数被缓存进了
config.summary和.qmake.cache,改参数后建议重新运行一次完整的 configure,而不是在旧目录上打补丁。参数变更如果不触发重新探测,可能出现配置和实际不一致的情况。
4. 编译、安装与静态产物的验证
4.1 并行编译与内存管理
configure 通过后就进入 make 阶段。这里的核心问题只有一个:并行度。Qt 编译加上后面的静态链接阶段,单个编译单元处理 qtdeclarative 时内存占用能飙到好几个 GB,一上来就make -j$(nproc)很容易触发 OOM,把编译进程杀掉,报出virtual memory exhausted或者直接Killed。
我的经验是分两段设置并行度。普通源文件编译阶段,可以用核数的一半到全部:
make -j$(nproc) 2>&1 | tee build.log一旦进入链接阶段(尤其是qtdeclarative、qtsvg这些模块),把并行度降到 2 到 4:
make -j4判断是否进入链接阶段,看日志里LINK、ld出现的频率。或者干脆稳一点,全程用-j4,多花点时间换稳定。aarch64 的编译在中等配置机器上大概需要 1.5 到 3 小时,视核数和模块数量而定。
还有一个容易被忽略的点:make中断后不要急着删目录重来。Qt 的 make 支持增量,重新make -j4会从断点继续。但如果你在中断时改过 configure 参数,那就必须重新 configure 甚至make distclean,否则缓存会对不上。
4.2 make install 与目录结构
编译成功后:
make install安装后的目录结构大致是:
install-static/ ├── bin/ (host 工具,比如宿主可用的一些辅助程序) ├── include/ (Qt 头文件) ├── lib/ │ ├── libQt5Core.a │ ├── libQt5Gui.a │ ├── libQt5Widgets.a │ ├── libQt5Network.a │ └── cmake/ (CMake 配置文件) ├── mkspecs/ ├── plugins/ │ ├── platforms/ │ ├── imageformats/ │ └── ... └── phrasebooks/注意lib/下全是.a静态库,看不到.so,这就是静态编译成功的标志。plugins/下的插件也是静态的.a文件。静态编译时,插件不是靠目录加载的,而是靠代码里显式导入,后面细说。
另外,如果你在 configure 里没指定-hostprefix,host 工具会被装进同一个 install 目录。因为 host 工具是宿主机可执行的,和目标静态库放在一起有点乱,所以建议用-hostprefix单独指定,把 host 工具和 target 库分开管理。
4.3 用一个最小程序验证静态链接
光看.a存在还不够,得实际编一个程序验证。我一般用最简的 Qt Widgets 程序:
// main.cpp #include <QApplication> #include <QLabel> int main(int argc, char *argv[]) { QApplication app(argc, argv); QLabel label("static build ok"); label.show(); return app.exec(); }用安装出来的 qmake 生成 Makefile:
export PATH=/opt/qt-build/install-static/bin:$PATH qmake -query QT_VERSION # 应该输出 5.14.2 qmake main.cpp.pro make编出来之后,先看文件类型和依赖:
file hello # 期望看到: ELF 64-bit LSB executable, ARM aarch64, statically linked ldd hello # 期望看到: not a dynamic executable如果file显示dynamically linked,说明某个环节还在用动态链接,回到 configure 确认-static是否生效;如果显示了ARM aarch64但ldd还列出几个库,那通常是libstdc++、libgcc这些编译器运行时没静态化,需要在链接时补上-static-libstdc++ -static-libgcc,或者直接在 qmake 配置里让链接器全静态。这个细节下一节展开。
5. 静态编译特有的坑与排查技巧
5.1 插件必须显式静态导入
这是静态编译和动态编译最大的差别,也是新手最容易栽的地方。动态编译时,Qt 会在运行时扫描plugins/platforms/目录自动加载插件;静态编译时没有运行时扫描,所有插件都得在代码里显式导入,用Q_IMPORT_PLUGIN宏。
平台插件是必须的。linuxfb 方案要导入:
#include <QtPlugin> Q_IMPORT_PLUGIN(QLinuxFbIntegrationPlugin)eglfs 方案则是:
#include <QtPlugin> Q_IMPORT_PLUGIN(QEglFSIntegrationPlugin)图片格式插件按需导入。如果你需要程序处理 jpg 图片,就必须导入 JPEG 插件,否则QPixmap::load("x.jpg")会静默失败:
Q_IMPORT_PLUGIN(QJpegPlugin) Q_IMPORT_PLUGIN(QGifPlugin)导入宏要写在有main()的源码里,或者至少是会被链接进来的某个源文件中。写在头文件里然后被多个源文件包含,会导致重复定义链接错误,这一点要留意。
导入的类名怎么确认?去install-static/plugins/目录下看,比如plugins/imageformats/libqjpeg.a对应的类名一般是QJpegPlugin,规则是把libq前缀去掉、首字母大写加 Plugin。实在拿不准,可以用nm libqjpeg.a | grep -i plugin找qt_plugin_instance_相关符号,符号名里通常能反推出类名。
提示:漏导入插件的症状很隐蔽。程序能启动、能显示窗口,但加载某种格式图片时返回空对象,或者剪贴板功能失效,却没有明确报错。遇到这类“某个功能静默失效”的问题,先把可能相关的插件都
Q_IMPORT_PLUGIN一遍。
5.2 字体与运行时资源
动态编译时,Qt 通过 fontconfig 从系统字体目录里找字体。静态编译后,程序跑在可能连字体目录都没有的环境里,fontconfig 很可能无法工作。所以静态方案里字体必须自己带。
两种做法。一是把 ttf 文件编进 qrc 资源,程序启动时用QFontDatabase::addApplicationFontFromData加载:
QFile fontFile(":/fonts/NotoSansCJK.ttf"); fontFile.open(QIODevice::ReadOnly); QFontDatabase::addApplicationFontFromData(fontFile.readAll());二是把 ttf 放在可执行文件旁边,用addApplicationFont从磁盘路径加载。我倾向第一种,因为资源编进二进制,部署时又少一个外部依赖,和静态编译的思路一致。
字体的坑还不止于此。中文字体动辄十几 MB,编进资源文件会让可执行文件体积再涨一截。实践中我会裁一个常用字库子集,或者选一个体积较小的开源中文字体。这不是偷懒,是嵌入式空间有限时的必要取舍。
图片资源、翻译文件(.qm)同理,都要用 qrc 的方式嵌进去,不能用运行时从目录读取那套。
5.3 常见报错速查
下面这些错误我在不同项目里都碰到过,整理成表方便对照。
| 报错信息 | 可能原因 | 处理办法 |
|---|---|---|
Unknown module(s) in QT: xcb | qmake 用到了没编译的模块 | 确认 configure 没有跳过相关模块,或改用 linuxfb/eglfs 平台 |
运行时no Qt platform plugin could be initialized | 平台插件没静态导入 | 补Q_IMPORT_PLUGIN并确认类名正确 |
链接时undefined reference to __cxa_throw | libstdc++ 没静态链接 | 链接加-static-libstdc++ -static-libgcc |
virtual memory exhausted/ 进程 Killed | 并行度太高触发 OOM | 降低-j数值,尤其链接阶段 |
| configure 报找不到编译器 | CROSS_COMPILE 前缀不对 | 检查工具链前缀和 PATH |
| 运行时段错误且无输出 | 目标板 glibc 低于工具链 glibc | 换用 glibc 版本不高于目标板的工具链 |
Cannot find -lGLESv2 | 缺少静态 GLES 库 | sysroot 中补静态库,或改用-no-opengl |
关于 glibc 那一条,我再补充一点。静态链接的程序虽然把 Qt 打了进去,但libc层面它通常还是动态依赖的(除非你连 libc 一起静态化,那会带来一堆坑)。所以目标板的 glibc 版本必须不低于工具链的 glibc。排查方法是在宿主机上执行aarch64-linux-gnu-strings /opt/toolchain/lib/libc.so.6 | grep GLIBC_,看最高版本号,再和目标板上的同名文件对比。
5.4 链接脚本里的静态化补全
如果你发现程序虽然目标架构对了,但还挂着几个动态依赖,通常是编译器运行时没静态化。可以在程序的.pro文件里补链接参数:
QMAKE_LFLAGS += -static-libstdc++ -static-libgcc -Wl,-Bstatic -lc -Wl,-Bdynamic这里-Wl,-Bstatic和-Wl,-Bdynamic是成对使用的包夹语法,把-lc夹在中间强制静态链接,其余仍可动态。全静态-static虽然也能做,但 glibc 全静态会遇到 NSS(名称服务切换)的问题,比如域名解析在静态程序里可能失效,网络功能受影响。除非你确认程序不需要解析域名、不需要用户组查询,否则不建议对 libc 做全静态。
这个取舍值得说清楚:qtbase 网络模块做 DNS 解析时会调用 glibc 的 NSS 机制,而 NSS 是运行时通过dlopen加载libnss_dns.so等模块的。全静态链接的程序没有dlopen能力(严格的静态环境),DNS 解析就可能退化甚至失败。所以如果你的设备要用 QtNetwork 做域名访问,链接时别用全-static打 libc。
6. 部署、体积优化和一点后续扩展
先说完整体积。一个带 Widgets、Network、SVG,并且嵌了中文字体的静态可执行文件,落在 25MB 到 45MB 之间是正常的。想压一压,有几个方向:configure 时加-optimize-size,把-release里的调试信息彻底剥掉(编译完成后aarch64-linux-gnu-strip hello),只链实际用到的模块(用不到 QtNetwork 就别在.pro里QT += network),字体裁子集。经过这几步,一个纯界面程序压到 15MB 以内完全可行。
strip这一步别忘。不 strip 的静态二进制里带着所有符号表,体积能大出将近一倍。strip 之后再拷到目标板,路径无所谓,放哪都能跑,这也是静态方案最舒服的地方。
调试方面要提前留一手。strip 之前务必把带符号的版本存档,否则线上出问题只能靠打印调试。更专业的做法是用objcopy --only-keep-debug把调试信息单独存成一个.debug文件,再objcopy --add-gnu-debuglink建立关联,这样发布的二进制是精简的,排查时又能用gdb加载符号。这个流程在嵌入式项目里值得一开始就建好。
后续如果设备支持 OTA,静态方案还有个附加好处:更新就是把一个新文件覆盖旧文件,失败回滚也是换回旧文件,没有库版本冲突这一层,部署脚本能写得很简单。我在这类项目里的做法是保留两份可执行文件加减一个启动脚本判断,几乎不出问题。要是后面设备要跑多个独立功能进程,静态方案导致的体积重复就会开始显现,那时再考虑改成动态或部分动态的混合模式,把共享的那部分 Qt 库单独拎出来,这是另一个话题了。