【开源软件移植】nomacs开源项目适配鸿蒙 PC 全流程笔记实战
本文详细介绍了将开源图片查看器nomacs移植到鸿蒙PC(HarmonyOS NEXT)的全过程。通过分析项目依赖矩阵,采用"双重stub"技术剥离exiv2等非必要依赖,完成Qt 5.14+到5.12的API降级,最终生成4.9MB的ARM aarch64共享库libnomacs.so。项目成功在华为MateBook Pro上运行,填补了鸿蒙PC生态中桌面级图片查看工具的空白。文章提供了从环境准
【开源软件移植】nomacs开源项目适配鸿蒙 PC 全流程笔记实战
欢迎加入开源鸿蒙 PC 社区:https://harmonypc.csdn.net/
本文成果:nomacs 3.17.2295(一款 2969 stars 的跨平台图片查看器)成功交叉编译为鸿蒙 PC(HarmonyOS NEXT,arm64-v8a)原生共享库
libnomacs.so(4.9 MB、ARM aarch64、零非 Qt 系统依赖、链接到 Qt-OHOS 5.12.12),并打包成 HAP 工程nomacsOhos在华为 MateBook Pro(HAD-W24)上已成功运行。
注意(本部分必看):
开始本文工作前需要完成:
从0创建项目指南,新手先看这优质的实战文章篇,,后续工作都在用HAP 壳工程:
https://blog.csdn.net/weixin_52908342/article/details/161343743
准备 OpenHarmony SDK
准备 Qt for HarmonyOS
复现最小 Qt Widgets Demo
准备 DevEco HAP 壳工程
完成上面步骤,即可跟着本文进行手把手教学适配nomacs!

本项目开源地址:https://atomgit.com/weixin_52908342/OH-nomacs
0. 写在前面
nomacs 是一款用 C++ + Qt 写的"小而美的免费图片查看器",社区维护近 12 年(最早可追溯到 2011 年),跨 Windows / Linux / macOS 三端,支持 JPEG / PNG / BMP / GIF / SVG / TIFF / RAW / HEIF / AVIF 等十几种图片格式 —— 是 Linux 桌面上最常被推荐的"看图三件套"之一(另外两个是 gThumb 和 Geeqie)。
把 nomacs 搬到鸿蒙 PC 上的意义有三:
- 填补鸿蒙 PC 的"通用图片查看器"工具链 —— 目前鸿蒙生态系统应用偏向"相册"+"详情页"模式,缺一款支持目录浏览、缩放/旋转/全屏、批量定位的桌面级 GUI;
- 首次实战"用 stub 头文件 + 整文件空实现 cpp 双重剥离非必要 C++ 库依赖" —— 上游强依赖 exiv2(一个写 EXIF 元数据的库,需要联动 zlib / expat / libxml2 一整堆),重新交叉编译这条依赖链至少要 1~2 天,本次用 stub 法 30 分钟搞定;
- 沉淀 Qt 5.14+/5.15+ → 5.12 API 降级清单 —— 前几篇 NitroShare / glogg / DiffPDF 都是 Qt 5.12 友好的"老项目",nomacs 是首次遇到"项目用了 Qt 5.14+ 新 API、但鸿蒙 Qt 还停在 5.12.12"的情况,这条降级清单未来可复用到所有"较新 Qt 项目"的鸿蒙移植。
整个适配过程严格按照下面 8 个阶段推进:
项目侦察 → 下载源码 → 改造方案设计 → CMake 顶层重写
→ exiv2 双重 stub → Qt API 降级 5 处 → 编译 + 5 项产物自检 → HAP 壳复用 + 上机调试




1. 选型:为什么是 nomacs

1.1 候选清单回顾
选择 nomacs —— 它是清单中**一款"原生 Qt5、用户感知强、依赖收敛于单点(exiv2)"**的候选。
1.2 nomacs 项目侧写

