本文记录 compose-imageLoader 接入 OpenHarmony 的完整过程,覆盖图像加载引擎盘点、鸿蒙 Compose 制品线选型、真实引擎编译、ohosArm64 / ohosX64 目标、源集层级重建、vendored Uri 与 poko 注解、curl 静态 TLS 栈、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和四轮真机验收。

compose-imageLoader 是 qdsfdhvh 开发的 Compose Multiplatform 图像加载库(Coil 风格):ImageLoader 引擎驱动 拦截器链(映射 → 内存缓存 → 磁盘缓存 → 网络抓取 → Skia 解码),组件注册表可插拔,扩展覆盖 ktor 网络抓取、SVG、GIF、九宫格。本次适配与社区早期路线(qrose 的"零依赖内核提取")有本质区别:社区 Maven 上已经出现完整的 Compose 鸿蒙制品线(org.jetbrains.compose.*:1.12.0-0.1.0-07,携带 skiko ohos 变体),因此不需要隔离任何 Compose 代码——引擎、拦截器链、DiskLruCache、Skia 解码器等上游 commonMain / skiaMain 原样编译为 OpenHarmony KLIB,一个带三级缓存、组件注册表、ktor 抓取、SVG 解码的完整图像加载引擎运行在真机上,ArkUI 页面作为验收宿主。

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

上游仓库: qdsfdhvh/compose-imageLoader

开发工具: 华为云码道

环境安装: 环境安装指南

一、背景

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

compose-imageLoader 为 Compose 应用提供"URL → 图片"这一件事的完整答案:一行 AsyncImage / rememberImagePainter 背后是 Mapper(String→Uri→Path)→ MemoryCache(三级:bitmap/image/painter)→ DiskCache(okio 事务写 DiskLruCache)→ Fetcher(ktor / Base64 / 字节 / 文件)→ Decoder(Skia / GIF / SVG)的完整管线。KMP 项目把这些能力放在 commonMain,由 Android、iOS、桌面、JS、Wasm 共享。OpenHarmony 应用同样需要这套能力,但上游构建(Kotlin 2.1.0 / Compose 1.6.11 / Gradle 8.12.1)不会生成 OpenHarmony 变体。

如果只把示例页面改成 ArkTS,页面可以显示按钮,却无法证明公共加载引擎在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证:引擎与拦截器链、内存/磁盘二级缓存、ktor 网络抓取(含 TLS)、Skia 解码、Kotlin/Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。

障碍具体问题
目标缺失必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。
工具链不一致上游 Kotlin 2.1.0 / Compose 1.6.11 与鸿蒙工具链无法共存于一个 Gradle 构建。
依赖矩阵Compose ui/foundation、coroutines、okio、ktor、atomicfu、collection 每一件都要有 ohos 变体才能解析。
uri-kmp 无 ohos 制品commonMain 的映射器直接 import com.eygraber.uri.Uri。
poko 注解无 ohos 变体MemoryKey 等 7 处 @Poko 类依赖编译插件生成 equals/hashCode,缺了它内存缓存键静默失效。
语言边界不同ArkTS 不能直接持有 Kotlin 对象 / Painter,需要 C ABI、C++ N-API 和 JSON。
交付链路复杂.so、CMake、N-API、HAP、签名和 hdc 都要单独验收。

因此,本项目把边界放在:完整引擎(5 个模块)的 ohos 编译、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。加载语义完全由上游原版实现负责,ArkTS 只负责调用引擎、解析 JSON 和展示结果。

1.2 库提供的能力

Demo 提供一个真实的图像加载会话,对应桥接的全部入口:

  • 初始化(init):注入沙箱磁盘缓存目录、容量上限与 TLS CA 证书路径,构建带三级缓存与完整组件集(ktor curl 抓取器、Base64/字节/okio 抓取器、SVG + Skia 解码器)的真实 ImageLoader;响应内嵌一次 MemoryKey("probe") == MemoryKey("probe") 探测——@Poko 代码生成是否在 ohos KLIB 里生效的运行时证据;
  • 加载(load):source 支持 https:// 网络图片(curl 静态 TLS 抓取)、file:// 本地文件(Uri→Path→okio 抓取)、data:image/png;base64,... 数据 URI(Base64Fetcher)三种来源;返回缓存来源(memory/disk/fetch,取自上游 ImageEvent.StartWith* 事件流——哪个缓存服务了本次请求的诚实信号)、耗时、结果类型、宽高与像素载荷;
  • 再加载:同一 source 二次执行,观察 cacheSource=memory 与耗时骤降——内存缓存命中;
  • 清空内存 / 清空磁盘 / 刷新统计:实时报告 memoryBitmapBytes / memoryImageKeys / memoryPainterKeys / diskCacheBytes / loads;
  • 持久化闭环:杀进程重启后再读,磁盘缓存仍命中——证明落盘而不是内存缓存;
  • 版本握手:ImageLoaderVersion() 返回 1.10.0-ohos.1,来自真实 Native 调用而非静态文字。

响应包含:

字段作用
ok操作是否成功,失败时 error 带原文。
op / cacheSource / elapsedMs回显本次操作、缓存来源(memory/disk/fetch)与耗时。
resultType / width / height解码结果类型(OfImage/OfBitmap/OfPainter)与宽高。
pngBase64 / pngFormat / pngValid像素载荷(rgba8888/bgra8888 直通为主,png/webp 为超大图回落)与原生导出自检结果。
memory* / diskCache* / loads缓存统计快照,每条响应内嵌。

引擎侧完整能力(映射拦截器、解码拦截器的磁盘重试、弱引用缓存、LruCache、组件注册表、拦截器扩展点)全部在上游 commonMain 里原样随编译进入 .so,JVM 侧保留 129 项 desktopTest 交叉验证用例证明 ohos .so 里执行的加载逻辑没有漂移。

1.3 实现适配

