将现有 Compose Multiplatform 业务接入 HarmonyOS:架构、适配与持续同步

将现有 Compose Multiplatform 业务接入 HarmonyOS:架构、适配与持续同步

一款应用已经在 iOS、Android 上积累了共享业务代码,鸿蒙端也有自己的账号、数据库、导航和媒体能力。此时引入 Compose Multiplatform(下文简称 CMP),最关键的问题是:共享页面怎样使用鸿蒙已有的真实业务,以及共享代码更新后,鸿蒙怎样继续跟进。

本文以一款学习类应用的词表与训练功能为例,介绍一次渐进接入的工程实践,包括架构选择、实际问题、排查过程和后续维护。仓库、分支、模块和业务接口均使用示例名称;不包含真实产品名称、私有地址、设备信息、账号数据或内部版本标识。公共技术名称与平台 API 名称予以保留。文中的示意目录和协议用于说明设计,需要结合实际工程调整。

接入并未在所有功能和设备场景上完成。文中会分别说明历史联调中出现的问题、源码审查发现的行为差异,以及仍待真机确认的适配,避免将不同阶段的结果混用。

一、先明确接入起点和复用范围

接入前,工程中已经存在两条链路。

一条是 iOS、Android 使用的 Kotlin 共享业务和 CMP 页面,包含数据模型、业务用例、界面状态及组件。另一条是鸿蒙宿主:页面使用 ArkTS,已有业务 SDK 通过 Kotlin/JS 提供账号、数据访问和持久化能力。

如果只接入一个能显示标题的 CMP 页面,只能证明基础渲染链路可用。要替换正式业务,还需要接通真实数据、写入回执、导航返回、资源加载和页面生命周期。

因此,迁移前先给已有代码确定职责:

层次 适合复用的内容 鸿蒙需要承接的内容
业务层 数据模型、查询与修改用例、业务规则 既有账号、数据库、网络和存储实现
状态层 筛选、排序、分组、答题判定、播放策略 生命周期绑定、宿主请求及异步回包处理
界面层 共享题面、列表组件、反馈和设置面板 平台资源实现、输入法、安全区和组件互操作
系统能力 能力接口及业务语义 导航、音频、系统播控、后台任务、提示与统计

复用范围需要逐项确认。这个案例中的根词表已经共用页面与状态容器;五类训练复用了题面和多项 Delegate,但仍由新增的 Native 会话承担部分流程编排,尚未整体复用原 ViewModel。

二、工程怎样拆分,最后产生几个包

为了兼顾原有工具链与鸿蒙目标,工程采用独立的鸿蒙适配构建。以下为简化后的示意结构:

text 复制代码
shared-suite/                    共享业务仓库
├── shared-api/                  模型与接口
├── shared-business/             业务用例与规则
├── shared-ui/                   CMP 页面、状态与组件
└── harmony-adapter/             独立的鸿蒙适配构建
    ├── contract/                会话协议、共享状态与规则
    ├── presentation/            共享 UI 引用及平台实现
    └── runtime/                 页面装配、Native 导出

compatible-business/             既有鸿蒙业务 SDK 的兼容工作区
harmony-client/                  鸿蒙宿主工程

这个案例使用 CPF 的 HarmonyOS 适配工具链编译共享 UI。它与原工程使用的 Kotlin、Compose 构件需要分别管理,并锁定兼容组合。不能因为都使用 Kotlin 和 Compose,就假定不同目标的二进制可以直接互换。

最终供鸿蒙集成的核心产物是两个 HAR:

产物 主要职责 本案例中的持有位置
业务 SDK HAR 查询、修改、账号上下文及业务持久化 公共基础 HSP
CMP 功能 HAR 共享词表与训练 UI、Native 会话、桥接入口及资源 应用入口模块

词表的不同 Tab、不同训练页面可以放在同一个 CMP HAR 中。内部拆成多个 Gradle 模块,是为了管理依赖和职责,并不意味着每个模块都要独立发包。

宿主安装时还涉及入口 HAP 和已有的公共 HSP。安装模块的数量由应用原有结构决定,不能直接等同于新增功能包的数量。

为什么暂时保留兼容业务工作区

共享业务上游与鸿蒙已有 SDK 可能处于不同演进阶段,接口、数据库迁移和导出类型不一定一致。直接替换整个业务 SDK,可能影响学习模块以外的既有功能。

