前言

这是「鸿蒙Flutter框架 0day适配实战」系列的最后一篇。前九篇把"装得上、跑得起来、改得动、发得出"讲完了,本篇回答最后一个问题:你凭什么说它是对的?

这个问题在鸿蒙适配这件事上格外尖锐,因为本批次撞到的两个最难缠的故障,都不在编译器能看见的地方:

  • P28 字体端口丢失:ninja 0 FAILED、har 正常产出、HAP 正常安装、App 正常启动、形状/颜色/布局全都对——只是所有文字与图标不可见。一路绿到最后一步才炸,而且炸得不像构建问题,像设计问题。
  • P29a 发布物装错包:debug 档完全正常,profile 档一启动就白屏 FATAL。同一份引擎代码,换个工作模式就死。

所以本篇的主线不是"我测了什么",而是**“验证为什么要分层,以及每一层各自挡不住什么”**。四道闸、三层证据链、两个装错包事故、一次跨会话独立复现,都在讲这一件事。

前置阅读:第 8 篇(引擎构建链,编译闸的判据)、第 9 篇(发布分发,发布闸与覆盖行)、第 3 篇(消费侧的三档验收口径,本篇给它的生产侧完整版)。

一、四道质量闸:由内到外,层层拦截

1.1 为什么必须分层

一条链路上有四种完全不同性质的"错",它们互相看不见:

编译闸          单测闸          构建闸          发布闸
ninja 0 FAILED  设备侧全量跑    三档 hap 冒烟   字体端口/布局核对
编译器能查的错  基线只认真机数  编译通过≠渲染对 静默错误的最后防线

分层的意义在于每道闸只挡一类错,不分层就会出现"我 ninja 全绿了啊"然后交付一个全屏无文字的 App。

在这里插入图片描述

上图三个板块对应本篇的三个论证层次:① 四道闸的顺序与各自判据,每道闸后面都站着一个真实的 P 编号事故;② 中间那排七件套(framework → engine → Dart → patched_sdk → gen_snapshot → libflutter.so → libapp.so)是版本闭环——必须同源同构建模式,P29a 就是"引擎改了、平台 dill 没配套"的产物;③ 底部红绿对照是本篇的诚实口径:有证据的部分归档在哪,没验证的部分明确列出(§十)。

1.2 四道闸的判据

闸判据挡住的错挡不住的错
① 编译闸三档引擎 ninja 0 FAILED;flutter analyze 0 error类型、符号、链接错误P28:端口不在,编译照样过
② 单测闸引擎 OHOS 单测真机 1723 全绿(模拟器 1703+20 环境性)平台层行为回归渲染结果正确性(单测不看像素)
③ 构建闸三档 flutter build hap 冒烟 + 设备渲染(文字/图标/输入法/多语言)工具链接线、AOT、打包P29a:debug 过、profile 炸
④ 发布闸字体端口三档在位、gen_snapshot 是 Mach-O、11 个附件与覆盖键逐条核对(第 9 篇 §5.6、§6.1)静默错误、半成品发布物设备侧运行时兼容性

第四道闸是静默错误的最后防线,也是本篇 §五 那份 SOP 存在的全部理由。

1.3 版本闭环:七件套必须同源同模式

framework → engine → Dart → patched_sdk → gen_snapshot → libflutter.so → libapp.so
   └────────────── 同一套源码 ──────────────┘└────── 同一个构建模式 ──────┘

任何一环串了档,症状都会出现在离根因很远的地方。本篇 §六 的 P29a 就是第 4 环(patched_sdk)装了产品档、第 3 环(引擎)是非 PRODUCT,两件事对不上导致的。

二、构建闸实录:flutter build hap 端到端冒烟

2.1 全链与产物

入口是一条命令,但它背后是完整的一串:

cd hap_smoke_demo && flutter build hap --release --no-codesign
# KernelSnapshot(frontend_server) → LinkHooks(recorded_uses)
#   → gen_snapshot(libapp.so) → hvigor 打包(libflutter.so + libapp.so + ets/modules.abc + flutter_assets)
# 产物:build/ohos/hap/entry-default-unsigned.hap(20.7 MB)

解包验证才算冒烟通过,光看命令退出码不够:

包内路径大小来源
libs/arm64-v8a/libflutter.so16 MB第 8 篇的 M2 引擎产物
libs/arm64-v8a/libapp.so3.2 MBAOT 快照
ets/modules.abc—ArkTS 侧
resources/rawfile/flutter_assets/—FontManifest / AssetManifest / shaders / fonts 齐全

2.2 六个拦路虎(按踩坑顺序)

这一段的价值在于症状与根因的距离:六个问题里没有一个的报错直接指向自己的根因。

  1. 官方 host 工具 URL 404——_getHostArtifactPath 拉 dart-x64 host 工具时 URL 多拼了一层 flutter_infra_release/,且 rev 用了 framework 侧提交号(9584c67…)而非 engine 提交号。修复:rev 读自 bin/internal/engine.version(06a2e2a110089dff50fe635cffd2a61e1b24fbcd),正确形态是 .../flutter_infra_release/flutter/<rev>/darwin-x64/artifacts.zip。
  2. frontend_server 白名单拒绝 ohos——"ohos" is not an allowed value for option "--target-os"。allowed values 来自 pkg/vm/lib/target_os.dart 的 TargetOS.names,snapshot 编译自无 ohos 补丁的源码就报此错。
  3. recorded_uses.json 未生成 → LinkHooks 找不到文件——common.dart 里原条件是 if (featureFlags.isRecordUseEnabled && !targetPlatform.isOhos),移除 !targetPlatform.isOhos 即可(3.13.3+ohos 的 frontend_server 已支持 --recorded-uses)。
  4. Could not resolve "../plugins/GeneratedPluginRegistrant"——project.dart 的 ensureReadyForPlatformSpecificTooling 里 anyPlatformEnabled 枚举不含 ohosPlatform,ohos 工程从不触发 _writeOhosPluginRegistrant。报错点(模块 resolve)远在触发点之外。
  5. compatibleSdkVersion 连锁两坑——"5.1.0(18)" 报 18 cannot be lower than 24;改 "5.0.5(24)" 报 00303083 不匹配。必须读 DevEco SDK 的 sdk-pkg.json 得到 "6.1.1(24)",猜兼容组合一定会撞。
  6. 签名拦截——build hap 的 --codesign 默认 true,而 checkOhosSignedInfo(hvigor.dart:368)只校验 build-profile.json5 的 app.signingConfigs 非空,空配置直接 [ERROR] ohos build not configured to sign。冒烟用 --no-codesign。