维度要求
代码复用image-loader 全部 commonMain、skiaMain(Skia 解码器/GifDecoder)、darwinMain(手写 LRU)、noCommonJvmMain 原样编译;仅新增 2 个 ohos actual 文件 + 2 个 vendored 文件(Uri、@Poko 注解)。ktor-network / svg / nine-patch / singleton 一并进入构建。
平台目标根构建直接声明 ohosArm64 + ohosX64,保留 jvm("desktop") 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。
构建隔离根构建替换为极简 ohos 构建(cmp_qrose 模式);app/*、test-*、build-logic、extension/imageio|moko-resources|compose-resources 留盘不进构建。
桥接稳定2 个 C ABI + 扁平 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。
依赖纪律JVM 交叉验证线保持上游版本主体;ohos 线换社区制品线(换线不换语义);上游测试零改动全绿。
UI 完整单页覆盖全部桥接入口:三种来源、加载/再加载、缓存清空、统计面板、日志与版本握手。
仓库规范项目说明、文章、效果图和源码链接统一使用 AtomGit。

本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。Composable 无法跨过 C ABI,因此 ArkTS Demo 通过桥驱动真实的无组合宿主引擎(loader.async() / execute()),并把解码结果以像素形式重新上屏——这与 eungabi / Vico / Reorderable 的社区先例一致。

二、实现路线图

第 1 阶段:项目初始化     ── 盘点模块与依赖边界、验证 Compose ohos 制品线、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.4.20-0.1.0-07 钉版、双依赖线
第 3 阶段:接缝补齐       ── 源集层级重建、ohos actual、vendored Uri、poko 0.23.2 + vendored 注解
第 4 阶段:桥协议建模     ── init/load/clear/stats JSON 契约、ImageEvent 缓存来源信号、错误通道
第 5 阶段:原生桥接       ── @CName C ABI、C++ N-API、参数校验和内存释放
第 6 阶段:ArkUI 宿主     ── 像素直通上屏、CA 证书解包注入、统计面板、日志和版本握手

三、逐步实现过程

第 1 阶段:项目初始化

1.1 工程边界
image-loader/                                上游引擎(进 ohos 构建,jvm desktop + 双 ohos)
image-loader/src/ohosMain/                   新增:Platform / DiskCacheBuilder actual(2 个文件)
image-loader/src/commonMain/.../com/eygraber/uri/Uri.kt      vendored 最小 Uri
image-loader/src/commonMain/.../dev/drewhamilton/poko/Poko.kt vendored @Poko 注解
image-loader-singleton/                      LocalImageLoader + setupDefaultComponents ohos actual
extension/ktor-network/                      curl 引擎 actual(ohosMain)
extension/svg/  extension/nine-patch/        原样编译(skiaMain 与 ohos 共享)
example/nativeApp/                           @CName 桥 × 2 + MiniJson + DisplayExport
example/harmonyApp/                          DevEco Stage、CMake N-API、ArkUI Demo、rawfile/cacert.pem
*.upstream                                   上游构建脚本逐文件保留
app/*  test-*  build-logic  kotlin-js-store  留盘不进构建(Android/iOS/js/wasm/JVM 专属或无 ohos 制品)
extension/imageio|moko-resources|compose-resources  留盘不进构建(javax.imageio / 无 ohos 线 / resources 404)

构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include 5 个库模块与 :example:nativeApp,rootProject.name = "ohos_compose_imageloader"。上游的 spotless / publish / dokka / baselineProfile / build-logic 全部随根构建替换剥离——本 fork 不发 Maven Central,原构建脚本以 *.upstream 逐文件保留。

1.2 工具链矩阵
项目配置
Kotlin MultiplatformHarmonyOS Kotlin 2.4.20-0.1.0-07(eazytec Maven)
JDK21(org.gradle.java.home 显式指定)
Composeohos 线 1.12.0-0.1.0-07(ohosMain)+ 上游线 1.7.3(desktopMain 交叉验证)
kotlinx(ohos 线)coroutines 1.11.0-0.1.0-07 · okio 3.17.0-0.1.0-07 · ktor(+curl) 3.5.1-0.1.0-01 · atomicfu 0.33.0-0.1.0-07 · collection 1.5.0-0.1.0-07
poko插件 0.23.2(与 Kotlin 2.4.20 同代编译),注解 vendored
OpenHarmony 目标ohosArm64 + ohosX64(jvm("desktop") 保留作交叉验证)
Gradle9.6.1(2.4.20 线支持 Gradle 9.x,eungabi 同款组合)
DevEco productcompatibleSdkVersion 5.1.0(18) 兼容
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleio.github.qdsfdhvh.imageloader
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点
模块源集依赖形态处置
image-loadercommonMain 62 文件纯 Kotlin + Compose 图形类型 + okio + coroutines整体编译(零改动)
image-loaderskiaMain 6 文件org.jetbrains.skia(skiko ohos 传递携带)整体编译(与 ohos 共享)
image-loaderdarwinMain / noCommonJvmMain手写 LRU / 包装 Map(纯 Kotlin)整体编译(与 ohos 共享)
image-loaderandroidMain / appleMain / js 等平台专属 API留盘不编译
extension/ktor-networkcommonMain + 引擎 actualktor client + 引擎接缝整体编译 + curl actual
extension/svgskiaMainskia SVGDOM整体编译(共享)
extension/nine-patchcommonMainCompose Painter整体编译
image-loader-singletoncommonMain + 平台 actualCompositionLocal + 组件集整体编译 + ohos actual

expect 声明共 12 处,ohos 侧的处置:

expectohos 处置
Image / Bitmap(+ 扩展)复用 skiaMain 的 typealias(零新增)
WeakReferenceactual typealias = kotlin.native.ref.WeakReference(与 apple 同款)
ioDispatcherDispatchers.Default(鸿蒙线 IO 为 internal,对齐 js/wasm)
defaultFileSystemokio FileSystem.SYSTEM(ohos 原生支持)
identityHashCodeAny.hashCode()(Native 未覆写时即指针身份哈希)
LruHashMap / LruMutableMap复用 darwinMain / noCommonJvmMain(零新增)
remainingFreeSpaceBytes512MB 常量(platform.posix 未导出 statvfs,对齐上游 js/wasm 降级)
httpEngine(ktor-network)Curl.create()(静态 TLS 栈内置)
createImageLoaderProvidableCompositionLocal / setupDefaultComponents(singleton)新增 ohos actual(镜像 desktop / apple)
1.4 页面边界
标题区          IMAGE LOADER · OPENHARMONY / 版本握手行(KMP 图像加载库 · ohosArm64/ohosX64)
来源区          网络图片 / 本地文件 / Base64 三个 chip + 可编辑 URL 输入框(默认大陆可达 https 图)
操作区          加载(深绿) / 再加载(蓝) / 清空内存(橙) / 清空磁盘(红) / 刷新统计(灰)
结果区          cacheSource 徽标 + 像素上屏(PixelMap)+ resultType/尺寸/bytes/耗时/事件明细
自检行          原生导出自检 format/valid/magic/roundtrip(编码往返诊断)
统计区          memoryBitmapBytes / memoryImageKeys / memoryPainterKeys / diskCacheBytes / loads
日志区          带时间戳的操作日志(最新在前,上限 100 条)
关于区          版本握手、引擎分层说明、仓库与社区链接

页面启动时先调 ImageLoaderVersion() 握手,再把 rawfile 里的 Mozilla CA 证书束解到 filesDir、连同 cacheDir 注入桥的 init。

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

2.1 制品线验证(本适配的成败手)

先查社区 Maven(https://maven.eazytec-cloud.com)上有没有 ohos 的引擎级制品。用 .module 元数据逐个验证:

org.jetbrains.compose.ui:ui:1.12.0-0.1.0-07
  variants: ohosArm64ApiElements-published / ohosX64ApiElements-published / jvm 线齐全
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0-0.1.0-07
  variants: 同上
com.squareup.okio:okio:3.17.0-0.1.0-07
  variants: 同上
io.ktor:ktor-client-core / ktor-client-curl:3.5.1-0.1.0-01
  variants: ohosArm64/ohosX64 齐全(curl 的 cinterop klib 内含静态 libcurl/libssl/libcrypto/libnghttp2)

四个关键结论:其一,Compose ohos 线已经齐备——qrose 适配时只能隔离 Compose 画笔,本库无需隔离任何源码;其二,ohos 制品同时发布 jvm 变体,但 Compose ohos 线不发布 *-desktop 子制品,所以 jvm 交叉验证线必须用上游线(1.7.3)单独解析;其三,uri-kmp 与 poko-annotations 没有 ohos 变体,处置见 2.4 与第 3 阶段;其四,ktor CIO 引擎虽有 ohos 变体但 Native 无 TLS(真机实测),curl 引擎的 ohos klib 静态内置整套 TLS 栈,是 https 的唯一通路。

2.2 工具链钉版(为什么是 2.4.20-0.1.0-07)

社区此前批准的钉法是 HarmonyOS Kotlin 2.2.21-1.0.0(qrose 用的,配 Gradle 8.14.1)。但本库上游源码为 Kotlin 2.1.0 / Compose 1.6.11 时代,而 Compose ohos 线 1.12.0-0.1.0-07 基于 Kotlin 2.4.20 元数据,旧线读不了。Kotlin 钉 2.4.20-0.1.0-07,与 eungabi 适配同款组合:Gradle 9.6.1 沿用、compose 编译器插件同版本解析。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_compose_imageloader"

include(":image-loader")
include(":image-loader-singleton")
include(":extension:ktor-network")
include(":extension:svg")
include(":extension:nine-patch")
include(":example:nativeApp")

dependencyResolutionManagement 同样置前——否则配置期直接 Unresolved reference: ohosArm64。

2.3 编译真实引擎

依赖双轨是本构建的核心手法:commonMain 不声明任何外部依赖,按目标源集分线声明(KSafe / eungabi 同款):

kotlin {
    applyDefaultHierarchyTemplate()
    jvm("desktop")
    ohosArm64()
    ohosX64()

    sourceSets {
        val common = commonMain.get()
        val desktop = getByName("desktopMain")
        val ohos = getByName("ohosMain")

        // 重建上游 build-logic 的中间源集层级:
        // common ─┬ noJsAndWasmMain ← desktop(Dispatchers.IO)
        //         ├ commonJvmMain   ← desktop(java.*)
        //         ├ noCommonJvmMain ← ohos(LruMutableMap)
        //         ├ skiaMain        ← desktop + ohos(Skia 解码器)
        //         └ darwinMain      ← ohos(手写 LRU)
        val commonJvmMain = create("commonJvmMain") { dependsOn(common) }
        val noJsAndWasmMain = create("noJsAndWasmMain") { dependsOn(common) }
        val noCommonJvmMain = create("noCommonJvmMain") { dependsOn(common) }
        val skiaMain = create("skiaMain") { dependsOn(common) }
        val darwinMain = create("darwinMain") { dependsOn(common) }

        desktop.dependsOn(commonJvmMain); desktop.dependsOn(noJsAndWasmMain); desktop.dependsOn(skiaMain)
        ohos.dependsOn(noCommonJvmMain); ohos.dependsOn(skiaMain); ohos.dependsOn(darwinMain)

        ohos.dependencies {
            api(libs.compose.ui)                 // 1.12.0-0.1.0-07
            api(libs.compose.foundation)
            api(libs.kotlinx.coroutines.core)    // 1.11.0-0.1.0-07
            api(libs.okio)                       // 3.17.0-0.1.0-07
            implementation(libs.compose.collection)
            implementation(libs.atomicfu)
        }
        desktop.dependencies { /* 上游线 1.7.3 / 1.10.1 / 3.10.2 … */ }
        commonTest.dependencies {
            implementation(kotlin("test"))
            implementation(libs.kotlinx.coroutines.test.jvm)
            implementation(libs.okio.fakefilesystem.jvm)
        }
    }
}

