本文记录 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 MultiplatformHarmonyOS Kotlin 2.4.20-0.1.0-07(eazytec Maven)
JDK21(Temurin 21.0.12,org.gradle.java.home 钉死;wrapper 启动前需 JAVA_HOME)
OpenHarmony 目标ohosArm64 + ohosX64(jvm 保留作交叉验证)
Gradle8.14.1(上游 9.2.1 降级,与 cmp_qrose 审核基线一致)
网络栈ktor-client-core / ktor-client-curl 3.5.1-0.1.0-01(唯一 ohos 移植代)
kotlinxcoroutines 1.11.0-0.1.0-07、okio 3.17.0-0.1.0-07(同代制品线)
DevEco product5.1.0(18) 兼容
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.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 → composeMainKamelConfig.kt / KamelConfigBuilder.kt / ResourceConfigBuilder.kt / ImageLoading.kt / AnimatedImage.kt / ConfigUtils.ktimport Painter / ImageBitmap / ImageVector / Density / IntSize
git mv → composeMain/testKamelConfigBuilderTest 等 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 ×5Platform / 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 方法覆盖内核
KamelInitkamelInit(config)Kamel.configure(cacheDir, memoryCacheSize, httpCacheSize, caBundlePath)
KamelLoadkamelLoad(request)Kamel.loadBytes(url, headers) + LRU 回填 + Base64 编码
KamelCacheInfokamelCacheInfo()内存缓存条目 / 上限 / 磁盘缓存目录

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.tomlKotlin 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/ ×5Platform / FileFetcher / FileUrlFetcher / Mappers / httpCache actual
example/nativeApp/build.gradle.kts双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填
nativeApp/.../Bridge.kt3 个 C ABI、runBlocking + withTimeoutOrNull、MiniJson、错误通道
harmonyApp/.../cpp/napi_init.cppN-API 导出、参数检查、DisposeString
harmonyApp/.../cpp/CMakeLists.txtIMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套
harmonyApp/.../ets/workers/KamelWorker.etsWorker 侧 workerPort 消息环
harmonyApp/.../ets/pages/Index.etsArkUI 验收页面 + CA bundle 安装
harmonyApp/entry/src/main/resources/rawfile/cacert.pemMozilla CA bundle(121 根证书)
README.OpenHarmony_CN.md鸿蒙适配主文档(25 节)

4.3 关键 API 对照

