本文记录 Krop 接入 OpenHarmony 的完整过程,覆盖 Compose Multiplatform 裁剪库盘点、composeMain 源码集隔离、纯 Kotlin 几何模型复刻、ohosArm64 / ohosX64 目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 手势页面、HAP 构建和真机验收。

Krop 是构建在 Coil3 之上的 Compose Multiplatform 图片裁剪库:Croppable 提供双指缩放、拖动、双击的手势容器,CroppableState 用 Animatable 维护 scale / translate 并推导可视裁剪窗口 cropWindow,crop() 把窗口换算回原图像素矩形再经 cropBitmap expect/actual(Android 走 android.graphics.Bitmap,iOS/桌面走 Skia)编码成 PNG。本次适配与存储类库"补一个 dispatcher 就能真编译"的路线不同:Krop 的公开 API 每一个成员都踩在 androidx.compose.* / coil3.* 上(Animatable、VelocityTracker、pointerInput、BoxWithConstraints、coil3 Image,且 coil3 没有 ohos 制品),因此走的是 cmp_qrose 已过审的 “composeMain 隔离 + KN 几何模型 + C ABI” 路径——上游 5 个 Compose/Coil 源文件原样迁入 composeMain 隔离保留,裁剪几何逐条复刻成纯 Kotlin 的 CropModel,像素裁剪用行拷贝 PixelCropper 替代各平台 actual,同一组输入在 Android / iOS / 桌面与鸿蒙上产出同一个裁剪矩形,手势 UI 由 ArkUI 等价实现。

项目地址: AtomGit/oh-tpc/Krop.git

上游仓库: timhuang1018/Krop

开发工具: 华为云码道

环境安装: 环境安装指南

一、背景

1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配

Krop 为 Compose Multiplatform 应用提供"图片裁剪"这一件事的完整答案:rememberCroppableState(minScale, maxScale, contentScale) 一行建状态,Croppable(state, cropHint) 包住任意图片组件即可获得双指缩放、拖动、双击与九宫格裁剪框,croppableState.crop() 直接返回裁剪结果的 PNG 字节。CropHint 定制裁剪框的背景、边框与网格线颜色,widget/CropHintWindow + SudokuGrid 负责把窗口画成九宫格。KMP 项目把这些能力放在 commonMain,由 Android、iOS、桌面共享。OpenHarmony 应用同样需要这套裁剪能力,但上游构建(Kotlin 2.0.20 / Compose 1.6.11 / AGP 8.5.0 / Coil 3.0.1)不会生成 OpenHarmony 变体。

如果只把示例页面改成 ArkTS,页面可以显示图片和按钮,却无法证明公共裁剪几何在 OpenHarmony ARM64 设备上完成编译、链接和执行,更无法证明鸿蒙算出的裁剪矩形与其它平台逐位一致。适配需要同时验证裁剪几何模型、像素裁剪管线、Native 动态库、C ABI、N-API、ArkUI 手势页面和 HAP 交付链路。

障碍具体问题
目标缺失必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。
工具链不一致上游 Kotlin 2.0.20 / Compose 1.6.11 / AGP 8.5.0 与鸿蒙工具链无法共存于一个 Gradle 构建;社区钉法 Kotlin 2.2.21-1.0.0 + Gradle 8.14.1 必须整体替换根构建。
依赖矩阵上游 commonMain 依赖 compose.runtime / compose.foundation / lifecycle-runtime-compose / coil3,全部没有 ohos 变体;coil3 连制品都不存在。
语言边界不同ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。
动画语义跨不了桥Animatable 的 spring / exponentialDecay 是 Compose runtime 概念,C ABI 只能同步一来一回。
交付链路复杂.so、CMake、N-API、HAP、签名和 hdc 都要单独验收。

因此,本项目把边界放在:裁剪几何的 ohos 编译(CropModel / PixelCropper)、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。裁剪语义完全由 KN 侧模型负责,ArkTS 只负责采样手势、调用 Native、解析 JSON 和渲染结果。

1.2 库提供的能力

Demo 提供一个完整的裁剪会话,对应桥接的全部入口:

  • 手势驱动(kropState):双指缩放(Pinch)、拖动(Pan)、双击缩放并居中点击点,每次手势变化把绝对 scale / translate 喂给 KN,返回钳制后的变换、可视窗口 window 与原图像素矩形 rect;
  • contentScale 四档:Fit / Crop / FillWidth / FillHeight 初始状态与上游 Croppable 内 LaunchedEffect 的四档公式一致;
  • 容器宽高比:1:1 / 4:3 / 3:4 三档,window 与 rect 随容器正确换算;
  • 裁剪闭环(kropCrop):整张原图 RGBA_8888 像素 + 当前状态上送,返回裁剪 PNG 的 Base64,页面直接解码显示;
  • 几何对账:状态行实时显示 scale / tx / ty / rect,真机数据可与公式手算逐位核对。

响应包含:

字段作用
ok操作是否成功,失败时 error 带原文。
scale / translateX / translateY钳制后的当前变换,直接喂 ArkUI Image 的 .scale() / .translate()。
childWidth / childHeightContentScale.Inside 布局的等价子尺寸(fitInsideSize 算出)。
window容器坐标系的可视裁剪窗口(九宫格覆盖层的定位依据)。
rect原图像素坐标的裁剪矩形(kropCrop 的实际裁剪依据,也是诊断数据)。