frontend_server 与 dart-sdk 是「编译时耦合」:白名单、选项、语言版本全部编译进 snapshot。换 SDK 版本的判断顺序是"先看 snapshot 支持什么,再决定重编什么"——源码有补丁 ≠ snapshot 有补丁。

第 2 条的排查走过三段:OBS 的 3.12.2+ohos 配套 dart-sdk-ohos(白名单含 ohos 但语言上限 3.12,报 language 3.13 too high)→ stock 3.13.3(语言 ✓ 白名单 ✗)→ 在打了 9 文件 ohos 补丁的 3.13.3 源码树重编 frontend_server:

cd dart-ws/sdk && python3 tools/build.py --no-rbe -m release -a arm64 frontend_server
# 138 秒;3.13.3 不接受 --no-goma,用 --no-rbe
# 产物白名单:[android, ohos, fuchsia, ios, linux, macos, windows] ✓

部署必须 product 配对:product snapshot 需 product VM 运行,dartaotruntime_product 拷为 bin/dartaotruntime、frontend_server_aot_product.dart.snapshot 拷为 bin/snapshots/frontend_server_aot.dart.snapshot。混用的症状是一启动就挂,且报错完全不指向配对问题。

2.3 API 24 是设备形态硬要求(三条独立证据链)

早期用 OpenHarmony 5.0.1(API 13)模拟器冒烟,两条路径都被拒,第三条是手工改包绕过:

hdc install entry-default-signed.hap
# 9568297 install failed due to older sdk version in the device   ← 证据①:安装检查
# 降级 compatibleSdkVersion 到 "5.0.1(13)" 重新构建:
# 00303083 Specification Limit Violation                          ← 证据②:构建期注入
#   原因:flutter-hvigor-plugin 往 har 的 module.json 写死 minAPIVersion: 60101024
hdc shell ./flutter_ohos_unittests
# Error loading shared library libnative_window.so                ← 证据③:运行时库面缺失

甚至试过免构建直接改包内 module.json 的 minAPIVersion 绕过安装检查——但 libflutter.so 的 NEEDED 面(libnative_window/drawing/vsync/buffer.so + libace_napi.z.so)与单测 exe 相同,5.0.1 开源镜像缺 NEXT 系 NDK 库面,即使绕过安装检查,引擎 dlopen 也会失败。

三条独立证据链闭环成一个结论:整链按 API 24(HarmonyOS 6.1.1)构建,API 24 是设备形态硬要求,不可通过配置或降级达成。

2.4 突破:本机早有匹配设备,只是没启用

卡了一天的"没有 API 24 设备",最后是在本地文件系统里解决的:

# 镜像真相来源(无需 GUI)
ls ~/Library/Huawei/Sdk/system-image/        # HarmonyOS-6.1.1(phone_all_arm, api 24, 4.4GB) 早就在
cat ~/.Huawei/Emulator/deployed/lists.json   # 已注册实例:Pura 90(api 24, imageDir phone_all_arm)

# CLI 直接启动(GUI 的 -t trace 管道可省)
/Applications/DevEco-Studio.app/Contents/tools/emulator/Emulator \
  -hvd "Pura 90" -path ~/.Huawei/Emulator/deployed \
  -imageRoot ~/Library/Huawei/Sdk
# 新设备随即以 127.0.0.1:5557 出现在 hdc list targets(模拟器 hdc 端口自 5555 递增)

冒烟结果(全链实测通过):

  1. compatibleSdkVersion "6.1.1(24)" 重建 unsigned HAP(20.7 MB)✓;
  2. 未签名 HAP 直接 hdc install → install bundle successfully(API 24 模拟器镜像不校签名)✓;
  3. aa start -a EntryAbility -b com.example.hap_smoke_demo → start ability successfully,进程 PID 存续 16s+、RSS 176 MB(引擎 + Dart VM 装载即证 libflutter.so 完整加载)✓;
  4. snapshot_display -f screen.jpeg 截屏 → Flutter counter app UI 完整渲染 ✓。

先查本机已注册设备,再等新镜像。~/.Huawei/Emulator/deployed/lists.json 是模拟器实例的真相来源,~/Library/Huawei/Sdk/system-image/<img>/info.json 是镜像的真相来源,两者都不需要 GUI。

三、单测闸:设备侧全量跑通的第三条路

3.1 为什么裸跑 exe 不行

flutter_ohos_unittests(32 MB,全量 OHOS 单测链接、0 FAILED)在 API 13/24 模拟器上裸跑都是 EXIT 127:

Error loading shared library libark_jsruntime.so: No such file or directory
panda::LocalScope 符号重定位失败

补 LD_LIBRARY_PATH=/system/lib64/platformsdk 无效。根因是 SELinux 域:模拟器 shell 进程在 sh 域,无权读 platformsdk 库面;而 app 宿主进程在 appdomain 域则完全正常(引擎能渲染就证明同面依赖没问题)。

于是有第三条路:把 libflutter_ohos_app_test.so 组进普通 app HAP,用 NAPI wrapper dlopen 全量执行——既不是裸跑 exe,也不必等 hypium instrumented test 工程。

3.2 AOT 快照符号修复(本段核心)

首跑 3 秒即 cppcrash:SIGSEGV @0x6b8 → OHOSShellHolder 构造 → DartVMRef::Create 返回空 → shell.cc:225 对空 vm 调 GetVMData() 空指针解引用。根因链有点长,但每一步都可验证:

// flutter/runtime/dart_snapshot.cc
// release(AOT) 下这个宏为「假」:
#define DART_SNAPSHOT_STATIC_LINK \
  ((FML_OS_WIN || FML_OS_ANDROID || FML_OS_OHOS) && FLUTTER_JIT_RUNTIME)
// → ResolveVMData/IsolateData 走 SearchMapping = dlopen(NULL) 找
//   kDartSnapshotData / kDartSnapshotText
// → 符号本体在 app_test .so 内(isolate_snapshot.bin.o / core_snapshot_text_linkable.o)
//   但被 version script 的 `local: *` 藏进非动态符号表 → dlopen(NULL) 查不到