通过 GitHub API 拿到的关键事实:
- 主页:https://github.com/nomacs/nomacs
- 协议:GPL v3
- ⭐ 2969 stars(在 Linux 看图器赛道里头部)
- 实现语言:C++(96%)+ CMake + 少量 Python/Shell
- 工程根:
ImageLounge/(不是仓库根) - 模块切分:
src/DkCore/(核心:解码、缓存、元数据)+src/DkGui/(界面:窗口、视图、菜单) - 源码量:46 个 .cpp + 48 个 .h,共 4.3 MB
- 顶层
CMakeLists.txt344 行
为什么不选最新 3.22.1 而是 3.17.2295?因为 3.17 系列是 nomacs 最后一代严格围绕 Qt5 设计的版本;3.19+ 开始往 Qt6 偏移,对 Qt5 的兼容性逐渐变差,而我们手头的 Qt-OHOS 是 5.12.12,所以从一致性出发选 3.17 最稳。
1.3 nomacs 默认依赖矩阵
来自 nomacs 顶层 CMakeLists.txt 的 9 个 option(...) 默认值:
| 依赖 | 默认 | 用途 | 鸿蒙策略 |
|---|---|---|---|
| exiv2 | REQUIRED(不可选) | EXIF/IPTC/XMP 元数据 | 🎯 stub 化 |
| OpenCV | ON | RAW + 高级 TIFF 解码 | OFF |
| libraw | ON | 相机 RAW(依赖 OpenCV) | OFF |
| libtiff | ON | 多层 TIFF | OFF |
| QuaZip | ON | 浏览 ZIP 内图片 | OFF |
| 翻译 | ON | 多语言 .qm | OFF |
| plugins | ON | 插件框架 | OFF |
| libheif | OFF | HEIF | OFF(保持) |
| libavif | OFF | AVIF | OFF(保持) |
| libjxl | OFF | JPEG-XL | OFF(保持) |
💡 第一关键发现:上游
cmake/Unix.cmake把 exiv2 写成pkg_check_modules(EXIV2 REQUIRED exiv2>=0.27)—— 即使把所有option(...)关到 OFF,只要走 Linux 分支,exiv2 仍然是硬性依赖。这条死路在后面"双重 stub"那一节解决。
新手必看前置步骤
从0创建项目指南,新手先看这篇:
https://blog.csdn.net/weixin_52908342/article/details/161343743
QT官方鸿蒙版开源地址:https://wiki.qt.io/Qt5.12.12_Open_Source_Release_for_HarmonyOS_zh
QT官方文档地址:https://wiki.qt.io/Qt_for_OpenHarmony/zh
环境要求
(HarmonyOS/OpenHarmony)鸿蒙版本 API20+
Qt Creator安装 安装电脑版Qt5.12或以上版本(5.14、5.15),获得QtCreator的IDE。
华为 DevEco Studio 安装 如果您想开发Qt for HarmonyOS应用程序,除了使用Qt Creator之外,还需要依赖DevEco Studio。
准备 DevEco HAP 壳工程
这一步在 DevEco Studio 里做。
创建工程
在 DevEco Studio 中:
File
New
Create Project
选择一个最简单的 Stage 模板。工程名可以叫:
QtOhosDemo
目标结构大致类似:
QtOhosDemo/
entry/
src/main/
ets/
cpp/
libs/
如果你使用的是 Qt for HarmonyOS 官方模板,里面通常已经有加载 Qt runtime 的代码。
设置加载库名
找到类似文件:
entry/src/main/ets/common/QtAppConstants.ets
设置:
export const APP_LIBRARY_NAME = 'libqt_ohos_demo.so';
如果你的模板没有这个文件,就搜索:
APP_LIBRARY_NAME
loadLibrary
libqohos
目标是让 HAP 启动时加载:
libqt_ohos_demo.so
放入动态库
创建目录:
entry/libs/arm64-v8a/
把下面文件放进去:
libqt_ohos_demo.so
libqohos.so
libQt6Core.so 或 libQt5Core.so
libQt6Gui.so 或 libQt5Gui.so
libQt6Widgets.so 或 libQt5Widgets.so
libqt_ohos_demo.so 来自:
~/qt-ohos-demo/build-ohos/libqt_ohos_demo.so
Qt runtime 的 .so 来自你的 Qt for HarmonyOS 安装目录。
签名 .so
在构建主机上签名:
export SIGN_TOOL=$OHOS_SDK_ROOT/toolchains/lib/binary-sign-tool
cd /path/to/QtOhosDemo/entry/libs/arm64-v8a
$SIGN_TOOL sign -inFile libqt_ohos_demo.so -outFile libqt_ohos_demo.so -selfSign 1
如果你自己拷贝了其他三方库 .so,也一起签名。
运行
在 DevEco Studio 中:
Sync Project
Build Hap
Run
成功标志:
鸿蒙 PC 上出现一个窗口,显示 Hello Qt on HarmonyOS PC