引擎侧完整语义(getBounds 平移边界、calculateCropArea 窗口推导、crop() 的 rect 换算、snapScaleTo / onZoomChange / drag)全部由 CropModel 在 KN 侧原样承担;PixelCropper + PngEncoder 对应上游 cropBitmap 各平台 actual——同像素,不同编码器。

1.3 实现适配

维度要求
几何复用CroppableState 的生效数学逐条复刻进 CropModel(纯字段、零 Compose 依赖);上游 5 个 Compose/Coil 源文件零改动迁入 composeMain 隔离保留,等 CMP 工具链对齐再接回。
平台目标根构建直接声明 ohosArm64 + ohosX64,保留 jvm() 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。
构建隔离根构建替换为极简 ohos 构建(cmp_qrose 模式);sample-multiplatform / iosApp / androidMain / nonAndroidMain 留盘不进构建。
桥接稳定使用 2 个 C ABI 和 UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。
渲染纪律ArkTS 侧不做任何一条裁剪几何公式——钳制、窗口、rect 换算全部在 KN,与其它平台共享同一份实现。
UI 完整单页覆盖全部桥接入口:手势三件套、contentScale 四档、容器三档比例、九宫格窗口、裁剪出图。
仓库规范项目说明、文章、效果图和源码链接统一使用 AtomGit。

本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。Croppable 手势组件本体没有在鸿蒙上编译,交互由 ArkUI 手势 + KN CropModel 等价实现——composeMain 是隔离保留,不是已运行,文档与提交说明里不要写成"Krop 的 Compose 裁剪组件已经在鸿蒙上跑起来"。

二、实现路线图

第 1 阶段:项目初始化     ── 盘点模块与依赖边界、确认 compose/coil3 无 ohos 制品、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版、Gradle 8.14.1
第 3 阶段:源码集拆分     ── 5 个 Compose 文件迁 composeMain、CropModel/PixelCropper 复刻进 commonMain
第 4 阶段:桥协议与原生桥接 ── kropState/kropCrop JSON 契约、@CName C ABI、C++ N-API
第 5 阶段:ArkUI 宿主     ── 手势三件套、九宫格覆盖层、contentScale/比例切换、裁剪出图
第 6 阶段:示例与验证     ── HAP、签名、设备安装、真机运行和效果图

三、逐步实现过程

第 1 阶段:项目初始化

1.1 工程边界
krop-core/                            上游库模块(进 ohos 构建,jvm() + ohosArm64 + ohosX64)
krop-core/src/composeMain/            上游 5 个 Compose/Coil 文件原样迁入(非源集,ohos 不编)
krop-core/src/commonMain/             新增 CropModel.kt + PixelCropper.kt(纯 Kotlin,全 fork 仅有的新增源码)
krop-core/src/androidMain|nonAndroidMain/  上游 cropBitmap actual 遗留,不在源集内,不动
sample-multiplatform/ iosApp/         上游示例,留盘不进 settings
example/nativeApp/                    Kotlin/Native 动态库(Bridge + Cropper + PngEncoder)
example/harmonyApp/                   DevEco Stage、CMake N-API 和 ArkTS 页面
README.OpenHarmony.md / _CN.md        鸿蒙适配说明(中英双份)
README.upstream.md                    上游原文保存
doc/                                  本文与效果图

构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :krop-core 与 :example:nativeApp,rootProject.name = "ohos_krop"。上游的 vanniktech 发布插件、AGP / Compose 编译器插件、maven-publish 配置段全部随根构建替换剥离——本 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 整体替换;2.2.21 线的社区钉法)
依赖commonMain 零依赖——裁剪几何不需要任何三方库,这是它比存储/网络类库好适配的根本原因
DevEco product5.1.0(18) 兼容
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.terminator.ohos.krop
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点
模块/目录文件依赖形态处置
krop-core/src/commonMain(上游)5全部引用 androidx.compose.* 或 coil3.*整目录迁 composeMain,内容零改动
krop-core/src/commonMain(新增)2纯 Kotlin,零依赖新增:CropModel.kt(裁剪几何)+ PixelCropper.kt(RGBA 行拷贝)
krop-core/src/androidMain1android.graphics.Bitmap留盘不进构建(androidTarget() 不再声明)
krop-core/src/nonAndroidMain1org.jetbrains.skia.*留盘不进构建

上游 commonMain 的 5 个文件——Croppable.kt、CroppableState.kt、Krop.kt、widget/CropHintWindow.kt、widget/SudokuGrid.kt——全部踩在 androidx.compose / coil3 上,鸿蒙侧一行都编不了。拆法与 qrose 相同:整目录迁移,内容零改动。composeMain 不是 Kotlin 源集,Gradle 不会把它加进任何 target 的编译,jvm / ohos 都天然看不到它;留下来是为了对照上游 API,也方便以后 CMP 工具链对齐时再接回。

真正的工作量在"剥":CroppableState 的 Animatable(scale / translate)、VelocityTracker(fling)是 Compose runtime 类型,不能跨语言;剥掉它们类就不成立,所以鸿蒙侧不是"编译原类",而是把生效的数学复刻成纯字段模型:

上游(Compose)鸿蒙(纯 Kotlin)公式
CroppableState.getBounds()CropModel.bounds()max(child*scale - container, 0) / 2
calculateCropArea()calculateCropWindow()startPoint / cropArea / cropWindow 三量,含"子缩放后小于容器时 window offset 为负"的行为
crop() 的 rect 换算cropRect()ratio = originalImageWidth / childWidth,四分量各 ratio * x / scale 后 toInt()
snapScaleTo / onZoomChange / drag同名同义scale coerce 进 [min, max],平移不自动钳制
LaunchedEffect 四档初始 scaleinitialScaleFor(mode, ...)Crop 取 max、Fit 取 min、FillWidth/FillHeight 单边
AsyncImage(ContentScale.Inside) 布局fitInsideSize(...)上游从布局量出 child,桥从算术得出