过渡期间,CMP 页面从共享业务仓库获取源码,鸿蒙继续使用兼容 SDK,并向其同步接入需要的接口、模型和业务用例。这个安排能够控制替换范围,但也带来双基线维护成本;尤其是数据库迁移,不能仅凭文件名相似就覆盖。

兼容工作区是当前工程的过渡方案,并非所有 CMP 项目都必须采用的结构。后续需要逐步统一业务基线,减少同一项修改在两个工作区之间的同步。

三、运行时怎样连接真实业务

本案例的主要调用关系如下。图中名称均为示意:
#mermaid-svg-bYnplV1Zek1r7zTB{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-bYnplV1Zek1r7zTB .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-bYnplV1Zek1r7zTB .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-bYnplV1Zek1r7zTB .error-icon{fill:#552222;}#mermaid-svg-bYnplV1Zek1r7zTB .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-bYnplV1Zek1r7zTB .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-bYnplV1Zek1r7zTB .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-bYnplV1Zek1r7zTB .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-bYnplV1Zek1r7zTB .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-bYnplV1Zek1r7zTB .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-bYnplV1Zek1r7zTB .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-bYnplV1Zek1r7zTB .marker{fill:#333333;stroke:#333333;}#mermaid-svg-bYnplV1Zek1r7zTB .marker.cross{stroke:#333333;}#mermaid-svg-bYnplV1Zek1r7zTB svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-bYnplV1Zek1r7zTB p{margin:0;}#mermaid-svg-bYnplV1Zek1r7zTB .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-bYnplV1Zek1r7zTB .cluster-label text{fill:#333;}#mermaid-svg-bYnplV1Zek1r7zTB .cluster-label span{color:#333;}#mermaid-svg-bYnplV1Zek1r7zTB .cluster-label span p{background-color:transparent;}#mermaid-svg-bYnplV1Zek1r7zTB .label text,#mermaid-svg-bYnplV1Zek1r7zTB span{fill:#333;color:#333;}#mermaid-svg-bYnplV1Zek1r7zTB .node rect,#mermaid-svg-bYnplV1Zek1r7zTB .node circle,#mermaid-svg-bYnplV1Zek1r7zTB .node ellipse,#mermaid-svg-bYnplV1Zek1r7zTB .node polygon,#mermaid-svg-bYnplV1Zek1r7zTB .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-bYnplV1Zek1r7zTB .rough-node .label text,#mermaid-svg-bYnplV1Zek1r7zTB .node .label text,#mermaid-svg-bYnplV1Zek1r7zTB .image-shape .label,#mermaid-svg-bYnplV1Zek1r7zTB .icon-shape .label{text-anchor:middle;}#mermaid-svg-bYnplV1Zek1r7zTB .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-bYnplV1Zek1r7zTB .rough-node .label,#mermaid-svg-bYnplV1Zek1r7zTB .node .label,#mermaid-svg-bYnplV1Zek1r7zTB .image-shape .label,#mermaid-svg-bYnplV1Zek1r7zTB .icon-shape .label{text-align:center;}#mermaid-svg-bYnplV1Zek1r7zTB .node.clickable{cursor:pointer;}#mermaid-svg-bYnplV1Zek1r7zTB .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-bYnplV1Zek1r7zTB .arrowheadPath{fill:#333333;}#mermaid-svg-bYnplV1Zek1r7zTB .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-bYnplV1Zek1r7zTB .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-bYnplV1Zek1r7zTB .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-bYnplV1Zek1r7zTB .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-bYnplV1Zek1r7zTB .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-bYnplV1Zek1r7zTB .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-bYnplV1Zek1r7zTB .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-bYnplV1Zek1r7zTB .cluster text{fill:#333;}#mermaid-svg-bYnplV1Zek1r7zTB .cluster span{color:#333;}#mermaid-svg-bYnplV1Zek1r7zTB div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-bYnplV1Zek1r7zTB .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-bYnplV1Zek1r7zTB rect.text{fill:none;stroke-width:0;}#mermaid-svg-bYnplV1Zek1r7zTB .icon-shape,#mermaid-svg-bYnplV1Zek1r7zTB .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-bYnplV1Zek1r7zTB .icon-shape p,#mermaid-svg-bYnplV1Zek1r7zTB .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-bYnplV1Zek1r7zTB .icon-shape .label rect,#mermaid-svg-bYnplV1Zek1r7zTB .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-bYnplV1Zek1r7zTB .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-bYnplV1Zek1r7zTB .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-bYnplV1Zek1r7zTB :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 数据请求与业务事件
业务结果
关联请求的回包
鸿蒙原有业务路由
ArkTS CMP 容器
HAR 门面与 N-API 桥
Kotlin/Native 共享 UI 与会话
ArkTS 宿主桥
公共 HSP:唯一业务 SDK
当前账号数据库、资源缓存与后端
导航、媒体、收藏面板等宿主能力