修复只有一行——给 version script 的 global 段补两个符号:

# flutter/shell/platform/ohos/testing/app_test_export.map
{
  global:
    ...原有导出...
    kDartSnapshotData;      # ← 补
    kDartSnapshotText;      # ← 补
  local: *;
};
# wrapper 以 RTLD_GLOBAL dlopen,符号进全局表即可见

⚠️ ninja 不追踪 map 文件内容——改完 map 输出「no work to do」,必须删掉 .so 强制重链。验证用 llvm-nm --dynamic --defined-only,应见 R kDartSnapshotData + T kDartSnapshotText。

3.3 结果文件为什么"看不见"

单测跑完了但结果文件找不到,是两个独立问题叠在一起:

  1. 可写目录:实测 /data/local/tmp 与 /data/storage/el2/base/files 在 app 域均不可写,结果从未落盘;真正可写的是 /data/app/el2/100/base/com.example.ut_host/files/。wrapper 因此加了可写目录探测(三个候选逐一 open 试写),Index.ets 侧轮询三个候选路径读取。
  2. 失联:首版宿主(修符号之前)在引擎测试处挂起,进程死等时全量进度完全不可读。

对策是 live-log 守护线程:unittest entry 内部监听器把每条 [ RUN ] / [ OK ] / [ FAILED ] 累积进内存(FlutterOhosCopyLiveLog C API 随时可调),wrapper 起后台线程每 2 秒增量落盘 ohos_ut_live.txt,5 分钟无进展写 ohos_ut_hang.txt 标记。

3.4 模拟器 1703/1726:20 项失败全是环境性

API 24 模拟器(Pura 90,127.0.0.1:5557)实跑,约 40 秒:

项值
用例注册总数1726
实际执行1723(= 1703 通过 + 20 失败)
崩溃0(faultlog 无新 cppcrash)
EXIT_CODE1

20 项失败分四组,均为环境性、非适配缺陷:

分组数量模拟器失败原因
OHBTextureSourceVKTest10模拟器无 Vulkan 设备,创建 context 返回空(断言 Value of: context)
OHOSContextVulkanImpellerTest3同上(Vulkan/Impeller 后端)
QoSFallbackOhosTest3OH_QoS_SetThreadQoS 在模拟器返回非 0,fallback 也非 0
TimerfdTest2源码注释明言的已知 bug 文档化测试(it_interval=it_value 周期性,fire_count=0),适配未改源
OhosExternalTextureTest2pixelmap / nativewindow stub 行为差异

Vulkan 那 13 项是重点:它们指向 Vulkan-Headers 定义的接口面,模拟器没有 GPU 后端,真机有——所以这组数字的正确读法是"待真机复核",不是"适配有缺陷"。

3.5 真机 1723 全绿(M5)

设备:ALN-AL00 / FMR0224904009635,API 24(HarmonyOS 6.1.0.135 Release)。

真机必须签名 HAP(否则 code:9568320 no signature file),而签名配置有个反直觉的点:

⚠️ ~/.ohos/config 里自动签名生成的 p12,其 storePassword / keyPassword 是密文(0000001A… 十六进制,长度前缀 + 密文),与本机 material(~/.ohos/config/material/fd|ac|ce)绑定,换机或手工方式无法还原明文——不要走 hap-sign-tool 手工签名路径。

正确姿势是 DevEco 自动签名一次(约 30 秒,需华为账号),密文配置写进 build-profile.json5 的 signingConfigs 之后,命令行构建即可自动解密产出 signed HAP,全流程可 CI 化:

/Applications/DevEco-Studio.app/Contents/tools/hvigor/bin/hvigorw assembleHap \
  --mode module -p product=default -p buildMode=debug --no-daemon
# → entry/build/default/outputs/default/entry-default-signed.hap(32.9 MB)

结果(EXIT_CODE=0):

项模拟器真机
实际执行17231723
通过17031723(全部)
失败200

20 项环境性失败全部翻转——Vulkan 13 项、QoS 3 项、Timerfd 2 项、ExternalTexture 2 项在真机上全部 OK。这既确认了引擎侧适配完整,也确认了那条判据:模拟器数字不能当基线(第 3 篇 §消费侧口径 讲过的同一件事,这里是生产侧证据)。

3.6 复用方法(四步)

  1. flutter_ohos_app_test.so(32.6 MB,含全量 OHOS 单测)+ libutwrapper.so(NAPI wrapper,NEEDED 仅 ace_napi / hilog / libc)放进 HAP 的 entry/libs/arm64-v8a/;
  2. 宿主工程 ut_host_demo/ 纯 ArkTS,EntryAbility 首页 aboutToAppear 调 utWrapper.runTests(filter, logPath),filter 支持 *、--gtest_filter 值、__list__ 特殊模式;
  3. 手工链:hvigor assembleHap(release)→ hdc install -r → aa force-stop + aa start → 轮询结果文件;
  4. 判定:EXIT_CODE=0 全过;live log 与失败明细双通道(hilog 侧 GTEST_FAILED 行 + 落盘 live log)。

四、P28:字体端口静默丢失

4.1 现象

HAP 里形状、颜色、布局全部正常,所有文字与图标不可见。编译全程零报错。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

上图左侧是修复前后的对照(左:应有;右:实际——只有文字与图标不见,方块是图标占位),右侧三层证据链是自上而下的判定顺序,底部三个陷阱是本次实测新发现、仓库此前零记录的东西。

4.2 根因链(源码 + 二进制双向确证)

  1. P19 整链清理后,OHOS 落到 txt/src/txt/platform.cc 的 generic 分支,GetDefaultFontManager() 返回 SkFontMgr::RefEmpty()——空字体管理器,任何查询都返回空字面;
  2. 上游 skia 侧:vanilla 的 gn/skia.gni:58 是 skia_use_freetype = is_android || is_linux || is_canvaskit——不含 ohos,于是 skia.gni:160-169 的 skia_enable_fontmgr_* 全部派生为 false;
  3. 连带后果(最容易被误判为"字体文件的问题"):pubspec 里 bundle 字体也救不了——lib/ui/text/font_collection.cc:163 与 lib/ui/text/asset_manager_font_provider.cc:121 都走 font_mgr->makeFromStream(),RefEmpty 一律返回 nullptr;
  4. 二进制证据:修复前 libflutter.so 有 0 个 FT_* 符号、0 个 SkFontMgr_New_*;3.22 时代的旧 har 两者都有。

