开源鸿蒙平台 CMP 三方库「Krop」适配全流程
本文记录
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()把窗口换算回原图像素矩形再经cropBitmapexpect/actual(Android 走android.graphics.Bitmap,iOS/桌面走 Skia)编码成 PNG。本次适配与存储类库"补一个 dispatcher 就能真编译"的路线不同:Krop 的公开 API 每一个成员都踩在androidx.compose.*/coil3.*上(Animatable、VelocityTracker、pointerInput、BoxWithConstraints、coil3Image,且 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 / childHeight | ContentScale.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 手势 + KNCropModel等价实现——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 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 整体替换;2.2.21 线的社区钉法) |
| 依赖 | commonMain 零依赖——裁剪几何不需要任何三方库,这是它比存储/网络类库好适配的根本原因 |
| DevEco product | 5.1.0(18) 兼容 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.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/androidMain | 1 | android.graphics.Bitmap | 留盘不进构建(androidTarget() 不再声明) |
krop-core/src/nonAndroidMain | 1 | org.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 四档初始 scale | initialScaleFor(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 方法 | 覆盖内核 |
|---|---|---|
KropState | kropState(req) | fitInsideSize + initialScaleFor + bounds / window / rect 全套几何 |
KropCrop | kropCrop(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.toml | Kotlin 2.2.21-1.0.0 单条目,AGP/Compose/Coil 全剥离 |
gradle.properties | org.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.kt | RGBA_8888 行拷贝裁剪(防御性 clamp) |
example/nativeApp/build.gradle.kts | 双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填 |
nativeApp/.../Bridge.kt | 2 个 C ABI、无状态模型重建、MiniJson、错误通道 |
nativeApp/.../PngEncoder.kt | zlib stored 最小 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 | 鸿蒙适配主文档(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 / drag | scale + zoom / translateX/Y + dx/dy | 先乘后加,与上游手势顺序一致 |
| 状态 | getBounds() / updateBounds() | 桥内部,影响 clamp 与 window | 不直接出现在响应里 |
| 窗口 | calculateCropArea() / cropWindow | 响应 window | 容器坐标,子小于容器时 offset 为负 |
| 裁剪 | crop() 的 rect 换算 | 响应 rect + kropCrop | ratio*x/scale 后 toInt,与上游逐位一致 |
| 裁剪 | Krop + cropBitmap expect/actual | PixelCropper + PngEncoder | 同像素不同编码器 |
| 布局 | AsyncImage(ContentScale.Inside) | fitInsideSize | 上游从布局量 child,桥从算术得 child |
| 布局 | ContentScale.Fit/Crop/FillWidth/FillHeight | initialScaleFor 四档 | 与上游 LaunchedEffect 公式一致 |
| UI | Croppable 双指/拖动/双击 | ArkUI Pinch/Pan/Tap(count:2) | 手势采样在 ArkTS,数学在 KN |
| UI | CropHint + CropHintWindow + SudokuGrid | Canvas 覆盖层按 window 定位 | 颜色线宽在 ArkTS 配 |
| Native | — | KropState / KropCrop | C 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 功能用例
- 启动加载 rawfile 样例图(1200×900 RGBA),首次
kropState不带scale,返回 Fit 初始态scale=1.0、childWidth/Height = 320/240; - 切 contentScale=Crop(1:1 容器),状态行变
scale=1.33 … rect=150,0,900,900,与 6.2 节手算逐位一致; - 双指缩放,
Imagetransform 与九宫格窗口实时跟随,scale不越[minScale, maxScale]; - 拖动超出边界后松手,
clamp=true收尾把平移拉回 bounds(对应上游 fling 落界内); - 双击在 fit 与 2.5 倍间切换,点击点居中(上游
animateDoubleTap的 distance 数学); - 点裁剪,
kropCrop返回的 PNG 尺寸与rect一致,解码显示在结果区,状态行裁剪成功 rect=… png=…B; - 切 4:3 / 3:4 容器比例,
window与rect随容器正确换算; - 每步操作状态行按
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 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配,必须让裁剪几何真正编译成
ohosArm64/ohosX64动态库,且 rect 与其它平台逐位一致; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;composeMain不是 Kotlin 源集——这正是隔离的原理,千万不要把它注册回sourceSets,一注册 ohos 编译立刻爆成片Unresolved reference: androidx;Animatable/VelocityTracker不能跨语言:模型必须剥成纯Float字段,动画与惯性留给宿主,clamp承担"落回界内"的收尾语义;- ArkUI 组件基类
CustomComponent自带.scale()属性方法,@State scale: number与之同名会报编译错10505001 Property 'scale' … not assignable——状态变量改名imgScale即解; - 当前 SDK 的
GestureEvent类型上没有x/y,双击坐标要取event.fingerList[0].localX / localY(手指相对组件坐标,语义不变),列表为空兜底容器中心; - ArkTS 的
getContext(this)在新 SDK 上有 deprecation 警告,换成this.getUIContext().getHostContext(); - CAdapter 链接期有一条 clang 警告(
api.cpp: non-void function does not return a value in all control paths),是 HarmonyOS Kotlin 生成器产出的 napi 胶水问题,可忽略,.so正常链出; - KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free();napi_get_value_string_utf8缓冲区非 const,先量长度再开std::vector<char>; readPixelsToBuffer的 buffer 尺寸必须精确等于w×h×4,createPixelMap显式desiredPixelFormat = RGBA_8888,否则桥端像素校验pixels buffer too small或颜色错乱;- HAP 缺当前 ABI 的 so 报安装失败
9568347,abiFilters必须双 ABI,每个 ABI 三件套缺一不可; - 签名材料由 DevEco 自动写入
build-profile.json5,提交前注意保护,证书绝不进入 AtomGit。
8.2 已知问题
- 鸿蒙侧没有 Compose UI:
Croppable手势组件本体不参与 ohos 编译,交互由 ArkUI 手势 + KNCropModel等价实现,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取数据指针 + KNusePinned),去掉 4MB 级字符串往返,几何协议不变; - 评估
.har形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套; - 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
- 等社区出现与上游 Compose 1.6.11 对齐的
compose.uiohos 制品,把composeMain5 个文件迁回源集,让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 与上游逐条对应; - 像素层:
PixelCropperRGBA 行拷贝 +PngEncoder最小 PNG,对应上游cropBitmapactual; - Native 层:生成双 ABI 动态库并通过 2 个 C ABI 输出 JSON;
- N-API 层:完成参数检查、字符串转换和 Native 内存释放;
- ArkTS 层:手势采样、状态管理、九宫格渲染、裁剪出图与错误文本;
- DevEco 层:完成 CMake、HAP、签名、安装和真机运行。
9.3 三条经验
- UI 库的适配边界要切在"数学与渲染"的接缝上:裁剪库的公开 API 全是 Compose 类型,但生效逻辑是纯几何——把数学复刻进 KN、把手势留给 ArkUI,既绕开了 Compose 工具链,又守住了"同一输入同一裁剪矩形"的验收底线;路线选择必须基于当天的制品可达性,而不是历史结论;
- 工具链版本跟着依赖走:
commonMain零三方依赖的库直接用社区已审钉法(2.2.21-1.0.0 + Gradle 8.14.1),有 kotlinx 制品线需求的才升 2.4 线(两条线的 Gradle 经验不通用); - 交叉验证要有可计算的锚点:真机状态行的
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。
参考文档
更多推荐


所有评论(0)