如果这里成功,说明:
Qt runtime 可以加载
HAP 壳工程可以运行
你的业务 .so 可以被鸿蒙应用加载
到这里就可以开始本章的nomacs适配工作了。
2. 阶段 1:环境搭建
从0创建项目指南,新手先看这篇:
https://blog.csdn.net/weixin_52908342/article/details/161343743
三个关键点(前几篇已验证,本次直接复用):
$QT_OHOS_ROOT指向 Qt-OHOS 5.12.12 交叉版;$OHOS_SDK_ROOT指向官方 OHOS NDK,llvm/bin/aarch64-unknown-linux-ohos-clang即交叉编译器;scripts/ohos-toolchain.cmake(43 行)从 NitroShare 项目原样拷贝。
# 只做一件事:把 NitroShare 那条 toolchain 拷过来
cp NitroShareGOGO/scripts/ohos-toolchain.cmake \
nomacsGOGO/scripts/ohos-toolchain.cmake
scp nomacsGOGO/scripts/ohos-toolchain.cmake root@SERVER:/root/nomacsGOGO/
3. 阶段 2:源码下载与项目侦察

mkdir -p /root/nomacsGOGO && cd /root/nomacsGOGO
wget -q 'https://github.com/nomacs/nomacs/archive/refs/tags/3.17.2295.tar.gz'
tar -xzf 3.17.2295.tar.gz
cd nomacs-3.17.2295/ImageLounge
注意 nomacs 的 工程根是 ImageLounge/,不是仓库根 —— 在仓库根跑 cmake 会找不到 CMakeLists.txt。这是个反直觉的小坑。
源码切分如下:
nomacs-3.17.2295/ImageLounge/
├── 3rdparty/
│ ├── drif/ # ★ Apple ProRAW 解码头(drif_image.h)
│ └── libqpsd/ # ★ PSD 格式解码器(必须随主 .so 编进来!)
├── cmake/
│ ├── Unix.cmake # ★ Linux 依赖检测(这里写死了 exiv2 REQUIRED)
│ └── UnixBuildTarget.cmake # Linux 链接规则
├── src/
│ ├── DkCore/ # 模型层:DkBasicLoader / DkImageLoader / DkMetaData ...
│ ├── DkGui/ # 视图层:DkNoMacs / DkViewPort / DkWidgets ...
│ ├── main.cpp
│ └── nomacs.qrc
└── CMakeLists.txt # 344 行顶层
4. 阶段 3:改造方案设计 —— 双重 stub 是核心
4.1 exiv2 调用面定位