层次API作用
引擎Kamel.configure(diskCacheDir, …, caBundlePath)初始化缓存与 TLS(新增入口)
引擎Kamel.loadBytes(url)内存 LRU → HttpUrlFetcher → HttpCache,返回原始字节
引擎HttpUrlFetcher.fetchktor 请求 + 进度流(进度事件在桥上折叠)
引擎DiskCacheStorage / DiskLruCacheKtor HttpCache 的 okio 磁盘后端(上游原件)
引擎StringMapperString → ktor Url(file:// 归一化,上游原件)
NativeKamelInit / KamelLoad / KamelCacheInfoC ABI JSON 导出
N-APIkamelInit / kamelLoad / kamelCacheInfo向 ArkTS 暴露能力
ArkTSimage.createImageSource → PixelMap系统 image kit 解码(位图 + SVG)

上游 Compose API 与鸿蒙侧等价物:

上游 Compose API鸿蒙侧等价物
asyncPainterResource(url) → Resource<Painter>kamelLoad(url) → base64 原始字节 → 系统 image kit → PixelMap
KamelImage composableArkUI 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 截图,全程不依赖手工:

  1. 启动日志 KamelNapi: kamelInit config bytes=170——初始化真实穿过 NAPI(含 caBundlePath);
  2. 首次加载 Photo 1:状态行 加载成功 source=network byteCount=7859 elapsedMs=3236,图片显示;
  3. 同一 URL 二次点击:source=memory byteCount=7859 elapsedMs=11——内存 LRU 命中,3236ms → 11ms;
  4. SVG 预设:source=network byteCount=2268,系统 image kit 直接解码渲染矢量占位图;
  5. Photo 3 预设(另一 URL):source=network byteCount=34158 elapsedMs=517,大图正常;
  6. 「缓存信息」按钮返回 memoryEntries / memoryMax / diskCacheDir 真实 Native 状态;
  7. 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 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配,必须让 :kamel-core 真正编译成 ohosArm64 / ohosX64 动态库;重 Compose 库的第一步是逐文件扫描 androidx.compose 引用,得出隔离清单;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. 鸿蒙移植库按 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——挑依赖先看它是哪一代编译的,再看版本号;
  4. Kotlin 2.4.20-0.1.0-08 的 KN 预构建发行版不完整(ClassNotFoundException: MainKt),退回 -07 即好;
  5. eazytec 镜像有元数据列表但选择性存 artifact(cache4k 404)——不可达的依赖要去除而不是降版本硬凑;
  6. kotlin.concurrent.SynchronizedObject 在该 ohos Kotlin 构建中不可用且不可继承,锁结构与上游 DiskLruCache 一致用 kotlinx.coroutines.internal.SynchronizedObject + @OptIn(InternalCoroutinesApi::class);
  7. LinkedHashMap(16, 0.75f, true) 的 accessOrder 构造是 JVM 独有,common Native 里用 get 时 remove + re-put 手动维护 LRU 访问顺序;
  8. skiko(org.jetbrains.skiko.hostOs)是 Compose 传递依赖,隔离后消失——平台 actual 里的传递依赖要逐一排查替换(改 System.getProperty("os.name"));
  9. 上游 desktop 目标名叫 desktop,本 fork 目标叫 jvm,expect 的 actual 目录要跟着 git mv,否则 expect has no actual;
  10. org.gradle.java.home 只作用于 fork 出来的 JVM,wrapper 启动本身要 JAVA_HOME;
  11. TLS:libcurl 在鸿蒙读不到系统信任库,HTTPS 报 CURL_PEER_FAILED_VERIFICATION——ktor curl 的 engine { caInfo = ... }(CURLOPT_CAINFO)+ 应用内置 Mozilla CA bundle 是确定性修复;
  12. API 18 SDK 的 Worker 三坑:@kit.ArkTS 不具名导出 ThreadWorker;其构造器必传 scriptURL(主线程专用);Worker 脚本侧入口是 worker.workerPort,且 MessageEvents / ErrorEvent 是模块级导出;
  13. getRawFileContent 在此 SDK 返回 Promise,同步读要用 getRawFileContentSync;
  14. hvigor 命令行打包需要 java 在 PATH(DevEco 自带 JBR 可用)与 DEVECO_SDK_HOME;hdc 在 MSYS shell 下有路径拼接问题,install 要 cd 到 HAP 目录用纯文件名;
  15. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();DevEco 打开工程生成的 example/harmonyApp/.idea/ 与构建产物要单独追加 gitignore。

8.2 已知问题

  • 鸿蒙侧无 Compose UI:KamelImage / asyncPainterResource / Skia 解码器在 composeMain 不编译,业务侧显示用 ArkUI Image;接回条件是社区 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 引擎 + caInfo CA 注入 + HttpCache 磁盘缓存;
  • Native 层:双 ABI 动态库并通过 3 个 C ABI 输出 JSON;
  • N-API 层:参数检查、字符串转换和 Native 内存释放;
  • ArkTS 层:Worker 消息环、Base64 → 系统 image kit 解码、页面状态与错误文本;
  • DevEco 层:CMake、HAP、签名、安装和设备运行。

9.3 三条经验

  1. 重 Compose 库的适配模式是"核心复用 + UI 隔离 + 系统解码":Kamel 的价值链是 load → cache → decode → display,前三段是纯 Kotlin 可复用,最后一段长在 Compose 上就隔离、交给系统 image kit——不必等 Compose 对齐才让库上鸿蒙,隔离区(composeMain)就是接回的预留接口;
  2. 网络栈上鸿蒙,TLS 信任链要自带:ktor 移植线解决了"引擎能不能编",caInfo + 内置 CA bundle 解决"HTTPS 能不能信"——两者都是必要条件,验收用例必须包含真实 HTTPS 而不是 http 或本地文件;
  3. 自动化验收让适配闭环不依赖手工: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 引擎 + caInfo TLS 信任链修复);
  • 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。

参考文档

Logo

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

更多推荐