其中,CMP UI 运行在 Kotlin/Native 一侧;既有业务 SDK 使用 Kotlin/JS,经 ArkTS 宿主调用。两侧通过受控协议交换数据和事件。

业务状态只有一个持有者

业务 SDK 由公共基础 HSP 统一持有和初始化,其他模块通过它获取接口。CMP Native 会话保存临时界面状态,例如当前词、筛选草稿、滚动位置和等待中的请求;账号、数据库及持久化状态由既有业务层负责。

这样可以避免多个模块各自初始化一份 SDK,导致账号上下文、缓存和类型身份不一致。真实认证凭证、数据库句柄和 Kotlin/JS 对象也不需要直接传入 Native UI。

跨运行时通信需要明确契约

协议至少要能够表达以下信息:

信息 用途
协议版本 判断两侧是否能够理解同一份消息
页面会话标识 确定请求属于哪个页面实例
上下文代次 在账号或业务上下文变化后淘汰旧请求
请求标识 将异步结果交给对应调用方
业务上下文 固定当前词书、数据模式或其他必要范围
操作标识 让同一次写入的重试能够保持幂等
成功、失败与变更结果 决定展示错误、重试、刷新或继续流程

接收回包时,需要检查会话是否仍然有效、账号上下文是否一致,以及请求是否仍在等待。页面已经退出或账号已经切换,旧回包应被丢弃。

对于包含 Kotlin Long 的数据,还要注意 JavaScript 数值精度。本案例让相应 Kotlin JSON 字符串原样经过 ArkTS,再由接收侧解码,避免中途解析为 JavaScript number 后重新序列化。也可以设计明确的字符串 ID 协议,但需要两侧统一约定。

真实数据接入要保留失败语义

词表应从当前账号的业务服务、资源缓存和后端加载,不能用演示数组代替,也不能在收藏服务失败时返回假空集合来制造成功页面。

不同数据可以采用不同加载策略:根词表用于全局筛选和统计,可使用完整的轻量快照;训练中的图片、音频等完整资源按窗口加载和预取。两者分别约束消息大小、缓存规模和请求时间,避免每次切词都跨运行时传输整本资源。

需要修改学习状态的动作,应沿用原业务规则。对需要持久化确认的动作,使用稳定的操作标识,并在业务层确认写入后再推进页面。这里的本地持久化成功与远端同步成功是不同状态,不能混为同一回执。音频训练也不应因为接入了统一会话,就被额外添加原业务没有的成绩提交。

四、从打通渲染到替换正式入口

开始之前,先确认以下条件。它们决定了"能编译"和"能使用"之间还缺少什么:

前置条件 需要确认的内容
共享源码 明确上游来源、自己的适配分支,以及本次实际参与编译的文件
构建环境 JDK、Kotlin/Native、Compose/CPF、DevEco SDK、Node 与 OHPM 使用经过验证的组合
运行基线 宿主最低系统版本满足 HAR 要求,Native 架构覆盖目标设备
宿主业务 SDK 已初始化,账号、数据库、当前业务计划和模块 Context 可用
数据与资源 词书及媒体可以加载,当前依赖的收藏后端可访问
交付与验收 明确配套 HAP/HSP、签名和安装方式,准备可以验证输入、音频与前后台的设备

这些条件满足后,实施可以分为四个连续阶段。

1. 先建立最小构建与渲染链路

先让共享代码通过适配工具链生成目标 Native 库和导出头文件,再由 HAR 封装 N-API 与 ArkTS 入口。这个案例构建了 ARM64 和 x86_64 两种架构产物;实际项目应根据支持设备和测试环境确定目标架构。

这一阶段验证的是加载、显示、生命周期通知和资源是否可达。诊断页应保留明确用途,避免被误当成正式业务已经接入。

2. 整理共享源码与平台实现边界

把可以共享的状态逻辑、规则和组件留在共享层;平台资源和系统能力通过接口、expect/actual 或宿主端口连接。

本案例在独立构建中通过源码目录与显式 include 选入原有文件。它能避免再复制一套页面源码,但也有一个维护陷阱:上游新增文件后,如果没有进入选入清单,构建仍有可能通过。