4.3 P27 结论的反转

第 8 篇 §五记的 P27 结论是「开关恢复不可行:3.47.4 的 skia checkout 没有 src/ports/skia_ohos/,且该移除是 P19 有意清理」。

前半句是事实,但推不出「不可恢复」:

  • skia 树是 DEPS 拉取、零 tracked 文件,本来就不该就地改;
  • 仓库早有 attachment 机制(第 9 篇 §3.5)专门托管「checkout 里没有、构建却需要」的文件;
  • 「官方有意清理」只说明上游不再维护,不构成我们不能自己维护。

正确的三分法是:checkout 里有没有 / 仓库能不能托管 / 构建图认不认。

4.4 修复四部分

#面内容
1端口源码取 fork 3.44.9 口味:FontConfig_ohos.{cpp,h}、FontInfo_ohos.h、SkFontMgr_ohos.{cpp,h}、SkFontStyleSet_ohos.{cpp,h}、SkTypeface_ohos.{cpp,h},加 include/ports/SkFontMgr_ohos_api.h;include 适配 3 处(<json/json.h>、两处 SkDebug.h 路径)
2attachment 固化attachment/repos/skia_ohos/、repos/skia_ohos_api/、repos/jsoncpp/ + config.json 三条 files 任务
3GN 接线flutter/skia/BUILD.gn 的 skia_enable_fontmgr_ohos = is_ohos;tools/gn 的 ohos 分支 skia_use_freetype = True;jsoncpp 交给 //third_party/jsoncpp:jsoncpp
4引擎侧胶水txt/src/txt/platform_ohos.cc、txt/BUILD.gn 的 is_ohos 分支、ohos_shell_holder.{h,cpp}、platform_view_ohos_napi.cpp(7 文件复原)

端口源码的托管镜像见 fluttertpc_skia。

五、字体端口诊断 SOP

这一节是每次发布前都要跑的清单(第 9 篇验收清单第 2 条的完整展开)。

5.1 第一步永远是分清「断在哪一层」

不同层的修法完全不同,先分层能省掉大量瞎猜:

症状断裂层往哪查
形状/颜色/布局全对,只有文字与图标全无字体管理器为空 / 字体解析§5.3 三层证据
部分字体缺(如中文有、Emoji 无)字体文件 / 回退表设备侧 /system/etc/fontconfig.json、/system/fonts
连布局都塌(行高、换行全乱)量度层不是本篇范围,查文本排版
字在但笔画怪异 / 缺笔画字形光栅化光栅化后端,不是字体管理器

「形状与布局都正常、只有文字全无」⇒ 断点在光栅化之前,不要去追渲染后端(Impeller / Skia GL)。这条判据在本批次一次锁定了方向。

5.2 三个静默陷阱

这三条共同特点是:错误做法不报错,只是安静地返回"看起来像故障"的结果。

陷阱 ① .har 是 gzip(tar.gz),不是 zip

unzip -l arm64_v8a_debug.har
# → End-of-central-directory signature not found.   ← 别据此断定"har 坏了"
tar -tzf  arm64_v8a_debug.har                       # ✅ 能用
# har 内部布局:package/libs/arm64-v8a/libflutter.so(不是通常 har 的 libs/ 顶层)

陷阱 ② 静默空输出 = 假阳性「端口丢了」(最危险)

空输出既可能是"产物里真的没有",也可能是你的命令本身错了:

# ❌ 实测:输出为空、exit=1 —— 和"端口丢了"长得一模一样
tar -xzOf x.har package/libs/arm64-v8a/libflutter.so | strings -a | grep -c fontconfig
#    隔离实验:把同样的数据 cat 进 strings 也一样失败
#    根因:macOS 的 /usr/bin/strings 从 stdin 读时不可靠,不是 tar 的问题
strings -a <file> | grep -c fontconfig     # ✅ 5  —— 必须把文件当参数传
grep -a -c fontconfig <file>               # ✅ 2  —— 能跑,但这是"按行"口径

# ❌ 实测:glob 匹配不上,同样静默返回空
tar -xzOf x.har --wildcards '*/libs/*/libflutter.so'
tar -xzOf x.har package/libs/arm64-v8a/libflutter.so    # ✅ 能取到

两条铁律:绝不吞 stderr(2>/dev/null 会把"命令错了"伪装成"没有这个文件");每个判据必须先在已知 good 的产物上校准过再用。

陷阱 ③ nm -D 数已定义符号会静默返回 0——但数未定义符号它恰恰是唯一可靠判据

这条要分两半读,混起来会得出相反的结论(-D/--dynamic = 只看 .dynsym):

你要看的东西用 -D实测
已定义符号(FT_、SkFontMgr_New_OHOS…)❌ 假阴性so.unstripped 上 nm -D --defined-only | grep -c FT_ = 0,而 nm --defined-only = 103
未定义符号(U,如 8 个 OH_Drawing_*)✅ 唯一可靠nm -D | grep -c ' U OH_Drawing_' = 8,且 strip 前后都是 8

为什么 U 反而更可靠:未定义符号必须留在 .dynsym 里——动态加载器靠它做符号解析,链接器不敢删、strip 也剥不掉。而 FT_ 这类已定义符号走 .symtab,默认不进 .dynsym;产物一旦 strip,.symtab 整个没了,连默认 nm 都报 no symbols。

5.3 三层证据链(每层都要标注口径)

层 1:二进制(最快,免设备)