# 谁还在引用 exiv2 命名空间?
grep -l 'Exiv2::' src/DkCore/*.cpp src/DkGui/*.cpp
# 结果:只有 src/DkCore/DkMetaData.cpp
# 谁 #include <exiv2/...>?
grep -l '#include.*exiv2' src/DkCore/*.h src/DkGui/*.h
# 结果:只有 src/DkCore/DkMetaData.h
# 调用次数
grep -c 'Exiv2::' src/DkCore/DkMetaData.cpp \
src/DkCore/DkBasicLoader.cpp \
src/DkCore/DkImageLoader.cpp
# DkMetaData.cpp:108
# DkBasicLoader.cpp:0
# DkImageLoader.cpp:0
💡 第二关键发现:exiv2 的所有调用 集中在唯一的文件
DkMetaData.cpp(1985 行)—— 这意味着可以对这一个文件实施"整文件替换",不会污染其他模块。
4.2 改造方案:双重 stub
| 上游 | 鸿蒙版改造 | 行数变化 |
|---|---|---|
CMakeLists.txt(顶层 344 行,包含 Win/Mac/Linux 多分支 + plugins + 翻译) |
重写为精简版 | 344 → 123 |
cmake/Unix.cmake + cmake/UnixBuildTarget.cmake(exiv2 REQUIRED + 多 target) |
不调用,绕开 | – |
src/DkCore/DkMetaData.cpp(1985 行 exiv2 重度调用) |
整体替换为空 stub | 1985 → 173 |
src/DkCore/DkMetaData.h + <exiv2/*.hpp>(编译期类型依赖) |
提供 7 个 stub 头 | 0 → 9 KB stub 头 |
4.3 产物形态
| 上游 | 鸿蒙版 |
|---|---|
add_executable(nomacs ...) + add_library(nomacsCore SHARED ...)(2 个 target) |
合并 成单一 add_library(nomacs SHARED ...),导出 T main 符号 |
默认带 SONAME = libnomacs.so.3 |
NO_SONAME TRUE(鸿蒙 dlopen 只识别裸名) |
5. 阶段 4:CMake 顶层重写 + exiv2 stub 头
5.1 全新精简版 CMakeLists.txt
把原 344 行的顶层 CMakeLists.txt 整体替换为以下精简版(关键节选):
cmake_minimum_required(VERSION 3.10)
project(nomacs CXX)
# ---- 全部可选依赖关闭 ----
option(ENABLE_OPENCV "" OFF)
option(ENABLE_RAW "" OFF)
option(ENABLE_TIFF "" OFF)
option(ENABLE_QUAZIP "" OFF)
option(ENABLE_TRANSLATIONS "" OFF)
option(ENABLE_PLUGINS "" OFF)
option(ENABLE_HEIF "" OFF)
option(ENABLE_AVIF "" OFF)
option(ENABLE_JXL "" OFF)
# ---- ★ exiv2 stub include 优先:让 stub 头胜出于真实 exiv2 头 ----
include_directories(BEFORE /root/nomacsGOGO/exiv2-stub)
find_package(Qt5 5.12 REQUIRED COMPONENTS
Core Gui Widgets Network PrintSupport Concurrent Svg)
# ---- 源码搜集 ----
file(GLOB GUI_SOURCES "src/DkGui/*.cpp")
file(GLOB CORE_SOURCES "src/DkCore/*.cpp")
list(REMOVE_ITEM GUI_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/src/DkPluginManager.cpp")
# ---- ★ 3rdparty/libqpsd 必须随主 .so 编进来,否则 dlopen 会报:
# Error relocating libnomacs.so: _ZN11QPsdHandlerC1Ev: symbol not found
set(LIBQPSD_SOURCES
${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/libqpsd/qpsdhandler.cpp
${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/libqpsd/qpsdhandler_p.cpp)
include_directories(
${CMAKE_BINARY_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/src
${CMAKE_CURRENT_SOURCE_DIR}/src/DkCore
${CMAKE_CURRENT_SOURCE_DIR}/src/DkGui
${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/libqpsd
${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/drif # ★ drif_image.h
)
qt5_add_resources(NOMACS_RCC src/nomacs.qrc)
# ---- ★ 关键:合并 binary + DkCore 成单一 SHARED 库,并导出 main ----
add_library(nomacs SHARED
${NOMACS_EXE_SOURCES} ${GUI_SOURCES} ${CORE_SOURCES}
${LIBQPSD_SOURCES}
${NOMACS_RCC})
set_target_properties(nomacs PROPERTIES
OUTPUT_NAME "nomacs"
PREFIX "lib"
NO_SONAME TRUE) # ★ 不要 libnomacs.so.3 这种带版本号的 SONAME
target_compile_definitions(nomacs PRIVATE
DK_CORE_DLL_EXPORT
DK_DLL_EXPORT
NOMINMAX)
target_link_libraries(nomacs
Qt5::Widgets Qt5::Gui Qt5::Network
Qt5::PrintSupport Qt5::Concurrent Qt5::Svg)
💡 关键设计点:
- 不调用
cmake/Unix.cmake:那里把 exiv2 写死成 REQUIRED;include_directories(BEFORE /root/nomacsGOGO/exiv2-stub):让 stub 头优先于真实 exiv2 被命中;add_library(nomacs SHARED)而不是add_executable:鸿蒙 HAP 的 Qt 加载器只 dlopen 一个 .so 然后dlsym("main")调用入口;NO_SONAME TRUE:避免生成带版本号的 SONAME;- ★
LIBQPSD_SOURCES必须显式列入 —— 这是后面踩坑 1 的根因,第一次写 CMake 时只加了 include 路径忘了源码,链接通过但 dlopen 时炸。
5.2 exiv2 stub 头

scripts/exiv2-stub/exiv2/exiv2.hpp 提供 nomacs 用到的 Exiv2 名字的最小定义(节选):
namespace Exiv2
{
typedef unsigned char byte;
class Exifdatum {
public:
std::string key() const { return {}; }
std::string toString() const { return {}; }
long_t toLong(long = 0) const { return 0; }
Exifdatum &operator=(const std::string &) { return *this; }
int setValue(const std::string &) { return 0; }
};
class Xmpdatum { /* ... 同样的空实现 ... */ };
class Iptcdatum { /* ... 同样的空实现 ... */ };
template <typename Datum> class DataIterator {
public:
Datum &operator*() { static Datum d; return d; }
DataIterator &operator++() { return *this; }
bool operator!=(const DataIterator &) const { return false; }
};
class ExifData {
public:
typedef DataIterator<Exifdatum> iterator;
iterator begin() { return {}; }
iterator end() { return {}; }
bool empty() const { return true; }
Exifdatum &operator[](const std::string &) { static Exifdatum d; return d; }
void add(const Exifdatum &) {}
};
class XmpData { /* ... */ };
class IptcData { /* ... */ };
class Image {
public:
typedef std::unique_ptr<Image> UniquePtr;
virtual void readMetadata() {}
virtual void writeMetadata() {}
ExifData &exifData() { return mExifData; }
XmpData &xmpData() { return mXmpData; }
IptcData &iptcData() { return mIptcData; }
private:
ExifData mExifData;
XmpData mXmpData;
IptcData mIptcData;
};
class ImageFactory {
public:
static Image::UniquePtr open(const std::string &)
{ return Image::UniquePtr(new Image); }
static Image::UniquePtr open(const byte *, long)
{ return Image::UniquePtr(new Image); }
};
} // namespace Exiv2
加上 6 个重定向头(image.hpp / preview.hpp / xmpsidecar.hpp 等),它们都只有两行:
#pragma once
#include <exiv2/exiv2.hpp>
总共 9 KB 的 stub 头就能让 DkMetaData.h 的所有 #include <exiv2/...> 编过。
5.3 DkMetaData.cpp 整体 stub(173 行替换原 1985 行)
#include "DkMetaData.h"
#include <QImage>
#include <QSize>
#include <QStringList>
#include <QVector2D>
namespace nmc
{
DkMetaDataT::DkMetaDataT() = default;
bool DkMetaDataT::isNull() { return true; }
QSharedPointer<DkMetaDataT> DkMetaDataT::copy() const
{ return QSharedPointer<DkMetaDataT>(new DkMetaDataT); }
void DkMetaDataT::readMetaData(const QString &filePath, QSharedPointer<QByteArray>)
{
mFilePath = filePath;
mExifState = no_data; // 关键:永远停在 no_data,让上层走"无元数据"分支
}
bool DkMetaDataT::saveMetaData(const QString &, bool) { return false; }
QString DkMetaDataT::getDescription() const { return {}; }
int DkMetaDataT::getOrientationDegree() const { return 0; }
QImage DkMetaDataT::getThumbnail() const { return {}; }
QImage DkMetaDataT::getPreviewImage(int) const { return {}; }
bool DkMetaDataT::hasMetaData() const { return false; }
/* ... 其余 60 多个 isXxx / getXxx / setXxx 全部返回默认值 ... */
QString DkMetaDataT::exiv2ToQString(std::string s) {
return QString::fromStdString(s); // 唯一保留的真实逻辑(不依赖 exiv2)
}
void DkMetaDataT::setQtValues(const QImage &cImg)
{
mQtKeys.clear(); mQtValues.clear();
if (!cImg.isNull()) {
mQtKeys << "Width" << "Height";
mQtValues << QString::number(cImg.width())
<< QString::number(cImg.height());
} // ← Width / Height 这种 Qt 自身就能算的值,给真实结果
}
// loadSidecar / setXMPValue 这些必须返回 Exiv2 类型的方法 —— 用 stub 类构造空对象
std::unique_ptr<Exiv2::Image> DkMetaDataT::loadSidecar(const QString &) const
{ return std::unique_ptr<Exiv2::Image>(new Exiv2::Image); }
bool DkMetaDataT::setXMPValue(Exiv2::XmpData &, QString, QString) { return false; }
// DkMetaDataHelper 的所有方法也是空实现
void DkMetaDataHelper::init() { /* clear all lists */ }
void DkMetaDataHelper::initialize() { getInstance().init(); }
QString DkMetaDataHelper::translateKey(const QString &key) const { return key; }
/* ... 其余全部返回空 ... */
} // namespace nmc
💡 关键设计点:
- DkMetaData 的所有方法签名 1:1 保留 —— 这样 DkGui 里上百处
metaData->getXxx()调用全部不需要改;- 大多数返回空值 / false / 0 —— 上层会走"该图无元数据"的分支,不会崩溃;
- 唯一保留真实逻辑的两个:
exiv2ToQString(纯字符串转换)和setQtValues(Width/Height 用 QImage 自己算)—— 这两个对"看图"主功能有真实用途。
6. 阶段 5:Qt 5.14+/5.15+ → 5.12 API 降级(5 处补丁)