实测:

./gradlew :image-loader:compileKotlinOhosArm64 :image-loader:compileKotlinOhosX64   # BUILD SUCCESSFUL
./gradlew :extension:ktor-network:compileKotlinOhosArm64 :extension:svg:compileKotlinOhosArm64
./gradlew :extension:nine-patch:compileKotlinOhosArm64 :image-loader-singleton:compileKotlinOhosArm64

引擎的 commonMain / skiaMain 文件一个没动,ohos 侧只新增了接缝文件。

2.4 三处依赖级难题的处置
  1. uri-kmp 无 ohos 制品 → 在 commonMain/kotlin/com/eygraber/uri/Uri.kt 放入同包最小实现(parse/parseOrNull/scheme/path/toString,eungabi 先例),uri-kmp 制品整体退出依赖表,两线统一消费 vendored 版本;
  2. poko:不是"注解找不到"而是"缓存键会静默失效" → MemoryKey 等 7 处 @Poko 类依赖 poko 编译插件生成 equals/hashCode,只补注解不跑插件的话编译照常通过、但 MemoryKey 退化为身份相等——内存缓存永远不命中且无任何报错。选 poko 0.23.2(恰以 Kotlin 2.4.20 编译);插件自动注入的 poko-annotations 无 ohos 变体(解析期直接失败),因此 configurations.configureEach { exclude(group = "dev.drewhamilton.poko") } 排除制品、注解以同包源码 vendored 进 commonMain——编译插件按全限定名匹配注解,代码生成不受影响;桥的 init 响应内嵌 MemoryKey("probe") == MemoryKey("probe") 探测,真机上直接看到 “MemoryKey equals ok”;
  3. commonTest 依赖无 ohos 制品(coroutines-test)→ 测试统一在 JVM 跑(desktopTest),根构建全局禁用 ohos 测试任务:
subprojects {
    tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
        .configureEach { enabled = false }
}

上游测试文件零改动保留,ohos 侧只承担编译与链接。

第 3 阶段:桥协议建模

3.1 JSON 契约
com.seiko.imageloader/**(上游引擎 + skia 解码器,原样编译)
        ↓ ImageLoader.async() / execute() / ImageEvent 事件流
Bridge.kt(@CName × 2,扁平 JSON 进出)
        ↓ UTF-8 JSON
N-API imageLoaderDispatch() / imageLoaderVersion()
        ↓
ArkUI Index.ets(单页滚动)

请求与响应:

请求 : {"op":"init","diskCacheDir":"/data/storage/.../image_cache","diskCacheMaxSizeMb":"256",
        "caFile":"/data/storage/.../files/cacert.pem","insecure":"false"}
       {"op":"load","source":"https://www.baidu.com/img/bd_logo1.png"}
       {"op":"clearMemory"} / {"op":"clearDisk"} / {"op":"stats"}
响应 : {"ok":true,"op":"load","cacheSource":"memory|disk|fetch","elapsedMs":135,
        "resultType":"OfBitmap","width":540,"height":258,
        "pngFormat":"bgra8888","pngValid":true,"pngBase64":"…",
        "memoryImageKeys":1,"diskCacheBytes":57344,"loads":2, …}
       {"ok":false,"op":"load","error":"…"}

两个关键设计:其一,缓存来源不靠推测——桥收集上游 ImageEvent 流,StartWithMemory / StartWithDisk / StartWithFetch 直接映射为 cacheSource,这是引擎本体发出的诚实信号;其二,显示载荷以原始像素为主通道——bitmap.readPixels() 读出 N32 像素(RGBA/BGRA8888)直接回传,ArkTS 用 image.createPixelMap(colors, opts) 构图,整条上屏链路不经过任何图像编解码器(原因见 8.1 踩坑 6),PNG/WEBP 编码往返(魔数校验 + makeFromEncoded 重解码 + 宽高比对)作为诊断信息上报,并在超大图(>1.5MP)时作为载荷回落。

suspend 调用统一 runBlocking 收进同步 C ABI,全程不碰 Dispatchers.Main(鸿蒙线跨线程派发有已知问题,resaca/KStore 记录过)。

3.2 缓存实例的自持与注入

桥不通过反射去掏引擎内部的缓存,而是自己创建、经公共 API 注入:

fresh.loader = ImageLoader {
    components {
        setupKtorComponents(httpClient)   // HttpClient(Curl){ engine{ caInfo = … } }
        setupBase64Components()
        setupSvgComponents()
        setupSkiaComponents()
        setupCommonComponents()
    }
    interceptor {
        bitmapMemoryCache(memoryCache = { fresh.bitmapCache })
        imageMemoryCache(memoryCache = { fresh.imageCache })
        painterMemoryCache(memoryCache = { fresh.painterCache })
        diskCache { fresh.diskCache }
    }
}

因此统计、清空、探测都是公共 API 上的正常调用。

3.3 错误通道

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

@CName("ImageLoaderDispatch")
fun imageLoaderDispatch(request: String): String = try {
    /* op 分发 */
} catch (t: Throwable) {
    """{"ok":false,"op":"dispatch","error":${jsonString(t.message ?: t.toString())}}"""
}

非法 JSON、缺 op/source、不支持的来源、磁盘写失败、TLS 握手失败,全部走同一个 ok:false 通道,ArkTS 侧统一渲染;ArkTS 侧再以逐字段提取的方式透出 BusinessError 的 name/code/message(JSON.stringify 会把 BusinessError 序列化成 {})。

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

4.1 三层桥接
ArkTS
  │ JSON string
  ▼
C++ N-API entry(libimageloader_napi.so)
  │ const char*
  ▼
Kotlin/Native C ABI(libohosimageloader.so)
  │ ImageLoaderDispatch / ImageLoaderVersion
  ▼
UTF-8 JSON + DisposeString

直接导出 Kotlin 对象会暴露 ABI 和生命周期问题,所以选择 C ABI + JSON。

4.2 Kotlin/Native 导出函数
@CName("ImageLoaderDispatch")
fun imageLoaderDispatch(request: String): String   // init / load / clear / stats 一口进

@CName("ImageLoaderVersion")
fun imageLoaderVersion(): String = "1.10.0-ohos.1" // 版本握手
C ABI 符号ArkTS 方法覆盖内核
ImageLoaderDispatchimageLoaderDispatch(req)ImageLoader 构建 + 加载/缓存/统计全套会话
ImageLoaderVersionimageLoaderVersion()fork 版本握手(真机版本行的数据来源)
4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
    {"imageLoaderDispatch", nullptr, NapiImageLoaderDispatch, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
    {"imageLoaderVersion", nullptr, NapiImageLoaderVersion, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};

每个回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohosimageloader_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohosimageloader_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与两个业务符号)。

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

add_library(imageloader_napi SHARED napi_init.cpp)
target_link_libraries(imageloader_napi PUBLIC
  libace_napi.z.so
  libhilog_ndk.z.so
  ohosimageloader)

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

export const imageLoaderDispatch: (request: string) => string;
export const imageLoaderVersion: () => string;

第 5 阶段:ArkUI 宿主

5.1 启动注入:CA 证书与磁盘缓存目录

curl 的静态 openssl 在设备上找不到系统 CA store(见踩坑 8),所以 Demo 把 Mozilla CA 证书束(121 张根证书)打进 rawfile,启动时解到沙箱并随 init 注入:

aboutToAppear(): void {
  this.versionText = imageLoaderVersion();          // 版本握手
  const caFile: string = extractCaBundle(context);  // rawfile → filesDir/cacert.pem
  this.dispatch({
    op: 'init',
    diskCacheDir: `${context.cacheDir}/image_cache`,
    caFile: caFile,
    insecure: caFile === '' ? 'true' : 'false',     // 解包失败时的兜底开关
  });
}
5.2 触发加载与像素上屏
private doLoad(): void {
  const res = this.dispatch({ op: 'load', source: this.sourceInput.trim() });
  if (res !== null && res.ok && res.pngBase64 !== '') {
    this.decodePixels(res.pngBase64, res.pngFormat);   // rgba8888/bgra8888 直通
  }
}

