开源鸿蒙平台 KMP 三方库「KMP-Capturable-Compose」适配全流程
本文记录
kmp-capturable-compose接入 OpenHarmony 的完整过程,覆盖截图捕获库盘点、Compose 源码隔离、可移植捕获内核泛型化、鸿蒙 kotlinx 制品线选型、真实引擎编译、ohosArm64/ohosX64目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。
kmp-capturable-compose是基于 Compose Multiplatform 的轻量截图库:CapturableBox包住任意 Composable,CapturableBoxController.capture()挂起等待下一绘制帧,经GraphicsLayer录制转成ImageBitmap,toByteArray(JPEG/PNG)落成字节,rememberShareSheet()一键调起系统分享——三件套把"截一个组件并分享出去"做成了一行调用。库很小(核心就一个控制器),但它的价值恰恰在"怎么安全地挂起与恢复":capture()停在唯一的 continuation 槽上、isCapturing翻旗、绘制帧到来时onCaptured()恢复调用者、reset()只清旗不恢复——这套语义是它区别于旧版轮询式截图 API 的分水岭。本次适配把这套引擎泛型化后编译进鸿蒙.so,ArkUI 侧用componentSnapshot提供像素,演示"组件 → 挂起 → 供帧 → PNG → 预览/分享"的完整闭环。
项目地址: AtomGit/oh-tpc/KMP-Capturable-Compose.git
上游仓库: suwasto/kmp-capturable-compose
说明:本文基于上游
0.1.1(适配启动时的稳定版本);上游最新1.0.0发布晚于本次适配,升级涉及 API 变更,需重新验证后另行升级。
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配
kmp-capturable-compose 为 Compose Multiplatform 应用提供"组件 ↔ 图片"这一件事的完整答案:rememberCaptureBoxController() 建控制器,CapturableBox(captureController) 包住内容,controller.capture() 挂起等待绘制帧,拿到 ImageBitmap 后要么自己消费(保存/上传),要么交给 rememberShareSheet() 调起系统分享面板;ImageBitmap.toByteArray(format, quality) 提供平台各自的编码出口(Android 走 Bitmap.compress,iOS 走 Skia encodeToData)。KMP 项目把这些能力放在 commonMain,由 Android、iOS 共享。OpenHarmony 应用同样需要"把界面截下来分享出去"的能力(收据、海报、成绩单、名片……),但上游构建(Kotlin 2.2.21 / Compose Multiplatform 1.9.3 / AGP 8.13.1)不会生成 OpenHarmony 变体。
如果只把示例页面改成 ArkTS,页面可以显示按钮,却无法证明公共捕获控制器在 OpenHarmony ARM64 设备上完成编译、链接和执行——挂起/恢复的状态机、单 continuation 槽的覆写语义、reset() 的边界,这些才是库的真正产品。适配需要同时验证捕获控制器、协程运行时、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | 必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。 |
| Compose 层不可编译 | CapturableBox composable 依赖 androidx.compose.ui 的 drawWithCache / GraphicsLayer / rememberGraphicsLayer,鸿蒙侧没有对应制品,源码必须隔离。 |
| 状态持有不同 | 上游 isCapturing 用 Compose 的 mutableStateOf,脱离 Compose 运行时无法工作,需要换可观测容器。 |
| 像素来源不同 | Android/iOS 的帧来自 GraphicsLayer.toImageBitmap(),鸿蒙侧帧必须由宿主(componentSnapshot)提供,控制器的供帧入口要从 internal 放开。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。 |
| 分享 API 不同 | ShareSheet 的 Android 实现(FileProvider + Intent chooser)与 iOS 实现(UIActivityViewController)都不能跨平台,鸿蒙侧要走 ShareKit。 |
| 交付链路复杂 | .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在:挂起式捕获内核的泛型化 ohos 编译、Compose 源码的 composeMain 隔离(cmp_qrose 模式)和 C ABI/N-API 桥接层。捕获语义完全由上游原版实现负责,ArkTS 只负责供像素、调 Native、解析 JSON 和展示结果。
1.2 库提供的能力
Demo 提供一个真实的餐厅收据截图会话,对应桥接的全部入口:
- 挂起式捕获(capture → frame):
op=capture让桥内协程真实调用controller.capture()挂起(isCapturing翻真、返回requestId),op=frame把componentSnapshot取到的像素编码成 PNG 后经onCaptured()送达,挂起的调用者被恢复——上游语义逐条保留,全程没有重写; - 字节导出(encode):独立编码入口,BGRA/RGBA 像素按 stride 解包 →(可选降采样)→ 零依赖 PNG,对应
ImageBitmap.toByteArray(PNG)的鸿蒙等价物; - reset 语义演示:
capture()挂起后调reset(),isCapturing回落为 false 但挂起的捕获不被取消,随后到达的帧仍能完成捕获——这是上游最容易误用的边界,Demo 有专门的五步演示; - 系统分享(ShareSheet 等价):最近一次捕获的 PNG 写入缓存目录,经
fileUri交给 ShareKitShareController调起系统分享面板; - 版本握手:
info返回 fork 版本号与工具链信息,来自真实 Native 调用而非静态文字。
响应包含:
| 字段 | 作用 |
|---|---|
ok | 操作是否成功,失败时 error 带原文。 |
op / id / requestId | 回显操作、会话与请求标识。 |
state | capture 返回 awaiting(已挂起),frame / result 返回 done / error / pending。 |
isCapturing | 控制器旗标实时值(capture / frame / state / reset 均回传)。 |
width / height / pngBytes / encodeMs | 结果帧尺寸、PNG 字节数与编码耗时。 |
pngBase64 | 编码后的 PNG(Base64),ArkTS 解码即可显示/分享。 |
issued / served / errored / pending | 会话级计数,观测捕获编排。 |
引擎侧完整语义(单 continuation 槽、旗标翻转、reset 边界、取消安全)全部在上游实现里逐条保留后编译进 .so,JVM 侧保留 4 项交叉验证用例,证明 ohos .so 里执行的挂起/恢复逻辑没有漂移。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 语义复用 | 上游 CapturableBoxController 的挂起/恢复/复位语义逐条保留,仅泛型化帧类型(ImageBitmap → T);全仓唯一新增的生产 Kotlin 源文件就是这份泛型控制器。 |
| 源码隔离 | 上游 12 个 Compose 源文件(含 android/ios 平台 actual 与测试)git mv 进 src/composeMain,字节级原样保留、不注册源码集,等 Compose Multiplatform ohos 制品到齐后可无冲突接回。 |
| 平台目标 | 库模块声明 ohosArm64 + ohosX64,保留 jvm() 作交叉验证,库出 KLIB;example/nativeApp 出双 ABI 动态库。 |
| 构建隔离 | 根构建替换为极简 ohos 构建(cmp_qrose 模式);:sharedUi / :sampleandroid 留盘不进 settings。 |
| 桥接稳定 | 单个 C ABI 和 UTF-8 扁平 JSON,会话式九 op 协议;异常永远不跨 FFI。 |
| 依赖纪律 | 协程换到 ohos 制品线(1.10.2-1.0.0)、atomicfu 换 0.31.0-1.0.0,换线不换版。 |
| UI 完整 | 单页覆盖全部桥接入口:收据卡片、截图、编码、reset 语义、分享、状态与日志。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。
CapturableBoxcomposable 本体在composeMain隔离不编译,鸿蒙侧的"被截图对象"由 ArkUI 复刻上游收据示例(Gourmet Bistro)承担,捕获编排仍然是上游控制器原件——这与社区已过审的 Capturable 鸿蒙仓同一条路线:本库是同一问题域(Compose 截图捕获)的 KMP 化后继(上游作者不同、API 换代:挂起式capture()取代轮询,内建 ShareSheet),两个仓库的桥名、so 名与 bundle 名已错开,互不冲突。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点模块与依赖边界、确认 Compose 源码隔离面、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版、ohos 协程制品线
第 3 阶段:内核泛型化 ── CapturableBoxController<T>、MutableStateFlow 替代 mutableStateOf、composeMain 隔离
第 4 阶段:桥协议与像素管线 ── 会话式九 op JSON 契约、BGRA/RGBA 解包、stride、降采样、stored-deflate PNG
第 5 阶段:原生桥接 ── @CName C ABI、C++ N-API、参数校验和 DisposeString 内存释放
第 6 阶段:ArkUI 宿主与验证 ── 收据页面、两段式捕获、ShareKit 分享、HAP、签名、真机验收和效果图
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
capturablecompose/ 上游库模块(进 ohos 构建,jvm() + ohosArm64 + ohosX64)
capturablecompose/src/commonMain/ 新增:泛型捕获控制器(全仓唯一新增生产源文件)+ 4 个 JVM 交叉验证用例
capturablecompose/src/composeMain/ 上游 12 个 Compose 源文件原样归档(不注册源码集,不编译)
sharedUi/ sampleandroid/ sampleIos/ 上游示例,留盘不进 settings(勿 sync)
example/nativeApp/ Kotlin/Native 动态库(Bridge + Pixels + PngEncoder)
example/harmonyApp/ DevEco Stage、CMake N-API 和 ArkTS 页面
README.OpenHarmony.md / _CN.md 鸿蒙适配说明(中英双份)
README.upstream.md 上游原文保存
构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :capturablecompose 与 :example:nativeApp,rootProject.name = "ohos_kmp_capturable_compose"。上游的 AGP / Compose Multiplatform / cocoapods / vanniktech 发布插件全部随根构建替换剥离——本 fork 不发 Maven Central。
1.2 工具链矩阵
| 项目 | 配置 |
|---|---|
| Kotlin Multiplatform | HarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven) |
| JDK | 21(Temurin,org.gradle.java.home 钉在 gradle.properties) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm() 保留作交叉验证) |
| Gradle | 8.14.1(wrapper 已随 fork 更新,与过审工具链一致) |
| kotlinx | coroutines-core 1.10.2-1.0.0、atomicfu 0.31.0-1.0.0(ohos 制品线,换线不换版) |
| DevEco product | 5.1.0(18) 兼容,runtimeOS: HarmonyOS |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.capturablecompose |
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点
| 模块 | 源文件 | 依赖形态 | 处置 |
|---|---|---|---|
capturablecompose/src/commonMain(上游) | 5 | 全部 import androidx.compose.*(Capturable / CapturableBox / 控制器 / toByteArray expect / ShareSheet expect) | 隔离:git mv 进 composeMain,字节不动 |
capturablecompose/src/androidMain / iosMain / iosTest | 7 | 平台 actual(Bitmap.compress / Skia)与模板测试 | 随迁 composeMain/androidMain / iosMain / iosTest |
capturablecompose/src/commonMain(新增) | 1 | 纯 Kotlin + coroutines,零 Compose | 整体编译(jvm + 双 ohos) |
sharedUi | — | Compose Multiplatform 示例 UI(收据页) | 留盘不进构建;ArkTS 侧复刻 |
sampleandroid / sampleIos | — | AGP 应用 / 空目录 | 留盘不进构建 |
上游 5 个 commonMain 文件里有价值又可移植的只有控制器:capture() 用 suspendCancellableCoroutine 停在单 continuation 槽、isCapturing 用 mutableStateOf 翻旗、onCaptured() 恢复调用者、reset() 只清旗。泛型化改造就是把这四条语义从 ImageBitmap 上解耦,全仓唯一新增生产源文件由此而来(见第 3 阶段)。
1.4 页面边界
标题区 kmp-capturable-compose · OPENHARMONY / 版本握手行(引擎已连接 · fork/上游/Kotlin 版本)
状态行 会话实时状态(isCapturing / issued / served / errored / pending)
收据卡片 Gourmet Bistro 餐厅收据(复刻上游 sharedUi 示例):店名、地址、电话、
Order #142、每秒跳动的 ISO 时钟、四行菜品、小计/税/折扣/TOTAL、致谢斜体
操作区 📸 截图并预览(capture → frame) / 🖼 编码演示(toByteArray) /
🔄 reset 语义 / 📤 分享(ShareSheet 等价) / 📊 会话状态 五按钮
结果区 截取结果卡片(PNG 预览 + 尺寸/字节/耗时元信息)
日志区 带时间戳的操作日志(monospace,上限 60 条)
页面启动时先调 info 做握手,再以每秒跳动的实时时钟证明"截到的是当下状态"。
第 2 阶段:目标与依赖打通
2.1 工具链钉版(为什么是 2.2.21-1.0.0)
ohosArm64() / ohosX64() 目标只存在于 HarmonyOS Kotlin 插件,来自社区 Maven(https://maven.eazytec-cloud.com)。库的协程依赖同样要换到 ohos 制品线:kotlinx-coroutines-core 1.10.2-1.0.0 与 atomicfu 0.31.0-1.0.0——版本主体与上游经 Compose Multiplatform 1.9.3 线使用的 1.10.x 一致,只是仓库换线(换线不换版),commonMain 里一行依赖语义都不用改。Gradle 钉 8.14.1(与 qrose / Capturable 等已过审仓库同一条 2.2.21 工具链线,该线必须配 8.14.1,不能照搬 2.4.20 线的 Gradle 9.x 经验)。
settings.gradle.kts 把 eazytec 仓库写在最前(两个仓库段都要,顺序错了配置期直接 Unresolved reference: ohosArm64):
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()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.PREFER_PROJECT)
repositories {
maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
maven("https://maven.eazytec-cloud.com/nexus/content/groups/harmonyos")
google()
mavenCentral()
}
}
rootProject.name = "ohos_kmp_capturable_compose"
include(":capturablecompose")
include(":example:nativeApp")
2.2 编译真实引擎
capturablecompose/build.gradle.kts 收敛为纯 KMP 模块:
plugins {
alias(libs.plugins.kotlin.multiplatform)
}
kotlin {
applyDefaultHierarchyTemplate()
jvmToolchain(21)
jvm() // 交叉验证用
ohosArm64()
ohosX64()
sourceSets {
commonMain.dependencies {
api(libs.kotlinx.coroutines.core) // 1.10.2-1.0.0
}
commonTest.dependencies {
implementation(kotlin("test"))
}
}
}
库模块不开 binaries.sharedLib——sharedLib 只放 example/nativeApp,否则每个模块都会吐一份带运行时的 so,HAP 体积和符号都会乱。库的 ohos 产物是 KLIB,由 nativeApp 链接进唯一的 so。
测试依赖的处置与家族一致:鸿蒙工具链没有 ohos 测试运行器,根构建全局禁用名字含 Ohos + Test 的任务,语义验证统一走 JVM:
subprojects {
tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
.configureEach { enabled = false }
}
第 3 阶段:内核泛型化(本适配的核心)
3.1 泛型捕获控制器
capturablecompose/src/commonMain/kotlin/io/github/suwasto/capturablecompose/capturablebox/CapturableBoxController.kt(全仓唯一新增生产源文件):
public class CapturableBoxController<T> {
private var continuation: ((T) -> Unit)? = null
private val _isCapturing = MutableStateFlow(false)
public val isCapturing: StateFlow<Boolean> get() = _isCapturing
public fun reset() {
_isCapturing.value = false
}
public suspend fun capture(): T = suspendCancellableCoroutine { cont ->
_isCapturing.value = true
continuation = { frame ->
if (cont.isActive) {
cont.resume(frame)
}
}
}
public fun onCaptured(frame: T) {
continuation?.invoke(frame)
}
}
与上游的逐条对应:capture() 的 suspendCancellableCoroutine 单槽挂起原样保留;isCapturing 从 mutableStateOf 换成 MutableStateFlow(脱离 Compose 运行时后仍可观测,未来 Compose 接回时同样可用);reset() 只清旗、不恢复挂起调用者的边界原样保留;onCaptured() 从 internal 放开为 public——上游唯一供帧者是 CapturableBox composable 的绘制回调,鸿蒙侧供帧者换成了宿主,入口必须公开。帧类型 T 在桥上固定为编码后的 PNG 帧。
3.2 Compose 源码隔离
上游 12 个源文件按 cmp_qrose 模式 git mv 归档,全部 100% rename、一个字节不改:
capturablecompose/src/commonMain/kotlin/** → capturablecompose/src/composeMain/kotlin/**
capturablecompose/src/androidMain/** → capturablecompose/src/composeMain/androidMain/**
capturablecompose/src/iosMain/** → capturablecompose/src/composeMain/iosMain/**
capturablecompose/src/iosTest/** → capturablecompose/src/composeMain/iosTest/**
composeMain 不在任何构建脚本里注册,KGP 不会为它创建源码集——这就是隔离的全部机制,零脚本成本。等 Compose Multiplatform 的 ohos 制品到齐后,把目录接回源码集即可恢复 Compose 侧 API(CapturableBox / rememberShareSheet / toByteArray expect-actual 全套),届时泛型控制器与 Compose 渲染层在同一模块内合流。
3.3 JVM 交叉验证
capturablecompose/src/commonTest 放 4 个用例,JVM 与 ohos 跑同一份代码,钉住"内核没漂":
| 用例 | 钉住什么 |
|---|---|
capture_suspendsUntilOnCaptured_andResumesWithFrame | capture() 挂起、旗标翻真、onCaptured() 恢复并交付帧 |
reset_clearsFlag_butDoesNotResumeParkedCaller | reset() 只清旗,挂起的 capture() 不被完成;后续帧仍能恢复调用者 |
secondCapture_overwritesTheSingleContinuationSlot | 单 continuation 槽:后来者覆盖,被覆盖者停留挂起(上游原语义) |
onCaptured_afterCallerCancelled_isIgnored | 调用者取消后供帧不崩溃(cont.isActive 守卫) |
第 4 阶段:桥协议与像素管线
4.1 JSON 契约
io.github.suwasto.capturablecompose.capturablebox/**(泛型控制器,原语义编译)
↓ CapturableBoxController<T>.capture() / onCaptured() / reset()
Bridge.kt(@CName × 1,会话式扁平 JSON 进出)
↓ UTF-8 JSON
N-API capturableComposeBridge()
↓
ArkUI Index.ets(单页滚动)
会话式九 op 协议(id 默认 default,失败一律 {"ok":false,"error":...}):
| op | 请求字段 | 响应(成功) |
|---|---|---|
info | — | lib/upstream/fork/kotlin/coroutines 版本握手 |
create | id | id(会话惰性创建,显式 create 便于观测) |
capture | id | requestId / state=awaiting / isCapturing=true / pending |
frame | id、pixels(Base64)、width、height、stride、format(bgra|rgba)、scale(1–8) | requestId / state=done / width / height / pngBytes / encodeMs / isCapturing=false / 计数 / pngBase64 |
result | id、requestId | 已完成:同 frame 完成响应(取出即删);未完成:state=pending |
encode | 同 frame | 独立编码,不占会话状态(≙ toByteArray(PNG)) |
state | id | isCapturing + issued / served / errored / pending |
reset | id | isCapturing=false(只清旗,不完成挂起请求——上游语义) |
dispose | id | id(会话销毁,下次调用自动重建) |
一次完整交互的报文样例(pixels 字段实际为数十万字符的 Base64,此处省略):
// ① 发起捕获:真实调用 controller.capture(),挂起
{"op":"capture","id":"default"}
→ {"ok":true,"op":"capture","requestId":"r-1","state":"awaiting","isCapturing":true,"pending":1}
// ② 供帧:componentSnapshot 的像素编码后经 onCaptured() 送达,挂起的调用者被恢复
{"op":"frame","id":"default","pixels":"iVBORw0…(省略)…","width":724,"height":812,"stride":2896,"format":"bgra"}
→ {"ok":true,"op":"frame","requestId":"r-1","state":"done","width":724,"height":812,
"pngBytes":31542,"encodeMs":7,"isCapturing":false,"issued":1,"served":1,
"errored":0,"pending":0,"pngBase64":"iVBORw0…(省略)…"}
两段式是刻意的:capture 与 frame 拆开,才能在真机上观察到"请求已挂起、旗标已翻、帧到达后恢复"的完整时序——这正是上游 CapturableBox 在 Compose 里一笔带过的部分,鸿蒙侧把它摊开演给你看。
4.2 像素管线(nativeApp 侧胶水)
Pixels.kt + PngEncoder.kt 是 Demo 侧胶水,不进库模块:ArkUI componentSnapshot 的 PixelMap 缓冲默认 BGRA 通道序,ImageInfo.stride 在高密度屏上大于 width×4;管线按行步进解包成 ARGB IntArray(format 字段支持 rgba 切换)、可选整数盒式降采样(scale 1–8),再交零依赖 PNG 编码器——RGBA 8bit IHDR(8,6) + IDAT + IEND,zlib 用 stored 模式(非压缩 deflate 块,头 78 01、尾 adler32),CRC32 查表实现,与 qrose / Capturable 家族适配共享同一实现形状。
第 5 阶段:原生桥接(技术难点)
5.1 三层桥接
ArkTS
│ JSON string
▼
C++ N-API entry(libcapturablecompose_napi.so)
│ const char*
▼
Kotlin/Native C ABI(libohoscapturablecompose.so)
│ CapturableComposeBridge
▼
UTF-8 JSON + DisposeString
HarmonyOS Kotlin 的 CAdapter 不会把 @CName 业务函数挂到 ArkTS 模块——ArkTS 直接 import 'libohoscapturablecompose.so' 只能看到运行时符号。所以必须两跳:KN so 被 C++ NAPI so 链接,ArkTS 只导入 libcapturablecompose_napi.so。三个名字严格一致:CMake target capturablecompose_napi、nm_modname、index.d.ts 对应的导入名。
5.2 Kotlin/Native 导出函数
@CName("CapturableComposeBridge")
fun capturableComposeBridge(request: String): String
| C ABI 符号 | ArkTS 方法 | 覆盖内核 |
|---|---|---|
CapturableComposeBridge | capturableComposeBridge(req) | CapturableBoxController<T> 全部语义 + 像素管线 + 会话管理 |
frame 内部顺序:解 Base64 → 像素管线编码 → controller.onCaptured(EncodedFrame) 恢复挂起调用者 → 轮询会话结果表(10 秒超时)同步返回。capture 内部在回复前用 1 秒超时确认旗标真的翻上(确定性握手)。异常统一 catch,永远不跨 FFI。
5.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
{"capturableComposeBridge", nullptr, NapiCapturableComposeBridge, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};
回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohoscapturablecompose_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohoscapturablecompose_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与一个业务符号),不拖整份生成头。
5.4 CMake 和类型声明
add_library(ohoscapturablecompose SHARED IMPORTED)
set_target_properties(ohoscapturablecompose PROPERTIES
IMPORTED_LOCATION ${KN_SO}
IMPORTED_NO_SONAME TRUE)
add_library(capturablecompose_napi SHARED napi_init.cpp)
target_link_libraries(capturablecompose_napi PUBLIC
libace_napi.z.so
libhilog_ndk.z.so
ohoscapturablecompose)
OHOS_ARCH 为空或 libohoscapturablecompose.so 缺失直接 FATAL_ERROR(提示先跑 :example:nativeApp:linkDebugSharedOhosArm64 / OhosX64);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。
export const capturableComposeBridge: (request: string) => string;
第 6 阶段:ArkUI 宿主与验证
6.1 快照与两段式捕获
async snapshotReceipt(): Promise<SnapshotData> {
const pm = await this.getUIContext().getComponentSnapshot().get('receipt');
const info = await pm.getImageInfo();
const stride = info.stride > 0 ? info.stride : info.size.width * 4;
const buffer = new ArrayBuffer(pm.getPixelBytesNumber());
await pm.readPixelsToBuffer(buffer);
const pixels = new util.Base64Helper().encodeToStringSync(new Uint8Array(buffer));
pm.release();
return { pixels, width: info.size.width, height: info.size.height, stride };
}
const issue = JSON.parse(capturableComposeBridge(
JSON.stringify({ op: 'capture', id: SESSION_ID }))) as BridgeResp;
// issue.state === 'awaiting',isCapturing === true
const frame = JSON.parse(capturableComposeBridge(JSON.stringify({
op: 'frame', id: SESSION_ID, pixels: snap.pixels,
width: snap.width, height: snap.height, stride: snap.stride, format: 'bgra'
}))) as BridgeResp;
// frame.state === 'done',pngBase64 → image.createImageSource(...).createPixelMap() 预览
分享走 ShareKit(库的 rememberShareSheet() 鸿蒙等价物):PNG 字节写 cacheDir/shared_image.png → fileUri.getUriFromPath → systemShare.SharedData(utd: uniformTypeDescriptor.UniformDataType.PNG)→ ShareController.show(...) 调起系统分享面板。
6.2 页面交互
页面自上而下:标题与握手行(Kotlin 引擎已连接 · kmp-capturable-compose 0.1.1-ohos.1(上游 0.1.1 · Kotlin 2.2.21-1.0.0))、会话状态行、收据卡片(含每秒跳动的 ISO 时钟——截图会把当下时刻定格,这是"截的是实时状态"的直观证据)、五按钮(📸 截图并预览 / 🖼 编码演示 / 🔄 reset 语义 / 📤 分享 / 📊 会话状态)、结果预览卡片与操作日志。reset 语义按钮按 ①capture ②state(旗标 true) ③reset(false) ④state ⑤frame 仍 done 五步打日志,把上游最容易误用的边界演成时间线。
6.3 工程结构与构建安装
example/
├── nativeApp/
│ ├── build.gradle.kts 双 ABI sharedLib(baseName=ohoscapturablecompose)+ cxxShared + copyKnSo*
│ └── src/ohosMain/kotlin/
│ ├── Bridge.kt @CName + 会话九 op + MiniJson(cmp_qrose 审核模式)
│ ├── Pixels.kt BGRA/RGBA 解包、stride、降采样
│ └── PngEncoder.kt 零依赖 stored-deflate PNG
└── harmonyApp/
├── build-profile.json5 signingConfigs 留空(签名由 DevEco 自动生成回填)
├── AppScope/app.json5 bundleName org.terminator.ohos.capturablecompose、versionName 0.1.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/libohoscapturablecompose_api.h
├── src/main/cpp/types/libcapturablecompose_napi/{index.d.ts, oh-package.json5}
└── src/main/ets/pages/Index.ets ArkUI 验收页面
./gradlew :capturablecompose:jvmTest # 4 项交叉验证
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
cd example/harmonyApp
hvigorw assembleHap -p product=default --no-daemon
签名在 DevEco Studio 的 File → Project Structure → Signing Configs 中勾选自动生成,证书写入本机配置,不进入仓库(仓库内 build-profile.json5 的 signingConfigs 保持空数组,勾选自动签名后由 IDE 回填完整配置)。
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.capturablecompose -m entry
四、完整代码对照
4.1 整体架构
io.github.suwasto.capturablecompose.capturablebox/**(泛型控制器,上游语义逐条保留)
│ capture() 挂起 / onCaptured() 恢复 / reset() 清旗 / isCapturing StateFlow
Bridge.kt + MiniJson + Pixels + PngEncoder
│ CapturableComposeBridge(会话九 op)
libcapturablecompose_napi.so
│ capturableComposeBridge
Index.ets
│ componentSnapshot 供帧 + JSON parse + @State 更新
▼
ArkUI 真机页面(收据卡片 + 五按钮,单页滚动)
4.2 文件清单
| 文件 | 职责 |
|---|---|
settings.gradle.kts | 极简 ohos 构建、eazytec Maven 置前、ohos_kmp_capturable_compose、sharedUi/sampleandroid 移出 |
gradle/libs.versions.toml | Kotlin 2.2.21-1.0.0 + coroutines 1.10.2-1.0.0 / atomicfu 0.31.0-1.0.0 ohos 制品线 |
build.gradle.kts(根) | 版本 0.1.1-ohos.1、全局禁用 Ohos*Test* 任务 |
capturablecompose/build.gradle.kts | 纯 KMP:jvm() + ohosArm64 + ohosX64,不开 sharedLib |
capturablecompose/src/composeMain/** | 上游 12 个 Compose 源文件原样归档(100% rename,不编译) |
capturablecompose/src/commonMain/.../CapturableBoxController.kt | 唯一新增生产源文件(泛型捕获控制器) |
capturablecompose/src/commonTest/.../CapturableBoxControllerTest.kt | 4 项 JVM 交叉验证 |
example/nativeApp/build.gradle.kts | 双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填 |
nativeApp/.../Bridge.kt | @CName、会话九 op、runBlocking 收口、MiniJson、错误通道 |
nativeApp/.../Pixels.kt / PngEncoder.kt | 像素管线与零依赖 PNG |
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 | 鸿蒙适配主文档(29 节) |
4.3 关键 API 对照
| 层次 | 上游 API | 鸿蒙等价物 |
|---|---|---|
| 控制器 | rememberCaptureBoxController() | {"op":"create","id":...}(会话即控制器) |
| 控制器 | controller.capture(): ImageBitmap(挂起) | {"op":"capture"} → requestId / state=awaiting |
| 渲染 | CapturableBox 绘制回调供帧 | ArkTS componentSnapshot.get('receipt') + {"op":"frame"} |
| 控制器 | controller.reset() | {"op":"reset"}(只清旗) |
| 编码 | ImageBitmap.toByteArray(PNG, quality) | {"op":"encode"}(像素管线 + PngEncoder) |
| 结果 | ImageBitmap | 响应 pngBase64 → createImageSource().createPixelMap() |
| 分享 | rememberShareSheet() | ArkTS ShareKit(SharedData + ShareController.show) |
| 观测 | isCapturing(内部) | 各 op 响应 isCapturing 字段 + {"op":"state"} |
4.4 ArkTS 与 Kotlin 的边界
// ArkTS 侧:供像素 + 收 PNG,不持有任何 Kotlin 对象
const issue = JSON.parse(capturableComposeBridge('{"op":"capture","id":"default"}'));
const frame = JSON.parse(capturableComposeBridge(JSON.stringify({
op: 'frame', id: 'default', pixels, width, height, stride, format: 'bgra'
})));
// frame.pngBase64 → 预览 / 分享
// Kotlin 侧:真实上游语义,无一处重写
scope.launch {
val frame = controller.capture() // 挂起,isCapturing 翻真
completeWith(requestId, frame) // onCaptured() 恢复后走到这里
}
// 供帧线程:controller.onCaptured(pixelPipeline.render(rawFrame))
两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构。
五、关键决策说明
决策 1:移植挂起内核 + composeMain 隔离,而不是等 Compose ohos 制品
CapturableBox composable 依赖 androidx.compose.ui 的 GraphicsLayer 体系,社区 CMP 鸿蒙制品尚未到齐(cmp_qrose / Capturable / DrawBox 适配时的同一结论)。库的价值在挂起/恢复编排而不在 composable 本体——把编排泛型化进 commonMain,渲染层原样归档等接回,既让上游语义今天就跑在鸿蒙上,又保住未来合流的零冲突路径。
决策 2:isCapturing 用 MutableStateFlow 而不是手写监听者
上游 mutableStateOf 是 Compose 状态;直接换成 MutableStateFlow 是最小改动且双向可用的选择——JVM 测试与桥都能 .value 同步读、.first{} 等待,未来 Compose 接回时 Compose 侧也能收集。手写监听者列表等于重造 StateFlow,还会引入线程安全新坑。
决策 3:两段式桥协议(capture / frame 分开)
一次性 capture(pixels) 更省事,但会把"挂起→供帧→恢复"压成黑盒。拆成两个 op 后,真机日志能看到旗标翻真、请求驻留、帧到达恢复的完整时序,reset 语义演示也才可能成立(在挂起与供帧之间插入 reset)。代价只是 ArkTS 多一次调用。
决策 4:像素管线放 nativeApp,不进库模块
BGRA 解包、stride、降采样、PNG 编码是"鸿蒙供帧"的平台胶水,不是库语义(家族先例:qrose 的光栅化、Capturable 的像素管线同样只活在 example 层)。库 commonMain 只保留控制器,将来上游控制器改动可以最小 diff 合并。
决策 5:jvm() 交叉验证 + 测试钉语义
4 个 jvmTest 用例钉住挂起/恢复/复位/取消四条边界——这是"内核没漂"的唯一可信证明。ohos 侧测试无运行器,按家族惯例根构建全局禁用 Ohos*Test*,语义验证统一 JVM。
六、测试与验证
6.1 测试环境
本次验证使用 Windows(x64)、JDK 21(Temurin,C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot)、DevEco Studio 5.1.0 Release(含 hvigor / ohpm / hdc 命令行工具)与鸿蒙真机(arm64)。
6.2 JVM 交叉验证
./gradlew :capturablecompose:jvmTest
4 项用例全部通过(见 3.3 表)。泛型化只动了状态容器(mutableStateOf → MutableStateFlow)与帧类型,挂起/恢复路径逐行对照上游——测试全绿即语义无漂移。
6.3 HAP 验证
:capturablecompose:compileKotlinJvm / jvmTest: BUILD SUCCESSFUL(4/4)
linkDebugSharedOhosArm64: libohoscapturablecompose.so 5,407,904 B + libc++_shared.so 1,850,184 B
linkDebugSharedOhosX64: libohoscapturablecompose.so 4,979,872 B + libc++_shared.so 1,731,432 B
llvm-nm -D: CapturableComposeBridge / libohoscapturablecompose_symbols 均为全局 T 符号
BuildNativeWithNinja: finished(双 ABI 编出 libcapturablecompose_napi.so)
CompileArkTS: finished
PackageHap: finished
BUILD SUCCESSFUL(entry-default-unsigned.hap 8,016,473 B)
install bundle successfully
start ability successfully
HAP 内双 ABI 各三件套(libcapturablecompose_napi.so / libohoscapturablecompose.so / libc++_shared.so)加 modules.abc;KN so strip 后 arm64 约 2.6 MB、x86_64 约 2.5 MB(debugShared 未 strip 时分别约 5.4 MB / 5.0 MB,含 Kotlin/Native 运行时与协程);NAPI 包装 so 约 65 KB。
6.4 功能用例
- 启动后状态行出现
Kotlin 引擎已连接 · kmp-capturable-compose 0.1.1-ohos.1(上游 0.1.1 · Kotlin 2.2.21-1.0.0)(来自真实 Nativeinfo握手,非静态文字); - 收据卡片完整渲染:Gourmet Bistro、Order #142、四行菜品、Subtotal $96.47 / Tax $7.72 / Discount -$10.00 / TOTAL $94.19,ISO 时钟每秒跳动;
- 点 📸 截图并预览,日志依次出现
componentSnapshot 完成:W×Hpx(stride S)→capture r-1 已挂起:state=awaiting · isCapturing=true→frame r-1 ✅ isCapturing=false · PNG xxB · 编码 xms,下方出现结果卡片,图内容与票面一致、时钟定格为点击瞬间; - 点 🖼 编码演示,返回独立编码结果与
encodeMs(对应ImageBitmap.toByteArray(PNG)); - 点 🔄 reset 语义,五步日志依次显示 旗标 true → reset 后 false → frame 仍
done——证明 reset 不取消已挂起的捕获(上游边界); - 点 📤 分享,系统分享面板调起,收到的 PNG 可保存/发送;
- 点 📊 会话状态,
isCapturing/issued/served/errored/pending实时刷新; - 每步操作的日志按时间戳滚动留痕(monospace,上限 60 条)。
6.5 验证结论
捕获控制器、协程运行时、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了完整的截图会话:版本握手、收据渲染、两段式捕获、编码、reset 语义与系统分享(见下节效果图)。
七、运行效果
7.1 真机截图

7.2 命令速查
git clone https://atomgit.com/oh-tpc/KMP-Capturable-Compose.git
cd KMP-Capturable-Compose
export JAVA_HOME="/path/to/jdk-21"
./gradlew :capturablecompose:jvmTest # 4 项交叉验证
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
cd example/harmonyApp
ohpm install --all # 需 NODE_HOME 指向 DevEco 自带 node
hvigorw assembleHap -p product=default --no-daemon
hdc install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b org.terminator.ohos.capturablecompose -m entry
hdc shell hilog | grep CapturableCompose
八、遗留问题与改进方向
8.1 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配,必须让
:capturablecompose真正编译成ohosArm64/ohosX64动态库; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;@CName业务函数不会出现在 ArkTS 模块导入里(CAdapter 不挂业务符号),必须走 C++ NAPI 二跳,且nm_modname/ 导入名 /index.d.ts三名一致;- 上游
isCapturing是mutableStateOf(Compose 运行时),脱离 Compose 后照抄不得——MutableStateFlow是语义与可观测性都对齐的替身; - ArkUI
PixelMap缓冲默认 BGRA 通道序、高密度屏stride大于width×4:颜色 R/B 互换或pixels too short都先查这两项,桥接format字段留bgra/rgba切换; componentSnapshot用this.getUIContext().getComponentSnapshot().get(id)(API 12+ 命名空间层写法),旧的挂载式 API 在新 SDK 上编译不过;- ShareKit 的
SharedData构造必须带一个SharedRecord参数、UTD 类型要用@kit.ArkData的uniformTypeDescriptor.UniformDataType.PNG、addRecord收单个 record——三个写法错一个就是10505001编译错误三连; - hvigor 守护进程会缓存启动时的环境变量:先在无 java 的环境起过 daemon,之后把 JDK 加进 PATH 也照样报
PackageHap 00308018 spawn java ENOENT——--no-daemon或杀掉 daemon 重启(本仓新踩); - 命令行
ohpm找不到 node 时,把NODE_HOME指向 DevEco 自带 node(<DevEco>/tools/node)并加入 PATH(本仓新踩); - HAP 缺当前 ABI 的 so 会报安装失败
9568347,abiFilters必须双 ABI(真机是 x86_64),每个 ABI 三件套缺一不可; - KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free(); - DevEco 打开原生工程后会生成
example/harmonyApp/.clang-tidy/.clangd等本机工具配置,要单独追加 gitignore,否则混进提交;signingConfigs保持空数组,签名材料由 IDE 自动回填,证书绝不进入 AtomGit。
8.2 已知问题
- Compose 层不编译:
CapturableBox/rememberShareSheet()/toByteArrayexpect-actual 仅存于composeMain,鸿蒙应用不能直接用 Compose API,等社区 CMP ohos 制品到齐后接回; - 像素必须由宿主提供:控制器不持有 UI,ArkTS 侧负责
componentSnapshot(这是平台分工,不是缺陷,但集成方要理解); - PNG 为 stored-deflate(无压缩 zlib),大图体积大于真压缩器;上游
CompressionFormat.JPEG在鸿蒙侧暂无等价编码器,桥仅支持 PNG,业务需要 JPEG 时先取 PNG 再用系统能力转码; - 单 continuation 槽是上游原语义:前一次
capture()未完成时再次capture()会覆盖槽位,被覆盖者停留挂起;集成方按 UI 节流即可规避; - 会话表只在 ArkTS 单线程调用前提下免锁;跨 Worker 并发调用同一会话未做承诺;
- 分享在鸿蒙侧由 ArkTS ShareKit 等价实现(Demo 侧胶水),不进库模块;
- android / ios 目标剥离(源文件归档在
composeMain),AGP、Compose Multiplatform、cocoapods、Maven 发布一并剥离,本 fork 不发 Maven Central。
8.3 未来优化方向
- Compose Multiplatform ohos 制品到齐后,把
composeMain接回源码集,恢复CapturableBox/rememberShareSheet()的 Compose 直用形态(泛型控制器与渲染层合流); - 补 JPEG 编码器(或接系统图像能力),对齐上游
CompressionFormat全集; - 增加真正的压缩 deflate(zlib huffman),降低大图 PNG 体积;
- 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;补充 uitest 自动化截图脚本;
- 评估
.har形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套。
九、总结
9.1 核心难点回顾
上游 CapturableBoxController(Compose 状态 + ImageBitmap 帧)
→ 泛型化捕获内核(MutableStateFlow 替身 + 帧类型 T),语义逐条保留
→ Compose 源码 12 文件 composeMain 隔离(100% rename,等接回)
→ 根构建替换(cmp_qrose 模式,Kotlin 2.2.21-1.0.0 + ohos 协程制品线)
→ ohosArm64 / ohosX64 KLIB(库不开 sharedLib)
→ C ABI(@CName × 1)+ 会话式九 op JSON + 两段式捕获
→ C++ N-API(DisposeString 纪律)
→ ArkTS componentSnapshot 供帧 + ShareKit 分享
→ ArkUI 真机单页验收(截图 / 编码 / reset 语义 / 分享 / 状态)
9.2 封装层次
- 内核层:上游语义原件
capture()单槽挂起 /onCaptured()恢复 /reset()清旗 /isCapturing可观测; - 隔离层:上游 Compose 渲染与平台 actual 原样归档
composeMain,字节不动、历史保留; - Native 层:生成双 ABI 动态库并通过 1 个 C ABI 输出会话式 JSON;
- N-API 层:完成参数检查、字符串转换和 Native 内存释放;
- ArkTS 层:管理页面状态、组件快照、两段式捕获、ShareKit 分享、日志与错误文本;
- DevEco 层:完成 CMake、HAP、签名、安装和真机运行。
9.3 三条经验
- 库的价值决定适配的切割面:截图库的产品是"挂起/恢复编排"而不是 composable 本体,把编排泛型化进
commonMain、渲染层归档隔离,是这类 CMP UI 库在 Compose ohos 制品到齐前的最优解;切割面前先回答"这个库真正卖的是什么"; - 状态容器的替身要选"语义与可观测性都对齐"的:
mutableStateOf→MutableStateFlow一处改动同时满足 JVM 测试、Native 桥和未来 Compose 接回三方,任何手写监听者都是负资产; - 桥协议要为"演示语义"服务:两段式 capture/frame 比一次性调用多一次往返,却把上游最容易被误用的挂起时序和 reset 边界摊开成可观测的时间线——桥不仅是集成面,也是验收面。
9.4 适配成果
当前适配已完成:
kmp-capturable-compose0.1.1(fork0.1.1-ohos.1)挂起式捕获内核的双 ABI Kotlin/Native 编译(上游语义零重写,全仓唯一新增生产源文件为泛型控制器);- 上游 12 个 Compose 源文件的
composeMain原样归档(100% rename,Compose ohos 制品到齐后可无冲突接回); - 1 个 C ABI 符号和 C++ N-API 包装层(
llvm-nm验证全局 T 符号); - DevEco Stage ArkUI 单页验收页面(截图、编码、reset 语义、分享、会话状态、操作日志);
- 4 项 JVM 交叉验证全绿 + HAP 构建(双 ABI 六件套 so,8.0 MB)、签名、设备安装和真机验证;
- AtomGit 项目文档、OpenHarmony README(中英双份,CN 版 29 节)和本地效果图。
适配过程 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)