# ⭐ 首选判据:stripped 交付物上唯一可用,且最强
nm -D libflutter.so | grep -c " U OH_Drawing_"        # 期望 8 —— 8/8 即在
# 次选:已定义符号,必须在未 strip 的产物上,且不能用 -D
nm --defined-only out/ohos_<mode>_arm64/so.unstripped/libflutter.so | grep -c FT_
检查对象命令实测值
U OH_Drawing_*(首选)strip / unstripped 均可nm -D f | grep -c " U OH_Drawing_"8(不多不少);0 → 端口不在;1–7 → 构建不完整,判不在
FT_ 符号数so.unstripped/libflutter.sonm --defined-only f | grep -c FT_debug 185 / profile 103 / release 103
入口符号同上... | grep -c SkFontMgr_New_OHOS1
已定义符号的假阴性对照同上加 -D0(正常产物也是 0,故不可用 -D 数已定义符号)
fontconfig 串har 内已 strip 的 sostrings -a <file> | grep -c fontconfig5(其中 3 条是 /system/etc/*.json 路径)
fontconfig 串(按行口径)同上grep -a -c fontconfig2(按行 ≠ 按串,两者不能混比)
SkFontMgr 串同上(已 strip)strings -a <file> | grep -c SkFontMgr0 ← 这个 0 是正常的,符号已被 strip

"0 → 非 0"就是这个故障的二进制指纹。误报防线也核过:整个引擎里只有端口源码引用 GetSystemFontConfigInfo / GetSystemFontFullNamesByType,没有第二条代码路径能引入这批符号。

⚠️ 首选判据的诚实边界:U OH_Drawing_* 只证明「端口编译进了引擎」,证明不了设备侧能加载——若设备上 libnative_drawing.so 缺这 8 个 API,产物全绿但设备仍会文字不可见。那是设备兼容问题,与"端口丢失"是两件事。

层 2:构建图(配置层)

grep -E 'skia_use_freetype|target_os|custom_toolchain' out/ohos_<mode>_arm64/args.gn
# 三档实测:skia_use_freetype = true ✅  target_os = "ohos" ✅
#           custom_toolchain = "/opt/homebrew/opt/llvm@19" ✅

# ⚠️ 不要用 grep FontConfig_ohos build.ninja 当判据 —— 实测为 0,是假阴性
find out/ohos_<mode>_arm64/obj -name '*FontConfig_ohos*'    # 1 = 编进去了
find out/ohos_<mode>_arm64/obj -name '*SkFontMgr_ohos*'     # 1 = 编进去了

源头开关是 flutter/skia/BUILD.gn 里的 skia_enable_fontmgr_ohos = is_ohos。

层 3:附件(端口源码在不在树里)

ls engine/src/flutter/attachment/repos/skia_ohos/                     # 源头
ls engine/src/flutter/third_party/skia/src/ports/     | grep -i ohos  # 落点
ls engine/src/flutter/third_party/skia/include/ports/ | grep -i ohos  # 落点

落点为空 = attachment 没跑或没跑对。注意第 9 篇 §3.5 那条:copy_dir 对已存在目录是 no-op,type:"files" 才是先删后拷的幂等覆盖;另外 type:"patch" 在 3.47.4 同步中已无实现,别写。

5.4 误导性信号清单

信号为什么误导
system font file not found(日志)CheckFontSource() 只认华为主题字体路径 /data/themes/[ab]/app/fonts/;它报"找不到"不等于设备没有字体——系统字体走 SkFontMgr_New_OHOS() 读 /system/etc/fontconfig.json + /system/fonts。这条日志曾把人带向"字体文件缺失"的错方向
交付 so 里 SkFontMgr 串 = 0正常(已 strip),去 so.unstripped/ 查符号
unzip -l x.har 报签名错正常,har 是 tar.gz
llvm-nm --dynamic = 0假阴性,去掉 --dynamic
grep FontConfig_ohos build.ninja = 0假阴性,看 obj/ 里的 .o
编译通过、HAP 装得上什么都不能说明——本故障全程零报错

5.5 端到端验证必须用系统字体验一次

在 API 24 模拟器上,一个 demo 页要同时覆盖:

  • 多语言:中文 / 日本語かなカナ / 한글 / Emoji 🎉;
  • 图标:≥4 个 Material 图标(+ / home / settings / favorite);
  • 输入:TextField 及其光标渲染。

demo 不要 bundle 任何中文字体——中文必须走系统字体回退(/system/fonts + /system/etc/fontconfig.json)。这样验过的才是"设备字体链路是活的",而不是"我塞的字体能用"。三档各自都要过,只验 debug 不够(§六 的 P29a 就是 profile 独有的)。

六、P29:三档引擎全部落地时的两个装错包事故

P28 只验了 debug。补齐 profile / release 时,撞到一个与字体端口完全无关的既有 bug。

6.1 P29a:profile 一启动即白屏

Dart Error: Type '_NetworkProfiling' not found in library 'dart.io'
→ FATAL:flutter/lib/io/dart_io.cc(38)] Check failed: !CheckAndHandleError(setup_result)
→ FML KILL PROCESS

根因三层都在源码里对得上:

  1. 非 PRODUCT 引擎里,third_party/dart/runtime/bin/dart_io_api_impl.cc:146 在 settings.enable_network_profiling 为真时做 DartUtils::GetDartType(kIOLibURL, "_NetworkProfiling"),该调用包在 #if !defined(PRODUCT) 里;
  2. Dart 侧 third_party/dart/sdk/lib/io/network_profiling.dart:44 的 @pragma('vm:entry-point', !bool.fromEnvironment("dart.vm.product"))——只在非产品编译里保证不被树摇;
  3. profile 引擎是非 PRODUCT(查找代码在)、profile 模式又开了 VM service(网络剖析开着),于是运行时真的去查这个类。

而 1.0.0 发布的 flutter_patched_sdk.zip,包内 flutter_patched_sdk/platform_strong.dill 装的其实是产品档 dill(md5 3251fb22…,应为 20464af0…)——产品档里该类已被树摇掉,查不到 → 致命错误。

改法是用 attachment/scripts/zip_flutter_patched_sdk.py 重新生成(它按 is_product 分别取 ohos_release_arm64/ 与 ohos_debug_arm64/ 下的 flutter_patched_sdk/)。修复后 profile 实测:libapp.so 6,685,576 B(md5 d21e32dd…),_NetworkProfiling 符号计数 0 → 2。

6.2 P29b:两个 patched_sdk 条目共用一个 marker

flutter_patched_sdk.zip 与 flutter_patched_sdk_product.zip 共用 artifact 目录 bin/cache/artifacts/engine/common/,但标记文件 .ohos-override-url 只有一个,两者互相覆盖对方记录的 URL,每次构建至少有一个被判过期而重下。

本机实测:一次 flutter build hap --release 两个 zip 都被重下,把手工放的修正版非产品 dill 又冲回错误的产品 dill。

  • 对正常流程无害(发布物对,重下装回来的就对);
  • 只影响手工热修:任何"直接改 cache 里文件"的验证都会被下一次构建抹掉。本轮做 profile 验证时就是靠临时把覆盖行第 42 行指到 http://127.0.0.1:8123/ 绕开的;
  • 根治要给两个 entry 各配一个标记文件,属 cache.dart 的独立改动,本批未做。

6.3 P29c:AOT 快照缓存在 .dart_tool/

改了平台 dill 后重建,libapp.so 却逐字节不变。原因:只删了 build/ohos/,而 AOT 快照缓存在 <app>/.dart_tool/flutter_build/<hash>/。

flutter build hap --profile -v 2>&1 | grep -i skipping
# Skipping target: kernel_snapshot_program
# Skipping target: ohos_aot_profile_ohos-arm64      ← 就是这两行定位的

rm -rf .dart_tool/flutter_build build/ohos          # 正确姿势:两个都要删

这条坑连吃了两次。产物 md5 相同不等于"改动没生效",要先问"这一步到底跑没跑"——而答案只在 -v 日志里。

6.4 三档验证结果(API 24 模拟器)

档平台 dill结果
debug20464af0…(非产品)文字 / 图标 / 光标全可见(P28)
profile20464af0…(非产品)修复 P29a 后正常,_NetworkProfiling 0→2
release3251fb22…(产品)正常,AppBar / 计数器 / 中文 / 日文 / 韩文 / Emoji / 图标 / 输入法全在

6.5 fvm 侧独立复现与范围划界

用户在 fvm 副本(独立目录、有自己的 bin/cache)上用 fvm flutter 构建——这才是用户实际走的路径,与上表在 flutter_flutter SDK 上直跑是两条独立链路。

项复现结果
传播不清缓存,7 个工件全部从 1.0.1 重下;.ohos-override-url 标记由 1.0.0 翻到 1.0.1(证明是标记比对触发,不是陈旧缓存假象)
P29a 装错包fvm 与 flutter_flutter 的 4 个 dill 逐字节一致:非产品 20464af0… 在产品目录之外、产品 3251fb22… 在产品目录内——装错包是真修好了,不是被掩盖
profile 运行HAP 27.3 MB 构建安装成功,进程存活;hilog 中 _NetworkProfiling|dart_io|KILL PROCESS|FATAL|not found in library 全空;截图正常渲染
排伪截图无 DEBUG 角标——排除"profile HAP 未装上、截到旧 debug 版"这一混淆

范围(两边互相如实划界,勿虚报):

  • flutter_flutter 侧 3/3 档;fvm 侧 2/3 档(debug + profile);release 未在 fvm 侧跑过——它用产品档 dill、本就未崩,且 7 个 release 工件在该次 fvm 运行中已随下载经 md5 核对,边际价值低;
  • P29c 仍只有单一来源——复现方是全新 profile 构建、无既有 AOT 状态,那条坑根本没被触发,不等于它被验证过;它只在"改动后重建"时咬人。

七、发布前回归:四道闸的执行顺序

第 9 篇给了 11 条清单,本篇给出判据来源与三条最容易被跳过的:

  1. 编译闸:三档 out/ohos_<mode>_arm64/ 齐全,ninja 0 FAILED;
  2. 单测闸:至少跑一次真机全量,基线数字用真机的 1723,不用模拟器的 1703;
  3. 字体端口(§五 全套):三档各查一遍,别跳过,丢了编译不报错;
  4. 构建闸:三档 flutter build hap + 设备渲染,重点看文字与图标;
  5. 发布闸:gen_snapshot 是 Mach-O、11 个附件布局逐条核对(3 个 har 包 + 2 个宿主 AOT + 2 个平台 dill + 4 个 Dart SDK 包)、新 tag、先传 1 个附件验证、11 条覆盖键 HEAD 全通、清缓存端到端。3.47.4 批次当时是 5 个 zip + 9 行覆盖键,Windows 两键到 3.47.7 才补(第 9 篇 §5.7、§6.1)。

三条最容易被"我本地都好了"心态跳过的:字体端口(零报错)、profile 档(debug 过不代表它过)、fvm 副本传播(本机仓库和 fvm 是两个 cache)。

7.1 真机验收的标准动作

flutter build hap 通过只代表"打得出来、装得上去",不代表设备上的运行结果对。本批次在真机上按下面这张表执行,缺一项按未验收处理:

维度最小覆盖为什么必须有这一项
构建模式debug / profile / release 三档各自装、各自跑P29a 只在 profile 触发(§6.1),debug 全绿说明不了它
冷启动每档连续 3 次:首启、杀进程再启、锁屏后再启首启才走 AOT snapshot 加载与字体端口初始化,第 2、3 次暴露缓存与复用路径
生命周期锁屏/解锁、前后台切换、横竖屏旋转surface 与线程 QoS 在后台切换时重新申请,单测覆盖不到这一层
输入中英混输 + 候选栏 + 退格回删输入法是平台通道,与引擎无关,但升级后最容易回归
文字与图标逐字看,不接受"看起来有字"P28 的形态就是布局全对、字没了(§四)
渲染后端Impeller 与 Skia 各跑一遍两者互为对照:Skia 出图、Impeller 黑屏,问题就在 Vulkan / swapchain 侧
插件工程实际用到的插件逐个跑单测 1723 全绿也挡不住插件侧平台通道的改动

三档 × 连续 3 次冷启动是冒烟下限,不是验收上限;正式升级还应补外部纹理、内存压力与长时间后台。渲染后端那一行属于渲染层,本系列不展开它的原理,但它作为验收项是必须的——编译通过 ≠ 渲染对,每个渲染开关都要真机记录结果。

7.2 升级后按症状分诊

换了 SDK 之后出问题,第一嫌疑人永远是七件套掉队(§1.3):只换引擎没换平台 dill、只换 gen_snapshot 没清 AOT 缓存,都算这一类。按症状直接查表:

症状第一判据处置明细
Wrong full snapshot versionsnapshot 与引擎不成套flutter clean + 删 .dart_tool/flutter_build,配套重新生成第 2 篇 §六
产物 md5 相同、行为却回退flutter build -v 出现 Skipping target先确认这步真跑了,再怀疑代码;缓存要一起清§6.3(P29c)
启动几步即 SIGSEGV,崩溃地址像被截断跨语言调用的 ABI 不一致对齐四份 FFI ABI 定义:Dart 侧、frontend 侧、C++ 侧、OHOS 注册顺序§3.2 同类取证法
Dart main 已跑但黑屏,日志 format(0) / Submit failed设备报的 surface 格式引擎不认识查 PixelFormat 映射与 swapchain,别猜 Dart 层§7.1 渲染后端行
Skia 能渲染、Impeller 黑屏两个后端共用同一份 Dart 代码定位到 Vulkan / swapchain 侧,不在本次适配的 Dart 代码里§7.1 同上
构建报找不到 flutter_embedding_<mode>.har / arm64_v8a_<mode>.harhar 由工具从 SDK 复制进工程见下方"har 相对路径",先确认工件到位再查路径第 9 篇 §6.9

har 相对路径这一条值得单说:flutter_tools/lib/src/ohos/hvigor.dart 会把 flutter_embedding_<mode>.har 与 arm64_v8a_<mode>.har 从 SDK 的 bin/cache/artifacts/engine/ohos-arm64/ 复制进鸿蒙工程,工程侧再用 file: 相对路径引用它们(ohos/entry/oh-package.json5 与 oh-package-lock.json5 里的 @ohos/flutter_ohos、flutter_native_arm64_v8a 两条依赖)。换 SDK 目录、换机器、或者引擎工件压根没下载下来,报的都是同一句"HAR 不存在"——所以顺序必须是先按第 9 篇 §6.9 逐条 HEAD 覆盖行 URL 确认工件,再查工程里的 file: 路径,反过来就是白折腾。

八、故障索引

现象判据 / 出路
文字与图标全无,形状布局正常§5.1 分层 → §5.3 三层证据 → §4.4 修复
拿不准"到底有没有端口"§5.2 陷阱 ②:先在已知 good 的产物上校准判据
profile / release 只有 103 个 FT_正常(debug 才是 185),别当回归
已 strip 的 so 查不到符号改查 so.unstripped/,或用 nm -D 数未定义符号
升级后端口源码没更新copy_dir 对已存在目录 no-op → 先删目标目录再重放
部分字体缺失(非全无)大概率不是本篇范围,查 /system/fonts 与回退表
profile 一启动就 _NetworkProfiling FATALP29a:patched_sdk 装成产品档 dill → §6.1
改了 dill 但 libapp.so 逐字节不变P29c:AOT 缓存在 .dart_tool/ → §6.3
手工改 cache 里的文件下次构建失效P29b:marker 冲突会重下 → 改发布物,别热修
单测 exe 裸跑 EXIT 127SELinux sh 域读不到 platformsdk → §3.1,走 app 宿主
app 域结果文件找不到/data/local/tmp 不可写 → §3.3,用 /data/app/el2/100/base/<bundle>/files/
改了 .map 但 ninja 说 no work to doninja 不追踪 map → §3.2,删 .so 强制重链
API 13 设备装不上三条证据链 → §2.3,API 24 是硬要求
真机报 code:9568320 no signature file走 DevEco 自动签名一次 → §3.5,别用 hap-sign-tool 手工签
--llvm18 下编不过 zlibclang 15 的 -Werror,-Wignored-attributes → 换 custom_toolchain = llvm@19
升级后报 HAR 不存在先 HEAD 覆盖行 URL 确认工件(第 9 篇 §6.9),再查 file: 相对路径 → §7.2
Skia 正常、Impeller 黑屏两者共用 Dart 代码,问题在 Vulkan / swapchain 侧 → §7.2,别改适配层
七件套只换了一部分§1.3 版本闭环;flutter build -v 看 Skipping target → §6.3

九、常见问题 FAQ

9.1 为什么"编译全绿"不能当验收标准?

因为本批次两个最难故障都在编译器视野外:P28 端口不在,RefEmpty() 是合法返回值,编译零报错;P29a 是发布物装错包,跟当前源码树是否编译通过无关。编译闸只挡"类型与链接"这一类错。

9.2 模拟器 20 项失败要不要修?

不要,也别忽略——它们是真机复核的输入。13 项 Vulkan 是模拟器没有 GPU 后端,3 项 QoS 是服务行为差异,2 项 Timerfd 是上游文档化的已知 bug 测试,2 项是 stub 差异。真机上这 20 项全部翻转(§3.5)。但"预期会翻转"不等于"已翻转",必须真机跑过才能收口。

9.3 为什么基线数字是 1723 而不是 1726?

1726 是用例注册总数,实际执行 1723(模拟器 1703 通过 + 20 失败 = 1723,真机 1723 全通过)。差值 3 项是该次运行未执行的用例。报数时必须同时给"执行数"和"结果",只报一个数就会造成口径混乱。

9.4 字体检查能不能自动化进 CI?

判据本身全是命令行(nm -D、find obj、strings -a),完全可以。但要注意 §5.2 的三条:不吞 stderr、判据先在 good 产物上校准、口径写进断言消息。否则 CI 会因为命令本身坏了而报出"端口丢了"的假阳性——那比没有 CI 更糟。

9.5 strings 为什么读不了管道?

macOS 的 /usr/bin/strings 从 stdin 读时不可靠(实测隔离验证:同样的数据 cat 进 strings 也一样失败,根因不在 tar)。必须把文件当参数传。Linux 的 binutils 版 strings 行为可能不同——本批次判据全部在 macOS + arm64 + llvm@19 上校准(§十)。

9.6 三档引擎为什么必须各自验?

因为它们共享代码但共享不了发布物:debug 和 profile 用非产品 dill、release 用产品 dill;gen_snapshot 只有 profile/release 用。P29a 就是"profile 独有的故障,debug 完全不触发"。只验 debug 等于没验。

十、未验证项与诚实口径

本篇的判据值全部在 macOS + arm64 + llvm@19 + API 24 上校准。以下是明确的未做/未验项:

  • 换 Linux 宿主口径未验证——llvm-nm / strings 行为可能不同;而且第 9 篇 §9.6 的实测结论是:Linux 既没有定制 Dart SDK 也没有宿主 AOT 产物,strings 版本串(… on "linux_simarm64")这类判据本身可能要改;Windows 侧只有 Dart SDK 两档已发布,宿主 AOT gen_snapshot 在 3.47.4-ohos-1.0.4 之后未再随批次发布;
  • §5.1 症状反推表的后两行(量度层 / 光栅化层)是推理得出的分流,未实测——本批次只撞到第一行;
  • 部分字体缺失(Emoji 专门缺字之类)未实测,只给了排查方向;
  • 真机复核只用了一台设备(ALN-AL00,HarmonyOS 6.1.0.135),其他机型 / API 面未覆盖;
  • P29c 只有单一来源(§6.5),不等于被独立验证过;
  • hypium instrumented test 那条"正统路径"未做——本批次走的是 app 宿主 + NAPI wrapper 的第三条路。

证据归档在工作目录的 evidence/ 下,两份单测结果同格式可 diff:

evidence/ohos_ut_result_fullrun.txt                          240 KB   # 模拟器 1703/20
evidence/ohos_ut_result_device_20260913_FMR0224904009635.txt 240 KB   # 真机 1723 全绿

外加三档构建产物与冒烟截图;每个 P 编号都按「现象 → 根因 → 修复」三段记录。

十一、本篇小结与系列导航

本篇走完里程碑轴上的 M3 与 M5 两段(编号轴定义见第 5 篇 §6.4):

  • 四道闸:编译 / 单测 / 构建 / 发布,每道只挡一类错,每道闸后面都站着一个真实事故;
  • 构建闸:六个拦路虎没有一个报错指向根因;frontend_server 与 dart-sdk 是编译时耦合;API 24 是设备形态硬要求(三条独立证据链);
  • 单测闸:SELinux 域决定裸跑不可行 → app 宿主 + NAPI wrapper 第三条路 → AOT 快照符号修复(.map 补两个 global)→ 真机 1723 全绿,模拟器 20 项环境性失败全部翻转;
  • P28:字体端口静默丢失,根因四层链,P27「不可恢复」被 attachment 机制反转,修复四件套;
  • 诊断 SOP:三层证据链(二进制 / 构建图 / 附件),三个静默陷阱,六条误导性信号;
  • P29:三档引擎——profile 装错包(0→2)、marker 冲突、AOT 缓存在 .dart_tool/,加一次跨会话独立复现与明确的范围划界;
  • 收口动作:真机验收的七个维度(三档 × 连续 3 次冷启动 × 生命周期 × 输入 × 文字图标 × 双渲染后端 × 插件)与升级后的六行症状分诊表,把"我本地好了"变成可执行清单。

「鸿蒙Flutter框架 0day适配实战」全系列 10 篇,本篇为第 10 篇(生产侧 M3 端到端冒烟 + M5 验证收口),全系列到此完结:

#篇目侧一句话主题
01背景、分叉仓库、版本命名与 fvm fork 安装消费三层架构、三个接缝、七层工作量、fvm 直装 tag、版本墙
02tag 版本的手动安装与三层核验消费浅克隆 bootstrap、首次初始化、SSH 修复、异常分诊
03鸿蒙模拟器/真机跑 Flutter + 消费侧踩坑集消费工具链三件套、HAP 构建/安装/运行、占位签名、截图取证、CP1-CP14
04flutter build hap 自动签名消费P12/CSR、AGC 证书与 Profile、纯 Dart AES-GCM、C0-C10 复用
05上游同步总览、BC 全景与端到端 SOP生产批次解剖、五阶段验证门、12 项适配表、端到端 SOP 与 P1-P29 索引
06Phase 1+2:基线建立与 Framework 适配生产代理 HTTP/1.1 修障、421 文件重打、Dart SDK 源码构建
07Phase 3 Engine:三方重放与冲突裁决生产826 文件、15 真冲突两分法、契约对齐 16 项
08引擎构建链攻防生产依赖树、工具链版本、musl 交叉链接、测试面漂移
09构建发布与产物分发生产版本锁、11 个工件(Dart SDK 双宿主)、11 条覆盖键、换新 tag 铁律
10验证与真机验收(本篇)生产四道质量闸、设备侧单测、三档引擎、字体静默故障

总结

十篇文章走完一个完整闭环:从「用别人发好的 SDK 跑起来」(01-04)到「自己把上游版本同步出来、编出来、发出去、验干净」(05-10)。

本篇的三个可迁移结论:

  1. 验证分层不是为了流程好看,是因为每类错误只有一种探测器能发现它。编译闸查不到端口丢失,单测闸查不到渲染结果,构建闸查不到发布物装错包,发布闸是静默错误的最后防线。
  2. 判据必须标口径,而且必须在已知 good 的对象上校准过。stripped / unstripped、按行 / 按串、debug / release、模拟器 / 真机——同一问题不同口径差好几倍(185 vs 103、0 vs 5、1703 vs 1723)。不标口径的数字比没有数字更危险。
  3. 「树里没有」≠「不可恢复」。先问仓库有没有托管机制。这条判据把 P27 的"放弃"结论反转成了 P28 的四件套修复,也是整个系列里最值钱的一次方法论修正。

如果你在自己的 fork 上做类似的平台适配,建议按 05 → 06 → 07 → 08 → 09 → 10 的顺序读,那正好是一次同步的真实执行顺序;只是想用现成 SDK 写应用,读 01 → 02 → 03 → 04 就够了。

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!


相关资源:

环境与版本

本文的示例与截图基于以下环境组合(2026-10 实测):

组件版本说明
上游 Flutter SDK3.47.8github.com/flutter/flutter
Flutter SDK(OH 适配版)3.47.8-ohos-1.0.0oh-flutter/flutter_flutter,oh-3.47-dev 分支
DevEco Studio26.0.0 Release提供 hvigor / ohpm / hdc / JBR 等工具链
OpenHarmony SDK / APIAPI 26(26.0.0)模拟器与 SDK 均为 API 26
运行设备OpenHarmony 模拟器API 26,1280×2832

环境搭建(分叉获取、DevEco Studio 与 SDK 配置、模拟器创建)请按官方指导操作:OpenHarmony-flutter 环境搭建指导。

本文正文是 2026-09 3.44.9 → 3.47.4 同步批次的实录:其中的版本号、提交号与实测数字(适配清单文件数、构建边数、单测用例数、工件字节数等)都是那一批的现场值,不随上表更新;上表是当前可复现、可照做的环境组合。方法论、验证门与坑模型对 3.47.8 线同样适用。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