本文记录 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 交给 ShareKit ShareController 调起系统分享面板;
  • 版本握手:info 返回 fork 版本号与工具链信息,来自真实 Native 调用而非静态文字。

响应包含:

字段作用
ok操作是否成功,失败时 error 带原文。
op / id / requestId回显操作、会话与请求标识。
statecapture 返回 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 实现。CapturableBox composable 本体在 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 MultiplatformHarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven)
JDK21(Temurin,org.gradle.java.home 钉在 gradle.properties)
OpenHarmony 目标ohosArm64 + ohosX64(jvm() 保留作交叉验证)
Gradle8.14.1(wrapper 已随 fork 更新,与过审工具链一致)
kotlinxcoroutines-core 1.10.2-1.0.0、atomicfu 0.31.0-1.0.0(ohos 制品线,换线不换版)
DevEco product5.1.0(18) 兼容,runtimeOS: HarmonyOS
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.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 / iosTest7平台 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_andResumesWithFramecapture() 挂起、旗标翻真、onCaptured() 恢复并交付帧
reset_clearsFlag_butDoesNotResumeParkedCallerreset() 只清旗,挂起的 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 版本握手
createidid(会话惰性创建,显式 create 便于观测)
captureidrequestId / state=awaiting / isCapturing=true / pending
frameid、pixels(Base64)、width、height、stride、format(bgra|rgba)、scale(1–8)requestId / state=done / width / height / pngBytes / encodeMs / isCapturing=false / 计数 / pngBase64
resultid、requestId已完成:同 frame 完成响应(取出即删);未完成:state=pending
encode同 frame独立编码,不占会话状态(≙ toByteArray(PNG))
stateidisCapturing + issued / served / errored / pending
resetidisCapturing=false(只清旗,不完成挂起请求——上游语义)
disposeidid(会话销毁,下次调用自动重建)

一次完整交互的报文样例(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 方法覆盖内核
CapturableComposeBridgecapturableComposeBridge(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.tomlKotlin 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.kt4 项 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.cppN-API 导出、参数检查、DisposeString
harmonyApp/.../cpp/CMakeLists.txtIMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套
harmonyApp/.../ets/pages/Index.etsArkUI 验收页面
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 功能用例

  1. 启动后状态行出现 Kotlin 引擎已连接 · kmp-capturable-compose 0.1.1-ohos.1(上游 0.1.1 · Kotlin 2.2.21-1.0.0)(来自真实 Native info 握手,非静态文字);
  2. 收据卡片完整渲染:Gourmet Bistro、Order #142、四行菜品、Subtotal $96.47 / Tax $7.72 / Discount -$10.00 / TOTAL $94.19,ISO 时钟每秒跳动;
  3. 点 📸 截图并预览,日志依次出现 componentSnapshot 完成:W×Hpx(stride S) → capture r-1 已挂起:state=awaiting · isCapturing=true → frame r-1 ✅ isCapturing=false · PNG xxB · 编码 xms,下方出现结果卡片,图内容与票面一致、时钟定格为点击瞬间;
  4. 点 🖼 编码演示,返回独立编码结果与 encodeMs(对应 ImageBitmap.toByteArray(PNG));
  5. 点 🔄 reset 语义,五步日志依次显示 旗标 true → reset 后 false → frame 仍 done——证明 reset 不取消已挂起的捕获(上游边界);
  6. 点 📤 分享,系统分享面板调起,收到的 PNG 可保存/发送;
  7. 点 📊 会话状态,isCapturing / issued / served / errored / pending 实时刷新;
  8. 每步操作的日志按时间戳滚动留痕(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 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配,必须让 :capturablecompose 真正编译成 ohosArm64 / ohosX64 动态库;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. @CName 业务函数不会出现在 ArkTS 模块导入里(CAdapter 不挂业务符号),必须走 C++ NAPI 二跳,且 nm_modname / 导入名 / index.d.ts 三名一致;
  4. 上游 isCapturing 是 mutableStateOf(Compose 运行时),脱离 Compose 后照抄不得——MutableStateFlow 是语义与可观测性都对齐的替身;
  5. ArkUI PixelMap 缓冲默认 BGRA 通道序、高密度屏 stride 大于 width×4:颜色 R/B 互换或 pixels too short 都先查这两项,桥接 format 字段留 bgra/rgba 切换;
  6. componentSnapshot 用 this.getUIContext().getComponentSnapshot().get(id)(API 12+ 命名空间层写法),旧的挂载式 API 在新 SDK 上编译不过;
  7. ShareKit 的 SharedData 构造必须带一个 SharedRecord 参数、UTD 类型要用 @kit.ArkData 的 uniformTypeDescriptor.UniformDataType.PNG、addRecord 收单个 record——三个写法错一个就是 10505001 编译错误三连;
  8. hvigor 守护进程会缓存启动时的环境变量:先在无 java 的环境起过 daemon,之后把 JDK 加进 PATH 也照样报 PackageHap 00308018 spawn java ENOENT——--no-daemon 或杀掉 daemon 重启(本仓新踩);
  9. 命令行 ohpm 找不到 node 时,把 NODE_HOME 指向 DevEco 自带 node(<DevEco>/tools/node)并加入 PATH(本仓新踩);
  10. HAP 缺当前 ABI 的 so 会报安装失败 9568347,abiFilters 必须双 ABI(真机是 x86_64),每个 ABI 三件套缺一不可;
  11. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();
  12. DevEco 打开原生工程后会生成 example/harmonyApp/.clang-tidy / .clangd 等本机工具配置,要单独追加 gitignore,否则混进提交;signingConfigs 保持空数组,签名材料由 IDE 自动回填,证书绝不进入 AtomGit。

8.2 已知问题

  • Compose 层不编译:CapturableBox / rememberShareSheet() / toByteArray expect-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 三条经验

  1. 库的价值决定适配的切割面:截图库的产品是"挂起/恢复编排"而不是 composable 本体,把编排泛型化进 commonMain、渲染层归档隔离,是这类 CMP UI 库在 Compose ohos 制品到齐前的最优解;切割面前先回答"这个库真正卖的是什么";
  2. 状态容器的替身要选"语义与可观测性都对齐"的:mutableStateOf → MutableStateFlow 一处改动同时满足 JVM 测试、Native 桥和未来 Compose 接回三方,任何手写监听者都是负资产;
  3. 桥协议要为"演示语义"服务:两段式 capture/frame 比一次性调用多一次往返,却把上游最容易被误用的挂起时序和 reset 边界摊开成可观测的时间线——桥不仅是集成面,也是验收面。

9.4 适配成果

当前适配已完成:

  • kmp-capturable-compose 0.1.1(fork 0.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。

参考文档

Logo

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

更多推荐