前言

第 6 篇把 Framework 侧收口:421 个 packages/ 文件三方重打、Dart SDK 从源码构建、flutter analyze 全量 0 error(G2 门)。本篇进入全批次工作量最高的一块——engine/ 子树的 826 个路径(619 增 + 207 改),占整个 2263 条适配清单的三分之一强。

难的不是文件数,而是引擎层与上游重构正面相遇:Impeller 的像素格式枚举、Vulkan 能力结构、blit-pass API、gmock 风格、glyph atlas 光栅化,全部在 3.45~3.47 之间动过刀。纯 diff 重打听起来会撞出几百个冲突——实际只有 15 个,因为 git apply --3way 把「鸿蒙单侧改动」全部自动干净落入,只有双方都改过同一区域才留冲突。

本篇把这次裁决完整拆开:

  1. 补丁生成与三方合并——--binary 放错位置会让二进制补丁静默残缺;
  2. 15 个真冲突的两分法——8 个格式吸收类、7 个手合类,判据完全不同;
  3. 吸收类的双向 diff 归零验证器——「内容与 3.47.4 逐字节一致」才算被吸收;
  4. 手合类的逐项 API 移植,包括最大一役 typographer 并行光栅化的整函数重建;
  5. 编译期才暴露的合并缺口——符号级同步 ≠ 文件级同步;
  6. embedder 契约对齐 16 项:审计型验证,零代码改动。

版本口径:本篇是 2026-09 3.44.9 → 3.47.4 批次的 Phase 3 实录,落库提交 ceb364245e。当前正式发布版为 Flutter OH 3.47.8-ohos-1.0.0(oh-flutter/flutter_flutter),配套 DevEco Studio 26.0.0 Release + OpenHarmony SDK/API 26;环境搭建见 OpenHarmony-flutter 环境搭建指导。

前置阅读:第 5 篇的基线表(四个坐标)与适配清单概念;第 6 篇的三方重放打法与 G1/G2 门。

一、Engine 侧为什么是全批次最重的一块

1.1 826 路径的构成

engine/ 的适配清单来自第 6 篇恢复出的权威文件表(sync-data/engine_files.txt):

类别路径数内容
新增(A)619shell/platform/ohos/ 平台层、impeller 的 OHOS 后端分支、testing/ohos/、Vulkan/GBM 相关新文件
修改(M)207公共引擎逻辑里的 is_ohos / __OHOS__ 分支、BUILD.gn 接线、DEPS 与构建脚本
合计826占 2263 条适配清单的 36.5%,是单一 Phase 里最大的一块

结构上是新增为主:引擎适配不是重写公共逻辑,而是给引擎补一个 ohos 平台目录——这正是第 1 篇「三层复用、一层重写」在引擎侧的体现。真正撞上公共引擎逻辑的是那 207 个修改文件,而其中双方都改过同一区域的少数,就是后面那 15 个真冲突的高发区。

1.2 与上游重构正面相遇

3.44.9 → 3.47.4 之间,上游至少在这五个面上动了鸿蒙适配所依赖的代码:

  • 像素格式枚举:PixelFormat::kB10G10R10A2UNorm 被整体重命名为 A10XR 系列;
  • Vulkan 能力结构:DeviceHolderVK 被吸收进命令池,OptionalDeviceExtensionVK 重构;
  • blit-pass / 纹理接口:GetRenderTargetView、SliceViews 加参,DlImage 抽象基类化;
  • glyph atlas 光栅化:UpdateAtlasBitmap 从并行改回顺序循环,AddCopies 删除;
  • 测试风格:gmock 统一为 (const, override),MockQueue 落地。

提示:这五个面不是「理论风险」,第 5 篇风险清单里排第一的「engine 1123 文件变化、embedder API 兼容性待排」,兑现形式就是本篇的 15 个真冲突 + 第六章那批编译期缺口。

1.3 一张图看全篇

在这里插入图片描述

上图三个横带就是本篇的主线:顶部是「补丁集 826 → 三方合并后 15 真冲突 → 按『上游是否已完成同名重构』分流」的两分法;中部是五个重灾区(PixelFormat 重命名、Vulkan 能力结构、blit-pass 接口、glyph atlas、OHOS 平台层);底部是裁决之后要过的门——契约对齐 16 项(G3),以及两条可迁移的方法:--binary 必须在 -- 之前、冲突要按语义裁决而不是按文件裁决。

二、补丁生成与三方合并

2.1 --binary 的位置决定成败

# 正确:--binary 是 git diff 的选项,必须在路径分隔符 -- 之前
git diff --binary 3.44.9 oh-3.44.9-dev -- engine/ > /tmp/engine_adapt.patch
# 产物:6.2 MB(含二进制文件的 GBM/纹理测试数据等)

写反了的后果不是报错,而是静默残缺:

# 错误示范:--binary 落在 -- 之后,被当成 pathspec(路径名)处理
git diff 3.44.9 oh-3.44.9-dev -- engine/ --binary > /tmp/engine_adapt.patch
# 结果:补丁正常生成、正常应用、退出码 0,但二进制段全部丢失
检查项正确姿势失败信号
补丁体积6.2 MB 量级明显偏小(二进制被剔除)
grep -c 'GIT binary patch'与二进制文件数同量级0
应用后 git status二进制文件为 M/A二进制文件不在改动列表里

教训:生成时不报错的残缺,会在构建期以「符号缺失」的形式回来找你。这类「静默降级」在本批次至少出现三次(--binary、尾换行、字体端口)。

2.2 --3way 的合并语义

git -C flutter_flutter apply --3way /tmp/engine_adapt.patch

--3way 以**当前 HEAD(= 3.47.4)**为 ours 基准做三方合并:

  • 只有鸿蒙侧改过的区域 → 自动干净落入,例如 MSAA 判定:
// engine/src/flutter/impeller/renderer/render_target.cc
// 上游无此改动,鸿蒙单侧新增 —— --3way 自动吸收,不留冲突
const bool is_msaa = desc.sample_count != SampleCount::kCount1;
  • 双方都改过同一区域 → 留 UU(both-sides-modified),必须人工裁决。

2.3 结果读数

619 A + 193 M  干净应用
 15 UU         both-sides-modified 真冲突

15 个真冲突按「上游是否已经自己完成同名重构」一分为二:

类型数量判据动作
格式吸收类8上游 3.47.4 已做同名格式化/重命名,鸿蒙侧语义已被覆盖取 3.47.4(OURS),丢弃本地补丁块
手合类7两侧语义都要保留逐项对齐新 API,最大一役是 typographer 整函数重建

这个两分法是可复用的:下一批次的 engine 冲突同样先分「格式吸收 / 手合」两类,再决定动作。

三、格式吸收类:8 个取上游,验证靠双向归零

3.1 八个文件分别被上游做了什么

文件上游 3.47.4 完成的事裁决
core/formats.h、renderer/backend/metal/formats_mtl.h、lib/gpu/formats.h、renderer/backend/vulkan/formats_vk.h、backend/gles/texture_gles.cckB10G10R10A2UNorm 移除 → kB10G10R10XR / XRSRGB / A10XR + BC/ASTC 家族OURS
command_pool_vk.hDeviceHolderVK 被上游吸收进命令池OURS
testing/mocks.hgmock (const, override) 风格统一OURS
impeller/base/flags.h上游新增 operator==,鸿蒙新增 glyph_raster_parallelizationMERGE(python 逐字合并,默认 false)

前七个是「取上游」,第八个 flags.h 是真合并:两侧新增的都要,鸿蒙的并行光栅化 flag 保留但默认关闭。

3.2 验证器:任何一侧判定都要双向跑

判定「被吸收」的标准不是看着像,而是与 3.47.4 逐字节一致:

# 对每个冲突文件 f,跑两条 diff:
git -C flutter_flutter diff --cached oh-3.44.9-dev -- "$f" | wc -l   # vs 旧 fork 基线
git -C flutter_flutter diff --cached 3.47.4          -- "$f" | wc -l   # vs 上游新版
# 实录判定逻辑(示意):取 OURS 的文件必须 vs 3.47.4 = 0 行差异
def judge(path, ours_side='3.47.4', fork_side='oh-3.44.9-dev'):
    vs_upstream = len(git_diff_cached(ours_side, path))
    vs_fork = len(git_diff_cached(fork_side, path))
    assert vs_upstream == 0, f'{path} 声称被吸收但与上游仍有 {vs_upstream} 行差异'
    return vs_fork  # 手合文件用这个值判断改动量级是否与语义相符

实测结果:7 个吸收文件 vs 3.47.4 全为 0(含 lib/gpu/formats.h 双向 0——鸿蒙侧恰好与 3.47.4 相同)。

3.3 「8 个文件解析失败」是个假象

首轮统计时 8 个文件报「无法解析」,根因是:7 个吸收文件的内容已经等于基线,git diff 为空 → 没有可 commit 的内容。

git -C flutter_flutter add -u engine/   # 补齐先前 7 个未暂存的解析

这就是第 6 篇那条复盘教训的引擎版:「干净」最危险。diff 为空既可能是「无事发生」,也可能是「吸收完成」,必须用双向 diff 区分。

四、手合类:7 处逐项 API 移植

4.1 allocator_vk.cc:保留重试链 + 补回 OH_RESTRACE

冲突本质:鸿蒙加了裸 vmaCreateImage + OH_RESTRACE,3.47.4 引入了 fixed-rate compression 重试链。裁决是两个都要:

// engine/src/flutter/impeller/renderer/backend/vulkan/allocator_vk.cc
// 结构取 3.47.4 的重试链;成功检查之后按平台补回鸿蒙的显存追踪
#if defined(FML_OS_OHOS)
  OH_RESTRACE(allocation, allocation_info.size);
#endif

4.2 capabilities_vk:双 case 并存