上游 calculateCropArea 里 frameWidth / frameHeight 两个中间变量没有参与任何实际计算,复刻时不搬——复刻的是生效的数学,不是逐行抄写。像素侧,PixelCropper.cropRgba 按 RGBA_8888 逐行 copyInto 拷出子区域,rect 先防御性 coerceIn 到图界(toInt() 截断在边缘最多差 1 像素,clamp 兜住它)——与上游 Bitmap.createBitmap / Skia.extractSubset 输出像素一致,差别只在编码器。

1.4 页面边界
标题区          krop · OpenHarmony / 副标题行(KMP 图片裁剪库 · Kotlin/Native ohosArm64/ohosX64)
状态行          最近一次操作结果(如"状态 scale=1.33 tx=0.0 ty=0.0 | rect=150,0,900,900")
裁剪容器        固定尺寸 Stack(1:1/4:3/3:4 三档),内含 Image 变换层 + 九宫格 Canvas 覆盖层
手势区          绑定容器:PinchGesture + PanGesture(并行组)+ 双击 TapGesture
contentScale    Fit / Crop / FillWidth / FillHeight 四档按钮
宽高比区        1:1 / 4:3 / 3:4 三档按钮
裁剪按钮        调 kropCrop,成功后解码 PNG 显示在结果区

页面 aboutToAppear 加载 rawfile 内置样例图(1200×900,渐变 + 网格 + 四角红块 + 中心 “KROP” 点阵字,便于肉眼核对裁剪位置),并做首次 kropState(不带 scale,由 KN 按 contentScale 取初始状态)。

第 2 阶段:目标与依赖打通

2.1 工具链钉版(为什么是 2.2.21-1.0.0 + 8.14.1)

Krop 与 KStore 这类"纯 Kotlin + kotlinx 制品线"的库不同:它的 commonMain 依赖的是 Compose 与 Coil,而社区 CMP 目前是 1.9.2、coil3 没有 ohos 制品——上游 Compose 1.6.11 体系没有可换线的对应物,所以不存在"制品升 2.4 线"的问题,直接采用社区已过审的钉法:Kotlin 2.2.21-1.0.0 + Gradle 8.14.1(wrapper 从社区已审仓库同款整体替换,含 wrapper jar)。

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_krop"

include(":krop-core")
include(":example:nativeApp")

gradle/libs.versions.toml 收敛到只剩 KMP 插件一个条目,AGP / Compose / Coil / Ktor 全部剥离:

[versions]
kotlin = "2.2.21-1.0.0"

[plugins]
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
2.2 编译纯几何内核

krop-core/build.gradle.kts 收敛为纯 KMP 模块:

plugins {
    alias(libs.plugins.kotlin.multiplatform)
}

kotlin {
    applyDefaultHierarchyTemplate()
    jvmToolchain(21)

    jvm()            // 交叉验证用
    ohosArm64()
    ohosX64()

    sourceSets {
        commonMain.dependencies {}   // 零依赖
    }
}

实测:

./gradlew :krop-core:compileKotlinJvm                                    # BUILD SUCCESSFUL
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64

composeMain 不在任何 target 的源集里,JVM 编译器也看不到 androidx.compose / coil3;commonMain 里只有 CropModel 和 PixelCropper 两个纯 Kotlin 文件。根构建全局禁用 ohos 测试任务:

subprojects {
    tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
        .configureEach { enabled = false }
}

第 3 阶段:桥协议建模

3.1 JSON 契约
io.keeppro.CropModel / PixelCropper(commonMain,纯 Kotlin)
        ↓ 裁剪几何 / 行拷贝
Bridge.kt(@CName × 2,扁平 JSON 进出)
        ↓ UTF-8 JSON
N-API kropState() / kropCrop()
        ↓
ArkUI Index.ets(手势采样 + 渲染,零几何公式)

两个导出都吃一份扁平 JSON、回一份扁平 JSON:

kropState 请求 : {"imageWidth":1200,"imageHeight":900,"containerWidth":320,"containerHeight":320,
                  "contentScale":"Crop","minScale":1,"maxScale":8,
                  "scale":1.33,"translateX":0,"translateY":0,"zoom":1,"dx":0,"dy":0,"clamp":false}
kropState 响应 : {"ok":true,"scale":1.3333333,"translateX":0,"translateY":0,
                  "childWidth":320,"childHeight":240,"minScale":1,"maxScale":8,
                  "window":{"x":-53.33,"y":0,"width":320,"height":320},
                  "rect":{"x":150,"y":0,"width":900,"height":900}}
kropCrop 请求  : 同状态字段 + "pixelsBase64"(整张原图 RGBA_8888 的 Base64)
kropCrop 响应  : {"ok":true,"rect":{...},"width":900,"height":900,"bytes":...,"pngBase64":"..."}
失败           : {"ok":false,"error":"..."}

字段语义的四个关键点:

  • scale 省略即初始态:首次调用不带 scale,桥按 contentScale 算初始值(对应上游 LaunchedEffect 首次执行);后续调用传绝对值。
  • zoom / dx / dy 是相对量:对应上游 onZoomChange / drag,桥内先乘后加,顺序与上游手势处理一致。
  • clamp 是收尾语义:上游 Animatable.updateBounds 只约束后续动画,惯性衰减自然落回界内;桥内 updateTransform 不钳制平移(允许瞬时越界),clamp=true 是显式"拉回界内"。手势进行中传 false,手势结束的最后一次调用传 true。
  • 单位一致性(vp):containerWidth/Height、translateX/Y、dx/dy 全用 vp;图像像素只通过 originalImageWidth / childWidth 的比例进入 rect,不需要 vp↔px 换算,但混用单位窗口和 rect 会整体错位。