因此,每次同步都要检查新增、移动和删除的文件,以及原 ViewModel、页面和 Delegate 的行为变化。长期应把稳定的共享边界整理为可复用模块,减少手工选入清单的维护。

3. 接入业务端口与宿主生命周期

宿主先准备业务 SDK、登录态和有效业务上下文,再创建页面业务会话与 Native 控制器。共享页面发起请求,宿主桥调用既有业务用例,将结果送回对应会话。

显示、隐藏、前后台和销毁需要分别处理。以"词表打开训练,再返回词表"为例:父词表被压栈遮挡时保留筛选、滚动和等待中的导航;训练结束后返回确认的变更结果,再刷新和定位。只有真正销毁或上下文失效时,才结束所属会话和请求。

网络请求需要超时,但用户停留在详情页的导航等待不宜直接套用同一网络超时策略。两类等待的结束条件不同,应分别设计。

4. 最后替换原有业务路由的承载页面

正式接入时,让原业务路由打开 CMP 容器,并保留原调用方所依赖的参数和返回语义。这样首页、快捷入口和其他模块能够继续通过同一个路由进入。

验收时应从这些真实入口进入,而不仅是从调试菜单打开新页面。需要确认当前账号、词书、筛选结果、训练起点以及返回定位都沿用了正确上下文。

五、接入过程中遇到的问题与处理

下面按问题介绍现象、定位依据、处理方式及验证边界。其中,源码审查暴露的差异不等同于已经发生的用户故障;已有修复也不等同于最新包已经通过设备验收。

1. HAR 能显示页面,正式入口仍然没有接入业务

早期包能够导航到 CMP 页面,但显示的是标题、路由和接入提示。控制器当时承载的是诊断界面,业务会话、真实题面与正式路由替换还没有完成。

这里需要检查整条调用链:原业务入口实际打开哪个容器,容器创建哪个控制器,控制器装配哪个共享页面,页面从哪里取得数据。只确认 HAR 导出了几个入口,无法回答这些问题。

后续将真实业务会话接入容器,并把原词表路由的承载页面切换为共享词表。早期验证包已经展示过真实列表,但完整列表交互和最新包仍待验收。这也说明迁移进度应分别记录"诊断页可显示""真实页面可用""正式入口已替换"。

2. 共享工程能构建,宿主仍然不能直接接包

早期宿主构建曾被最低 API 要求阻断:宿主声明的兼容基线低于 CMP 依赖要求。处理时对齐了宿主与依赖的运行基线,随后构建通过。这个调整同时改变了最低支持范围,不能只把它视为消除一条编译报错,也不能降低配置后就宣称恢复旧系统兼容。

另一个阻碍来自业务 SDK。直接使用共享上游生成的业务整包时,完整应用编译暴露了旧鸿蒙业务仍需要的接口缺口,涉及既有登录、支付及内容管理等能力;部分 Kotlin/JS 导出类型也不同。

因此保留鸿蒙业务兼容基线,向其中同步训练和词表需要的模型、用例与接口,CMP UI 则继续从共享源码构建。这个选择解决了当前整包兼容问题,同时留下了必须持续管理的双基线成本。以后每次上游修改业务接口,都需要重新审查这个边界。

3. 类型名称相同,instanceof 却返回失败

早期联调出现过这样的现象:业务 SDK 已回传首页状态,但宿主对状态类型的判断不成立。数据本身已经返回,界面却没有进入预期分支。

原因是业务 HAR 同时进入了公共 HSP 和其他模块。运行时使用了不同模块中的类型实现,即使类名相同,也不保证构造器身份相同。与此同时,只覆盖安装入口 HAP,会让设备上已有的旧业务 HSP 继续参与运行。

修复包含两个相互关联的部分:业务 HAR 只由公共基础 HSP 持有,其他模块统一从它导入 SDK 与类型;构建和安装则使用同一批次的配套模块。随后核对最终模块中的业务实现归属,并在早期设备联调中确认首页恢复了真实业务状态。

遇到"升级后没有变化"或"字段看起来正确但类型判断失败",应优先检查依赖归属和设备实际安装的模块组合。修改 UI 判断之前,先确认参与运行的确实是同一份业务实现。

4. 首次答题停在结果待确认,根因在数据库事务

一次早期真机联调中,真实题目已经显示,首次答对后页面却停在结果确认阶段。追踪发现,评分写入后紧接着查询结果,返回值为空。

