本文记录 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 页面作为验收宿主,通过 @CName C 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 带原文。
typeactivated / 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 MultiplatformHarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven)
JDK21(org.gradle.java.home 指向本机 Temurin 21)
OpenHarmony 目标ohosArm64 + ohosX64(jvm 保留作交叉验证)
Gradle8.14.1(wrapper 由上游 9.0.0 降级;2.2.21 旧线必须配 8.x)
Compose不参与 ohos 编译(隔离决策见决策 1)
DevEco product5.1.0(18) 兼容
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.terminator.ohos.rebugger
export JAVA_HOME="/path/to/jdk-21"   # gradlew.bat 启动需要;org.gradle.java.home 只管 daemon
java -version
1.3 源码依赖度盘点
模块文件依赖形态处置
rebuggerRebuggerConfig.kt纯 Kotlin原样编译(jvm + 双 ohos)
rebuggerRebugger.kt 的比对逻辑纯 Kotlin,但内嵌在 @Composable 里提取为 RebuggerTracker(唯一一次「翻译」,语义逐行映射,见第 4 阶段)
rebuggerRebugger.kt 的 Compose 壳Composable / LaunchedEffect / SideEffect / remember / mutableStateOf移入 composeMain,ohos 不编,改为委托引擎
rebuggerfindComposableName expect + 各平台 actual栈帧反射是 JVM/JS 实现细节expect 随 Composable 移入 composeMain;jvm 的 actual 移出编译集;其余平台目录留盘
rebuggerdefaultLogger 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 方法覆盖内核
RebuggerTrackrebuggerTrack(req)会话隔离 + Box 建模 + RebuggerTracker.track() 全部判定
RebuggerVersionrebuggerVersion()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.tomlKotlin 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.ktdefaultLogger actual(println)
rebugger/src/commonTest/.../RebuggerTrackerTest.kt10 项语义钉板测试
example/nativeApp/build.gradle.kts双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填
nativeApp/.../Bridge.kt2 个 C ABI、Box 建模、MiniJson、错误通道
harmonyApp/.../cpp/napi_init.cppN-API 导出、参数检查、DisposeString
harmonyApp/.../cpp/CMakeLists.txtIMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套
harmonyApp/.../ets/pages/Index.etsArkUI 验收页面
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
NativeRebuggerTrack / RebuggerVersionC ABI JSON 导出
N-APIrebuggerTrack(req) / rebuggerVersion()向 ArkTS 暴露能力
ArkTStrack(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(含 🤔 全文逐字断言)
unknownIsSuppressedExactlyOnceAfterReasonunknown 在一次 reason 后被吞噬恰好一次
keyAddedMidSessionNeverReports中途加 key 永不产出该 key 的 reason
multipleReasonsKeepUpstreamFormatting多 reason 的 \n\t 拼接格式
resetRestartsSessionreset 后重新握手
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 功能用例

  1. 启动后标题下出现 native version: 1.0.1-ohos.1(来自真实 Native 调用,非静态文字);
  2. 首次点任意操作按钮,日志出现 🐞 Rebugger activated on \VehicleUi``(type=activated);
  3. 点 car 值改变,日志出现 `car` changed from `Car(name=Fit, speed=10)` to `Car(name=Fit, speed=20)`,(type=reasons),再点一次同值不重复报;
  4. 点 car 实例重建,日志出现 `car` instance changed, but content remains the same -> `Car(...)`,只报一次;
  5. 点 无变化重组,日志出现 reason is unknown … trackMap? 🤔(type=unknown);
  6. flag 吞噬:值改变后紧跟无变化重组 → type=silent(无日志行);再点一次才出现 unknown;
  7. 点 重置会话 后下一次 track 回到 activated(重新握手);
  8. 修改 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 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配,必须让 :rebugger 真正编译成 ohosArm64 / ohosX64 动态库;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. gradlew.bat 启动报 JAVA_HOME is not set:org.gradle.java.home 只管 daemon,不管 wrapper 自身启动,执行前必须显式 set JAVA_HOME;
  4. Kotlin/Native 没有 synchronized(JVM 专属),kotlin.concurrent.Lock 在 2.2.21 线也不可见——NAPI 回调来自 ArkTS 主线程时按 cmp_qrose 先例直接无锁;
  5. JSON 转义要覆盖全部 C0 控制字符:只转义 \n 不转义 \t,响应含裸 TAB 时 ArkTS JSON.parse 直接抛错,页面显示 rebuggerTrack threw: {} 且错误对象 JSON.stringify 后为空 {},极具迷惑性——本 bug 由真机首轮流水暴露;
  6. ArkTS 严格模式不接受 string | null | undefined 赋给 string:res.error ?? res.message 必须补最终兜底;
  7. @Builder 按值传参的参数不参与状态刷新,参数行要内联直接读 @State;数组展开 [x, ...arr] 也有兼容风险,改 unshift + splice 最稳;
  8. findComposableName 的 expect 移入 composeMain 后,jvmMain 的 actual 悬空会让 JVM 编译失败——expect 与 actual 的进出编译集必须成对处理;
  9. DevEco 打开工程后生成的 example/harmonyApp/.idea/(含 clangd 缓存数据库)要单独追加 gitignore,否则混进提交;
  10. build-profile.json5 的 abiFilters 必须双 ABI,每个 ABI 三件套(librebugger_napi.so / libohosrebugger.so / libc++_shared.so)缺一不可,缺 KN so 安装直接报 9568347;
  11. ArkTS 只 import librebugger_napi.so,直接 import KN so 会报 does not provide an export named 'rebuggerTrack'——HarmonyOS Kotlin 的 CAdapter 不把 @CName 业务符号挂给 ArkTS;
  12. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();签名材料由 DevEco 自动写入,绝不进入 AtomGit。

8.2 已知问题

  • 鸿蒙侧无 @Composable API: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 目标,@Composable API 原样回归,桥降级为非 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 三条经验

  1. 适配路线跟着「依赖的宿主性」走:判定语义不碰 Compose,Compose 只是宿主——提取引擎比把整套 Compose 线拉进来对齐便宜一个数量级;反之像 KStore 那种引擎本身就是全部价值的库,真编译整库才是正解。路线选择看依赖的本质,不看依赖的存在;
  2. 交叉验证是「提取没有引入漂移」的唯一可信证明:上游没有测试,就为提取产物新钉 10 项逐字断言;这组测试在开发中真实抓到过实例身份的语义疏漏——测试自己也会错,但逐字断言 + 语义对照表的组合能把错误压到暴露而非沉淀;
  3. 桥上每个「跨不过去的语义」都要显式建模:!== 身份用 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。

参考文档

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