private async decodePixels(b64: string, format: string): Promise<void> {
  const bytes: Uint8Array = new util.Base64Helper().decodeSync(b64);
  const opts: image.InitializationOptions = {
    size: { width: this.outcome.width, height: this.outcome.height },
    pixelFormat: format === 'rgba8888' ? image.PixelMapFormat.RGBA_8888 : image.PixelMapFormat.BGRA_8888,
    alphaType: image.AlphaType.UNKNOWN,
  };
  this.pixelMap = await image.createPixelMap(bytes, opts);   // 无编解码器参与
}

put 前先校验、ImageSource 在 finally 里 release、旧 PixelMap 及时释放——三条纪律保证连续加载不泄漏。

5.3 页面交互

操作区自上而下:来源 chips(网络/本地/Base64)、加载(深绿白字)/再加载(蓝)、清空内存(橙)/清空磁盘(红)/刷新统计(灰)。结果区展示 cacheSource 徽标与渲染图,自检行显示 原生导出自检 format=bgra8888 valid=true magic=… codec check: png-ok。每条日志带时间戳,→ 行显示请求、← 行显示响应,最新在前、上限 100 条。

第 6 阶段:示例与验证

6.1 工程结构
example/
├── nativeApp/
│   ├── build.gradle.kts                      双 ABI sharedLib(baseName=ohosimageloader)+ cxxShared + copyKnSo*
│   └── src/ohosMain/kotlin/example/nativeapp/
│       └── Bridge.kt                         @CName × 2 + MiniJson + DisplayExport(像素直通 + 编码往返诊断)
└── harmonyApp/
    ├── build-profile.json5                   signingConfigs 留空(签名由 DevEco 自动生成回填)
    ├── AppScope/app.json5                    bundleName io.github.qdsfdhvh.imageloader、versionName 1.10.0-ohos.1
    ├── entry/build-profile.json5             externalNativeOptions + abiFilters [arm64-v8a, x86_64]
    ├── entry/libs/{arm64-v8a,x86_64}/        linkDebugShared* 经 copyKnSo* 回填三件套(gitignore)
    ├── entry/src/main/resources/rawfile/cacert.pem   Mozilla CA 证书束(121 根证书)
    ├── entry/src/main/cpp/CMakeLists.txt     IMPORTED 引入 KN so、$ORIGIN rpath
    ├── entry/src/main/cpp/napi_init.cpp      N-API 导出、参数检查、DisposeString
    ├── entry/src/main/cpp/include/libohosimageloader_api.h
    ├── entry/src/main/cpp/types/libimageloader_napi/{index.d.ts, oh-package.json5}
    └── entry/src/main/ets/pages/Index.ets    ArkUI 验收页面
6.2 构建、签名和安装
./gradlew :image-loader:desktopTest                                    # 129 项交叉验证
./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 保持空数组)。

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 io.github.qdsfdhvh.imageloader -m entry

四、完整代码对照

4.1 整体架构