当时的鸿蒙数据库适配把事务写入和普通查询混在一起使用,读操作没有保证处于同一个事务连接内,因而读不到尚未提交的数据。共享业务用例虽然没有变,底层驱动却没有提供它依赖的读写一致性。

修复后,通过 createTransaction 取得事务对象,事务内的查询、写入、提交和回滚都使用同一对象。嵌套业务事务复用这条物理事务,事务外访问等待它结束。数据库 Promise 一旦开始执行,并不会随上层协程取消而自动停止,因此还要等实际操作结束后再释放事务锁,结果集也要在映射结束后关闭。

对于一次答题写入,预期顺序可以概括为:

text 复制代码
绑定当前账号与操作标识
  → 在同一事务内读取、评分并写入业务记录
  → 保存操作回执并提交
  → 将确认结果返回页面
  → 页面进入下一题

页面等待真实确认的约束予以保留;失败时进入可恢复状态,同一次重试沿用操作标识。不能通过提前切到下一题来掩盖数据库问题。

已有业务层 SQLite 回归覆盖原子写入、回滚和重复回执等规则;鸿蒙事务驱动修复后,早期真机也完成过真实答题保存并进入下一题。两类证据覆盖的层次不同,尚不能据此宣布所有设备和所有提交分支均已验证。

5. 拼写键盘需要适配布局归属和焦点时序

适配检查发现,宿主键盘避让与 Compose 的 IME 布局存在叠加处理。另一个时序问题是:弹层刚关闭或应用刚回到前台,Native 输入连接可能还没有建立,单次请求显示键盘不一定能生效。每次文本变化都重新请求焦点,也会干扰正常输入。

当前处理方式是让拼写页可见期间由 Compose 消费键盘高度,宿主暂时关闭该页的额外避让,并在隐藏、退出或初始化失败时恢复原设置。焦点恢复跟随题目阶段、词条和前台恢复代次;等待提交或资源时保留输入节点,弹层卸载后再进行有时限的焦点恢复。

这些修改已有状态回归覆盖,但系统输入法、第三方候选栏、连续切词、硬键盘和触摸仍待真机确认。英文键盘提示也不能被理解为可以完全控制所有第三方输入法行为。坐标方面还要核对逻辑单位、物理像素和密度换算,目前不应把待测项写成已经确认发生的点击偏移故障。

6. 页面退出后,异步工作不一定已经结束

生命周期审查和回归重点覆盖了几种交错情况:播放器尚在创建时退出页面、文件仍在打开时切走、收藏查询返回前销毁详情页,以及旧账号写入完成时已经切换账号。

这些问题具有共同根因:取消页面等待,并不一定取消底层 Promise 或已经开始的业务操作。如果回调只根据"请求成功"继续执行,就可能启动迟到的播放、更新已关闭页面,或触发新账号的错误刷新。

当前在创建请求时绑定会话与上下文代次,异步返回后再次检查;迟到的播放器和文件句柄需要释放,已失效结果不能继续驱动界面或新账号业务。对于已经提交的持久化动作,不能把页面消失解释为写入自动撤销,应保留原账号的操作回执,并阻止它跨入新上下文。

同时,词表被子页面遮挡时仍然属于有效导航栈,不能按销毁处理。父页面保留状态,真正退出或账号变化才使所属会话失效。这些交错已有针对性自动回归,持续后台运行和系统媒体表现仍需要设备验证。

7. 共用了题面,返回、试听和系统播控仍可能不同

源码对照发现,速刷的旧适配没有在普通返回时记录当前已浏览单词。原因是共享 Content 已经复用,但 Native 会话重新承担了退出编排,遗漏了原 ViewModel 的一条结束路径。原适配测试还曾把这一差异当成预期行为。

修复后,普通返回沿用原记录规则,等待确认后退出;重复返回不重复写入,结果未知时保留同一操作重试,尚未加载词条时不虚构记录。这个案例提醒我们:测试预期也要以原业务语义为依据,不能只验证新实现自洽。

听写的媒体适配也发现了类似遗漏。旧代码将当前单词放入系统媒体标题,会让系统播控显示训练中本应隐藏的答案;切换口音则没有完整沿用立即试听的规则。这是源码审查发现的行为问题,并非已经记录到的用户锁屏事故。

当前系统媒体标题已改为与答案无关的状态文案;口音保存成功后停止旧播放并试听新口音,试听不消耗训练次数,失败或页面隐藏后的迟到结果不触发试听。相关逻辑已有回归,最新包的实际锁屏界面和音频表现仍待验收。