nomacs 3.17 已经开始用 Qt 5.14+ 的新 API,而 Qt-OHOS 是 5.12.12,这里有 5 处必须降级:
6.1 三处批量替换(sed 一把搞定)
grep -rln 'Qt::SkipEmptyParts\|Qt::endl\|moveToTrash' src/ | while read f; do
sed -i 's|Qt::SkipEmptyParts|QString::SkipEmptyParts|g' "$f"
sed -i 's|Qt::endl|endl|g' "$f"
sed -i 's|return file\.moveToTrash();|return file.remove();|g' "$f"
done
| 新 API | 出现于 Qt | 替代方案 | 影响 |
|---|---|---|---|
Qt::SkipEmptyParts |
5.14 | QString::SkipEmptyParts(旧位置) |
等价 |
Qt::endl |
5.14 | 全局 endl(来自 <QTextStream>) |
等价 |
QFile::moveToTrash() |
5.15 | QFile::remove() |
降级:直接删除而不是移到回收站 |
6.2 两处单点改写
// src/DkGui/DkWidgets.cpp:3101
- QScreen *myScreen = screen(); // QWidget::screen() 是 5.14+
+ QScreen *myScreen = QGuiApplication::primaryScreen();
// src/DkCore/DkBaseViewPort.cpp:498
- zoomLeveled(factor, event->position()); // QWheelEvent::position() 是 5.14+
+ zoomLeveled(factor, event->posF());
6.3 一处缺标准头
// src/DkCore/DkMath.h
#pragma once
+ #include <ostream> // ← 用 std::ostream<<"..." 必须显式 include
#include <QDebug>
💡 第三关键发现:
std::ostream << "字符串字面量"在 musl-libc / libc++ 组合下必须显式#include <ostream>,否则会去匹配到QDebug的重载,报"invalid operands ... const char[2]"。glibc + libstdc++ 上因为传递包含经常碰巧能编过,迁到 OHOS NDK(musl + libc++)就暴露了。
7. 阶段 6:编译 + 5 项产物自检
7.1 一次配置 + 一次编译