上游把扩展枚举重构成 Optional 系列,鸿蒙的 kEXTIncrementalPresent 与 OHOS 扩展要各自落到新枚举上:

// capabilities_vk.cc —— 两条 case 并存,不互相覆盖
case vk::StructureType::kPhysicalDeviceTextureCompressionAstcHdrFeaturesEXT:  // 上游新名
case vk::StructureType::kPhysicalDeviceIncrementalPresentFeaturesKHR:         // 原 OHOS 扩展面

函数侧同理:GetMaxSamplerAnisotropy 取上游实现,SupportsFramebufferColorSampleCount2x 的声明上游仍在、实现被收进 __OHOS__ 守卫——声明与实现要分别核对,只看头文件会误判成「上游已删」。

4.3 surface_context_vk.h:助手方法与颜色空间两存

// 鸿蒙的 swapchain 状态助手(3 个)+ 上游新增的 FinishQueue() override 同时保留
bool GetAndResetChangedFlag();
bool IsPreload() const;
void SetIsPreload(bool value);
#if defined(FML_OS_OHOS)
  // 鸿蒙侧颜色空间对(OHOS 合成器要求)
#endif
impeller::FinishQueue& GetFinishQueue() override;  // 3.47.4 新增纯虚

4.4 mock_vulkan.cc:沿用文件内既有惯例

鸿蒙的 mapped-memory 记账 vs 上游新增 MockQueue——合并结果是两者共存,并照抄文件内既有的线程安全惯例:

  MockQueue queue_;                              // 3.47.4
  std::map<VkDeviceMemory, size_t> mapped_memories_;   // 鸿蒙
  std::map<VkDeviceMemory, size_t> allocation_sizes_;  // 鸿蒙
  Mutex mutex_;
  IPLR_GUARDED_BY(mutex_)   // 新成员一律挂到既有锁上,不自造同步风格

4.5 run_tests.py:元组取鸿蒙,文案取上游

# 鸿蒙把 'ohos' 加进平台类型元组;上游改了 assert 文案
# 裁决:功能面(元组)取鸿蒙,文本面(文案)取上游
#文件冲突本质裁决
1allocator_vk.cc裸调用 + OH_RESTRACE vs 重试链重试链 + 守卫内补回
2-3capabilities_vk.{h,cc}枚举重构 + OHOS 扩展双 case 并存、守卫内保实现
4surface_context_vk.h助手 vs FinishQueue()两存
5test/mock_vulkan.cc记账 vs MockQueue共存 + 沿用 GUARDED_BY
6testing/run_tests.py平台元组 vs assert 文案分面取用
7typographer_context_skia.cc并行光栅化 vs 顺序循环整函数重建(下一章)

五、最大一役:typographer 整函数重建

5.1 为什么逐 hunk 拼装不可行

UpdateAtlasBitmap 两侧都做了大改:鸿蒙引入 scan_fn / rasterize_fn 并行光栅化 + AddCopies;上游改成顺序循环 + 逐 glyph AddCopy。三方合并给出的 common 文本不可信:

git 的 diff3 按行对齐是启发式的——out.bitmap、item.bounds 这类只可能属于一侧的代码被混进了「公共区」。逐 hunk 拼装会把语义不同的行缝在一起,编译能过、渲染会错。

结论:放弃逐 hunk,直接比对两侧原始版本后整函数重写。

5.2 结构取鸿蒙,API 移植上游

// 重建后的骨架:三段式流程保留鸿蒙,调用的 API 全部换成 3.47.4 的
// 1) scan_fn:扫描阶段,含 kColorGlyphBaseCost 调度代价
// 2) GlyphAtlasParallelizer::Rasterize(flags, ...):并行光栅化
//    —— 该类型是鸿蒙新增文件,--3way 已干净落入;flag 关闭时内部单线程回退
// 3) 归并上传

三处必须移植的上游 API 变更:

旧(3.44.9 + 鸿蒙)新(3.47.4)处理
atlas->AddCopies(copies)已删除改逐 glyph AddCopy 循环(BufferToTextureCopy 结构体幸存)
DrawGlyph(...) 7 参含 has_color6 参,has_color 已删去参,色彩语义由类型判断承担
GetImageInfo() 2 参3 参重载 support_light_glyphslight-glyph 的 ToA8Bitmap 转换搬进 rasterize_fn

5.3 验收判据:flag 关闭时必须与上游逐 glyph 一致

# 关键判据:并行开关关闭(默认)时,纹理结果与上游 3.47.4 逐 glyph 一致
# 也就是「不引入行为差异」是默认态,并行是显式开关

这条判据是整个 Phase 3 的裁决底线:手合不是「把两边代码都留下」,而是「默认行为等于上游,平台特性走开关」。

六、编译期才暴露的缺口:符号级同步 ≠ 文件级同步

三方合并与手合解决的是「同一文件同一区域」的冲突。另一类缺口在 diff 视野里完全看不见,只有编译器能暴露——它们是 Phase 3 的真实工作量尾巴(编号沿用生产侧 P 系列,索引见第 5 篇)。