仍未补齐的音频差异也需要单独列出:速听的无音频遍历范围、分页失败重试及中断恢复,以及听写的手动首尾循环、当前列表项恢复和准备中反馈。尤其要区分手动操作与自动播放:补手动循环,不代表可以改变自动播放结束后的停止规则。

8. 真实数据与详情导航接通了,离线和返回语义仍有缺口

当前收藏数据来自真实后端,查询绑定本次会话账号,并保留并发限制及可重试错误。由于这条路径与所核对 iOS 的宿主插件快照不同,即使词书与学习状态已有本地缓存,断网时仍可能因收藏查询失败而影响词表加载或修改。这是当前依赖关系带来的限制,不能包装成已经复现过的故障。

用空收藏兜底会把"查询失败"错误地解释为"用户没有收藏"。目前继续保留真实失败语义;后续若补离线能力,需要接入正式收藏缓存与同步体系,明确账号隔离、缓存就绪和失效条件。

词典详情则是另一种边界:当前能够打开鸿蒙已有详情页,但共享词典中的部分学习状态操作、会员内容及复杂词条尚未迁入,操作后回题或进入下一词的返回语义也不完整。传递了一个能力参数,不代表目标页已经消费它并实现对应功能。

因此,"数据真实""页面能打开""业务行为一致""离线可用"应作为不同验收项。完整共享词典和收藏离线能力仍是后续工作,不能由已经打通的入口推导为完成。

9. 上游已经合并,新功能却没有进入鸿蒙构建

这里暴露的是源码选入机制的盲区。独立适配构建使用显式 include,上游新增文件后,即使 Git 已经合并成功,也可能完全没有参与鸿蒙编译。没有编译到的代码自然不会产生错误,因此"编译通过"不足以证明新功能进入了产物。

本案例的共享词典就是明确边界:源码已经随上游进入仓库,但未被当前适配构建选入,运行时仍使用宿主详情。与此同时,上游原 ViewModel 有改动时,即使题面继续编译,也需要检查 Native 编排是否遗漏新的状态转换。

为此增加来源检查,同时比较上游功能目录、实际选入文件、依赖资源和兼容 SDK,并检查上游是否已经进入当前提交历史。相关回归专门覆盖"上游新增文件不在现有 include 中"的情况;已知未接入的功能也持续保留为限制项。

更新工具在 Git 合并完成后,仍可能因发现未审查来源变化而返回非零。这时需要查看差异并继续适配,不能把它直接理解为 Git 合并失败,也不能只刷新来源锁让检查变绿。

10. Native 哈希不一致,原因是打包裁剪了符号

加入产物来源校验后,曾出现输入 Native 库与 HAR 内同名库的 SHA-256 不一致。最初按字节相同进行比较,校验因此失败。

排查确认,打包工具会裁剪调试符号,文件字节发生变化,但不意味着引用了另一版链接产物。修正校验后,分别记录输入库哈希与最终库哈希,并通过当前工具链保留的 GNU build-id 核对链接产物身份。重新打包后的来源与结构检查通过。

这里没有取消校验,而是让不同阶段使用对应的比较方式:裁剪前后的库核对链接身份;最终 HAR、依赖解析目录和 HAP 中应保持一致的文件,再逐项检查字节。build-id 也不能代替整个安装产物的完整性记录。

资源检查需要沿用同样的完整路径:共享源资源经过生成、转换和打包,再进入运行包。字体文件存在还要验证音标与字重,动画文件存在还要验证可见性和销毁;HSP 私有资源则需要正确的模块 Context。结构检查能发现漏包,实际加载与显示仍由设备验收确认。

六、只维护自己的鸿蒙分支,怎样跟随上游更新

共享业务上游继续由 iOS、Android 团队维护,鸿蒙在自己的下游分支保留适配。更新路径是:拉取上游、合入下游、审查变化、补齐适配、重新构建产物。

下面的分支名都是示例。命令应在共享业务仓库执行;假设下游分支已经创建,本地改动也已妥善提交:

bash 复制代码
git status --short
git switch feature/harmony-adapter

git fetch --no-tags origin \
  refs/heads/feature/shared-ui:refs/remotes/origin/feature/shared-ui

git merge --no-edit --no-overwrite-ignore origin/feature/shared-ui

发生冲突时,按共享业务语义和鸿蒙平台边界处理,并在自己的分支完成合并。原业务源码更新进入共享仓库后,仍需要编译成 HAR,由鸿蒙工程更新依赖并重新打包,手机中的旧包不会随着 Git 拉取自动改变。

