开源鸿蒙平台 CMP 三方库「Rebugger」适配全流程
本文记录
Rebugger接入 OpenHarmony 的完整过程,覆盖重组原因调试库的选型分析、引擎语义提取、真实引擎编译、ohosArm64/ohosX64目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。
Rebugger是基于 Compose Multiplatform 的重组原因调试库:把想追踪的参数塞进trackMap,每次重组时逐 key 比对,在 Logcat 里直接打印「为什么重组」——值变了、对象换了、还是原因不明。本次适配与 KStore 的「真编译整库」路线有一个关键差异:上游唯一的公共 API 是@Composable Rebugger(trackMap = ...),它依赖androidx.compose.runtime,而社区 Compose 线与上游 Compose 版本对不齐。因此本仓走 cmp_qrose 已过审的「引擎提取」路线——把重组原因引擎(逐 key 比对、值变更 / 实例变更 / 未知原因三种判定、基线刷新、flag 一次性吞噬)逐行提取到commonMain原样编译成 OpenHarmony KLIB,@Composable壳保留在composeMain源码集不参与 ohos 编译,ArkUI 页面作为验收宿主,通过@CNameC ABI + CMake N-API 调用同一个引擎。
项目地址: AtomGit/oh-tpc/rebugger.git
上游仓库: theapache64/rebugger
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配
Rebugger 为 Compose 应用回答一个所有 Compose 开发者都问过的问题:「这个 Composable 为什么又重组了?」。社区有 RecompositionHighlighter、LogComposition、Layout Inspector 这些计数工具,但没有一个能直接给出原因。Rebugger 的用法是把想追踪的参数塞进 trackMap:
@Composable
fun VehicleUi(car: Car, bike: Bike) {
var human by remember { mutableStateOf(Human("John")) }
Rebugger(trackMap = mapOf("car" to car, "bike" to bike, "human" to human))
}
之后每次重组,Logcat 就会出现:
🐞 Rebugger activated on `VehicleUi`
🐞VehicleUi recomposed because
`car` changed from `Car(name=Fit)` to `Car(name=Pilot)`,
`human` instance changed, but content remains the same -> `Human(name=John)`
🐞VehicleUi recomposed, but reason is unknown. Are you sure you added all params to `trackMap`? 🤔
OpenHarmony 应用同样需要这套「重组归因」能力——CMP 应用迁移鸿蒙后,列表页反复刷、详情页无谓重绘,第一件事就是分清「值变了」「对象换了」「都不是」。但上游构建(Kotlin 2.2.10 / Compose 1.9.0-beta03 / Gradle 9.0.0 / AGP 9.0.0-alpha01)不会生成 OpenHarmony 变体。
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | 必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。 |
| 工具链不一致 | 上游 Gradle 9.0.0 / AGP 9.0.0-alpha01 与鸿蒙工具链无法共存于一个 Gradle 构建;旧钉法 Kotlin 2.2.21-1.0.0 必须配 Gradle 8.14.1。 |
| Compose 依赖 | 上游核心逻辑内嵌在 @Composable 里,依赖 androidx.compose.runtime(Composable / remember / LaunchedEffect / SideEffect);社区 Compose 线与上游 1.9.0-beta03 对不齐,强行对齐等于再打一套 Compose 发行版。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。 |
| 身份语义跨不了桥 | 上游的 !==(内容相同但实例不同)依赖 Kotlin 对象身份,JSON 序列化后身份消失,需要在桥上重建语义。 |
| 交付链路复杂 | .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在:重组原因引擎的 ohos 编译、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。判定语义完全由提取自上游的引擎负责,ArkTS 只负责调用 Native、解析 JSON 和展示结果。
1.2 库提供的能力
Demo 提供一个真实的重组归因会话,对应桥的全部入口:
- 版本握手(version):
rebuggerVersion()返回 fork 版本号,来自真实 Native 调用而非静态文字; - 值变更(reasons·A):
car 值改变等按钮改变 trackMap 中的值,引擎产出`car` changed from `Car(name=Fit, speed=10)` to `Car(name=Fit, speed=20)`,,基线刷新后同值不再重复报; - 实例重建(reasons·B):
car 实例重建按钮保持内容不变但声明fresh,引擎产出`car` instance changed, but content remains the same -> `Car(...)`,只报一次; - 未知原因(unknown):
无变化重组按钮不改任何值,引擎产出reason is unknown … trackMap? 🤔; - flag 吞噬(silent):有原因的重组之后紧跟一次无变化重组,被上游 flag 语义吞掉(type=silent,无日志行),再点一次才出现 unknown;
- 会话重置(reset):
重置会话丢弃引擎状态,下一次 track 重新握手(等价重新进入 Composable)。
响应包含:
| 字段 | 作用 |
|---|---|
ok | 操作是否成功,失败时 error 带原文。 |
type | activated / reasons / unknown / silent 四态,对应引擎 TrackType。 |
count | 会话内第几次 track(上游 SideEffect 计数)。 |
tag | 回显 RebuggerConfig.tag(默认 Rebugger,上游同款可配置项)。 |
composableName | 回显被追踪的 Composable 名(上游 composableName 参数)。 |
message | 与 Android Logcat 逐字一致的日志行;silent 时为 null。 |
reasons | 逐 key 的 reason 原文数组,便于 UI 分行渲染。 |
引擎侧完整能力(三种判定、基线按 reason 刷新、中途加 key 静默入基线、flag 一次性吞噬、RebuggerConfig 配置)全部在 commonMain 里随编译进入 .so,JVM 侧保留 10 项 jvmTest 交叉验证用例,证明 ohos .so 里执行的判定逻辑与上游没有漂移。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | 判定语义从上游 @Composable 逐行提取为 RebuggerTracker(commonMain),不重写、不翻译;@Composable 壳改为委托引擎留档;全仓新增 Kotlin 源文件仅 3 个。 |
| 平台目标 | 根构建直接声明 ohosArm64 + ohosX64,保留 jvm 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。 |
| 构建隔离 | 根构建替换为极简 ohos 构建(cmp_qrose 模式);上游 composeApp / iosApp 示例与 AGP / 发布插件全部留盘不进构建。 |
| 桥接稳定 | 使用 2 个 C ABI 和 UTF-8 JSON,不传递 Kotlin 对象地址;!== 身份语义用 Box 显式建模;异常永远不跨 FFI。 |
| 依赖纪律 | 库模块 commonMain 零第三方依赖(连 Compose 也剥离),ohos 编译器无需解析任何 androidx 制品;Kotlin 钉 2.2.21-1.0.0(cmp_qrose / guidekit / MaterialThemePrefs 已审核线)。 |
| UI 完整 | 单页覆盖全部桥接入口:六按钮触发四种 track 结果、最近结果卡、带时间戳日志(上限 100 条)、版本握手。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。
Rebugger的价值在引擎而非渲染面,适配面天然落在「判定语义跨平台不漂移」上;ArkTS Demo 是把桥暴露的归因闭环画出来。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点模块与依赖边界、确认 Compose 隔离决策、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版、wrapper 降 8.14.1
第 3 阶段:桥协议建模 ── trackMap 载荷、四态结果契约、Box 身份建模、错误通道
第 4 阶段:原生桥接 ── @CName C ABI、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主 ── 版本握手、六按钮触发器、结果卡、时间戳日志
第 6 阶段:示例与验证 ── HAP、签名、设备安装、真机运行和效果图
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
rebugger/ 上游库模块(进 ohos 构建,jvm + ohosArm64 + ohosX64)
src/commonMain/…/RebuggerConfig.kt 上游配置对象 + defaultLogger expect(原样编译)
src/commonMain/…/RebuggerTracker.kt 新增:提取的重组原因引擎 + RebuggerMessages 消息格式
src/commonTest/…/RebuggerTrackerTest.kt 新增:10 项用例钉死引擎语义(JVM 跑)
src/jvmMain/…/RebuggerConfigImpl.kt 上游 actual(原样编译)
src/ohosMain/…/RebuggerConfigImpl.kt 新增:defaultLogger actual(println,同 jvm/ios)
src/composeMain/…/Rebugger.kt 上游 @Composable,改为委托引擎,ohos 不编
src/androidMain|iosMain|jsMain|wasmJsMain 留盘不进构建(上游平台 actual)
example/nativeApp/ Kotlin/Native 动态库(Bridge.kt + MiniJson)
example/harmonyApp/ DevEco Stage、CMake N-API 和 ArkTS 页面
composeApp/ iosApp/ 上游 CMP 示例,留盘不进 settings.gradle.kts
README.OpenHarmony.md / _CN.md 鸿蒙适配说明(中英双份)
README.upstream.md 上游原文保存
gradle/libs.versions.toml.upstream 上游版本目录保存
构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :rebugger 与 :example:nativeApp,rootProject.name = "ohos_rebugger"。上游的 AGP classpath、com.vanniktech.maven.publish 发布插件、org.jetbrains.compose / kotlin.plugin.compose 编译插件、GitHub Actions 发布流全部随根构建替换剥离——本 fork 不发 Maven Central。
1.2 工具链矩阵
| 项目 | 配置 |
|---|---|
| Kotlin Multiplatform | HarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven) |
| JDK | 21(org.gradle.java.home 指向本机 Temurin 21) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm 保留作交叉验证) |
| Gradle | 8.14.1(wrapper 由上游 9.0.0 降级;2.2.21 旧线必须配 8.x) |
| Compose | 不参与 ohos 编译(隔离决策见决策 1) |
| DevEco product | 5.1.0(18) 兼容 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.rebugger |
export JAVA_HOME="/path/to/jdk-21" # gradlew.bat 启动需要;org.gradle.java.home 只管 daemon
java -version
1.3 源码依赖度盘点
| 模块 | 文件 | 依赖形态 | 处置 |
|---|---|---|---|
rebugger | RebuggerConfig.kt | 纯 Kotlin | 原样编译(jvm + 双 ohos) |
rebugger | Rebugger.kt 的比对逻辑 | 纯 Kotlin,但内嵌在 @Composable 里 | 提取为 RebuggerTracker(唯一一次「翻译」,语义逐行映射,见第 4 阶段) |
rebugger | Rebugger.kt 的 Compose 壳 | Composable / LaunchedEffect / SideEffect / remember / mutableStateOf | 移入 composeMain,ohos 不编,改为委托引擎 |
rebugger | findComposableName expect + 各平台 actual | 栈帧反射是 JVM/JS 实现细节 | expect 随 Composable 移入 composeMain;jvm 的 actual 移出编译集;其余平台目录留盘 |
rebugger | defaultLogger expect | 各平台一行实现 | commonMain 保留 expect,新增 ohosMain actual(println,与上游 jvm/ios 同款) |
expect 声明全库只有 2 处:defaultLogger(跟随配置对象留在编译集)和 findComposableName(跟随 Composable 壳出编译集)。ohos 只补前者一个 actual——这就是整个 fork 在库模块的全部新增源码(引擎除外)。
1.4 页面边界
标题区 REBUGGER · OPENHARMONY / 版本握手行(native version: 1.0.1-ohos.1)
状态区 composableName 输入框(默认 VehicleUi)+ car/bike/human 三个被追踪参数的当前值
操作区 六按钮:car 值改变 / car 实例重建 / human 值改变 / bike 值改变 / 无变化重组 / 重置会话
结果区 最近一次结果(type · render #N · tag + message 全文)
日志区 带时间戳的归因日志(最新在前,上限 100 条,与 Android Logcat 输出逐字一致)
关于区 版本握手、库简介、架构分层、仓库与社区链接
页面启动时先调 rebuggerVersion() 做握手;每个按钮触发一次 rebuggerTrack(一次点击 = 一次 recomposition)。
第 2 阶段:目标与依赖打通
2.1 制品线验证(本适配的成败手)
与 KStore 先查 kotlinx 制品不同,rebugger 适配要先验证 Compose 制品线的可达性。用 .module 元数据核查社区 Maven(https://maven.eazytec-cloud.com):
org.jetbrains.compose.runtime:runtime:1.9.2-OH.0.1.2-32
variants: ohosArm64ApiElements-published / ohosX64ApiElements-published(klib)
org.jetbrains.kotlin.plugin.compose:2.2.21-1.0.0
✅ 存在(与 HarmonyOS Kotlin 同线)
结论:ohos 的 compose.runtime 制品存在,但它属于社区 1.9.2-OH 线;上游是 Compose 1.9.0-beta03 + Kotlin 2.2.10,commonMain 若保留 implementation(compose.runtime),ohos 编译就要按社区线解析整套 Compose——版本主体虽近,但 @Composable 壳之外的一切(构建插件、编译器插件、发布配置)都会被拖进对齐成本。对照 cmp_qrose 已过审的处置(Painter 隔离进 composeMain),本仓选择零依赖路线:把引擎提取到不依赖 Compose 的 commonMain,ohosArm64() / ohosX64() 编译时不需要解析任何 androidx / org.jetbrains.compose 制品。这不是能力妥协——rebugger 的全部判定语义本来就不碰 Compose API,Compose 只是它的宿主。
2.2 工具链钉版(为什么是 2.2.21-1.0.0 + Gradle 8.14.1)
社区已批准的钉法是 HarmonyOS Kotlin 2.2.21-1.0.0(cmp_qrose / guidekit / MaterialThemePrefs 用的)。rebugger 上游是 Kotlin 2.2.10,语言层面同代,源码级兼容成本为零。但上游 wrapper 是 Gradle 9.0.0、AGP 是 9.0.0-alpha01:
- Kotlin 2.2.21 旧线不支持 Gradle 9.x,必须降 8.14.1(cmp_qrose 同款 wrapper);
- AGP 9.0.0-alpha01 要 Gradle 9——而本 fork 的根构建已把 Android 目标整体剥离(
composeApp留盘),AGP 没有存在必要,随根构建替换一并去掉。
settings.gradle.kts 把 eazytec 仓库写在最前:
pluginManagement {
repositories {
maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
maven("https://maven.eazytec-cloud.com/nexus/content/groups/harmonyos")
gradlePluginPortal()
google()
mavenCentral()
}
}
rootProject.name = "ohos_rebugger"
include(":rebugger")
include(":example:nativeApp")
2.3 编译真实引擎
rebugger/build.gradle.kts 收敛为纯 KMP 模块:
plugins {
alias(libs.plugins.kotlin.multiplatform)
}
kotlin {
applyDefaultHierarchyTemplate()
jvmToolchain(21)
jvm() // 交叉验证用
ohosArm64()
ohosX64()
sourceSets {
commonMain.dependencies {} // 零依赖:引擎不碰 Compose
commonTest.dependencies {
implementation(libs.kotlin.test)
}
}
}
不 apply org.jetbrains.compose 与 kotlin.plugin.compose——commonMain 已无 Compose 依赖,ohos 编译器无需解析任何 androidx 制品。实测:
./gradlew :rebugger:compileKotlinJvm :rebugger:compileKotlinOhosArm64 :rebugger:compileKotlinOhosX64
# BUILD SUCCESSFUL × 3
引擎与配置对象的全部 commonMain 文件只有 RebuggerTracker.kt 一个是新的(提取产物),ohosMain 的 defaultLogger actual 是新的,其余原样编译。
2.4 测试的处置:上游没有测试,语义靠新钉
与 KStore「上游 51 项测试零改动」不同,rebugger 上游没有 commonTest——判定语义全部内嵌在 @Composable 里,无法脱离 Compose 宿主测试。引擎提取后恰好补上了这块:commonTest/RebuggerTrackerTest.kt 新增 10 项用例,把三种日志字符串逐字钉死(与 Android Logcat 同款)。ohos 侧不跑测试,根构建全局禁用:
subprojects {
tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
.configureEach { enabled = false }
}
这 10 项 jvmTest 就是「提取没有引入漂移」的闸门——ohos .so 里执行的是同一份通过测试的 commonMain 代码。
第 3 阶段:桥协议建模
3.1 JSON 契约
com.theapache64.rebugger/**(提取引擎 + RebuggerConfig,commonMain 编译)
↓ RebuggerTracker.track():比对 / 判定 / 基线刷新
Bridge.kt(@CName × 2,扁平 JSON 进出)
↓ UTF-8 JSON
N-API rebuggerVersion() / rebuggerTrack()
↓
ArkUI Index.ets(单页滚动)
请求与响应:
请求 : {"session":"demo","composableName":"VehicleUi","reset":false,
"trackMap":{"car":"Car(name=Fit, speed=10)","human":"Human(name=John)"},
"fresh":["car"]}
响应 : {"ok":true,"type":"reasons","count":2,"tag":"Rebugger",
"composableName":"VehicleUi","message":"🐞…","reasons":["`car` changed …"]}
{"ok":false,"error":"..."}
一个对称设计:trackMap 的值是各参数的字符串表示(等价上游 $oldArg / $newArg 插值时的 toString()),响应的 message 是引擎拼好的完整日志行、reasons 是逐 key 数组——ArkTS 侧一次 JSON.parse 既可整行展示(与 Logcat 逐字一致)也可分行渲染,两条路都不做二次拼接。
3.2 载荷语义:!== 在桥上怎么活
上游的判定核心是两段比较:
val reason = when {
oldArg != newArg -> "`$key` changed from `$oldArg` to `$newArg`, "
oldArg !== newArg -> "`$key` instance changed, but content remains the same -> `$oldArg`"
else -> null
}
!= 好办——字符串内容比较即可。!== 是 Kotlin 对象身份,JSON 序列化后身份必然丢失。桥用 Box 重建语义:
private class Box(val text: String) {
override fun equals(other: Any?): Boolean = other is Box && text == other.text
override fun toString(): String = text
}
for ((key, text) in requested) {
val current = session.boxes[key]
val box = when {
current == null -> Box(text) // 首见:静默入基线
current.text == text && key in fresh -> Box(text) // fresh:内容同、身份新 → reason B
current.text == text -> current // 未变:重发同一实例 → 无 reason
else -> Box(text) // 值变:新内容 → reason A
}
session.boxes[key] = box
trackMap[key] = box
}
三个分支与上游 remember 槽位的语义一一对应:值未变且未声明 fresh 时重发同一个 Box 实例(!= / !== 均不触发);声明 fresh 时发内容相等的新 Box(!== 触发,reason B);值改变时发新内容 Box(!= 触发,reason A,基线刷新)。桥侧 Session 同时持有 RebuggerTracker 与 boxes 快照,两者基线严格同步。
会话按 session key 隔离——对应上游「一个 Composable 位置一个 remember { RebuggerTracker() } 实例」;reset:true 丢弃会话,等价重新进入 Composable。
3.3 错误通道
每个导出整体 try/catch,Kotlin 异常永远不跨 FFI:
@CName("RebuggerTrack")
fun rebuggerTrack(request: String): String = try {
/* 解析请求 → 更新 Box → tracker.track → 拼 JSON */
} catch (t: Throwable) {
"""{"ok":false,"error":${jsonString(t.message ?: t.toString())}}"""
}
非法 JSON、请求不是对象、trackMap 不是对象,全部走同一个 ok:false 通道,ArkTS 侧统一渲染。
第 4 阶段:原生桥接(技术难点)
4.1 三层桥接
ArkTS
│ JSON string
▼
C++ N-API entry(librebugger_napi.so)
│ const char*
▼
Kotlin/Native C ABI(libohosrebugger.so)
│ RebuggerTrack / RebuggerVersion
▼
UTF-8 JSON + DisposeString
直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;只导出整数又会让页面复制判定语义,所以选择 C ABI + JSON。
4.2 Kotlin/Native 导出函数
@CName("RebuggerTrack")
fun rebuggerTrack(request: String): String // 四态判定一口进
@CName("RebuggerVersion")
fun rebuggerVersion(): String = "1.0.1-ohos.1" // 版本握手
| C ABI 符号 | ArkTS 方法 | 覆盖内核 |
|---|---|---|
RebuggerTrack | rebuggerTrack(req) | 会话隔离 + Box 建模 + RebuggerTracker.track() 全部判定 |
RebuggerVersion | rebuggerVersion() | fork 版本握手(真机版本行的数据来源) |
JSON 解析用随桥自带的 MiniJson(递归下降,支持对象 / 数组 / 字符串 / 裸标量,约 150 行)——继承自 cmp_qrose 审核模式,但比 qrose 的扁平解析多了嵌套 trackMap 与 fresh 数组支持。
4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
{"rebuggerTrack", nullptr, NapiRebuggerTrack, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
{"rebuggerVersion", nullptr, NapiRebuggerVersion, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};
每个回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohosrebugger_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohosrebugger_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与两个业务符号),不拖整份生成头。
4.4 CMake 和类型声明
add_library(ohosrebugger SHARED IMPORTED)
set_target_properties(ohosrebugger PROPERTIES
IMPORTED_LOCATION ${KN_SO}
IMPORTED_NO_SONAME TRUE)
add_library(rebugger_napi SHARED napi_init.cpp)
target_link_libraries(rebugger_napi PUBLIC
libace_napi.z.so
libhilog_ndk.z.so
ohosrebugger)
OHOS_ARCH 为空或 libohosrebugger.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。
export const rebuggerVersion: () => string;
export const rebuggerTrack: (request: string) => string;
第 5 阶段:ArkUI 宿主
5.1 版本握手和触发操作
aboutToAppear(): void {
this.version = rebuggerVersion(); // 版本握手:来自真实 Native 调用
}
private track(reset: boolean, fresh: string[]): void {
const request: TrackRequest = {
session: 'demo',
composableName: this.composableName,
reset: reset,
trackMap: { car: this.carValue, bike: this.bikeValue, human: this.humanValue },
fresh: fresh
};
const res = JSON.parse(rebuggerTrack(JSON.stringify(request))) as TrackResponse;
this.apply(res); // @State 刷新结果卡 + 时间戳日志
}
六个按钮共享一个 track():值改变类按钮先改 @State 再 track(false, []);实例重建按钮不改值、track(false, ['car']);无变化重组按钮直接 track(false, []);重置会话 track(true, [])。
5.2 页面交互
单页自上而下:标题与版本握手、被追踪参数区(composableName 输入框 + car/bike/human 当前值)、操作区六按钮(值变更深绿 / 实例重建蓝 / 无变化琥珀 / 重置灰)、最近结果卡(type · render #N · tag + 消息全文)、日志区([HH:mm:ss] + type 着色 + 消息原文,最新在前、上限 100 条)、关于区(分层说明与仓库 / 社区链接)。
5.3 错误边界与 ArkTS 严格模式
ok:false 时错误原文进结果卡与日志;Native 调用本身抛错同样被 try/catch 收进日志区。ArkTS 严格模式在真机联调时暴露了两处必须处理的点:
res.error ?? res.message的结果类型是string | null | undefined,不能赋给string——要补最终兜底?? 'unknown error';- 被
@Builder按值传参的参数不参与状态刷新——参数行改为内联Row直接读@State。
第 6 阶段:示例与验证
6.1 工程结构
example/
├── nativeApp/
│ ├── build.gradle.kts 双 ABI sharedLib(baseName=ohosrebugger)+ cxxShared + copyKnSo*
│ └── src/ohosMain/kotlin/example/nativeapp/
│ └── Bridge.kt @CName × 2 + Box 建模 + MiniJson(继承自 cmp_qrose 审核模式)
└── harmonyApp/
├── build-profile.json5 signingConfig "default" 预置(材料由 DevEco 自动签名回填)
├── AppScope/app.json5 bundleName org.terminator.ohos.rebugger、versionName 1.0.1-ohos.1
└── entry/
├── build-profile.json5 externalNativeOptions + abiFilters [arm64-v8a, x86_64]
├── libs/{arm64-v8a,x86_64}/ linkDebugShared* 经 copyKnSo* 回填的三件套(gitignore)
├── src/main/cpp/CMakeLists.txt IMPORTED 引入 KN so、$ORIGIN rpath
├── src/main/cpp/napi_init.cpp N-API 导出、参数检查、DisposeString
├── src/main/cpp/include/libohosrebugger_api.h
├── src/main/cpp/types/librebugger_napi/{index.d.ts, oh-package.json5}
└── src/main/ets/pages/Index.ets ArkUI 验收页面
6.2 构建、签名和安装
./gradlew :rebugger:jvmTest # 10 项交叉验证
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
cd example/harmonyApp
hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon
签名在 DevEco Studio 的 File → Project Structure → Signing Configs 中勾选自动生成,证书写入本机,不进入仓库。
hdc list targets -v
hdc -t <设备序列号> install -r \
entry/build/default/outputs/default/entry-default-signed.hap
hdc -t <设备序列号> shell aa start \
-a EntryAbility -b org.terminator.ohos.rebugger -m entry
四、完整代码对照
4.1 整体架构
com.theapache64.rebugger/**(提取引擎,commonMain 原样编译)
│ RebuggerTracker / RebuggerMessages / RebuggerConfig
src/composeMain/Rebugger.kt(上游 @Composable 壳,委托引擎,ohos 不编)
│ LaunchedEffect + remember { RebuggerTracker() }
Bridge.kt + MiniJson
│ RebuggerTrack / RebuggerVersion(Box 身份建模)
librebugger_napi.so
│ rebuggerTrack / rebuggerVersion
Index.ets
│ JSON parse + @State 更新
▼
ArkUI 真机页面(单页滚动)
4.2 文件清单
| 文件 | 职责 |
|---|---|
settings.gradle.kts | 极简 ohos 构建、eazytec Maven 置前、ohos_rebugger、composeApp/iosApp 移出 |
gradle/libs.versions.toml | Kotlin 2.2.21-1.0.0 单插件钉版(上游目录留档 .upstream) |
build.gradle.kts(根) | 版本 1.0.1-ohos.1、全局禁用 Ohos*Test* 任务 |
rebugger/build.gradle.kts | 纯 KMP:jvm + ohosArm64 + ohosX64,零第三方依赖 |
rebugger/src/commonMain/.../RebuggerTracker.kt | 引擎 + 四态结果 + 消息格式(核心新增) |
rebugger/src/ohosMain/.../RebuggerConfigImpl.kt | defaultLogger actual(println) |
rebugger/src/commonTest/.../RebuggerTrackerTest.kt | 10 项语义钉板测试 |
example/nativeApp/build.gradle.kts | 双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填 |
nativeApp/.../Bridge.kt | 2 个 C ABI、Box 建模、MiniJson、错误通道 |
harmonyApp/.../cpp/napi_init.cpp | N-API 导出、参数检查、DisposeString |
harmonyApp/.../cpp/CMakeLists.txt | IMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套 |
harmonyApp/.../ets/pages/Index.ets | ArkUI 验收页面 |
README.OpenHarmony_CN.md | 鸿蒙适配主文档(11 节) |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| 引擎 | RebuggerTracker.track(name, trackMap) | 一次重组的完整判定,返回四态 TrackResult |
| 引擎 | RebuggerTracker.reset() | 清空基线 / flag / 计数(重新进入 Composable) |
| 引擎 | RebuggerTracker.renderCount | 完成的 track 次数(上游 SideEffect 计数) |
| 引擎 | RebuggerMessages | 三种日志字符串的唯一出处(activated / recomposed / unknown) |
| 配置 | RebuggerConfig.tag / logger / init() | 上游同款可配置项,随编译进 .so |
| Native | RebuggerTrack / RebuggerVersion | C ABI JSON 导出 |
| N-API | rebuggerTrack(req) / rebuggerVersion() | 向 ArkTS 暴露能力 |
| ArkTS | track(reset, fresh) | 触发一次重组归因并刷新 UI |
4.4 ArkTS 与 Kotlin 的边界
const raw = rebuggerTrack(JSON.stringify({
session: 'demo',
composableName: 'VehicleUi',
reset: false,
trackMap: {
car: 'Car(name=Fit, speed=10)',
bike: 'Bike(name=Duke, cc=200)',
human: 'Human(name=John)'
},
fresh: [] // 本次「内容相同但实例被重建」的 key
}));
const res = JSON.parse(raw);
// res.type === 'reasons';res.message 与 Android Logcat 逐字一致
val result = session.tracker.track(name, trackMap) // trackMap 的值是 Box
when (result.type) {
TrackType.Activated -> RebuggerMessages.activated(name)
else -> result.message
}
两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构。
五、关键决策说明
决策 1:引擎提取而不是整库编进 Compose 线
社区 Maven 上 compose.runtime 的 ohos 制品(1.9.2-OH 线)是存在的,但把 @Composable 编进 ohos 意味着整套构建按社区 Compose 线对齐——与上游 Compose 1.9.0-beta03 / Kotlin 2.2.10 的差异全部要由 fork 吸收。而 rebugger 的判定语义(逐 key 比对、基线刷新、flag 吞噬)一行 Compose 都不碰,Compose 只是宿主。提取引擎进零依赖 commonMain,ohos 编译不需要解析任何 androidx 制品,composeMain 留档等社区线与上游对齐后一行不改接回——这是 cmp_qrose(Painter 隔离)已过审的同款处置。
决策 2:工具链钉 2.2.21-1.0.0,Gradle 降 8.14.1
库模块零第三方依赖,Kotlin 版本没有「跟着依赖走」的约束,直接落社区已审核的 2.2.21-1.0.0 线(cmp_qrose / guidekit / MaterialThemePrefs 同款)。代价是 wrapper 必须从上游 9.0.0 降到 8.14.1,AGP 9.0.0-alpha01 无法保留——本就随根构建替换剥离。与 KStore 的 2.4.20 新线(Gradle 9.x)是两条独立工具链线,经验不能互相套用。
决策 3:根构建替换(cmp_qrose 模式),示例留盘
上游根构建(AGP classpath / vanniktech 发布 / compose 编译插件 / GitHub Actions 发布流)对鸿蒙没有保留价值,整个替换为极简 ohos 构建。composeApp / iosApp 上游 CMP 示例留盘不进 settings.gradle.kts——依赖旧版本目录与 AGP,sync 必炸,存在方式只有留档一种。
决策 4:!== 用 Box + fresh 显式建模
上游「内容相同但实例不同」的判定依赖 Kotlin 对象身份,JSON 序列化后身份必然丢失。桥上用 Box 实例还原:未变重发同一实例、fresh 发相等新实例、值变发新内容实例。调用方(ArkTS)对「对象重建」有天然知情权(是它自己 new 的),fresh 数组只是把这份知情显式交给引擎——语义零损失,且与上游「data class 重建触发隐性重组」的真实场景一一对应。
决策 5:新增 10 项 commonTest 钉语义
上游没有测试,引擎提取缺少「没漂」的证明。10 项用例把三种日志字符串逐字断言(含 🐞、\n\t 缩进、🤔)、基线刷新时机、flag 恰好吞噬一次、中途加 key 永不产出全部钉死。开发中这组测试还真抓到一次测试自身的实例身份疏漏——每次传新建的等值 data class,按上游语义本就该报「实例变更」——修正后引擎与上游行为严格一致。交叉验证的价值就在这:它连自己的错误都能抓住。
六、测试与验证
6.1 测试环境
本次验证使用 Windows(x64)、JDK 21(Temurin 21.0.12.1)、DevEco Studio 5.1.0 Release、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙设备(arm64)。
6.2 JVM 交叉验证
./gradlew :rebugger:jvmTest
10 项用例全部通过(新增语义钉板):
| 用例 | 钉住什么 |
|---|---|
firstTrackOnlyActivates | 首次 track 只建立基线,无消息 |
valueChangeIsReportedAndRebaselined | 值变更产出 reason A;同实例再 track 是 silent |
equalInstanceSwapIsReportedOnce | 相等实例交换产出 reason B |
equalInstanceSwapAfterValueChangeIsAlsoReported | 基线刷新后的等值新实例仍触发 reason B |
unchangedRecompositionIsUnknown | 无变化重组是 unknown(含 🤔 全文逐字断言) |
unknownIsSuppressedExactlyOnceAfterReason | unknown 在一次 reason 后被吞噬恰好一次 |
keyAddedMidSessionNeverReports | 中途加 key 永不产出该 key 的 reason |
multipleReasonsKeepUpstreamFormatting | 多 reason 的 \n\t 拼接格式 |
resetRestartsSession | reset 后重新握手 |
messageFormatsMatchUpstream | 三种消息格式逐字断言(与 Android Logcat 同款) |
6.3 HAP 验证
:rebugger:compileKotlinJvm / OhosArm64 / OhosX64: BUILD SUCCESSFUL
:rebugger:jvmTest: 10 项全绿
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL
产物符号检查: RebuggerTrack / RebuggerVersion / libohosrebugger_symbols 均在双 ABI so 中
BuildNativeWithCmake: finished(双 ABI 编出 librebugger_napi.so)
CompileArkTS: finished
PackageHap: finished
BUILD SUCCESSFUL
install bundle successfully
start ability successfully
libohosrebugger.so debugShared arm64 约 2.3MB、x86_64 约 2.0MB(含 Kotlin/Native 运行时;调试库体量可接受)。CAdapter 链接期有一条已知无害警告(api.cpp: non-void function does not return a value in all control paths),是 HarmonyOS Kotlin 生成的 napi 胶水,与业务代码无关,.so 正常链出——与 cmp_qrose 记录一致。
真机首轮联调暴露了一个桥上转义 bug:jsonString() 转义了 \n 但漏了 \t,而重组原因消息里恰好有 \n\t 缩进,响应 JSON 含裸 TAB,ArkTS 严格 JSON.parse 直接抛 rebuggerTrack threw: {}。修复为逐字符转义全部 C0 控制字符(\n \r \t \b \f 及 < 0x20 的 \uXXXX)后重链双 ABI,归因日志在真机完整显示。
6.4 功能用例
- 启动后标题下出现
native version: 1.0.1-ohos.1(来自真实 Native 调用,非静态文字); - 首次点任意操作按钮,日志出现
🐞 Rebugger activated on \VehicleUi``(type=activated); - 点
car 值改变,日志出现`car` changed from `Car(name=Fit, speed=10)` to `Car(name=Fit, speed=20)`,(type=reasons),再点一次同值不重复报; - 点
car 实例重建,日志出现`car` instance changed, but content remains the same -> `Car(...)`,只报一次; - 点
无变化重组,日志出现reason is unknown … trackMap? 🤔(type=unknown); - flag 吞噬:值改变后紧跟无变化重组 → type=silent(无日志行);再点一次才出现 unknown;
- 点
重置会话后下一次 track 回到 activated(重新握手); - 修改
composableName输入框,后续日志行换用新名字(上游composableName参数同款能力)。
6.5 验证结论
重组原因引擎、消息格式层、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙设备上成功安装并进入前台,页面展示了真实的归因会话:版本握手、值变更、实例重建、未知原因、flag 吞噬与重置闭环(见下节效果图)。
七、运行效果
7.1 真机截图

7.2 命令速查
git clone https://atomgit.com/oh-tpc/rebugger.git
cd rebugger
export JAVA_HOME="/path/to/jdk-21"
./gradlew :rebugger:compileKotlinOhosArm64 :rebugger:compileKotlinOhosX64 # klib
./gradlew :rebugger:jvmTest # 10 项交叉验证
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
cd example/harmonyApp
hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon
hdc install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b org.terminator.ohos.rebugger -m entry
hdc shell hilog | grep Rebugger
八、遗留问题与改进方向
8.1 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配,必须让
:rebugger真正编译成ohosArm64/ohosX64动态库; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;gradlew.bat启动报JAVA_HOME is not set:org.gradle.java.home只管 daemon,不管 wrapper 自身启动,执行前必须显式set JAVA_HOME;- Kotlin/Native 没有
synchronized(JVM 专属),kotlin.concurrent.Lock在 2.2.21 线也不可见——NAPI 回调来自 ArkTS 主线程时按 cmp_qrose 先例直接无锁; - JSON 转义要覆盖全部 C0 控制字符:只转义
\n不转义\t,响应含裸 TAB 时 ArkTSJSON.parse直接抛错,页面显示rebuggerTrack threw: {}且错误对象JSON.stringify后为空{},极具迷惑性——本 bug 由真机首轮流水暴露; - ArkTS 严格模式不接受
string | null | undefined赋给string:res.error ?? res.message必须补最终兜底; @Builder按值传参的参数不参与状态刷新,参数行要内联直接读@State;数组展开[x, ...arr]也有兼容风险,改unshift+splice最稳;findComposableName的 expect 移入composeMain后,jvmMain 的 actual 悬空会让 JVM 编译失败——expect 与 actual 的进出编译集必须成对处理;- DevEco 打开工程后生成的
example/harmonyApp/.idea/(含 clangd 缓存数据库)要单独追加 gitignore,否则混进提交; build-profile.json5的abiFilters必须双 ABI,每个 ABI 三件套(librebugger_napi.so/libohosrebugger.so/libc++_shared.so)缺一不可,缺 KN so 安装直接报9568347;- ArkTS 只 import
librebugger_napi.so,直接 import KN so 会报does not provide an export named 'rebuggerTrack'——HarmonyOS Kotlin 的 CAdapter 不把@CName业务符号挂给 ArkTS; - KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free();签名材料由 DevEco 自动写入,绝不进入 AtomGit。
8.2 已知问题
- 鸿蒙侧无
@ComposableAPI:Rebugger(trackMap=…)的 Compose 入口在composeMain留档不编,社区 Compose 线与上游版本对齐后接回(引擎不动,只把源集挂回目标); - 桥上
trackMap值以字符串表达,结构化 equals 语义(data class 嵌套字段)由调用方负责序列化稳定; - 实例身份靠
fresh数组显式声明:ArkTS 侧无法自动探测「对象重建」,但调用方天然知情,语义零损失; findComposableName栈帧取名不移植(JVM/Android 反射实现细节),鸿蒙侧用composableName参数显式传入——上游本身也提供该参数作为自动取名的修正手段;- android / js / wasmJs / apple 目标剥离(源文件留在树里),发布、dokka、kover 一并剥离,本 fork 不发 Maven Central;
- HAP 安装必须使用与 bundle name
org.terminator.ohos.rebugger匹配的签名。
8.3 未来优化方向
- 社区 Compose 线对齐后把
composeMain挂回 ohos 目标,@ComposableAPI 原样回归,桥降级为非 Compose 宿主的备用通道; - 引擎补
RebuggerConfig.init(tag, logger)的桥接入口(tag / 自定义 logger 热更新); - 评估
.har形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套; - 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
- 补充模拟器(x86_64)与其他设备 ABI 的验收矩阵和自动化截图脚本。
九、总结
9.1 核心难点回顾
上游 rebugger(判定逻辑内嵌 @Composable)
→ 验证社区 Compose 制品线(存在但版本对不齐 → 隔离决策)
→ 引擎提取:RebuggerTracker 进零依赖 commonMain
→ 工具链对齐 Kotlin 2.2.21-1.0.0(Gradle 降 8.14.1)
→ 根构建替换(cmp_qrose 模式)
→ ohosArm64 / ohosX64 KLIB + 1 个 ohos actual
→ C ABI(@CName × 2)+ Box 身份建模 + MiniJson
→ C++ N-API(DisposeString 纪律)
→ ArkTS JSON 解析(严格模式 + C0 转义教训)
→ ArkUI 真机单页验收(值变更 / 实例重建 / 未知 / flag 吞噬 / 重置)
9.2 封装层次
- 引擎层:提取自上游的
RebuggerTracker——三种判定、基线按 reason 刷新、flag 一次性吞噬、四态结果; - 消息层:
RebuggerMessages单点定义三种日志字符串,桥与 Composable 壳共用; - Native 层:生成双 ABI 动态库并通过 2 个 C ABI 输出 JSON;
- N-API 层:完成参数检查、字符串转换和 Native 内存释放;
- ArkTS 层:管理页面状态、请求构造、结果渲染、日志与错误文本;
- DevEco 层:完成 CMake、HAP、签名、安装和真机运行。
9.3 三条经验
- 适配路线跟着「依赖的宿主性」走:判定语义不碰 Compose,Compose 只是宿主——提取引擎比把整套 Compose 线拉进来对齐便宜一个数量级;反之像 KStore 那种引擎本身就是全部价值的库,真编译整库才是正解。路线选择看依赖的本质,不看依赖的存在;
- 交叉验证是「提取没有引入漂移」的唯一可信证明:上游没有测试,就为提取产物新钉 10 项逐字断言;这组测试在开发中真实抓到过实例身份的语义疏漏——测试自己也会错,但逐字断言 + 语义对照表的组合能把错误压到暴露而非沉淀;
- 桥上每个「跨不过去的语义」都要显式建模:
!==身份用 Box + fresh 还原,SideEffect计数用 renderCount 回显,remember槽位用会话隔离对应——桥不是翻译层,是语义保真层;宁可让调用方多声明一个fresh,不让引擎少判一种原因。
9.4 适配成果
当前适配已完成:
- Rebugger
1.0.1重组原因引擎的双 ABI Kotlin/Native 编译(commonMain零依赖提取,ohos actual 仅 1 个println); - 2 个 C ABI 符号和 C++ N-API 包装层(产物符号检查验证
RebuggerTrack/RebuggerVersion全局导出); - DevEco Stage ArkUI 单页验收页面(版本握手、值变更、实例重建、未知原因、flag 吞噬、重置、时间戳日志);
- 10 项 JVM 交叉验证全绿 + HAP 构建(双 ABI 六件套 so)、签名、设备安装和真机验证(含真机暴露的 JSON 转义 bug 修复回归);
- AtomGit 项目文档、OpenHarmony README(中英双份)和本地效果图。
适配过程 FAQ
以下问题来自
ohosArm64目标接入、NAPI 桥接、HAP 构建与真机验收的实际流程,按出现频率排序。
Q1:DevEco Studio Sync 失败或 ohpm 依赖下载超时,怎么处理?
- 确认 DevEco Studio 版本与工程
build-profile.json5的compatibleSdkVersion一致; - 在
File > Settings > Build, Execution, Deployment > Ohpm配置可用的 ohpm 仓库镜像或代理; - 删除
oh_modules、oh-package-lock.json5与.hvigor缓存后重新 Sync; - 公司网络需要代理时,同时给 ohpm 与 hvigor 配置
http.proxy。
Q2:CMake 报 Missing .../libxxx.so 或找不到 Kotlin/Native 动态库?
- 先在
example目录执行./gradlew :nativeApp:linkDebugSharedOhosArm64(模拟器用linkDebugSharedOhosX64); - 确认
libxxx.so与libc++_shared.so已复制到ohos/entry/libs/<abi>/; - 检查
entry/build-profile.json5的abiFilters与设备 ABI 是否一致(真机arm64-v8a,模拟器x86_64); - 清理
entry/.cxx与entry/build后重试。
Q3:ArkTS 报 Cannot find module 'libxxx_napi.so' 或调用返回 undefined?
- 确认 CMake 的
add_library(<target> SHARED ...)名称与import ... from 'libxxx_napi.so'完全一致; - 解包 HAP,确认
libs/<abi>/libxxx_napi.so已打包; - 删除
entry/build、entry/.cxx与oh_modules后重新构建; - 检查
module.json5的srcEntry与页面路径是否正确。
Q4:真机安装报 signature verification failed 或 install failed?
- 在 DevEco Studio 的
Signing Configs配置证书、Profile 与密码; bundleName必须与签名 profile 中的 bundle name 一致;- 设备需加入 profile 的
deviceIds,调试证书有效期需覆盖当前时间; - 用
hdc list targets确认设备在线,再执行hdc install -r xxx-signed.hap。
Q5:应用启动白屏、闪退或 NAPI 调用崩溃?
- 用
hdc hilog | grep -i "napi\|kotlin"查看原生日志; - 确认
libc++_shared.so与 Kotlin/Native 工具链版本匹配,且 ABI 与设备一致; - 检查 C ABI 返回的字符串是否由 Native 侧分配、ArkTS 侧复制后是否调用
XxxFree释放; - 在 DevEco 的 Native 调试配置中附加
lldb,定位空指针与越界访问。
Q6:Gradle 报 ohosArm64 目标不存在或 Kotlin/Native 编译失败?
- 使用鸿蒙发行版 Kotlin(如
2.2.21-1.0.0),并在settings.gradle.kts配置 eazytec 仓库; - Gradle 建议 8.x、JDK 21,版本不匹配时先统一工具链;
- 确认
kotlin { ohosArm64() }已应用,且local.properties的 SDK 路径有效; - 清理
~/.gradle/caches中对应版本的 Kotlin/Native 缓存后重试。
Q7:HAP 构建报 SDK 版本不匹配或 compatibleSdkVersion 错误?
- 对齐 DevEco Studio、HarmonyOS SDK 与
build-profile.json5中的compatibleSdkVersion; - 升级 DevEco Studio 后同步升级 hvigor 与 SDK,避免
modelVersion与 IDE 不匹配; - 命令行构建时设置
DEVECO_SDK_HOME指向 DevEco 的sdk目录。
Q8:命令行 hvigor 构建失败,但 DevEco 里能构建?
- 使用 DevEco 自带的 node 与 hvigor:
"$DEVECO/Contents/tools/node/bin/node" "$DEVECO/Contents/tools/hvigor/bin/hvigorw.js" ...; - 设置
JAVA_HOME、NODE_HOME、DEVECO_SDK_HOME; - 加上
--no-daemon排除守护进程缓存影响,并清理entry/build与.hvigor。
参考文档
更多推荐



所有评论(0)