6.1 P7:diff 为零的文件,仍在引用被删符号

content_context.cc:547:31: error: no member named 'kCount2' in 'impeller::SampleCount'
(同批还有 :615 / :623 / :705 三处)

上游在 3.44 → 3.47 删掉了 SampleCount::kCount2(枚举成员 + 全部引用),而 fork 的 OHOS 渲染路径有意在用 2x MSAA:content_context.cc、render_target.cc、swapchain_transients_vk.cc 的 #ifdef __OHOS__ 分支都引用它。Phase 3 把 formats.h 解决成 3.47.4 时,commit message 写着「ohos references absorbed identically, diff = 0」——那只对 formats.h 本身成立,枚举成员删除是全局的,引用面散布在多个 fork 保留文件里。

修复方向不是把 kCount2 改成 kCount4(那会丢掉 2x MSAA 语义,且 OHOS 侧配套有 SupportsFramebufferColorSampleCount2x 能力检测),而是恢复枚举成员并加注释说明 fork 语义:

// impeller/geometry.h —— 上游删除只因上游无引用,非平台特性移除
kCount2 = 2,  // OHOS: 2x MSAA path (see SupportsFramebufferColorSampleCount2x)

方法论:符号级同步 ≠ 文件级同步。上游删一个 API/枚举成员后,diff 为零的保留文件仍可能因「引用被删符号」与上游解耦。这一面只能靠编译器暴露——所以「编译通过」不只是产物验证,还是在反向审计合并质量:每次失败都要问一句「这是上游行为变化,还是我的合并遗漏?」

6.2 P9:重命名类变更,先认上游新语义再决定去留

formats_gles.cc:123:28: error: no member named 'kB10G10R10A2UNorm' in 'impeller::PixelFormat'
formats_gles.cc:216:9: error: duplicate case value 'kB10G10R10A10XR'

上游把 10-bit 格式整体改名(kB10G10R10A2UNorm → kB10G10R10A10XR,另加 XRSRGB / XR 变体),并顺手删掉 fork 的「10bit → GL_RGB10_A2 / 截图 / DisplayP3 编码」映射。fork 在 4 个 site 保留了旧 case:formats_gles.cc、rasterizer.cc、image_encoding_impeller.cc、image_decoder_impeller.cc。

裁决是全部跟随上游删除:上游判定 10bit 在 GLES 返回 nullopt、截图归入 kUnknown、无编码映射;DisplayP3 映射若保留会造成 extended-range 语义错配(A10XR 是新语义,旧 case 是旧语义字符串)。

# 重命名类变更的定位定式:在上游一侧找新符号名
git log -S 'kB10G10R10A2UNorm' --oneline -- engine/src/flutter/impeller
git show 9584c6:engine/src/flutter/impeller/renderer/backend/gles/formats_gles.cc | sed -n '110,135p'

判据口诀:先认上游新语义,再评估 fork 旧 case 是不是自己的平台特性。是特性(P7 的 2x MSAA)就留,与上游新行为冲突(P9 的 DisplayP3 编码)就删。

6.3 P10:私仓 pin 差异与 vendored 头缺失(最长的一战)

error: no member named 'ExternalFormatOHOS' in namespace 'impeller::vk'
error: unknown type name 'YUVConversionDescriptorVK'
(从 yuv_conversion_vk.h 出发级联 7+ 个 TU)

根因在 DEPS 机制层:fork 通过 DEPS_ohos 把 8 个第三方仓库换成 openharmony-sig 的私有 flavor(如 fluttertpc_vulkan-deps@050bd02a),其 vulkan.hpp 是从含 VK_OHOS_external_memory 的 DevEco 头生成的;同步到 3.47.4 后 skia/vulkan-deps 的 pin 换回 vanilla 上游,vendored 树里既没有原型也没有 dispatch 表项。

恢复定式(不依赖 vendored wrapper):

// 1) 动态 proc 取函数指针
auto* fn = reinterpret_cast<PFNVkQueueSignalReleaseImageOHOS>(
    VULKAN_HPP_DEFAULT_DISPATCHER.vkGetDeviceProcAddr(device_, "vkQueueSignalReleaseImageOHOS"));
// 2) vendored 树缺 typedef → 文件内本地声明
using PFNVkQueueSignalReleaseImageOHOS = VkResult (*)(VkQueue, uint32_t, const VkSemaphore*, VkImage, int*);
// 3) handle 不能 reinterpret_cast 数组:Vulkan-Hpp 的 handle wrapper 是结构化成员
std::vector<VkSemaphore> raw;  // 先展开到裸句柄再传

6.4 P19:一个 feature 的清理要跨 7 个文件

华为 skia 的 fontmgr_ohos 端口在 3.47.4 的 build 层已被关闭,但源码使用方和测试面还挂着——整链清理涉及 7 个文件:

层文件动作
buildtxt/BUILD.gn删 is_ohos 分支的 platform_ohos.cc,回落 generic platform.cc
源码txt/src/txt/platform_ohos.cc删文件
shellohos_shell_holder.{h,cpp}删 SkFontMgr_ohos.h include、OHOSLastFontPath、5 个静态函数、2 个方法、local_font_path_ 成员
契约napi/platform_view_ohos_napi.cpp保留 napi 接口面(Dart 侧调用不崩),方法体清空为 LOGD + 参数校验
测试ohos_shell_holder_unittests.cpp、ohos_unittests_entry.cc删 2 个 TEST、extern 声明与 reset 里的 clear()

教训:「对齐」必须三面一致——build 层开关、源码使用方、测试面。判断某个 feature 是否清理干净,不能只看 BUILD.gn,要 grep 符号使用方(本轮收割 OHOSLastFontPath / SkFontMgr_ohos / platform_ohos 四组符号)。删除方向还能用符号引用图验证无暗挂——napi 保留壳就是特例:契约上 Dart 侧仍可能调用,删实现不删接口。

6.5 P20/P21:删多了与没跟上的两对镜像

  • P20(删多了):QueueVK::QueueSignalReleaseImageOHOS 在前序轮次按「官方对齐」删除,但 grep 未覆盖 shell/platform/ohos/,调用方(华为 GPU 栅栏路径,把 vkSemaphore 交给 GraphicQueue 换 fence_fd)是雇员 → 按 6.3 的动态 proc 定式恢复,并给 QueueVK 构造器加第三参 vk::Device device = {}。
  • P21(没跟上):FenceWaiterVK::AddFence 上游扩为 (fence, submit_callback, completion_callback),OHOS 路径已自行 QueueVK::Submit,故 submit 回调传空成功:
const auto added_fence = fence_waiter_->AddFence(
    fence, [](impeller::vk::Fence) { return fml::Status(); }, completion_callback);