本案例进一步封装了同步和来源检查:检查当前分支及工作区、记录合并前位置、拉取合并,并对比来源锁。来源锁记录上游提交、共享功能目录、实际选入文件、资源和兼容 SDK 的源码指纹。

检查发现变化时,应先审查和适配,再更新来源锁。锁文件描述的是被接受的源码组合,不能代替编译、业务回归或设备验收。尤其是已知尚未接入的功能,不能因为更新了锁文件就改成"已完成"。

涉及业务接口或数据库的变化,还要同步审查兼容 SDK。当前流程仍保留这一步,并未实现任意上游更新后自动生成全功能可用的鸿蒙版本。

七、构建、包管理和版本追踪

本案例按以下顺序交付:

  1. 确认共享源码与兼容 SDK 的组合,执行来源检查和相关回归。
  2. 如果业务 SDK 有变化,先构建相应业务 HAR。
  3. 构建 CMP Native 库,发布导出头文件及编译资源,再生成 CMP HAR。
  4. 使用明确的产物版本更新鸿蒙依赖声明,并通过 OHPM 更新锁文件。
  5. 构建入口 HAP 与相关 HSP,核对实际包内容,再进行配套安装和设备验证。

日常维护可以按改动位置确定重建范围:

改动位置 通常需要更新的产物 重点回归
仅鸿蒙路由或 ArkTS 宿主实现 受影响的宿主模块及其配套安装集合 参数、返回、前后台与桥接调用
共享题面、状态或 Native 会话 CMP HAR 与宿主安装包 原业务行为、目标架构、资源和真机交互
业务用例、查询、数据库适配 业务 HAR 与宿主安装包;契约受影响时同时更新 CMP HAR 原子写入、迁移、账号与旧业务兼容
跨运行时协议 协议两侧及受影响的 HAR、宿主模块 版本兼容、错误语义、回包和旧会话失效
工具链或共享 UI 资源 受影响的 Native/HAR 与宿主产物 最低运行基线、ABI、资源及完整包组成

因此,只修改宿主代码时,不必无条件重编全部 Native 代码。也不要直接修改依赖缓存中的生成文件,否则重新安装依赖就可能丢失改动。

发布记录应能够回答:这个包来自哪些源码、使用什么工具链、包含哪些架构、搭配哪个业务包,以及通过了哪些验证。源码仍有未提交改动时,单独记录分支名和提交号不足以复现产物,需要先保存对应源码。

发布记录还应保存输入与最终产物的对应关系,包括前文说明的 Native 裁剪前后身份、最终 HAR 哈希,以及最终安装包内 Native 库与资源的核对结果。

八、验证结果应怎样表述

验证需要分层进行,因为每一层回答的问题不同:

验证层 能确认什么
来源与选入检查 是否接受了正确上游,是否出现未审查的文件、资源或兼容 SDK 变化
共享状态与业务回归 筛选、判定、提交、重试和恢复等逻辑是否符合预期
跨运行时协议回归 回包匹配、重复操作、账号切换、销毁与迟到结果是否正确处理
原共享工程编译与回归 下游调整是否影响原共享源码的编译及对应测试
Native、HAR 与宿主构建 目标代码、资源和依赖是否能够组成安装产物
真机业务验收 正式入口、输入法、触摸、媒体、后台及异常恢复是否实际可用

本文案例已经完成源码合并、相关自动回归、Native 与宿主构建,并在早期验证包中显示过真实词表。最新构建包仍待安装验收,不能沿用早期设备记录宣布新版本已经通过完整测试。

功能接入也仍处于阶段性状态:

功能范围 当前接入情况
根词表 正式入口已经使用共享页面和状态容器,完整交互仍待验收
选义与速刷 主体和真实业务已经接入,词典内部分操作及返回联动尚未对齐
速听 主体已经接入,无音频处理、弱网及播放恢复仍有差距
拼写 主体及键盘适配已有,输入法、焦点和触摸仍待真机确认
听写 主体已经接入,手动首尾循环、列表恢复及加载反馈仍需补齐
完整共享词典 尚未接入,当前由鸿蒙已有详情页承载

九、这套架构带来的收益与维护成本

与所核对的原 iOS 接法相比,鸿蒙沿用了已有业务 SDK,并通过额外的运行时边界接入共享 UI。根词表能够集中维护筛选、排序和展示逻辑,多个训练页面也能继续复用原题面和规则;同时,平台桥接、包组合和兼容业务基线成为新的维护工作。