3.2 为什么桥是无状态的、数学在 KN

ArkTS 每次手势变化组一份状态 JSON 调 kropState,KN 侧按请求重建 CropModel:先 fitInsideSize 定 child、按 contentScale 取初始 scale(或采用请求里的绝对 scale),再 onZoomChange → 平移 → updateBounds → 可选 clampTranslation → calculateCropWindow → cropRect。这条处理顺序与上游 Croppable 手势处理逐条对应。无状态设计让 C ABI 保持 const char* -> const char* 纯函数形态,也和上游"每次布局变化重算"的 LaunchedEffect 行为一致;代价只是快速手势时每帧一次 FFI 调用,NAPI 调用本身毫秒级,实测流畅。

所有裁剪几何公式都在 KN 侧的 CropModel 里——这是本适配最重要的纪律。ArkTS 只做两件事:采样手势(数字)、渲染响应(数字)。在 ArkTS 手写任何一条钳制/窗口/rect 公式,迟早和其它平台漂移,CMP 适配就失去意义。

3.3 错误通道

每个导出整体 try/catch,Kotlin 异常永远不跨 FFI:

@CName("KropState")
fun kropState(request: String): String = try {
    /* 几何处理 */
    stateOk(model)
} catch (t: Throwable) {
    """{"ok":false,"error":${jsonString(t.message ?: t.toString())}}"""
}

非法 JSON、缺必填字段、像素缓冲区不足 w*h*4,全部走同一个 ok:false 通道,ArkTS 侧统一渲染到状态行,不崩页面。

第 4 阶段:原生桥接(技术难点)

4.1 三层桥接
ArkTS
  │ JSON string
  ▼
C++ N-API entry(libkrop_napi.so)
  │ const char*
  ▼
Kotlin/Native C ABI(libohoskrop.so)
  │ KropState / KropCrop
  ▼
UTF-8 JSON + DisposeString

直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;只导出矩阵又会让页面复刻裁剪语义,所以选择 C ABI + JSON。

4.2 Kotlin/Native 导出函数
@CName("KropState")
fun kropState(request: String): String   // 手势 → 变换 + 窗口 + rect