if (!added_fence.ok()) {   // 旧代码是 if (!added_fence)

先读实现再填回调:3.47 的语义是「提交失败就拒绝进 wait set」,不是旧版的「直接等」。同批还有 DlImage::Make → DlImageSkia::Make(DlImage 在 3.47.4 是 157 行纯抽象接口,工厂按后端下放)。

6.6 P8/P11:只咬「同步新增文件」的格式坑

SkLog_ohos.cpp:49:38: error: no newline at end of file [-Werror,-Wnewline-eof]

同步时新写的文件由脚本转写,不保尾换行;vanilla 仓库里被同步的文件天然带 \n,所以这类问题只咬新增内容。修复 printf '\n' >>,流程沉淀是「每个同步写出的新文件过一遍 newline-eof 预检」。同族的 P11 是 OHOS 补丁吞括号——#ifdef 配平错误,症状最骗人(报错点离根因很远)。

方法论:-Werror 全家桶对「非上游原文」的所有新增内容都有效。新文件除了语义正确,还要满足仓库既有格式约束(换行、tab、行宽)——这类问题不进 diff 视野,只能被编译器或 lint 暴露。

6.7 P12-P18:API 面扩张、系统头宏与私有依赖

剩下七个编号是同一种压力的七个出口——3.44 → 3.47 的公共 API 面扩张,加上华为 develop 树的私有依赖。它们都不在 diff 视野里,全部由编译器暴露:

编号缺口类型修复动作
P12TextureSourceVK::GetRenderTargetView 上游由 0 参扩为 (mip_level, array_layer),fork 的 OHB override 未跟随虚函数加参override 对齐新签名(实现保持单视图语义,注释说明 mip/array 恒为 0),unittests 4 处调用补 (0, 0)
P13QueueVK::QueueSignalReleaseImageOHOS 失去唯一调用方孤儿 API按上游删除整函数,queue_vk.h/.cc 同步清理
P14command_pool_vk.cc:87 redefinition of 'device_holder'补丁贴错位置fork 把 3.44 版析构整段(14 行)贴在 3.47.4 新版前面,删 fork 插入段、保留上游顺序
P15socket_base_posix.cc 三处 -Wsign-compare,note 指向 sysroot 的 sys/socket.h系统头宏dart_config 加 if (target_os == "ohos") cflags += [ "-Wno-sign-compare" ]
P16VkSurfaceCreateFlagsOHOS 未知类型,且同轮其它 vulkan TU 正常include-guard 时序改命令行 define:build/config/compiler/BUILD.gn 的 OHOS 分支追加 VK_USE_PLATFORM_OHOS
P17SkGlyphRasterWorkerSlot.h file not found私有依赖缺失按使用方 + unittests 反推契约,本地 from-scratch 实现 30 行 header-only
P18SliceViews 上游加第 5 参,OHOS shell 调用点停在 4 参自由函数加参按上游惯例补 {}(语义:无 underlay-preserved 视图)

三条值得单独展开。

① P12 与 P18 是同一条律:上游函数加参数(虚函数或自由函数都一样),fork 侧所有调用点都要过一遍,mock/fake 里的同名签名同理。编译器只会报「签名不匹配 / non-virtual member function marked ‘override’ hides virtual」,根因却是「3.44→3.47 API 面扩张、调用方未跟上」。P18 还有个级联假象:修复调用后,随之出现的 lambda 捕获错误自动消失——别被它带偏去动 lambda。

② P15 不是我们的代码错,是平台头质量差:DevEco sysroot 的 OHOS libc(musl 系)里 CMSG_NXTHDR 展开成 unsigned long >= long,恒定 sign-compare;glibc / bionic 的同名宏是 unsigned:unsigned,不触发。它随 -Wall 打开、随 -Werror 升级,任何合法使用 CMSG_NXTHDR 的上游可移植代码(dart:io 的 socket 接收路径完全合法)都会在 OHOS 目标上炸。源码侧修不了——告警生成在宏体内部的比较表达式,clang 把位置映射到使用点,只能改宏或加 flag。

# third_party/dart/runtime/BUILD.gn → config("dart_config")
# 这个清单本身就是「上游维护的 -Wno 列表」:已有 -Wno-unused-parameter、
# -Wno-comments、fuchsia 分支的 -Wno-deprecated-copy,为平台头加豁免有先例
if (target_os == "ohos") {
  cflags += [ "-Wno-sign-compare" ]  # sysroot CMSG_NXTHDR 宏展开触发
}

方法论:「平台头文件宏」逼出的 -Wno 豁免是 OHOS 适配的常态化工作,不是 hack。挂点选在第三方自己的 flags 清单里(dart_config)而不是工程全局,保持影响面最小。出现这类错误先问「警告源于哪个头」:C 库里来的按平台豁免,自己代码里来的才修源码。

③ P16 是本轮最反直觉的一条:同一个宏、同一批文件,surface_context_vk.cc 编译正常,device_buffer_vk.cc 报错——时序敏感。

device_buffer_vk.h:13 → vma.h:11 → flutter_vma.h → vk_mem_alloc.h:132
    → #include <vulkan/vulkan.h>      ← 此刻 VK_USE_PLATFORM_OHOS 还没定义
                                        vulkan.h 的 OHOS 分支被跳过
                                        但 VULKAN_H_ include guard 已置位
之后 vk.h 才定义该宏 → vulkan.hpp:42 的 #include <vulkan/vulkan.h>
    → 命中 guard 直接返回 → vulkan_ohos.h 永远没进来
    → enums.hpp 的 OHOS 段引用的是空气

修法是把平台宏做成命令行 define,任何 TU 从第一个 include 起宏就存在,彻底消除顺序依赖(这也是 DevEco CMake 工程的惯常做法)。验证流程值得抄:先拿 compile_commands.json 里该 TU 的命令加 -DVK_USE_PLATFORM_OHOS 做单 TU -fsyntax-only,通过后再落 gn、重启 ninja。

规则:vk.h 内 define 宏只覆盖「经由 vk.h 的路径」;任何第三方头(VMA 系)直接 include vulkan C 头都会绕开它。平台宏不要依赖某个 wrapper 头里的 define。

P17 的处置决策过程比结论更有价值——先回答「这文件该不该存在」:三方取证(官方 skia 3.47.4 pin ×无、skia.googlesource.com/main ×无、华为 skia fork 全历史 ×无)确认它只存在于华为未发布的分支;再看引用面(横跨 shell 层的 glyph_raster_parallelization 开关,删除是大手术);最后确认契约能否反推——unittests 是缺失接口的权威规范(977 行里有 SlotCountIsThree、RegisterReturnsAscendingSlots、ThreadLocalIsolation 这类精确断言)。三者齐备 → 本地实现而非删除:

// third_party/skia/src/ports/SkGlyphRasterWorkerSlot.h(30 行,header-only)
// 注释写明「from-scratch 实现 + 契约依据」,不冒充华为原文件
constexpr int kSkGlyphRasterWorkerSlotCount = 3;
constexpr int kSkGlyphRasterWorkerSlotNone  = -1;   // 兼作「已满」返回值
// Get/Set 必须共享同一 TLS:用 namespace 级 inline thread_local,
// 用函数内 static thread_local 会导致 Get/Set 各一份
inline thread_local int t_skGlyphRasterWorkerSlot = kSkGlyphRasterWorkerSlotNone;

同步华为 develop 树时,DEPS 之外的私有补丁面必然出现「引用方在、被引用方缺」。三个判断依次是:符号是否存在于任何可达镜像(查证要彻底)、引用面有多大、契约能否从使用方 + 测试反推。齐备则本地实现(P2 的 SkLog_ohos.cpp 同模式),而非删除。

6.8 P22:测试面的 #define private public 撞上 <ranges>

lazy_split_view.h:144: error: '__outer_iterator' redeclared with 'public' access

OHOS 测试文件有 24 处 #define private public 窗口 hack;libc++ 的 lazy_split_view 在 private 段前向声明迭代器、后续位置重新定义——改写 private 宏会让 clang 把同一嵌套类型看成两种访问性。include 链经 shell.h → runtime_controller.h → ui_dart_state.h → abseil span → <ranges> 首次解析到 ranges 就踩中。

// 最小修复、零构建改动:窗口打开前先把 ranges 在干净环境下解析完
#include <ranges>  // must precede `#define private public`
#define private public
#include "flutter/shell/platform/ohos/xxx.h"
#undef private

替代方案是 -fno-access-control 加到测试 target 并删掉 24 处 hack(官方渠道,但改动面大);移动 hack 窗口无效,挡不住被包含头的链。

本条是首例;同型问题在 ninja25-27 于另外 4 个 TU 复现,批量规律与统一修法见第 8 篇 §5.2(测试面漂移五规律之四)。

七、embedder 契约对齐 16 项:审计型验证,零代码改动

冲突裁决完成后,还要证明「引擎对外契约没被同步打断」。这一步不写代码,只做逐项审计(G3 门)。

7.1 windowning FFI:18/18 符号 + 10/10 字段序

契约面检查内容结论
符号名Dart 侧 lookupFunction 的符号串 ⇄ ohos_window_controller.{h,cpp} 的 InternalFlutter_Window_*(16 个)+ InternalFlutter_WindowController_Create*Window(4 个)+ OHOS_WindowingSupported18/18 符号对齐
结构体_WindowCreationRequest(Dart)⇄ FlutterWindowCreationRequest(C)10/10 字段序一致
判别器has_parent 设计保留必要——parent_view_id == 0 是合法父窗口(主窗口即 view 0)

7.2 其余三个契约面

# engine/src/build/config/BUILDCONFIG.gn
current_os == "ohos" → is_ohos = true
# engine/src/flutter/shell/platform/BUILD.gn
} else if (is_ohos) {
  deps = [ "ohos" ]
}
  • render-texture top-down:ohos_external_texture.cpp:1020-1042 的 TransformMatrixV2 默认垂直翻转 + 显式 flip 回正,契约实现与单测在位;
  • accessibility 语义结构:桥接消费的旗标子集(isButton / isTextField / isLink / isChecked / SemanticsTristate::isToggled / hasImplicitScrolling …)在 3.47.4 全数存活,无移除项;
  • 构建接线:current_os → is_ohos → deps 完整。