方面 收益 需要持续承担的工作
共享代码 页面和规则修改能够随上游合并进入下游 审查原状态编排与 Native 会话是否同步
原有业务 保留鸿蒙账号、数据库和既有业务调用 管理兼容 SDK 与共享业务的接口差异
系统能力 继续使用现有导航、媒体和基础服务 处理跨运行时异步、生命周期及错误边界
发布 两类 HAR 的职责和版本可以分别追踪 核对工具链、资源、实际依赖与安装模块组合

目前没有建立可比的性能基线,因此不能据这套结构推断鸿蒙比 iOS 更快或更省内存。首开、长列表、桥接流量、峰值内存和后台行为都需要测量。

后续工程工作将优先补齐完整共享词典和已确认的训练差距,再推进共享模块边界、兼容业务基线合流及组合验证。每当一项逻辑真正回到共享状态机中,下一次上游更新就能少维护一处平台流程副本。

十、后续维护时如何快速定位问题

出现异常时,先确认运行产物和源码是否对应,再沿调用链定位。下面的表格用于缩小排查范围,其中部分条目是前述问题的排查经验,部分是后续回归路径,并不表示每种现象都已经发生过。

观察到的现象 优先核对 可以帮助定位的证据
更新后仍是旧页面或旧行为 正式路由、所用 HAR、设备 HAP/HSP 组合 路由实际目标、包版本、哈希和安装回执
有回包但界面没有进入正确状态 SDK 是否重复打包,类型从哪里导入 最终模块实现归属与回包类型身份
首次写入卡住,或重试结果异常 事务内读写路径、操作标识与确认结果 事务结束状态、对应操作回执和失败分支
返回后丢失位置或出现旧状态 父会话是否被误销毁,是否接收了迟到回包 会话生命周期、上下文代次和返回结果
键盘反复弹出、布局移动异常 避让由谁负责,焦点何时恢复 页面阶段、输入节点与弹层/前后台顺序
播放次数或恢复位置与原端不同 原播放策略、Native 编排和中断原因 同一词序下的动作与状态转换记录
有本地词书却加载失败 收藏等关联数据是否仍依赖网络 失败发生的数据源、缓存就绪状态及错误类型
上游代码已更新但新功能未出现 文件是否被选入,状态编排是否同步 来源差异、编译选入报告与行为对照
Native 校验失败或资源未显示 当前处于哪个打包阶段,资源是否进入正确模块 链接身份、最终库哈希和编译资源清单

排查记录保留足够关联一次操作的最小信息即可。对外分享时将会话和请求标识映射为示例值,不附账号凭证、原始业务响应或个人设备日志。每次修复也应记录"原行为、鸿蒙差异、处理方式、验证层次",使下一次上游同步能继续沿用这份对照。

相关推荐
星栖与芯2 小时前
LiteOS-M 切换汇编逐行图解(1):汇编是什么·寄存器与栈
汇编·stm32·嵌入式硬件·harmonyos·鸿蒙系统
HwJack202 小时前
【HarmonyOS开发小实践】ArkUI 应用级状态AppStorage 与跨页面共享、持久化
ui·华为·性能优化·harmonyos
小玮看世界3 小时前
鸿蒙录音误删之痛:最近删除清空后为何难以恢复,以及产品该怎样兜底
鸿蒙
万物智能信息科技6 小时前
板载按键key的ADC转换和信号控制—【万物智能之开源鸿蒙OpenHarmony系统实战开发系列教程】
嵌入式硬件·华为·开源·harmonyos·鸿蒙
威哥爱编程7 小时前
HarmonyOS 6.1 端侧 3DGS 重建实战:重建在 C 层,ArkTS 只管"看"和"改"
华为·harmonyos·arkts
威哥爱编程7 小时前
HarmonyOS 6.1 沉浸光感实战:接口路径选错,代码不报错、页面没效果
harmonyos
贾伟康8 小时前
【HarmonyOS 7新能力|014】冷启网络预建链入门实战:从能力边界到最小可运行链路
harmonyos·arkts·启动优化·网络优化·harmonyos 7
Sunny_G8 小时前
CodeMirror 6 代码块渲染踩坑:一个空行毒死全文档(鸿蒙编辑器卡片化/折叠/点击进源码)
ai编程·harmonyos
不羁的木木8 小时前
给鸿蒙 App 增加广播收发能力 —— flutter_broadcasts 的鸿蒙使用指南
flutter·harmonyos