cd /root/nomacsGOGO/nomacs-3.17.2295/ImageLounge
mkdir build && cd build
cmake -G 'Unix Makefiles' \
-DCMAKE_TOOLCHAIN_FILE=/root/nomacsGOGO/ohos-toolchain.cmake \
-DCMAKE_BUILD_TYPE=Release ..
# Configuring done (0.1s)
make -j4 2>&1 | tee build.log
# [ 60%] Building CXX object .../DkCore/DkTimer.cpp.o
# [ 62%] Building CXX object .../DkCore/DkUpdater.cpp.o
# [ 66%] Linking CXX shared library libnomacs.so
# [100%] Built target nomacs
grep -cE 'error:' build.log
# 0
7.2 5 项产物自检

| # | 项 | 期望 | 实际 |
|---|---|---|---|
| 1 | file libnomacs.so |
ELF aarch64 shared object | ✅ ELF 64-bit LSB shared object, ARM aarch64 |
| 2 | size | 几 MB | ✅ 4.9 MB |
| 3 | llvm-nm -D 是否有 T main |
T main | ✅ 0x21a19c T main |
| 4 | llvm-readelf -d NEEDED |
仅 Qt5 + libc | ✅ 只有 Qt5Network/PrintSupport/Concurrent/Svg/Widgets/Gui/Core + libc++_shared + libc,没有 libexiv2 / libopencv / libraw |
| 5 | LOAD 段对齐 | 0x1000 (4 KB) | ✅ 4 个 LOAD 段全 0x1000 |
5/5 全过。
8. 阶段 7:HAP 工程构建
从0创建项目指南,HAP 工程构建先看这篇:
https://blog.csdn.net/weixin_52908342/article/details/161343743
QT官方鸿蒙版开源地址:https://wiki.qt.io/Qt5.12.12_Open_Source_Release_for_HarmonyOS_zh
QT官方文档地址:https://wiki.qt.io/Qt_for_OpenHarmony/zh
8.1 壳复用
# 1) 复制demoOhos 整壳,去掉构建产物
cp -R demoOhos nomacsOhos
rm -rf nomacsOhos/{.idea,build,oh_modules,*/build,*/oh_modules}
# 2) 替换库文件
rm nomacsOhos/entry/libs/arm64-v8a/libnitroshare.so
cp build/libnomacs.so nomacsOhos/entry/libs/arm64-v8a/libnomacs.so
# 3) 5 处文案替换(grep 出来一共只有 3 个文件 6 处)
# 文件 1:AppScope/resources/base/element/string.json "value": "nomacs"
# 文件 2:entry/.../element/string.json module_desc / QAbility_*
# 文件 3:entry/.../common/QtAppConstants.ets APP_LIBRARY_NAME = 'libnomacs.so'
# LOG_TAG = 'nomacsOhos'