这一章的价值在于「零改动」:16 项全部通过,说明同步没有破坏 embedder 契约面。若有一项不过,就是必须回补的适配点,而不是可以忽略的告警。

八、落库核对与 G3 门

8.1 826 = 819 staged + 7 归零

git -C flutter_flutter add -u engine/
git -C flutter_flutter commit -m "engine: land oh-3.44.9-dev engine adaptation onto 3.47.4 base (Phase 3)"
# [oh-3.47.4-dev ceb364245e] 819 files changed, 145777 insertions(+), 351 deletions(-)

落库提交正文(ceb364245e,2026-09-13 09:06,父提交就是第 6 篇的 Phase 2 收官提交 bd061a09d6)把这两组数字同时写清了:

Re-applies the OpenHarmony engine adaptation (826 paths enumerated in
sync-data/engine_files.txt: 619 added, 207 modified) onto the stock
3.47.4 engine tree via a 3-way merge against the oh-3.44.9-dev branch.

Conflict resolutions (15 conflict files):
- formats.h / formats_mtl.h / formats_vk.h / lib/gpu/formats.h /
  texture_gles.cc / mocks.h / command_pool_vk.h: resolved to 3.47.4 …
- impeller/base/flags.h: merged both sides …

826 条路径是清单数,619 新增 + 207 修改是清单内的构成,819 是真正产生 diff 的文件数——三者不矛盾:207 个"修改"里有 7 个在重放后与基线逐字节相同,无 diff 可提交。这条提交正文同时列出 15 个冲突文件与各自的裁决方式,等于把第三章、第四章的表格在仓库里留了一份底账。

别拿托管平台的 commit API 数字当验收依据:AtomGit 的 /commits/{sha} 对这个量级的提交不返回文件清单,stats 也只给出 +6364 / −79(明显小于 git show --stat 的 14.5 万行)。落库规模一律以本机 git commit 输出与 git show --name-only 为准,API 只用来核对 sha / 提交标题 / 时间 / 父提交。

项数值说明
清单路径826sync-data/engine_files.txt
实际提交819有内容变更的文件
差额7吸收类文件内容与基线一致,无 diff 可 commit
变更量+145777 / −351新增为主,符合 1.1 的结构判断

差额必须显式解释,否则就是漏提交。核对脚本很简单:

# 清单里每个路径都要么在 commit 里,要么被判定为「吸收归零」
comm -23 <(sort sync-data/engine_files.txt) <(git show --name-only --format= ceb364245e | sort)

8.2 G3 门判据

  1. 15 个真冲突全部裁决:8 吸收(双向 diff 归零)+ 7 手合(语义量级相符);
  2. 契约对齐 16 项通过,零 API 缺口;
  3. typographer 并行光栅化保留,flag 门控、默认行为与上游逐 glyph 一致;
  4. 落库差额可解释(819 + 7 = 826)。