com.seiko.imageloader/**(上游引擎原样编译)
    │ ImageLoader / 拦截器链 / MemoryCache×3 / DiskCache / ComponentRegistry
extension/ktor-network/**(curl 引擎 actual)
extension/svg/**(skia SVGDOM,共享)
    │ loader.async(ImageRequest(source))
Bridge.kt + MiniJson + DisplayExport
    │ ImageLoaderDispatch / ImageLoaderVersion
libimageloader_napi.so
    │ imageLoaderDispatch / imageLoaderVersion
Index.ets
    │ JSON parse + createPixelMap(rgba/bgra) + @State 更新
    ▼
ArkUI 真机页面(单页滚动)

4.2 文件清单

文件职责
settings.gradle.kts极简 ohos 构建、eazytec Maven 置前、ohos_compose_imageloader、模块裁剪
gradle/libs.versions.toml双依赖线钉版(ohos 线 + jvm 交叉验证线)
build.gradle.kts(根)版本 1.10.0-ohos.1、全局禁用 Ohos*Test* 任务
image-loader/build.gradle.ktsjvm desktop + 双 ohos、源集层级重建、poko 0.23.2 + 注解制品排除
image-loader/src/ohosMain/.../Platform.ohos.ktWeakReference typealias / ioDispatcher=Default / FileSystem.SYSTEM / identityHashCode
image-loader/src/ohosMain/.../DiskCacheBuilder.ohos.kt剩余空间降级常量(对齐上游 js/wasm)
image-loader/src/commonMain/.../com/eygraber/uri/Uri.ktvendored 最小 Uri
image-loader/src/commonMain/.../dev/drewhamilton/poko/Poko.ktvendored @Poko 注解
extension/ktor-network/src/ohosMain/.../KtorUrlFetcher.ohos.ktcurl 引擎 actual
image-loader-singleton/src/ohosMain/...LocalImageLoader + setupDefaultComponents actual
example/nativeApp/build.gradle.kts双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填
nativeApp/.../Bridge.kt2 个 C ABI、runBlocking、MiniJson、像素直通 + 编码往返诊断、CA 注入
harmonyApp/.../cpp/napi_init.cppN-API 导出、参数检查、DisposeString
harmonyApp/.../cpp/CMakeLists.txtIMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套
harmonyApp/.../rawfile/cacert.pemMozilla CA 证书束(curl 静态 openssl 的 CA 来源)
harmonyApp/.../ets/pages/Index.etsArkUI 验收页面(加载/再加载/双缓存清空/统计/日志)

4.3 关键 API 对照

层次API作用
引擎ImageLoader { components { … } interceptor { … } }构建带组件集与缓存的真实加载器
引擎loader.async(ImageRequest(source))挂起加载 + ImageEvent 事件流
引擎MemoryCache / DiskCache三级内存缓存 / okio 事务写磁盘缓存
组件setupKtorComponents(httpClient)ktor 抓取器 + URL 映射/键器(引擎配置可注入)
组件setupSkiaComponents / setupSvgComponentsSkia / SVG 解码器
桥ImageLoaderDispatch / ImageLoaderVersionC ABI JSON 导出
N-APIimageLoaderDispatch(req) / imageLoaderVersion()向 ArkTS 暴露能力
ArkTSdoLoad() / decodePixels()触发加载并从颜色数组构 PixelMap

4.4 ArkTS 与 Kotlin 的边界

const res = JSON.parse(imageLoaderDispatch(JSON.stringify({
  op: 'load', source: 'https://www.baidu.com/img/bd_logo1.png'
})));
// res.ok === true; 再加载时 res.cacheSource === 'memory'、耗时骤降
// 杀进程重启后 cacheSource === 'disk' —— 磁盘缓存持久化命中
val result = runBlocking {
    var r: ImageResult? = null
    loader.async(ImageRequest(source)).collect { action ->
        when (action) {
            is ImageEvent -> events.add(action.eventName)   // StartWithMemory/Disk/Fetch
            is ImageResult -> r = action
            else -> Unit
        }
    }
    r
}

两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构。

五、关键决策说明

决策 1:真编译整库而不是隔离 Compose 代码

qrose 适配时社区 Compose ohos 线尚不可用,只能把 Compose 画笔隔离在构建之外。本适配先验证制品线(Compose ui/foundation 1.12.0-0.1.0-07 的 ohos 变体齐备、skiko ohos 传递携带),再决定路线——引擎、拦截器链、Skia 解码器全部上游原件,无任何源码隔离或重写。

决策 2:工具链升到 Kotlin 2.4.20-0.1.0-07,Gradle 沿用 9.6.1

Compose ohos 线的元数据基于 Kotlin 2.4.20,旧钉法 2.2.21 读不了;eungabi 已验证 2.4.20 + Gradle 9.6.1 组合,本仓原样沿用。两条工具链线的经验不能互相套用。

决策 3:依赖双轨——commonMain 零外部依赖

Compose ohos 线不发布 *-desktop 子制品,jvm 目标无法从 ohos 线解析。commonMain 不声明任何外部依赖、ohosMain 与 desktopMain 分线声明,两线编译同一份源码;jvm 线同时承担上游测试的交叉验证。

决策 4:显示通道原始像素直通,编码往返只做诊断

真机实测 OHOS 系统编解码器(image.createImageSource)对 ohos skiko 重编码的合法 PNG 返回 undefined——两个解码器意见不合时谁也说服不了谁,干脆绕开:原生侧直读位图像素回传,ArkTS 从颜色数组构 PixelMap;PNG/WEBP 编码往返作为诊断信号保留(codec check: png-ok/webp-ok/codec-failed),把分歧暴露在日志里而不是让它阻塞显示。

决策 5:TLS 三段式落地

CIO 在 Native 无 TLS(真机报错坐实)→ 换 curl 引擎(klib 静态内置 libcurl/libssl/libcrypto/libnghttp2,链接期仅需系统 -lz)→ 静态 openssl 无 CA store → 随 HAP 打包 Mozilla CA 证书束经 CURLOPT_CAINFO 注入,另留 insecure 兜底开关。每一段都在真机上被下一轮验收确认。

决策 6:desktopTest 交叉验证 + 测试零改动

引擎逻辑在 commonMain,JVM 与 ohos 跑同一份代码;129 项 desktopTest 跑的就是上游原测试文件。coroutines-test 没有 ohos 制品,处置不是改测试,而是"测试统一 JVM + 根构建禁用 Ohos*Test*"——测试文件一个字节不动,交叉验证的有效性才成立。

六、测试与验证

6.1 测试环境

本次验证使用 Windows x64、JDK 21、DevEco Studio 5.1.0 Release(E:\DE\DevEco Studio)、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙真机(arm64)。

6.2 JVM 交叉验证

./gradlew :image-loader:desktopTest :extension:ktor-network:desktopTest

129 项用例全部通过(上游原测试文件,零改动),覆盖 DiskLruCache 事务写/日志恢复、DiskCache 快照编辑器、RealMemoryCache 强弱缓存淘汰、拦截器链、ImageRequest 构建等 10 个测试类。依赖换线没有 API 漂移:coroutines 1.10.1、okio 3.10.2、compose 1.7.3 组成的 jvm 线跑的正是上游声明的版本主体——"换线不换语义"的直接证据。

6.3 HAP 验证

:image-loader:compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL(5 个库模块全部通过)
:example:nativeApp:linkDebugSharedOhosArm64: arm64-v8a/libohosimageloader.so(~49.8MB debug,含静态 TLS 栈)
:example:nativeApp:linkDebugSharedOhosX64:  x86_64/libohosimageloader.so(~48.2MB debug)
llvm-nm -D: ImageLoaderDispatch / ImageLoaderVersion / libohosimageloader_symbols 均为全局 T 符号
BuildNativeWithCmake: finished(双 ABI 编出 libimageloader_napi.so)
CompileArkTS: finished
PackageHap: finished
BUILD SUCCESSFUL
install bundle successfully / start ability successfully

entry-default-unsigned.hap 约 69.5MB(debug 双 ABI,未 strip;HAP 内 DoNativeStrip 后显著缩小)。

6.4 功能用例

  1. 启动后日志出现 imageLoaderVersion() → 1.10.0-ohos.1(来自真实 Native 调用,非静态文字);
  2. init 响应含 MemoryKey equals ok(@Poko 代码生成在 ohos KLIB 生效的运行时证据)与 TLS CA=cacert.pem;
  3. 默认网络图加载返回 ok:true,cacheSource=fetch,图片渲染上屏,diskCacheBytes 增长;
  4. 再加载:cacheSource=memory、耗时从千毫秒级降至百毫秒级以内、memoryImageKeys 增长——内存缓存命中;
  5. 清空内存后加载:cacheSource=disk——磁盘缓存命中;
  6. 杀掉应用重启后再加载:仍 cacheSource=disk——落盘持久化成立,不是内存缓存;
  7. Base64 chip 加载 1×1 PNG:resultType=OfBitmap、原始像素直通上屏;
  8. 自检行显示 format=bgra8888 valid=true … codec check: png-ok——编码往返诊断与像素主通道同时可见;
  9. 每步操作按时间戳滚动留痕,缓存统计随响应实时刷新。

6.5 验证结论

加载引擎、三级缓存、ktor 抓取(含 TLS)、Skia 解码、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实的加载会话:版本握手、@Poko 探测、网络抓取、内存/磁盘缓存命中、重启持久化闭环(见下节效果图)。

七、运行效果

7.1 真机截图

在这里插入图片描述

7.2 命令速查

git clone https://atomgit.com/oh-tpc/compose-imageloader.git
cd compose-imageloader
export JAVA_HOME="/path/to/jdk-21"
./gradlew :image-loader:compileKotlinOhosArm64 :image-loader:compileKotlinOhosX64   # klib
./gradlew :image-loader:desktopTest                                                # 129 项交叉验证
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
cd example/harmonyApp
hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon    # 签名在 DevEco Studio 完成
hdc install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b io.github.qdsfdhvh.imageloader -m entry
hdc shell hilog | grep ImageLoaderNapi

八、遗留问题与改进方向

8.1 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配,必须让 5 个库模块真正编译成 ohosArm64 / ohosX64 动态库;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. 上游 build-logic 约定插件不可用时,中间源集要手工重建;此工具链的 KGP 上 dependsOn/freeCompilerArgs 写法与上游文档有差异——sourceSets.create("…"){ dependsOn(common) } + 扩展级 compilerOptions {}(KSafe 同款)才是可用组合;
  4. poko 是隐形地雷:poko-annotations 无 ohos 变体会让解析期直接失败,但只靠"补个注解"而不跑编译插件,编译全绿、内存缓存却永远不命中——MemoryKey 的 equals/hashCode 是缓存键的命脉。插件选与 Kotlin 同代的 0.23.2,注解 vendored、制品 exclude,init 时运行时探测;
  5. Dispatchers.IO 在鸿蒙 coroutines 线是 internal(FFRT 调度无独立 IO 池),ohos actual 映射 Dispatchers.Default;
  6. OHOS 系统编解码器与 ohos skiko 的 PNG 编码产物不互认:skia 整字节重解码通过的 PNG,image.createImageSource 返回 undefined 且不抛错(表现为 Cannot read property createPixelMap of undefined)。显示通道因此改为原始像素直通,ImageSource 必须 release(不释放连续加载几次后同样引发解码失败);
  7. CIO 在 Native 没有 TLS:任何 https 都报 TLS sessions are not supported on Native platform,与制品是否存在无关;换 curl 引擎解决,其 ohos klib 静态内置 libcurl/libssl/libcrypto/libnghttp2(解包 klib 的 targets/ohos_arm64/included/ 可见 .a 文件),链接期仅需系统 -lz;
  8. 静态 openssl 没有系统 CA store:百度等大陆必达站点也报 CURL_SSL_CONNECT_ERROR(error 35),不是网络问题而是证书库缺失——把 Mozilla CA 证书束打进 rawfile、解到沙箱后经 CURLOPT_CAINFO 注入即可;
  9. Demo 默认 URL 要选设备网络可达的站点:raw.githubusercontent.com 在大陆网络 TLS 握手被 SNI 阻断,curl 报的同样是 error 35,极易与 CA 问题混淆——先用必达站点分辨,再看证书;
  10. BusinessError 用 JSON.stringify 只会得到 {},必须逐字段提取 name/code/message,否则真机错误码全部隐形;
  11. commonTest 的测试依赖(coroutines-test)没有 ohos 制品:测试统一 JVM 跑 + 根构建全局禁用 Ohos*Test* 任务,上游测试文件零改动;
  12. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();
  13. 签名材料由 DevEco 自动写入 build-profile.json5,仓库内 signingConfigs 保持空数组,证书绝不进入 AtomGit。

8.2 已知问题

  • extension/compose-resources、moko-resources、imageio 未进构建:前者社区线 404,后两者分别是浏览器 / javax.imageio 专属语义;
  • identityHashCode 以 Any.hashCode() 替代:ohos 工具链未导出 compose 与 kotlin.native.internal 两处实现;缓存值(skia 外部类、不可变 Painter)未覆写 hashCode,语义等价,但覆写了 hashCode 的可变 Painter 理论上会削弱 WeakMemoryCache 的变更检测;
  • 磁盘剩余空间查询降级为 512MB 常量(上游 js/wasm 同款降级):maxSizePercent 语义受影响,精确容量用 maxSizeBytes(Demo 即如此);
  • OfPainter 结果(SVG/GIF Painter)不做位图导出,Demo 中如实标注;
  • android / js / wasm / apple 目标剥离(源文件留在树里),发布、dokka、kover 一并剥离,本 fork 不发 Maven Central;
  • debug .so 未 strip 约 50MB(含静态 TLS 栈),HAP 内 DoNativeStrip 后显著缩小,发布可用 linkReleaseShared。

8.3 未来优化方向

  • 为 OHOS 系统编解码器与 skiko 编码产物的分歧做专项排查(自检行已把 codec check 结果暴露在日志里,具备采集条件);
  • 把 OfPainter(SVG/GIF)栅格化导出补上(离屏 Surface + DrawScope);
  • 评估 .har 形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套;
  • 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
  • 补充模拟器(x86_64)验收矩阵与自动化截图脚本。

九、总结

9.1 核心难点回顾

上游 compose-imageLoader 1.10.0(commonMain + skiaMain 引擎原件)
    → 验证社区 Compose/kotlinx ohos 制品线(1.12.0-0.1.0-07 全链可达)
    → 工具链对齐 Kotlin 2.4.20-0.1.0-07(Gradle 9.6.1,eungabi 同款组合)
    → 根构建替换(cmp_qrose 模式,app/test/build-logic 模块留盘)
    → 手工重建源集层级:ohos 复用 skiaMain/darwinMain,仅补 2 个 actual 文件
    → vendored Uri + vendored @Poko + poko 0.23.2 插件(保住 MemoryKey 相等性)
    → ktor curl 引擎 actual(静态 TLS 栈)+ rawfile CA 证书束注入
    → 129 项 desktopTest 交叉验证全绿(上游测试零改动)
    → @CName × 2 + 扁平 JSON + 自持缓存实例 + ImageEvent 诚实信号
    → C++ N-API(DisposeString 纪律)→ ArkUI Demo(像素直通上屏)
    → 双 ABI .so + HAP + 四轮真机验收(解码 → TLS → CA → 缓存命中)

9.2 封装层次

  • 引擎层:上游原版 ImageLoader、拦截器链、三级内存缓存、DiskLruCache、组件注册表;
  • 扩展层:上游原版 ktor 抓取器(curl 引擎 actual)、SVG 解码器(skia SVGDOM)、九宫格拦截器;
  • Native 层:生成双 ABI 动态库并通过 2 个 C ABI 输出 JSON;
  • N-API 层:完成参数检查、字符串转换和 Native 内存释放;
  • ArkTS 层:管理页面状态、JSON 校验、CA 证书解包、像素上屏、统计与日志;
  • DevEco 层:完成 CMake、HAP、签名、安装和真机运行。

9.3 三条经验

  1. 制品线决定路线:qrose 时代只能隔离 Compose,本库适配当天 Compose ohos 线已齐备——真编译整库 + 补 2 个 actual 就够了;路线选择必须基于当天的制品可达性,而不是历史结论;
  2. 真机是唯一裁判:CIO 无 TLS、静态 openssl 缺 CA、OHOS 编解码器与 skiko 的分歧、ImageSource 泄漏,全部是桌面/编译期全绿、真机才暴露的问题;本轮四轮真机迭代(解码失败 → TLS 失败 → CA 缺失 → 缓存命中)每一轮都推翻了一个"应该没问题"的假设;
  3. 诚实信号设计让每层可证伪:ImageEvent 事件流回答"哪个缓存服务了请求",MemoryKey 相等性探测回答"poko 代码生成是否生效",编码往返自检回答"两个编解码器是否互认"—— Demo 不只是演示功能,也是一套把故障定位到层的仪表盘。

9.4 适配成果

当前适配已完成:

  • compose-imageLoader 1.10.0 真实引擎的双 ABI Kotlin/Native 编译(5 个模块,上游 commonMain / skiaMain 文件零重写,仅 2 个 ohos actual + 2 个 vendored 文件);
  • Compose ohos 制品线(1.12.0-0.1.0-07)在图像加载库上的完整落地(含 skiko ohos 变体);
  • 2 个 C ABI 符号和 C++ N-API 包装层(llvm-nm 验证全局 T 符号);
  • TLS 完整通路:curl 静态 TLS 栈 + 随 HAP 分发的 Mozilla CA 证书束;
  • DevEco Stage ArkUI 单页验收页面(网络/文件/Base64 三来源、加载、再加载、双缓存清空、重启持久化、版本握手、原生导出自检、操作日志);
  • 129 项 JVM 交叉验证全绿(上游原测试文件)+ HAP 构建(双 ABI)、签名、设备安装和真机验证;
  • AtomGit 项目文档、OpenHarmony README(中英双份)和本地效果图。

适配过程 FAQ

以下问题来自 ohosArm64 目标接入、NAPI 桥接、HAP 构建与真机验收的实际流程,按出现频率排序。

Q1:DevEco Studio Sync 失败或 ohpm 依赖下载超时,怎么处理?

  • 确认 DevEco Studio 版本与工程 build-profile.json5 的 compatibleSdkVersion 一致;
  • 在 File > Settings > Build, Execution, Deployment > Ohpm 配置可用的 ohpm 仓库镜像或代理;
  • 删除 oh_modules、oh-package-lock.json5 与 .hvigor 缓存后重新 Sync;
  • 公司网络需要代理时,同时给 ohpm 与 hvigor 配置 http.proxy。

Q2:CMake 报 Missing .../libxxx.so 或找不到 Kotlin/Native 动态库?

  • 先在 example 目录执行 ./gradlew :nativeApp:linkDebugSharedOhosArm64(模拟器用 linkDebugSharedOhosX64);
  • 确认 libxxx.so 与 libc++_shared.so 已复制到 ohos/entry/libs/<abi>/;
  • 检查 entry/build-profile.json5 的 abiFilters 与设备 ABI 是否一致(真机 arm64-v8a,模拟器 x86_64);
  • 清理 entry/.cxx 与 entry/build 后重试。

Q3:ArkTS 报 Cannot find module 'libxxx_napi.so' 或调用返回 undefined?

  • 确认 CMake 的 add_library(<target> SHARED ...) 名称与 import ... from 'libxxx_napi.so' 完全一致;
  • 解包 HAP,确认 libs/<abi>/libxxx_napi.so 已打包;
  • 删除 entry/build、entry/.cxx 与 oh_modules 后重新构建;
  • 检查 module.json5 的 srcEntry 与页面路径是否正确。

Q4:真机安装报 signature verification failed 或 install failed?

  • 在 DevEco Studio 的 Signing Configs 配置证书、Profile 与密码;
  • bundleName 必须与签名 profile 中的 bundle name 一致;
  • 设备需加入 profile 的 deviceIds,调试证书有效期需覆盖当前时间;
  • 用 hdc list targets 确认设备在线,再执行 hdc install -r xxx-signed.hap。

Q5:应用启动白屏、闪退或 NAPI 调用崩溃?

  • 用 hdc hilog | grep -i "napi\|kotlin" 查看原生日志;
  • 确认 libc++_shared.so 与 Kotlin/Native 工具链版本匹配,且 ABI 与设备一致;
  • 检查 C ABI 返回的字符串是否由 Native 侧分配、ArkTS 侧复制后是否调用 XxxFree 释放;
  • 在 DevEco 的 Native 调试配置中附加 lldb,定位空指针与越界访问。

Q6:Gradle 报 ohosArm64 目标不存在或 Kotlin/Native 编译失败?

  • 使用鸿蒙发行版 Kotlin(如 2.2.21-1.0.0),并在 settings.gradle.kts 配置 eazytec 仓库;
  • Gradle 建议 8.x、JDK 21,版本不匹配时先统一工具链;
  • 确认 kotlin { ohosArm64() } 已应用,且 local.properties 的 SDK 路径有效;
  • 清理 ~/.gradle/caches 中对应版本的 Kotlin/Native 缓存后重试。

Q7:HAP 构建报 SDK 版本不匹配或 compatibleSdkVersion 错误?

  • 对齐 DevEco Studio、HarmonyOS SDK 与 build-profile.json5 中的 compatibleSdkVersion;
  • 升级 DevEco Studio 后同步升级 hvigor 与 SDK,避免 modelVersion 与 IDE 不匹配;
  • 命令行构建时设置 DEVECO_SDK_HOME 指向 DevEco 的 sdk 目录。

Q8:命令行 hvigor 构建失败,但 DevEco 里能构建?

  • 使用 DevEco 自带的 node 与 hvigor:
    "$DEVECO/Contents/tools/node/bin/node" "$DEVECO/Contents/tools/hvigor/bin/hvigorw.js" ...;
  • 设置 JAVA_HOME、NODE_HOME、DEVECO_SDK_HOME;
  • 加上 --no-daemon 排除守护进程缓存影响,并清理 entry/build 与 .hvigor。

参考文档

Logo

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

更多推荐