8.2 4 项 HAP 自检
| # | 项 | 结果 |
|---|---|---|
| 1 | APP_LIBRARY_NAME 指向 |
✅ libnomacs.so |
| 2 | entry/libs/arm64-v8a/libnomacs.so 就位 |
✅ 4.9 MB |
| 3 | bundleName |
✅ com.example.gloggohos(沿用 GloggOhos 签名证书,免重新申请,可直接 ▶ Run) |
| 4 | 桌面显示名 / module 文案 | ✅ 全部 nomacs |
完成后,DevEco Studio 打开 nomacsOhos/,▶ Run,鸿蒙 PC 桌面就出现一个 nomacs 图标。

9. 阶段 8:上机调试 —— 两个 dlopen 期 symbol-not-found 坑
第一次部署到华为 MateBook Pro 上,启动 1 秒就 SIGABRT 闪退。看 LastFatalMessage:
LastFatalMessage:
dlopen() failed to open library '/data/storage/el1/bundle/libs/arm64/libnomacs.so':
Error relocating libnomacs.so: _ZN11QPsdHandlerC1Ev: symbol not found
9.1 坑 1:_ZN11QPsdHandlerC1Ev: symbol not found
反混淆:QPsdHandler::QPsdHandler() —— 3rdparty/libqpsd/ 里 PSD 格式解码器的构造函数。
根因:我在重写新版 CMakeLists.txt 时,只把 3rdparty/libqpsd 加进了 include 路径,但忘了把它的 .cpp 文件加进 add_library 源码列表。DkBasicLoader.cpp 里有 new QPsdHandler(...) 直接引用了这个类,链接时 musl-libc 的链接器默认允许未解析符号通过(只警告不报错),运行时鸿蒙 dlopen 严格校验所有符号必须能 relocate,于是炸。
修复(在 CMakeLists.txt 里):
set(LIBQPSD_SOURCES
${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/libqpsd/qpsdhandler.cpp
${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/libqpsd/qpsdhandler_p.cpp)
add_library(nomacs SHARED
...
${LIBQPSD_SOURCES} # ← 把它显式加进来
${NOMACS_RCC})
注意不要带 qpsdplugin.cpp —— 那是 Qt image plugin 的入口,独立 .so 才需要,主程序里加进来反而会污染 plugin 注册系统。
9.2 坑 2:qt_resourceFeatureZlib: symbol not found
修完坑 1 重新部署,第二次 SIGABRT:
LastFatalMessage:
dlopen() failed to open library '/data/storage/el1/bundle/libs/arm64/libnomacs.so':
Error relocating libnomacs.so: qt_resourceFeatureZlib: symbol not found
根因:服务器上的 host rcc-qt5 是 Qt 5.15.11 版本(系统 yum 包),它在生成 qrc_nomacs.cpp 时引用了 Qt 5.15+ 才有的 qt_resourceFeatureZlib 符号;但 target 是 Qt-OHOS 5.12.12,没这个符号 —— 经典的 host/target Qt 版本错配。
[root]# which rcc-qt5
/usr/bin/rcc-qt5
[root]# rcc-qt5 --version
rcc-qt5 5.15.11 # ← 来自 host 系统包
修复:用 rcc --no-compress 重新生成 qrc_nomacs.cpp,避开 zlib 特性:
cd build
rcc-qt5 --no-compress --name nomacs ../src/nomacs.qrc -o qrc_nomacs.cpp
# 旧 cpp:17341 行,2 处 qt_resourceFeatureZlib
# 新 cpp:17997 行,0 处 qt_resourceFeatureZlib ✅
rm CMakeFiles/nomacs.dir/qrc_nomacs.cpp.o
make -j4 # 重新 link
--no-compress 让资源数据按字面量展开(cpp 多 656 行),代价就是 .so 大几 KB,但运行时不再需要 zlib 解压。
9.3 防御性自检:不再有第三个坑了
修完两个坑后,我写了一个 check_missing_symbols.sh 脚本主动扫所有 _qt* / qt_* / Q_* / _q_* 开头的 undefined 符号,确认这类 host/target 错配的隐患全部清零:
=== nomacs.so 中所有 "qt_*" / "_qt*" / "Q_*" / "_q_*" 开头的 undef 符号 ===
(empty — 0 hits)
=== 全部 zlib / inflate / deflate / compress 相关 undef ===
_ZN12QImageWriter14setCompressionEi # 这个是 QImageWriter::setCompression,
# 由 libQt5Gui 提供,不是 zlib,正常
第三次部署,应用启动成功,nomacs 主窗口正常显示,可浏览图片。🎉



10. 总结:本次移植的 4 个独立看点
回顾本系列前 5 篇(KDiff3 / DiffPDF / glogg / QElectroTech / NitroShare)和本篇 nomacs,nomacs 这一篇的"独门技巧"集中在 4 点:
10.1 双重 stub:解决"项目深度依赖一个无法剥离的 C++ 库"
之前几篇遇到的依赖都是相对好处理的:
- glogg:boost::program_options → 用
QCommandLineParser替换(功能等价); - NitroShare:openssl → 直接 dlopen 失败时降级(功能裁剪);
nomacs 的 exiv2 不一样:它的 100+ 处调用嵌入了核心数据流(DkMetaData),且类型系统(Exiv2::Image / Exiv2::XmpData)出现在头文件的 public/protected 接口中。
解法是首次实战的"双重 stub":
- stub 头(9 KB,7 个文件)让
#include <exiv2/...>编过; - stub cpp(173 行,替换原 1985 行)让所有方法变成空实现;
- 调用点(DkGui、DkBasicLoader 等)一行不改。
10.2 Qt 5.14+/5.15+ → 5.12 降级清单
第一份"完整可复用的 Qt 新版 API → 5.12 降级清单":
| 新 API | Qt | 降级方案 |
|---|---|---|
Qt::SkipEmptyParts |
5.14 | QString::SkipEmptyParts |
Qt::endl |
5.14 | 全局 endl |
QFile::moveToTrash() |
5.15 | QFile::remove() |
QWidget::screen() |
5.14 | QGuiApplication::primaryScreen() |
QWheelEvent::position() |
5.14 | QWheelEvent::posF() |
未来移植任何用 Qt 5.14+ 写的项目到 Qt-OHOS 5.12,这张表都可以一键贴上。
10.3 musl-libc + libc++ 组合下的"伪传递包含"陷阱
std::ostream << "字符串" 在 GNU 工具链(glibc + libstdc++)下经常因为别的头偷偷拉进 <ostream> 而碰巧编过;切到 OHOS NDK 的 musl-libc + libc++ 组合就立刻暴露 —— 它的标准头不会偷传递,必须显式 #include。
这条规则未来可以快速辅助定位只在鸿蒙交叉编译失败、本机能编的诡异错误。
10.4 “链接通过 ≠ 运行通过”——主动扫 undefined 符号
本次最痛的两个坑(QPsdHandler 和 qt_resourceFeatureZlib)共同特征是:
- 链接阶段全部通过(musl 链接器默认 lazy/弱解析)
- dlopen 阶段才 SIGABRT(鸿蒙严格校验所有符号必须可 relocate)
经验:鸿蒙 PC 移植在 link 成功后必须再跑一次 llvm-nm -u 扫 undefined 符号,确认没有形如 qt_* / _q_* 这类引用,否则上机必崩。
更多推荐



所有评论(0)