遗留项:Invalid SDK hash(host dart-sdk 与 dart-sdk-ohos build ID 不匹配)不属于签名或合并问题,是工具链 SDK 管理面,留到第 9 篇处理。

8.3 同一天还有一次 Phase 3 收尾提交

ceb364245e 落库后当天 23:20 又有一笔 04b8cc9e38(提交标题 Phase 3 follow-up: engine adaptations + flutter_tools OHOS fixes (M3),+450 / −283):把引擎侧余下的适配与 flutter_tools 的鸿蒙修复一起补上。826 文件的重放不等于 Phase 3 结束——工具链侧还要跟着引擎改才能编译过,这是 M3 的常态,下一批次请预留这一笔。

九、常见问题 FAQ

9.1 为什么只有 15 个冲突,不是几百个?

因为 --3way 把「鸿蒙单侧改动」全部自动落入,只有双方都改过同一区域才留冲突。这也是第 5 篇选「全新分支 + 三方重放(不 merge)」打法的直接收益:merge 会把上游的格式化改动也变成冲突面。

9.2 「取上游」会不会把鸿蒙适配丢掉?

不会——前提是用双向 diff 证明它已被吸收。7 个吸收文件 vs 3.47.4 全为 0 行差异,说明上游做了同名重构且覆盖了鸿蒙侧语义。反例是 P7:formats.h 本身归零了,但跨文件的符号引用没归零,这类只能靠编译器抓。

9.3 手合时怎么决定「留鸿蒙」还是「跟上游」?

一句话:默认行为等于上游,平台特性走开关。P7 的 2x MSAA 是平台特性(有配套能力检测)→ 恢复枚举成员;P9 的 DisplayP3 编码与上游新语义冲突 → 删;typographer 的并行光栅化 → flag 门控,默认关闭。

9.4 为什么 typographer 不能逐 hunk 合并?

因为 diff3 的 common 区不可信:out.bitmap、item.bounds 这类只可能属于一侧的行被启发式对齐进了公共区。逐 hunk 拼装会产出「编译能过、渲染会错」的缝合函数,只能整函数重写后与两侧原始版本逐行比对。

9.5 契约对齐要改代码吗?

不改。它是审计型验证:符号名、结构体字段序、旗标存活、gn 接线四类逐项比对,通过即记录,不通过才回补。它的意义是把「embedder 没被同步打断」变成可复核的证据,而不是感觉。

9.6 下一批次这套裁决能直接复用吗?

两分法与判据可复用(先分格式吸收/手合、双向 diff 归零、默认等于上游),具体文件清单不可复用——每个批次的冲突面取决于上游改了哪些引擎文件。参数化流程见第 5 篇的 SOP 章。

十、本篇小结与系列导航

本篇把 Phase 3 完整走通:

  • 打法:git diff --binary 生成 6.2 MB 补丁 → apply --3way → 619 A + 193 M 干净落入,15 个真冲突;
  • 裁决:8 个格式吸收(双向 diff 归零验证)+ 7 个手合(逐项 API 移植),typographer 整函数重建;
  • 尾巴:编译期才暴露的符号级缺口——P7 枚举悬空、P9 重命名、P10 私仓 pin、P19 整链清理、P20/P21 删除与扩参、P8/P11 格式、P22 测试面;
  • 收口:契约对齐 16 项(G3 门)+ 落库 819 + 7 = 826 可解释。

「鸿蒙Flutter框架 0day适配实战」全系列 10 篇,本篇为第 7 篇(生产侧 Phase 3):

#篇目侧一句话主题
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引擎构建链攻防生产依赖树、工具链版本、链接与测试面 27 坑
09构建发布与产物分发生产版本锁、11 个工件(Dart SDK 双宿主)、11 条覆盖键、换新 tag 铁律
10验证与真机验收生产三档引擎、设备侧单测、字体图标静默故障

总结

Phase 3 的全部难度可以压成三句话:冲突要按语义裁决,不能按文件一把梭取 OURS 或取 THEIRS(两分法 + 双向归零验证器);手合的底线是「默认行为等于上游,平台特性走开关」(typographer 的 flag 门控是样板);「文件级同步」不等于「符号级同步」,diff 为零的保留文件仍可能引用被上游删除的符号,这一面只有编译器能暴露——所以构建期的每一次失败,都是在反向审计合并质量。

下一篇(第 8 篇)进入引擎构建链攻防:旧 obj/*.ninja 混合图、CIPD 预编译缺失、libcxx 对 clang 最低版本的硬约束、LLVM 版本选择的连锁反应、OHOS 链接三件套与测试面漂移——12458 条边的产物链怎么一步步编到 0 FAILED。

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


相关资源:

环境与版本

本文的示例与截图基于以下环境组合(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、测试、元服务和应用上架分发等。

更多推荐