@CName("KropCrop")
fun kropCrop(request: String): String    // 几何 → 裁剪 PNG
C ABI 符号ArkTS 方法覆盖内核
KropStatekropState(req)fitInsideSize + initialScaleFor + bounds / window / rect 全套几何
KropCropkropCrop(req)cropRect() + PixelCropper 行拷贝 + PngEncoder
4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
    {"kropState", nullptr, NapiKropState, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
    {"kropCrop", nullptr, NapiKropCrop, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};

每个回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohoskrop_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。API 26 的 NAPI 头把 napi_get_value_string_utf8 的缓冲区标成非 const,正确做法是先量长度再开 std::vector<char>,拷完再构造 std::string。

4.4 CMake 和类型声明
add_library(ohoskrop SHARED IMPORTED)
set_target_properties(ohoskrop PROPERTIES
  IMPORTED_LOCATION ${KN_SO}
  IMPORTED_NO_SONAME TRUE)

add_library(krop_napi SHARED napi_init.cpp)
target_link_libraries(krop_napi PUBLIC
  libace_napi.z.so
  libhilog_ndk.z.so
  ohoskrop)

OHOS_ARCH 为空或 libohoskrop.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。

export const kropState: (request: string) => string;
export const kropCrop: (request: string) => string;

第 5 阶段:ArkUI 宿主

5.1 手势映射
GestureGroup(GestureMode.Parallel,
  PinchGesture()
    .onActionStart(() => { this.pinchStartScale = this.imgScale; /* 记录平移 */ })
    .onActionUpdate((event: GestureEvent) => {
      const targetScale: number = this.pinchStartScale * event.scale;
      this.applyState(true, targetScale, this.activePanX, this.activePanY, false);
    })
    .onActionEnd(() => { this.clampToEnd(); }),
  PanGesture()
    .onActionUpdate((event: GestureEvent) => {
      this.activePanX = this.panStartX + event.offsetX;   // 绝对平移
      this.activePanY = this.panStartY + event.offsetY;
      this.applyState(true, this.imgScale, this.activePanX, this.activePanY, false);
    })
    .onActionEnd(() => { this.clampToEnd(); }),
  TapGesture({ count: 2 })
    .onAction((event: GestureEvent) => { this.onDoubleTap(event); })
)

onActionEnd 统一以当前显示值重调一次 clamp:true 归位,对应上游 fling 结束落在界内的效果。双击按上游 animateDoubleTap 的 distance 数学换算:translate += (容器中心 - 点击点),在 fit scale 与 2.5 倍之间切换。注意当前 SDK 的 GestureEvent 类型上没有 x / y,双击坐标取 event.fingerList[0].localX / localY(手指相对组件坐标,语义一致),列表为空时兜底容器中心。

5.2 页面交互与渲染

渲染层严格"只消费 KN 的数字":

Stack() {
  Image(this.pixelMap)
    .width(this.childW).height(this.childH)          // KN 返回的 child 尺寸,Stack 内居中
    .scale({ x: this.imgScale, y: this.imgScale })   // KN 返回
    .translate({ x: this.translateX, y: this.translateY })
  Canvas(this.canvasContext)                          // 九宫格覆盖层
    .width(this.winW).height(this.winH)
    .position({ x: -this.winX, y: -this.winY })      // 对应上游 CropHintWindow 的 -topLeft
}
.width(this.containerW).height(this.containerH).clip(true)

九宫格覆盖层画外边框 + 3×3 网格线(白色半透明,对应上游 CropHint.Default 的 SudokuGrid),window.x/y 为负时覆盖层自动居中——与上游"子缩放后小于容器时 offset 为负"的行为逐位对应。裁剪结果经 Base64Helper.decodeSync → image.createImageSource → createPixelMap 显示在结果区,状态行同步 裁剪成功 rect=... png=...B。

5.3 对比度和错误边界

主操作裁剪按钮高对比蓝底白字,contentScale 选中档深底白字、比例选中档青绿底白字,语义与主次对齐;ok:false 时错误原文进状态行,Native 调用本身抛错(理论不该发生)同样被 try/catch 收进状态行,页面不崩。

第 6 阶段:示例与验证

6.1 工程结构
example/
├── nativeApp/
│   ├── build.gradle.kts                      双 ABI sharedLib(baseName=ohoskrop)+ cxxShared + copyKnSo*
│   └── src/ohosMain/kotlin/
│       ├── Bridge.kt                         @CName × 2 + MiniJson(扁平 JSON,继承自 cmp_qrose 审核模式)
│       ├── Cropper.kt                        CropRect → PixelCropper → PngEncoder 管线胶水
│       └── PngEncoder.kt                     最小 PNG(color type 6 RGBA、filter 0、zlib stored)
└── harmonyApp/
    ├── build-profile.json5                   signingConfigs 留空(签名由 DevEco 自动生成回填)
    ├── AppScope/app.json5                    bundleName org.terminator.ohos.krop、versionName 1.0.5-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/libohoskrop_api.h
        ├── src/main/cpp/types/libkrop_napi/{index.d.ts, oh-package.json5}
        ├── src/main/ets/pages/Index.ets      ArkUI 验收页面
        └── src/main/resources/rawfile/sample.png  1200×900 测试图(渐变+网格+KROP 点阵字)
6.2 构建、签名和安装
./gradlew :krop-core:compileKotlinJvm
./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 中勾选自动生成,证书写入本机 ~/.ohos/config,不进入仓库(仓库内 build-profile.json5 的 signingConfigs 保持空数组,勾选自动签名后由 IDE 回填带 material 的完整配置)。

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.krop -m entry

四、完整代码对照

4.1 整体架构

krop-core/src/composeMain/**(上游 Compose/Coil 源码,隔离保留不编译)
    │ Croppable / CroppableState / CropHint / CropHintWindow / SudokuGrid
krop-core/src/commonMain/**(新增纯 Kotlin,jvm + 双 ohos 编译)
    │ CropModel(几何) + PixelCropper(行拷贝)
Bridge.kt + Cropper.kt + PngEncoder.kt
    │ KropState / KropCrop
libkrop_napi.so
    │ kropState / kropCrop
Index.ets
    │ 手势采样 + JSON parse + @State 更新
    ▼
ArkUI 真机页面(变换层 + 九宫格覆盖层 + 裁剪结果区)

4.2 文件清单

文件职责
settings.gradle.kts极简 ohos 构建、eazytec Maven 置前、ohos_krop、上游示例移出
gradle/libs.versions.tomlKotlin 2.2.21-1.0.0 单条目,AGP/Compose/Coil 全剥离
gradle.propertiesorg.gradle.java.home(Temurin 21)、version=1.0.5-ohos.1、KN 下载开关
build.gradle.kts(根)版本 1.0.5-ohos.1、全局禁用 Ohos*Test* 任务
krop-core/build.gradle.kts纯 KMP:jvm() + ohosArm64 + ohosX64,commonMain 零依赖
krop-core/.../CropModel.kt裁剪几何纯字段模型(bounds / window / rect / 四档初始 scale / fitInside)
krop-core/.../PixelCropper.ktRGBA_8888 行拷贝裁剪(防御性 clamp)
example/nativeApp/build.gradle.kts双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填
nativeApp/.../Bridge.kt2 个 C ABI、无状态模型重建、MiniJson、错误通道
nativeApp/.../PngEncoder.ktzlib stored 最小 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鸿蒙适配主文档(16 节)

4.3 关键 API 对照

层次上游 API鸿蒙对应说明
状态rememberCroppableState(minScale, maxScale, contentScale)minScale / maxScale / contentScale 请求字段上游 maxScale 默认 Float.MAX_VALUE,桥默认 8,可传大值还原
状态CroppableState.scale / translateX / translateY(Animatable)响应 scale / translateX / translateY纯字段,动画插值由宿主完成
状态snapScaleTo / onZoomChange / dragscale + zoom / translateX/Y + dx/dy先乘后加,与上游手势顺序一致
状态getBounds() / updateBounds()桥内部,影响 clamp 与 window不直接出现在响应里
窗口calculateCropArea() / cropWindow响应 window容器坐标,子小于容器时 offset 为负
裁剪crop() 的 rect 换算响应 rect + kropCropratio*x/scale 后 toInt,与上游逐位一致
裁剪Krop + cropBitmap expect/actualPixelCropper + PngEncoder同像素不同编码器
布局AsyncImage(ContentScale.Inside)fitInsideSize上游从布局量 child,桥从算术得 child
布局ContentScale.Fit/Crop/FillWidth/FillHeightinitialScaleFor 四档与上游 LaunchedEffect 公式一致
UICroppable 双指/拖动/双击ArkUI Pinch/Pan/Tap(count:2)手势采样在 ArkTS,数学在 KN
UICropHint + CropHintWindow + SudokuGridCanvas 覆盖层按 window 定位颜色线宽在 ArkTS 配
Native—KropState / KropCropC ABI JSON 导出
N-API—kropState(req) / kropCrop(req)向 ArkTS 暴露能力

4.4 ArkTS 与 Kotlin 的边界

// ArkTS:手势收尾,喂状态、拿窗口
const st = JSON.parse(kropState(JSON.stringify({
  imageWidth: 1200, imageHeight: 900,
  containerWidth: 320, containerHeight: 320,
  contentScale: 'Crop', minScale: 1, maxScale: 8,
  scale: cur.scale, translateX: cur.translateX, translateY: cur.translateY,
  zoom: 1, dx: 0, dy: 0, clamp: true
})));
// st.window → 九宫格覆盖层   st.rect → 状态行   st.scale/translate → Image transform
// Kotlin/Native:钳制、窗口、rect 全在 CropModel
val model = CropModel(minScale = 1f, maxScale = 8f)
val (childW, childH) = CropModel.fitInsideSize(1200, 900, 320, 320)   // 320×240
model.prepareImage(1200)
model.updateContainerAndChildSize(320, 320, childW, childH)
model.snapScaleTo(CropModel.initialScaleFor(ContentScaleMode.Crop, 320, 320, childW, childH))
// …onZoomChange / drag / updateBounds / clampTranslation…
model.calculateCropWindow()
val rect = model.cropRect()   // 150, 0, 900, 900 —— 与 Android/iOS/Desktop 逐位一致

两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构;ArkTS 侧不出现任何一条裁剪几何公式。

五、关键决策说明

决策 1:composeMain 隔离 + 纯字段模型,而不是硬编 Compose 或重写交互

上游公开 API 的每个成员都踩在 androidx.compose / coil3 上,社区 CMP 1.9.2 与上游 Compose 1.6.11 工具链对不齐,coil3 无 ohos 制品——硬编必死。但把交互在 ArkTS 重写一遍也不行:裁剪库的价值在"同一输入同一裁剪矩形",几何公式散落在 ArkTS 迟早漂移。最终形态是:Compose 源码零改动进 composeMain 隔离保留;CropModel 把生效数学逐条复刻成纯字段(Animatable 剥成 Float,setter 里完成 coerce),KN 与 JVM 跑同一份实现;ArkUI 手势只当采样器。

决策 2:工具链钉 2.2.21-1.0.0 + Gradle 8.14.1(不跟 KStore 的 2.4 线)

工具链版本跟着依赖走:KStore 升 2.4.20 线是因为 kotlinx ohos 制品线的 stdlib 元数据是 2.4;Krop 的 commonMain 零三方依赖,不需要任何 ohos 制品线,直接采用社区已过审的 2.2.21-1.0.0 + 8.14.1 钉法,wrapper 从已审仓库同款整体替换。两条工具链线的经验不能互相套用——2.2.21 旧线必须 Gradle 8.14.1,只有 2.4.20 线才支持 9.x。

决策 3:根构建替换(cmp_qrose 模式),上游示例与平台 actual 留盘

上游根构建(vanniktech 发布 / AGP classpath / Compose 编译器插件 / maven-publish)对鸿蒙没有保留价值,整个替换为极简 ohos 构建。sample-multiplatform、iosApp 依赖 Compose/Coil 生态,androidMain / nonAndroidMain 的 cropBitmap actual 依赖 Bitmap / Skia——都不进 settings.gradle.kts,留盘不参与 sync。composeMain 同理"留盘不编译",但它还多一层使命:等社区出现与上游对齐的 compose.ui ohos 制品,把 5 个文件迁回源集即可让 Croppable 直接跑,CropModel 降级为几何内核。

决策 4:像素走 Base64 JSON 通道,裁剪结果回 PNG

KN 侧没有图片解码器(没有 Skia、没有 ImageKit),文件解码由 ArkTS 的 createPixelMap(显式 desiredPixelFormat = RGBA_8888)一行完成,业务图往往也已经在 PixelMap 里——桥收已解码 RGBA 字节,保持 KN 零依赖,与上游 cropBitmap 收已解码 bitmap 的定位一致。1200×900 RGBA 约 4.32MB、Base64 后约 5.76MB,demo 尺寸完全可接受;crop() 返回 PNG 字节的语义用 zlib stored 最小 PNG 原样保留(KN 没有 java.util.zip,不值得为 demo 尺寸引入压缩库)。生产大图可换 ArrayBuffer/指针 ABI,几何协议不用动。

决策 5:jvm() 交叉验证 + 双 ABI 出库

jvm() 目标保留,让 CropModel 的几何在 JVM 上可跑、可与上游 CroppableState 公式逐条对账——这是"几何没漂"的闸门。ohosArm64 + ohosX64 双 ABI 都出:真机 arm64、DevEco 真机 x86_64,缺哪个 ABI 的 so 哪边就装不上(失败码 9568347)。sharedLib 只放 example/nativeApp,库模块只出 KLIB,避免每个模块吐一份带运行时的 so。

六、测试与验证

6.1 测试环境

本次验证使用 Windows(x86_64)、JDK 21(Eclipse Temurin,org.gradle.java.home 钉版)、DevEco Studio 5.1.0 Release、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙真机(arm64)。

6.2 编译层与几何对账

:krop-core:compileKotlinJvm: BUILD SUCCESSFUL
linkDebugSharedOhosArm64: BUILD SUCCESSFUL → arm64-v8a/libohoskrop.so ≈3.77MB + libc++_shared.so ≈1.85MB
linkDebugSharedOhosX64:  BUILD SUCCESSFUL → x86_64/libohoskrop.so  ≈3.48MB + libc++_shared.so ≈1.73MB
llvm-nm -D: KropState / KropCrop 均为全局 T 符号
BuildNativeWithCmake: finished(双 ABI 编出 libkrop_napi.so)
CompileArkTS: finished
PackageHap: finished
install bundle successfully
start ability successfully

几何对账用真机状态行的数据反向验证公式(contentScale=Crop、容器 1:1 320×320、样例图 1200×900):

fitInsideSize(1200,900,320,320) → child = 320×240          (Inside 布局等价)
initialScaleFor(Crop)           → max(320/320, 320/240) = 4/3 ≈ 1.33   (状态行 scale=1.33 ✓)
startPoint                      → x = (320×4/3 − 320)/2 = 53.33,y = (240×4/3 − 320)/2 = 0
ratio                           → 1200/320 = 3.75
cropRect                        → (3.75×53.33÷(4/3), 0, 3.75×320÷(4/3), 3.75×320÷(4/3))
                                → 150, 0, 900, 900                      (状态行 rect=150,0,900,900 ✓)

真机返回值与手算逐位一致:Crop 档在 1:1 容器下裁掉左右各 150 原图像素、保留整高——CropModel 与上游 CroppableState 的数学没有漂移。

6.3 HAP 验证

HAP 内含双 ABI 各三件套(libkrop_napi.so / libohoskrop.so / libc++_shared.so);copyKnSo* 任务在 link 后自动把 KN so 与对应 triple 的 libc++ 回填到 entry/libs/<abi>/。abiFilters 必须双 ABI 全写,缺当前 ABI 的 so 安装阶段直接失败码 9568347;缺 NAPI so 则 ArkTS 报 does not provide an export named 'kropState'。

6.4 功能用例

  1. 启动加载 rawfile 样例图(1200×900 RGBA),首次 kropState 不带 scale,返回 Fit 初始态 scale=1.0、childWidth/Height = 320/240;
  2. 切 contentScale=Crop(1:1 容器),状态行变 scale=1.33 … rect=150,0,900,900,与 6.2 节手算逐位一致;
  3. 双指缩放,Image transform 与九宫格窗口实时跟随,scale 不越 [minScale, maxScale];
  4. 拖动超出边界后松手,clamp=true 收尾把平移拉回 bounds(对应上游 fling 落界内);
  5. 双击在 fit 与 2.5 倍间切换,点击点居中(上游 animateDoubleTap 的 distance 数学);
  6. 点裁剪,kropCrop 返回的 PNG 尺寸与 rect 一致,解码显示在结果区,状态行 裁剪成功 rect=… png=…B;
  7. 切 4:3 / 3:4 容器比例,window 与 rect 随容器正确换算;
  8. 每步操作状态行按 scale / tx / ty | rect 滚动留痕,异常走 error 通道不崩页面。

6.5 验证结论

裁剪几何模型、像素裁剪管线、Native 动态库、C ABI、N-API、ArkUI 手势页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实的裁剪会话:手势缩放拖动、九宫格窗口跟随、Crop 档初始态与手算逐位一致、裁剪 PNG 闭环输出(见下节效果图)。

七、运行效果

7.1 真机截图

在这里插入图片描述

7.2 命令速查

git clone https://atomgit.com/oh-tpc/Krop.git
cd Krop
export JAVA_HOME="/path/to/jdk-21"
./gradlew :krop-core:compileKotlinJvm                                          # 几何内核 JVM 编译
./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.krop -m entry
hdc shell hilog | grep Krop

八、遗留问题与改进方向

8.1 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配,必须让裁剪几何真正编译成 ohosArm64 / ohosX64 动态库,且 rect 与其它平台逐位一致;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. composeMain 不是 Kotlin 源集——这正是隔离的原理,千万不要把它注册回 sourceSets,一注册 ohos 编译立刻爆成片 Unresolved reference: androidx;
  4. Animatable / VelocityTracker 不能跨语言:模型必须剥成纯 Float 字段,动画与惯性留给宿主,clamp 承担"落回界内"的收尾语义;
  5. ArkUI 组件基类 CustomComponent 自带 .scale() 属性方法,@State scale: number 与之同名会报编译错 10505001 Property 'scale' … not assignable——状态变量改名 imgScale 即解;
  6. 当前 SDK 的 GestureEvent 类型上没有 x / y,双击坐标要取 event.fingerList[0].localX / localY(手指相对组件坐标,语义不变),列表为空兜底容器中心;
  7. ArkTS 的 getContext(this) 在新 SDK 上有 deprecation 警告,换成 this.getUIContext().getHostContext();
  8. CAdapter 链接期有一条 clang 警告(api.cpp: non-void function does not return a value in all control paths),是 HarmonyOS Kotlin 生成器产出的 napi 胶水问题,可忽略,.so 正常链出;
  9. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();napi_get_value_string_utf8 缓冲区非 const,先量长度再开 std::vector<char>;
  10. readPixelsToBuffer 的 buffer 尺寸必须精确等于 w×h×4,createPixelMap 显式 desiredPixelFormat = RGBA_8888,否则桥端像素校验 pixels buffer too small 或颜色错乱;
  11. HAP 缺当前 ABI 的 so 报安装失败 9568347,abiFilters 必须双 ABI,每个 ABI 三件套缺一不可;
  12. 签名材料由 DevEco 自动写入 build-profile.json5,提交前注意保护,证书绝不进入 AtomGit。

8.2 已知问题

  • 鸿蒙侧没有 Compose UI:Croppable 手势组件本体不参与 ohos 编译,交互由 ArkUI 手势 + KN CropModel 等价实现,composeMain 是隔离保留而非已运行;
  • 双击动画以瞬时切换近似:上游 animateDoubleTap 的 spring 过渡没有等效物,业务要动画可在 ArkTS 侧分多帧调 kropState 补插值;
  • fling 惯性未实现:上游 VelocityTracker + exponentialDecay 的滑行未桥接,以 clamp=true 收尾等价(位置合法,无滑行);
  • 像素经 Base64 JSON 传输:大图有明显的内存与拷贝开销(4.32MB 像素 → 约 5.76MB 字符串,往返各一份);
  • PNG 用 zlib stored 未压缩:大裁剪区域体积大,输出约等于原始 RGBA 字节数,与上游平台编码器压缩率不可比;要瘦身只换 IDAT 内部实现,JSON 字段不动;
  • 像素格式固定 RGBA_8888,桥不做格式协商;
  • cropBitmap 各平台 actual 与 PixelCropper 是"同像素不同编码器"关系,对账时比像素,不要比 PNG 字节。

8.3 未来优化方向

  • 惯性动画下沉:先在 ArkTS 帧循环按 exponentialDecay 逐帧调 kropState,长期把 VelocityTracker + 指数衰减移植成纯 Kotlin 放进 CropModel(桥加 velocityX/velocityY 字段),ArkTS 只上报速度;
  • 大图 ArrayBuffer 化:C ABI 加指针参数(napi_get_typedarray_info 取数据指针 + KN usePinned),去掉 4MB 级字符串往返,几何协议不变;
  • 评估 .har 形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套;
  • 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
  • 等社区出现与上游 Compose 1.6.11 对齐的 compose.ui ohos 制品,把 composeMain 5 个文件迁回源集,让 Croppable 直接在 Compose 里跑,CropModel 降级为几何内核。

九、总结

9.1 核心难点回顾

上游 krop-core(Compose/Coil 全依赖)
    → 确认 compose/coil3 无 ohos 制品,锁定 cmp_qrose 路径
    → 工具链对齐 Kotlin 2.2.21-1.0.0 + Gradle 8.14.1(根构建替换)
    → 5 个 Compose 文件迁 composeMain(内容零改动,非源集天然隔离)
    → CropModel 纯字段几何复刻 + PixelCropper 行拷贝(commonMain 零依赖)
    → ohosArm64 / ohosX64 KLIB + 双 ABI 动态库
    → C ABI(@CName × 2)+ 无状态 JSON 契约
    → C++ N-API(DisposeString 纪律)
    → ArkUI 手势宿主(采样器 + 渲染器,零几何公式)
    → HAP、签名、真机验收(rect 与公式手算逐位一致)

9.2 封装层次

  • 几何层:CropModel 纯字段模型,bounds / window / rect / 四档初始 scale 与上游逐条对应;
  • 像素层:PixelCropper RGBA 行拷贝 + PngEncoder 最小 PNG,对应上游 cropBitmap actual;
  • Native 层:生成双 ABI 动态库并通过 2 个 C ABI 输出 JSON;
  • N-API 层:完成参数检查、字符串转换和 Native 内存释放;
  • ArkTS 层:手势采样、状态管理、九宫格渲染、裁剪出图与错误文本;
  • DevEco 层:完成 CMake、HAP、签名、安装和真机运行。

9.3 三条经验

  1. UI 库的适配边界要切在"数学与渲染"的接缝上:裁剪库的公开 API 全是 Compose 类型,但生效逻辑是纯几何——把数学复刻进 KN、把手势留给 ArkUI,既绕开了 Compose 工具链,又守住了"同一输入同一裁剪矩形"的验收底线;路线选择必须基于当天的制品可达性,而不是历史结论;
  2. 工具链版本跟着依赖走:commonMain 零三方依赖的库直接用社区已审钉法(2.2.21-1.0.0 + Gradle 8.14.1),有 kotlinx 制品线需求的才升 2.4 线(两条线的 Gradle 经验不通用);
  3. 交叉验证要有可计算的锚点:真机状态行的 scale=1.33 rect=150,0,900,900 与公式手算逐位一致,比"能跑"两个字有说服力得多——CMP 适配的验收底线从来不是"界面出来了",而是与其它平台逐位一致。

9.4 适配成果

当前适配已完成:

  • Krop 1.0.5 裁剪几何的双 ABI Kotlin/Native 编译(上游 5 个 Compose 文件零改动隔离保留,新增 CropModel / PixelCropper 两个纯 Kotlin 源文件);
  • 2 个 C ABI 符号和 C++ N-API 包装层(llvm-nm 验证全局 T 符号);
  • DevEco Stage ArkUI 单页验收页面(双指缩放、拖动、双击、九宫格窗口、contentScale 四档、容器三档比例、裁剪 PNG 闭环);
  • JVM 编译层 + 双 ABI 链接层 + 真机几何对账验证(scale / rect 与公式手算逐位一致)、HAP 构建、签名、设备安装和真机验证;
  • 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开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