开源鸿蒙平台 CMP 三方库「Kamel」适配全流程
本文记录
Kamel接入 OpenHarmony 的完整过程,覆盖重 Compose 库的源码集手术、鸿蒙网络移植线选型、加载/缓存引擎编译、ohosArm64/ohosX64目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI Worker 页面、TLS 信任链修复、HAP 构建与设备自动化验收。
Kamel是 Compose Multiplatform 生态里最主流的异步媒体加载库之一:asyncPainterResource/KamelImage两条 API 把 load / cache / decode / display 串成一条管线,Ktor 抓取、内存 LRU + 磁盘 HttpCache 双层缓存、按平台注入的网络引擎。它的适配难题与存储类库(KStore)完全不同——上游一切能力都长在 Compose 上:解码终点是Painter,位图来自 Skiko/Skia,KamelConfig的四个缓存槽装的全是 Compose 类型。社区 Compose for OpenHarmony 移植线与上游 CMP 1.10.0 API 不齐,直接编译 Compose 源码不可行。因此本次适配采取"核心复用,Compose 隔离,系统解码"路线:把纯 Kotlin 的加载/缓存引擎(HttpUrlFetcher、DiskLruCache、DiskCacheStorage、Mapper 体系)原样编进 ohos KLIB,Compose 与 Skia 解码源码物理隔离进src/composeMain不参与编译,ArkTS 侧拿到原始图片字节后交给系统 image kit 解码成PixelMap显示——与社区 cmp_qrose / Vico / Reorderable 已审核路线同宗。
项目地址: AtomGit/oh-tpc/Kamel.git
上游仓库: Kamel-Media/Kamel
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配
Kamel 给 Compose Multiplatform 应用提供"URL → 图片"这一件事的完整答案:KamelConfig 装配 Mapper(String/URL/URI → ktor Url)、Fetcher(HTTP / file:// / 本地文件)、Decoder(位图 / 矢量 / SVG / 动图)与四个类型化缓存槽;asyncPainterResource(data) 返回 Resource<Painter> 流,KamelImage 在 Composable 里消费它并带 onLoading / onFailure / crossfade。网络层由 kamel-image-default 按平台注入 Ktor 引擎(Android→OkHttp、桌面→CIO、Apple→Darwin、JS→js),磁盘缓存挂在同一个 HttpClient 的 HttpCache 插件上,底层是移植自 Coil 的 okio DiskLruCache。
OpenHarmony 应用同样需要这套异步加载能力,但上游构建(Kotlin 2.3.0 / CMP 1.10.0 / AGP 8.12.3 / ktor 3.3.3)不会生成 OpenHarmony 变体,且障碍比存储类库多出一层——解码与显示全部依赖 Compose:
| 障碍 | 具体问题 |
|---|---|
| Compose 版本不齐 | 上游按 CMP 1.10.0 / Kotlin 2.3.0 编写,社区 ohos Compose 移植线版本与之不齐,KamelImage / asyncPainterResource / Skia 解码源码无法为 ohos 编译。 |
| 目标缺失 | 必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。 |
| 工具链代际 | ktor 的 ohos 移植版只有 3.5.1-0.1.0-01 一代(Kotlin 2.4.20 代产物),2.2.x 编译器读不了其 2.4.0 元数据。 |
| 依赖矩阵 | 引擎依赖 ktor / coroutines / okio / cache4k,逐个验证 ohos 变体可达性;cache4k 有元数据无 artifact,必须去除。 |
| 解码层 | 非 Android 平台位图解码走 org.jetbrains.skia.Image(Skiko),隔离 Compose 后不复存在。 |
| TLS 信任链 | curl 引擎在鸿蒙上读不到系统信任库,HTTPS 直接 CURL_PEER_FAILED_VERIFICATION。 |
| 语言边界 | ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。 |
| 线程模型 | 网络 IO 秒级耗时,同步 C ABI 不能占 UI 线程。 |
因此,本项目把边界放在:加载/缓存引擎的 ohos 编译、Compose 源码集手术(cmp_qrose 审核模式)、Ktor curl 引擎接入与 TLS 信任链修复、C ABI/N-API 桥接层。解码交给 HarmonyOS 系统 image kit,ArkTS 只负责调用 Native、解析 JSON、解码字节和展示 PixelMap。
1.2 库提供的能力
Demo 提供一次真实的异步图片加载会话,对应桥接的全部入口:
- 初始化(init):
KamelInit注入应用cacheDir与 CA bundle 路径,一次性配置内存 LRU 容量(默认 100 条)与磁盘 HttpCache 上限(默认 10 MiB); - 加载(load):
KamelLoad走StringMapper → 内存 LRU → HttpUrlFetcher(ktor-curl) → HttpCache 磁盘缓存管线,返回原始图片字节(Base64),ArkTS 侧image.createImageSource → PixelMap解码显示; - 缓存命中:同一 URL 二次加载
source=memory,毫秒级返回; - 缓存观测(cacheInfo):
KamelCacheInfo返回内存缓存条目数 / 上限与磁盘缓存目录,来自真实 Native 状态而非静态文字。
响应包含:
| 字段 | 作用 |
|---|---|
ok | 操作是否成功,失败时 error 带原文。 |
url / source | 回显本次 URL 与数据来源(memory / disk / network / none)。 |
byteCount / elapsedMs | 字节数与 Native 侧耗时,是缓存命中验收的核心指标。 |
base64 | 原始图片字节(PNG / JPEG / WebP / GIF / SVG 原文),解码交给系统 image kit。 |
引擎侧完整能力(HttpUrlFetcher 进度流、DiskLruCache 日志式 LRU、DiskCacheStorage 的 Ktor CacheStorage 实现、Cache / LruCache、Mapper 体系、FileFetcher / FileUrlFetcher)全部在上游 commonMain 里随编译进入 .so,JVM 侧保留测试作交叉验证。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | cache / fetcher / mapper / utils 的全部 commonMain 文件原样编译;Compose 依赖的 6 个主源码 + 5 个测试文件 git mv 物理隔离进 src/composeMain(保留历史,不参与任何 target 编译);ResourceConfig 摘除 Compose 字段(Density / maxBitmapDecodeSize)留在 commonMain。 |
| 平台目标 | 根构建直接声明 ohosArm64 + ohosX64,保留 jvm 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。 |
| 构建隔离 | 根构建替换为极简 ohos 构建(cmp_qrose 模式);kamel-image / kamel-image-default / 全部 decoder / fetcher / mapper 子模块与 kamel-samples 留盘不进构建。 |
| 桥接稳定 | 3 个 C ABI 和 UTF-8 扁平 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI;网络阻塞收进 Worker 线程。 |
| 网络纪律 | 引擎换到 ohos 移植线 ktor 3.5.1-0.1.0-01 + curl;HTTPS 证书经 caInfo(CURLOPT_CAINFO)注入应用内置 Mozilla CA bundle。 |
| UI 完整 | 单页覆盖全部桥接入口:URL 输入、五个预设(位图 ×4 + SVG)、加载 / 缓存信息按钮、状态行验收输出。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。鸿蒙侧显示用 ArkUI
Image+ 系统 image kit 解码;KamelImage/asyncPainterResource留在composeMain,待社区 Compose 移植线与上游 API 对齐后接回。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点 Compose 依赖边界、验证 ktor ohos 移植线、确定交付物
第 2 阶段:源码集手术 ── composeMain 隔离、ResourceConfig 瘦身、新增 Kamel 纯 Kotlin 入口、ohos actual ×5
第 3 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.4.20-0.1.0-07 代际钉版、去 cache4k / 去 skiko
第 4 阶段:桥协议建模 ── 扁平 JSON 契约(init / load / cacheInfo)、MiniJson、错误通道
第 5 阶段:原生桥接 ── @CName C ABI ×3、C++ N-API、Worker 线程、内存释放
第 6 阶段:TLS 与验收 ── CA bundle 注入、HAP、签名、设备安装、自动化截图验收
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
kamel-core/ 加载/缓存引擎(进 ohos 构建,jvm + ohosArm64 + ohosX64)
kamel-core/src/commonMain/ cache / fetcher / mapper / utils + 新增 Kamel 入口(纯 Kotlin)
kamel-core/src/ohosMain/ 新增:File/URL/URI、FileFetcher、FileUrlFetcher、Mappers、httpCache actual ×5
kamel-core/src/composeMain/ Compose 隔离区(6 个主文件 + 5 个测试文件,git mv 保留历史)
kamel-image / kamel-image-default / Compose UI 与解码器,留盘不进 settings
kamel-decoder/* / kamel-fetcher/* / kamel-mapper/* / kamel-samples
example/nativeApp/ Kotlin/Native 动态库(Bridge + MiniJson + copyKnSo)
example/harmonyApp/ DevEco Stage、CMake N-API、Worker、ArkTS 页面
README.OpenHarmony.md / _CN.md 鸿蒙适配说明(中英双份,中文主文档 25 节)
README.upstream.md 上游原文保存
doc/ 本文章与效果图
构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :kamel-core 与 :example:nativeApp。上游的 compose / AGP / vanniktech 发布插件、refreshVersions、Android 命名空间与 publishLibraryVariants 全部随根构建替换剥离——本 fork 不发 Maven Central。
1.2 工具链矩阵
| 项目 | 配置 |
|---|---|
| Kotlin Multiplatform | HarmonyOS Kotlin 2.4.20-0.1.0-07(eazytec Maven) |
| JDK | 21(Temurin 21.0.12,org.gradle.java.home 钉死;wrapper 启动前需 JAVA_HOME) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm 保留作交叉验证) |
| Gradle | 8.14.1(上游 9.2.1 降级,与 cmp_qrose 审核基线一致) |
| 网络栈 | ktor-client-core / ktor-client-curl 3.5.1-0.1.0-01(唯一 ohos 移植代) |
| kotlinx | coroutines 1.11.0-0.1.0-07、okio 3.17.0-0.1.0-07(同代制品线) |
| DevEco product | 5.1.0(18) 兼容 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.kamel |
$env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
java -version
1.3 源码依赖度盘点
逐文件扫描 kamel-core 的 androidx.compose 引用,得出手术清单:
| 处置 | 文件 | 依赖形态 |
|---|---|---|
| git mv → composeMain | KamelConfig.kt / KamelConfigBuilder.kt / ResourceConfigBuilder.kt / ImageLoading.kt / AnimatedImage.kt / ConfigUtils.kt | import Painter / ImageBitmap / ImageVector / Density / IntSize |
| git mv → composeMain/test | KamelConfigBuilderTest 等 5 个测试 | 依赖上述装配类与 Compose resources |
| 留 commonMain(瘦身) | ResourceConfig.kt | 摘除 density / maxBitmapDecodeSize 两个 Compose 字段,保留 requestData + coroutineContext |
| 留 commonMain(原样) | Cache / LruCache / DiskLruCache / DiskCacheStorage / HttpFetcher / Fetcher / Mapper / Mappers / DataSource / Resource 等 | 纯 Kotlin + ktor + okio |
| 新增 | Kamel.kt / HttpClientFactory.kt | 非 Compose 公共入口与平台 HttpClient 工厂 |
| 新增 ohosMain actual ×5 | Platform / FileFetcher / FileUrlFetcher / Mappers / httpCache | 补齐 expect 声明 |
expect 声明共 6 处:Dispatchers.Kamel / File / URL / URI(Platform.kt)、FileFetcher / FileUrlFetcher(fetcher)、URLMapper / URIMapper(mapper)、httpCacheStorage(cache)。jvm 侧由上游 commonJvmMain 原有 actual 满足,ohos 侧全部新写——其中 File / URL / URI 是最小实现(okio POSIX 读文件、字符串包装),httpCacheStorage 返回 DiskCacheStorage(FileSystem.SYSTEM, cacheDir/http-cache, maxSize),上游磁盘缓存引擎原样落鸿蒙。
1.4 页面边界
标题区 KAMEL · OPENHARMONY(CMP 图片加载库 · Kotlin/Native ohosArm64/ohosX64)
输入区 URL 输入框 + 五个预设按钮(Photo 1-4 + SVG,点选即加载)
操作区 加载 / 缓存信息 两按钮
图片区 Image(PixelMap).objectFit(Contain),换新前 release 旧 PixelMap
状态行 最近一次结果("加载成功 source=network byteCount=7859 elapsedMs=3236")
说明行 二次加载同 URL 验证 source=memory;杀进程重启走磁盘缓存
页面 aboutToAppear 先拷 CA bundle 并调 kamelInit,加载请求经 Worker 线程调 kamelLoad。
第 2 阶段:目标与依赖打通
2.1 制品线验证(本适配的成败手)
先查社区 Maven(https://maven.eazytec-cloud.com)上网络栈的 ohos 制品。用 .module 元数据逐个验证:
io.ktor:ktor-client-core:3.5.1-0.1.0-01
variants: ohosArm64Api-published / ohosX64Api-published(指向 ktor-client-core-ohosarm64 klib)
io.ktor:ktor-client-curl:3.5.1-0.1.0-01
同代存在(HTTP 引擎有了)
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0-0.1.0-07
依赖 kotlin-stdlib 2.4.20-0.1.0-08(Kotlin 2.4.20 代产物)
com.squareup.okio:okio:3.17.0-0.1.0-07
与 coroutines 同列发布
三个关键结论:其一,ktor 的 ohos 移植版只有 3.5.1-0.1.0-01 一代,上游声明的 3.3.3 没有对应移植线,引擎必须换代(换线且换版);其二,ktor-client-okhttp 虽有同代版本号但 .module 里没有 ohos 变体,候选排除;其三,镜像上 cache4k 有 maven-metadata 列表但拉不到 artifact(选择性镜像),依赖必须去除——LruCache 改为手写线程安全 LRU(LinkedHashMap + 锁,语义与 cache4k 的 access-order 淘汰一致)。
2.2 代际钉版(为什么是 2.4.20-0.1.0-07)
社区此前批准的钉法是 HarmonyOS Kotlin 2.2.21-1.0.0(cmp_qrose 用的)。但 coroutines 1.11.0-0.1.0-07 依赖 kotlin-stdlib 2.4.20(元数据 2.4.0),Kotlin 2.2.21 只能读到 2.3.0,编译期直接报:
Class 'kotlin.Unit' was compiled with an incompatible version of Kotlin.
The actual metadata version is 2.4.0, but the compiler version 2.2.0 can read versions up to 2.3.0.
工具链版本跟着依赖走:Kotlin 钉 2.4.20-0.1.0-07(-08 的 KN 预构建发行版不完整,compileKotlinOhosArm64 报 ClassNotFoundException: MainKt,实测退回 -07)。Gradle 取 8.14.1 与 cmp_qrose 审核基线一致。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()
mavenCentral()
google()
}
}
rootProject.name = "Kamel"
include(":kamel-core")
include(":example:nativeApp")
2.3 编译真实引擎
kamel-core/build.gradle.kts 收敛为纯 KMP 模块:
kotlin {
compilerOptions.freeCompilerArgs.add("-Xexpect-actual-classes")
explicitApi = ExplicitApiMode.Warning
applyDefaultHierarchyTemplate()
jvmToolchain(21)
jvm() // 交叉验证用
ohosArm64()
ohosX64()
sourceSets {
val commonMain by getting {
dependencies {
implementation(libs.kotlinx.coroutines.core) // 1.11.0-0.1.0-07
implementation(libs.ktor.client.core) // 3.5.1-0.1.0-01
implementation(libs.okio) // 3.17.0-0.1.0-07
}
}
val commonJvmMain by creating { dependsOn(commonMain) } // 上游 jvm actual 所在源集
val jvmMain by getting { dependsOn(commonJvmMain) }
ohosMain.dependencies {
implementation(libs.ktor.client.curl) // ohos 网络引擎 + CA 注入点
}
}
}
上游 desktopMain 的 httpCache actual 随目标改名 git mv 到 jvmMain,并去除 org.jetbrains.skiko.hostOs 依赖(skiko 是 Compose 传递依赖,隔离后不复存在),改用 System.getProperty("os.name") 判定。实测:
./gradlew :kamel-core:compileKotlinJvm :kamel-core:compileTestKotlinJvm # BUILD SUCCESSFUL
./gradlew :kamel-core:compileKotlinOhosArm64 :kamel-core:compileKotlinOhosX64 # BUILD SUCCESSFUL
cache / fetcher / mapper 引擎一个没动,新增的只有 Kamel 入口、HttpClient 工厂与 5 个 ohos actual。
2.4 测试依赖的处置
ohos 测试任务在鸿蒙线没有运行宿主,根构建全局禁用,测试统一在 JVM 跑:
subprojects {
tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
.configureEach { enabled = false }
}
Compose 依赖的 5 个测试文件随源码一起隔离进 composeMain/test,留在 commonTest 的是纯 Kotlin 用例(LruCache / DiskCacheStorage / Mappers / StringMapper)。
第 3 阶段:桥协议建模
3.1 JSON 契约
io.kamel.core/**(上游引擎原样编译 + 新增 Kamel 入口)
↓ StringMapper → LruCache → HttpUrlFetcher(ktor-curl) → HttpCache/DiskCacheStorage
Bridge.kt(@CName × 3,扁平 JSON 进出)
↓ UTF-8 JSON
N-API kamelInit() / kamelLoad() / kamelCacheInfo()
↓ Worker 线程
ArkUI Index.ets(单页滚动)
请求与响应:
KamelInit 请求: {"cacheDir":"...","memoryCacheSize":100,"diskCacheBytes":10485760,"caBundlePath":".../cacert.pem"}
响应: {"ok":true,"cacheDir":"..."} | {"ok":false,"error":"..."}
KamelLoad 请求: {"url":"https://...","headers":"a:1;b:2","timeoutMs":15000}
响应: {"ok":true,"url":"...","source":"memory|disk|network","byteCount":7859,"elapsedMs":3236,"base64":"..."}
| {"ok":false,"url":"...","error":"...","elapsedMs":45}
KamelCacheInfo 响应: {"ok":true,"memoryEntries":3,"memoryMax":100,"diskCacheDir":"..."}
两个设计点:headers 是 name:value 以 ; 连接的扁平字符串——桥内 JSON 解析器 MiniJson 只做顶层扁平解析、不支持嵌套对象(与 cmp_qrose 审核模式同款,避免拉 kotlinx.serialization);base64 是原始图片字节而非解码产物,解码语义完全交给系统 image kit(按内容嗅探,响应不需要 contentType)。
3.2 非 Compose 入口:Kamel 对象
上游装配类 KamelConfig 整体在 composeMain,桥不能引用。参照 cmp_qrose 为库补非 Compose 入口(Qrose.encodeQr 模式):
public object Kamel {
public suspend fun loadBytes(
data: String,
request: HttpRequestBuilder.() -> Unit = {},
): Resource<ByteArray> {
val url = StringMapper.map(data)
synchronized(lock) { memoryCache?.get(url) }?.let {
return Resource.Success(it, DataSource.Memory) // 内存命中
}
var outcome: Resource<ByteArray>? = null
HttpUrlFetcher(client()).fetch(url, resourceConfig).collect { resource ->
if (resource is Resource.Success<ByteReadChannel>) {
val bytes = resource.value.toByteArray()
memoryCache[url] = bytes // 回填 LRU
outcome = Resource.Success(bytes, resource.source)
}
}
return outcome ?: Resource.Failure(IllegalStateException("no result"))
}
}
client() 经 expect/actual 工厂创建:JVM 侧引擎由 classpath 发现;ohos 侧显式 HttpClient(Curl),并在此处注入 CA(见第 6 阶段)。
3.3 错误通道与超时
每个导出整体 try/catch,Kotlin 异常永远不跨 FFI;KamelLoad 内部 runBlocking { withTimeoutOrNull(timeoutMs) { ... } }(桥内收敛 1s–120s),超时返回 {"ok":false,"error":"timed out after ...ms"},ArkTS 侧统一渲染。
第 4 阶段:原生桥接(技术难点)
4.1 三层桥接
ArkTS(Worker 线程)
│ JSON string
▼
C++ N-API entry(libkamel_napi.so)
│ const char*
▼
Kotlin/Native C ABI(libohoskamel.so)
│ KamelInit / KamelLoad / KamelCacheInfo
▼
UTF-8 JSON + DisposeString
4.2 Kotlin/Native 导出函数
| C ABI 符号 | ArkTS 方法 | 覆盖内核 |
|---|---|---|
KamelInit | kamelInit(config) | Kamel.configure(cacheDir, memoryCacheSize, httpCacheSize, caBundlePath) |
KamelLoad | kamelLoad(request) | Kamel.loadBytes(url, headers) + LRU 回填 + Base64 编码 |
KamelCacheInfo | kamelCacheInfo() | 内存缓存条目 / 上限 / 磁盘缓存目录 |
linkDebugSharedOhosArm64 / OhosX64 产出 libohoskamel.so(baseName ohoskamel),copyKnSo* 任务作为 finalizedBy 把 KN so 与对应 triple 的 libc++_shared.so 拷进 entry/libs/{arm64-v8a,x86_64}。
4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
{"kamelInit", nullptr, NapiKamelInit, nullptr, nullptr, nullptr, attrs, nullptr},
{"kamelLoad", nullptr, NapiKamelLoad, nullptr, nullptr, nullptr, attrs, nullptr},
{"kamelCacheInfo", nullptr, NapiKamelCacheInfo, nullptr, nullptr, nullptr, attrs, nullptr},
};
每个回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohoskamel_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。CMake 侧 IMPORTED 引入 KN so + IMPORTED_NO_SONAME TRUE + $ORIGIN rpath + POST_BUILD 拷贝三件套,与社区已审仓库同款。
4.4 Worker 线程(API 18 SDK 的三个坑)
KamelLoad 是秒级阻塞调用,必须离开 UI 线程。鸿蒙 Worker 有两个角色,API 18 SDK 的写法细节踩了三个坑:
// Index.ets(主线程):ThreadWorker 构造器必传 scriptURL
const w: worker.ThreadWorker = new worker.ThreadWorker('entry/ets/workers/KamelWorker.ets');
w.onmessage = (e: MessageEvents) => { this.onLoadResult(e.data as string); };
w.postMessage(request);
// KamelWorker.ets(Worker 侧):不要 new,入口是 workerPort
import { kamelLoad } from 'libkamel_napi.so';
import { worker } from '@kit.ArkTS';
const port = worker.workerPort;
port.onmessage = (e: MessageEvents): void => {
const raw: string = kamelLoad(e.data as string); // 同步阻塞,跑在 Worker 线程
port.postMessage(raw);
};
坑一:@kit.ArkTS 不具名导出 ThreadWorker 类(只能走 worker 命名空间);坑二:ThreadWorker 构造器必传 scriptURL(无参 new 直接编译错误,它是主线程专用);坑三:Worker 脚本侧的正规入口是 worker.workerPort(ThreadWorkerGlobalScope),MessageEvents / ErrorEvent 是模块级导出不在 worker 命名空间里。KamelInit / KamelCacheInfo 瞬时,主线程直调。
第 5 阶段:ArkUI 宿主
5.1 初始化与解码显示
aboutToAppear(): void {
const ctx = this.getUIContext().getHostContext();
const caBundlePath = this.installCaBundle(ctx); // rawfile → filesDir/cacert.pem
const raw: string = kamelInit(JSON.stringify({
cacheDir: ctx.cacheDir, caBundlePath, memoryCacheSize: 100, diskCacheBytes: 10485760
}));
}
private decodeImage(b64: string): void {
const bytes: Uint8Array = new util.Base64Helper().decodeSync(b64);
const sliced = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
const src = image.createImageSource(sliced); // 不能省 slice:视图可能带 byteOffset
src.createPixelMap().then((pm: image.PixelMap) => { // 系统 image kit 解码(含 SVG)
this.replacePixelMap(pm); // 换新前 release 旧 PixelMap
});
}
module.json5 申请 ohos.permission.INTERNET(图片加载库的硬前提,cmp_qrose 无网络不需要);getRawFileContentSync 读 rawfile(此 SDK 的 getRawFileContent 是 Promise 版)。
5.2 页面交互
单页自上而下:URL 输入框、五个预设按钮(选中高亮,点选即加载)、加载 / 缓存信息双按钮、图片区(Image(PixelMap).objectFit(Contain))、状态行(成功深灰 / 失败红色)、缓存语义说明行。验收主线:同一预设二次点击状态行从 source=network 变为 source=memory。
第 6 阶段:TLS 信任链修复与验收
6.1 现象与根因
设备实测 HTTPS 全部失败:
请求失败 CURL_PEER_FAILED_VERIFICATION - TLS peer certificate verification failed
根因:libcurl 在鸿蒙上读不到系统信任库——/system/etc/security 对应用进程不可见,TLS 握手进行到证书验证时没有根证书可用(设备时间正常,排除 notYetValid)。网络栈能通,TLS 信任链要自带,这是 curl 类引擎上鸿蒙的通病。
6.2 修复:caInfo + 内置 CA bundle
下载 ktor 3.5 移植 klib 检查 CurlClientEngineConfig,确认引擎原生支持 CA 注入(caInfo → CURLOPT_CAINFO / caPath → CURLOPT_CAPATH)。修复链:
// kamel-core/src/ohosMain/.../HttpClientFactory.ohos.kt
internal actual fun createKamelHttpClient(httpCacheSize: Long, caBundlePath: String?): HttpClient =
HttpClient(Curl) {
engine {
if (!caBundlePath.isNullOrBlank()) {
caInfo = caBundlePath // CURLOPT_CAINFO
}
}
install(HttpCache) { publicStorage(httpCacheStorage(httpCacheSize)) }
}
Demo 打包 Mozilla CA bundle(rawfile/cacert.pem,121 根证书,188 KB)进 HAP,启动时 getRawFileContentSync 读出写入 filesDir,路径经 KamelInit 的 caBundlePath 字段传入。重编 so、重打 HAP、覆盖安装后 HTTPS 恢复。
6.3 构建、签名和安装
$env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
.\gradlew.bat :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
$env:DEVECO_SDK_HOME = "E:\DE\DevEco Studio\sdk"
$env:Path = "E:\DE\DevEco Studio\jbr\bin;$env:Path" # hvigor 打包需要 java
node "E:\DE\DevEco Studio\tools\hvigor\bin\hvigorw.js" --mode module `
-p product=default -p buildMode=debug -p module=entry@default assembleHap --no-daemon
hdc install -r entry\build\default\outputs\default\entry-default-unsigned.hap
hdc shell aa start -a EntryAbility -b org.terminator.ohos.kamel
签名在 DevEco Studio 的 File → Project Structure → Signing Configs 中勾选自动生成;仓库内 build-profile.json5 的 signingConfigs 保持空数组,证书绝不进入 AtomGit。
四、完整代码对照
4.1 整体架构
io.kamel.core.cache/**(上游引擎原样编译)
│ Cache / LruCache / DiskLruCache / DiskCacheStorage / httpCacheStorage
io.kamel.core.fetcher + mapper + config(上游原样 + ResourceConfig 瘦身)
│ HttpUrlFetcher / FileFetcher / FileUrlFetcher / StringMapper / ResourceConfig
io.kamel.core.Kamel(新增非 Compose 入口)
│ configure / loadBytes / clearMemoryCache / cacheInfo
HttpClientFactory(expect/actual,ohos = Curl + caInfo + HttpCache)
Bridge.kt + MiniJson
│ KamelInit / KamelLoad / KamelCacheInfo
libkamel_napi.so
│ kamelInit / kamelLoad / kamelCacheInfo
KamelWorker.ets(Worker 线程)+ Index.ets
│ JSON parse + Base64 → createImageSource → PixelMap
▼
ArkUI 真机页面(Image 组件显示)
4.2 文件清单
| 文件 | 职责 |
|---|---|
settings.gradle.kts | 极简 ohos 构建、eazytec Maven 置前、Compose 子模块移出 |
gradle/libs.versions.toml | Kotlin 2.4.20-0.1.0-07 + ktor/coroutines/okio ohos 制品线钉版 |
build.gradle.kts(根) | 版本 1.0.9-ohos.1、全局禁用 Ohos*Test* 任务 |
kamel-core/build.gradle.kts | 纯 KMP:jvm + ohosArm64 + ohosX64,ohosMain 挂 ktor-client-curl |
kamel-core/src/composeMain/ | Compose 隔离区(git mv,6 主文件 + 5 测试) |
kamel-core/src/commonMain/.../Kamel.kt | 新增非 Compose 入口 |
kamel-core/src/{commonMain,jvmMain,ohosMain}/.../HttpClientFactory.*.kt | 平台 HttpClient 工厂(ohos 注入 caInfo) |
kamel-core/src/ohosMain/ ×5 | Platform / FileFetcher / FileUrlFetcher / Mappers / httpCache actual |
example/nativeApp/build.gradle.kts | 双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填 |
nativeApp/.../Bridge.kt | 3 个 C ABI、runBlocking + withTimeoutOrNull、MiniJson、错误通道 |
harmonyApp/.../cpp/napi_init.cpp | N-API 导出、参数检查、DisposeString |
harmonyApp/.../cpp/CMakeLists.txt | IMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套 |
harmonyApp/.../ets/workers/KamelWorker.ets | Worker 侧 workerPort 消息环 |
harmonyApp/.../ets/pages/Index.ets | ArkUI 验收页面 + CA bundle 安装 |
harmonyApp/entry/src/main/resources/rawfile/cacert.pem | Mozilla CA bundle(121 根证书) |
README.OpenHarmony_CN.md | 鸿蒙适配主文档(25 节) |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| 引擎 | Kamel.configure(diskCacheDir, …, caBundlePath) | 初始化缓存与 TLS(新增入口) |
| 引擎 | Kamel.loadBytes(url) | 内存 LRU → HttpUrlFetcher → HttpCache,返回原始字节 |
| 引擎 | HttpUrlFetcher.fetch | ktor 请求 + 进度流(进度事件在桥上折叠) |
| 引擎 | DiskCacheStorage / DiskLruCache | Ktor HttpCache 的 okio 磁盘后端(上游原件) |
| 引擎 | StringMapper | String → ktor Url(file:// 归一化,上游原件) |
| Native | KamelInit / KamelLoad / KamelCacheInfo | C ABI JSON 导出 |
| N-API | kamelInit / kamelLoad / kamelCacheInfo | 向 ArkTS 暴露能力 |
| ArkTS | image.createImageSource → PixelMap | 系统 image kit 解码(位图 + SVG) |
上游 Compose API 与鸿蒙侧等价物:
| 上游 Compose API | 鸿蒙侧等价物 |
|---|---|
asyncPainterResource(url) → Resource<Painter> | kamelLoad(url) → base64 原始字节 → 系统 image kit → PixelMap |
KamelImage composable | ArkUI Image 组件 + Worker 线程加载 |
KamelConfig(四类型缓存槽) | Kamel.configure(内存缓存原始字节,解码缓存交给 PixelMap 生命周期) |
onLoading 进度回调 | 同步 C ABI 无流式回调;elapsedMs / byteCount 事后返回(已知限制) |
| Skia 解码器 ×4 | 系统 image kit 统一解码(PNG / JPEG / WebP / GIF / SVG) |
| 平台 Ktor 引擎注入 | ktor-client-curl 3.5.1-0.1.0-01 + caInfo CA 注入 |
4.4 ArkTS 与 Kotlin 的边界
// ArkTS:Worker 线程发起,主线程解码显示
const res = JSON.parse(kamelLoad(JSON.stringify({
url: 'https://picsum.photos/300/200', timeoutMs: 15000
})));
if (res.ok) {
const bytes = new util.Base64Helper().decodeSync(res.base64);
image.createImageSource(bytes.buffer.slice(bytes.byteOffset,
bytes.byteOffset + bytes.byteLength)).createPixelMap().then(pm => { this.pixelMap = pm; });
}
// Kotlin:桥内一次加载
val url = StringMapper.map(data)
memoryCache[url]?.let { return Resource.Success(it, DataSource.Memory) }
HttpUrlFetcher(client()).fetch(url, config).collect { /* toByteArray + 回填 LRU */ }
两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构。
五、关键决策说明
决策 1:Compose 隔离而不是硬编 Compose 源码
上游按 CMP 1.10.0 编写,社区 ohos Compose 移植线与之 API 不齐,硬编等于把版本地狱背进仓库。物理隔离进 composeMain(git mv 保留历史)让加载/缓存引擎先落地鸿蒙,UI 层留待社区 Compose 对齐后原样接回——这是 cmp_qrose / Vico / Reorderable 已审核的同一条路。
决策 2:解码交给系统 image kit
非 Android 平台上游用 Skiko/Skia 解码,隔离 Compose 后 skiko 不复存在。系统 image kit 按内容嗅探统一解码 PNG / JPEG / WebP / GIF / SVG,桥只传原始字节——比在 Native 侧手搓解码器更稳,且与 ArkUI Image 的渲染管线天然衔接。
决策 3:引擎换代(3.3.3 → 3.5.1-0.1.0-01),工具链跟着依赖走
ktor 的 ohos 移植版只有一代,没有"换线不换版"的选项;连带 coroutines / okio 同列 -0.1.0-07、Kotlin 升 2.4.20-0.1.0-07。网络 API(bodyAsChannel / HttpCache / publicStorage)在 3.5 线稳定,引擎源码零改动完成换代。
决策 4:TLS 内置 CA bundle(caInfo)
curl 引擎读不到鸿蒙系统信任库是平台现状。ktor 3.5 的 CurlClientEngineConfig.caInfo 直通 CURLOPT_CAINFO,应用内置 Mozilla bundle(188 KB)是确定性最高的方案:不依赖系统证书路径、不受 SELinux 影响、自签场景替换文件即可。可选优化是 caPath 指向系统散列目录,但可行性未证,内置 bundle 先行。
决策 5:同步 C ABI + Worker 线程
C ABI 只能同步一来一回,进度流过不了桥;网络 IO 秒级耗时又不能占 UI 线程。折中是 runBlocking + withTimeoutOrNull 收进同步调用、ArkTS 侧 Worker 线程执行、elapsedMs / byteCount 事后观测——Demo 的 source=memory 11ms 验收行证明这个模型对缓存命中同样成立。
六、测试与验证
6.1 测试环境
本次验证使用 Windows(x64)、JDK 21(Temurin 21.0.12)、DevEco Studio 6(SDK 5.1.0(18))、OpenHarmony 命令行工具(hvigor / hdc / uitest / uinput)与 DevEco 模拟器(arm64 镜像,127.0.0.1:5555)。
6.2 JVM 交叉验证
./gradlew :kamel-core:jvmTest
17 项用例 15 项通过。剩余 2 项(StringMapperTest.testAbsoluteFilePath / testRelativeFilePath)是上游遗留失败:用例注释自标 “This is currently broken”(上游 PR #78 跟踪),ktor 3.3.3 → 3.5.1 后 Url("/path/…") 被规范化为 http://localhost/path/…,断言差异暴露得更明显——StringMapper 本身零改动,与本次适配无关。留在 commonTest 的纯 Kotlin 用例(LruCache / DiskCacheStorage / Mappers / StringMapper)证明换线后的缓存与映射引擎没有漂移。
6.3 HAP 验证
:kamel-core:compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL(合计 1m28s,copyKnSo* 自动回填)
llvm-nm -D: KamelInit / KamelLoad / KamelCacheInfo / libohoskamel_symbols 均为全局 T 符号(双 ABI)
BuildNativeWithCmake: finished(libkamel_napi.so ×2)
CompileArkTS: finished
PackageHap: finished(BUILD SUCCESSFUL)
install bundle successfully / start ability successfully
entry-default-unsigned.hap 约 26.2 MB,内含双 ABI 六件套 so(libkamel_napi.so / libohoskamel.so / libc++_shared.so × arm64-v8a / x86_64)与 resources/rawfile/cacert.pem;debug 未裁剪 KN so arm64 约 18.9 MB、x86_64 约 18.2 MB(含 Kotlin/Native 运行时 + Ktor + curl)。
6.4 功能用例(uitest 自动化验收)
用 uitest dumpLayout 定位按钮、uinput -T -c 模拟点击、snapshot_display 截图,全程不依赖手工:
- 启动日志
KamelNapi: kamelInit config bytes=170——初始化真实穿过 NAPI(含caBundlePath); - 首次加载 Photo 1:状态行
加载成功 source=network byteCount=7859 elapsedMs=3236,图片显示; - 同一 URL 二次点击:
source=memory byteCount=7859 elapsedMs=11——内存 LRU 命中,3236ms → 11ms; - SVG 预设:
source=network byteCount=2268,系统 image kit 直接解码渲染矢量占位图; - Photo 3 预设(另一 URL):
source=network byteCount=34158 elapsedMs=517,大图正常; - 「缓存信息」按钮返回
memoryEntries / memoryMax / diskCacheDir真实 Native 状态; - TLS 修复前同一页面 HTTPS 全挂(
CURL_PEER_FAILED_VERIFICATION),修复后上述全部通过——caBundlePath是唯一变量。
6.5 验证结论
加载/缓存引擎、ktor curl 网络栈、TLS 信任链、Native 动态库、C ABI、N-API、Worker 线程、系统 image kit 解码、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙设备上成功安装并进入前台,页面展示了真实的异步加载会话:HTTPS 拉取、内存命中、SVG 解码与缓存观测闭环(见下节效果图)。
七、运行效果
7.1 设备截图

7.2 命令速查
git clone https://atomgit.com/oh-tpc/Kamel.git
cd Kamel
export JAVA_HOME="/path/to/jdk-21"
./gradlew :kamel-core:compileKotlinJvm :kamel-core:compileKotlinOhosArm64 # klib + 交叉验证
./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-unsigned.hap
hdc shell aa start -a EntryAbility -b org.terminator.ohos.kamel -m entry
hdc shell hilog | grep Kamel
八、遗留问题与改进方向
8.1 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配,必须让
:kamel-core真正编译成ohosArm64/ohosX64动态库;重 Compose 库的第一步是逐文件扫描androidx.compose引用,得出隔离清单; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;- 鸿蒙移植库按 Kotlin 编译器代际成套出现:ktor 3.5.1-0.1.0-01 / coroutines 1.11.0-0.1.0-07 是 2.4.20 代产物,2.2.21 编译器报
metadata version is 2.4.0——挑依赖先看它是哪一代编译的,再看版本号; - Kotlin
2.4.20-0.1.0-08的 KN 预构建发行版不完整(ClassNotFoundException: MainKt),退回-07即好; - eazytec 镜像有元数据列表但选择性存 artifact(cache4k 404)——不可达的依赖要去除而不是降版本硬凑;
kotlin.concurrent.SynchronizedObject在该 ohos Kotlin 构建中不可用且不可继承,锁结构与上游DiskLruCache一致用kotlinx.coroutines.internal.SynchronizedObject+@OptIn(InternalCoroutinesApi::class);LinkedHashMap(16, 0.75f, true)的 accessOrder 构造是 JVM 独有,common Native 里用get时remove+ re-put手动维护 LRU 访问顺序;- skiko(
org.jetbrains.skiko.hostOs)是 Compose 传递依赖,隔离后消失——平台 actual 里的传递依赖要逐一排查替换(改System.getProperty("os.name")); - 上游 desktop 目标名叫
desktop,本 fork 目标叫jvm,expect 的 actual 目录要跟着git mv,否则expect has no actual; org.gradle.java.home只作用于 fork 出来的 JVM,wrapper 启动本身要JAVA_HOME;- TLS:libcurl 在鸿蒙读不到系统信任库,HTTPS 报
CURL_PEER_FAILED_VERIFICATION——ktor curl 的engine { caInfo = ... }(CURLOPT_CAINFO)+ 应用内置 Mozilla CA bundle 是确定性修复; - API 18 SDK 的 Worker 三坑:
@kit.ArkTS不具名导出ThreadWorker;其构造器必传 scriptURL(主线程专用);Worker 脚本侧入口是worker.workerPort,且MessageEvents/ErrorEvent是模块级导出; getRawFileContent在此 SDK 返回 Promise,同步读要用getRawFileContentSync;- hvigor 命令行打包需要
java在 PATH(DevEco 自带 JBR 可用)与DEVECO_SDK_HOME;hdc 在 MSYS shell 下有路径拼接问题,install 要 cd 到 HAP 目录用纯文件名; - KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free();DevEco 打开工程生成的example/harmonyApp/.idea/与构建产物要单独追加 gitignore。
8.2 已知问题
- 鸿蒙侧无 Compose UI:
KamelImage/asyncPainterResource/ Skia 解码器在composeMain不编译,业务侧显示用 ArkUIImage;接回条件是社区 Compose 移植线与上游 1.10.0 API 对齐; - 同步 C ABI 无逐字节进度流(上游
onLoading无等价物),elapsedMs/byteCount事后返回;大图 base64 传输有内存峰值(膨胀约 1/3); - 内存 LRU 缓存原始字节而非解码位图,解码缓存交给系统 PixelMap 生命周期;
- GIF 由系统 image kit 处理(首帧语义),与上游
Codec逐帧动画路径不同;复杂 SVG 与上游 SVGDOM 渲染可能有差异; - 响应无
contentType(image kit 按内容嗅探,不需要);http明文流量未配置,demo 预设全 https;自签证书场景需替换rawfile/cacert.pem; - jvmTest 有 2 项上游遗留失败(
StringMapper相对路径用例,上游 PR #78 跟踪),与本适配无关; - android / js / wasmJs / apple 目标剥离(源文件留在树里),发布链路一并剥离,本 fork 不发 Maven Central。
8.3 未来优化方向
- 评估
caPath(CURLOPT_CAPATH)指向系统证书散列目录的可行性,减少 188 KB 内置 bundle; - 增加 N-API 线程安全函数(ThreadSafeFunction)做进度推送,替代事后观测;
- 把
file://加载路径补进 Demo 验收(FileFetcher/FileUrlFetcher已编进 so); - 评估
.har形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套; - 在 CI 中加入 ohos 编译、ELF 符号检查、HAP 构建与 uitest 自动化截图;
- 补充物理真机验收矩阵与断网磁盘缓存(杀进程重启)用例。
九、总结
9.1 核心难点回顾
上游 kamel-core(Compose 重依赖)
→ 逐文件扫描得出 composeMain 隔离清单(6 主 + 5 测试)
→ 验证 ktor ohos 移植线(唯一代 3.5.1-0.1.0-01 + curl)
→ 工具链代际对齐 Kotlin 2.4.20-0.1.0-07(Gradle 8.14.1)
→ 源码集手术:ResourceConfig 瘦身 + 新增 Kamel 入口 + ohos actual ×5
→ 去依赖手术:LruCache 去 cache4k、jvmMain 去 skiko、desktop→jvm
→ ohosArm64 / ohosX64 KLIB + 双 ABI libohoskamel.so
→ C ABI(@CName × 3)+ MiniJson + runBlocking/withTimeoutOrNull
→ C++ N-API(DisposeString 纪律)+ Worker 线程(workerPort)
→ TLS 修复:caInfo + 内置 Mozilla CA bundle
→ ArkUI 验收(HTTPS / 内存命中 / SVG / 缓存观测)+ uitest 自动化
9.2 封装层次
- 引擎层:上游原版
HttpUrlFetcher、DiskLruCache、DiskCacheStorage、Mapper 体系 + 新增Kamel非 Compose 入口; - 网络层:ohos 移植线 ktor 3.5.1-0.1.0-01 + curl 引擎 +
caInfoCA 注入 + HttpCache 磁盘缓存; - Native 层:双 ABI 动态库并通过 3 个 C ABI 输出 JSON;
- N-API 层:参数检查、字符串转换和 Native 内存释放;
- ArkTS 层:Worker 消息环、Base64 → 系统 image kit 解码、页面状态与错误文本;
- DevEco 层:CMake、HAP、签名、安装和设备运行。
9.3 三条经验
- 重 Compose 库的适配模式是"核心复用 + UI 隔离 + 系统解码":Kamel 的价值链是 load → cache → decode → display,前三段是纯 Kotlin 可复用,最后一段长在 Compose 上就隔离、交给系统 image kit——不必等 Compose 对齐才让库上鸿蒙,隔离区(composeMain)就是接回的预留接口;
- 网络栈上鸿蒙,TLS 信任链要自带:ktor 移植线解决了"引擎能不能编",
caInfo+ 内置 CA bundle 解决"HTTPS 能不能信"——两者都是必要条件,验收用例必须包含真实 HTTPS 而不是 http 或本地文件; - 自动化验收让适配闭环不依赖手工:hvigor CLI(DEVECO_SDK_HOME + JBR PATH)+ hdc install + uitest dumpLayout / uinput click / snapshot_display,把"编译-打包-装机-点击-截图"变成可重复脚本,缓存命中(3236ms → 11ms)这类核心指标才有留痕。
9.4 适配成果
当前适配已完成:
- Kamel
1.0.9加载/缓存引擎的双 ABI Kotlin/Native 编译(cache / fetcher / mapper 引擎零重写,Compose 源码 git mv 隔离保留历史); - ktor ohos 移植线在网络加载库上的完整落地(
3.5.1-0.1.0-01+ curl 引擎 +caInfoTLS 信任链修复); - 3 个 C ABI 符号和 C++ N-API 包装层(
llvm-nm验证全局 T 符号,双 ABI); - DevEco Stage ArkUI 验收页面(HTTPS 加载、内存 LRU 命中、SVG 解码、缓存观测、Worker 线程模型);
- JVM 交叉验证(15/17 通过,2 项上游遗留)+ HAP 构建(双 ABI 六件套 so + 内置 CA bundle,约 26.2 MB)、设备安装和 uitest 自动化验收;
- AtomGit 项目文档、OpenHarmony README(中英双份,中文主文档 25 节)、本文与本地效果图。
适配过程 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)