开源鸿蒙平台 KMP 三方库「compose-imageLoader」适配全流程
本文记录
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 Multiplatform | HarmonyOS Kotlin 2.4.20-0.1.0-07(eazytec Maven) |
| JDK | 21(org.gradle.java.home 显式指定) |
| Compose | ohos 线 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") 保留作交叉验证) |
| Gradle | 9.6.1(2.4.20 线支持 Gradle 9.x,eungabi 同款组合) |
| DevEco product | compatibleSdkVersion 5.1.0(18) 兼容 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | io.github.qdsfdhvh.imageloader |
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点
| 模块 | 源集 | 依赖形态 | 处置 |
|---|---|---|---|
image-loader | commonMain 62 文件 | 纯 Kotlin + Compose 图形类型 + okio + coroutines | 整体编译(零改动) |
image-loader | skiaMain 6 文件 | org.jetbrains.skia(skiko ohos 传递携带) | 整体编译(与 ohos 共享) |
image-loader | darwinMain / noCommonJvmMain | 手写 LRU / 包装 Map(纯 Kotlin) | 整体编译(与 ohos 共享) |
image-loader | androidMain / appleMain / js 等 | 平台专属 API | 留盘不编译 |
extension/ktor-network | commonMain + 引擎 actual | ktor client + 引擎接缝 | 整体编译 + curl actual |
extension/svg | skiaMain | skia SVGDOM | 整体编译(共享) |
extension/nine-patch | commonMain | Compose Painter | 整体编译 |
image-loader-singleton | commonMain + 平台 actual | CompositionLocal + 组件集 | 整体编译 + ohos actual |
expect 声明共 12 处,ohos 侧的处置:
| expect | ohos 处置 |
|---|---|
Image / Bitmap(+ 扩展) | 复用 skiaMain 的 typealias(零新增) |
WeakReference | actual typealias = kotlin.native.ref.WeakReference(与 apple 同款) |
ioDispatcher | Dispatchers.Default(鸿蒙线 IO 为 internal,对齐 js/wasm) |
defaultFileSystem | okio FileSystem.SYSTEM(ohos 原生支持) |
identityHashCode | Any.hashCode()(Native 未覆写时即指针身份哈希) |
LruHashMap / LruMutableMap | 复用 darwinMain / noCommonJvmMain(零新增) |
remainingFreeSpaceBytes | 512MB 常量(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 三处依赖级难题的处置
- uri-kmp 无 ohos 制品 → 在
commonMain/kotlin/com/eygraber/uri/Uri.kt放入同包最小实现(parse/parseOrNull/scheme/path/toString,eungabi 先例),uri-kmp 制品整体退出依赖表,两线统一消费 vendored 版本; - 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”; - 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 方法 | 覆盖内核 |
|---|---|---|
ImageLoaderDispatch | imageLoaderDispatch(req) | ImageLoader 构建 + 加载/缓存/统计全套会话 |
ImageLoaderVersion | imageLoaderVersion() | 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.kts | jvm desktop + 双 ohos、源集层级重建、poko 0.23.2 + 注解制品排除 |
image-loader/src/ohosMain/.../Platform.ohos.kt | WeakReference typealias / ioDispatcher=Default / FileSystem.SYSTEM / identityHashCode |
image-loader/src/ohosMain/.../DiskCacheBuilder.ohos.kt | 剩余空间降级常量(对齐上游 js/wasm) |
image-loader/src/commonMain/.../com/eygraber/uri/Uri.kt | vendored 最小 Uri |
image-loader/src/commonMain/.../dev/drewhamilton/poko/Poko.kt | vendored @Poko 注解 |
extension/ktor-network/src/ohosMain/.../KtorUrlFetcher.ohos.kt | curl 引擎 actual |
image-loader-singleton/src/ohosMain/... | LocalImageLoader + setupDefaultComponents actual |
example/nativeApp/build.gradle.kts | 双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填 |
nativeApp/.../Bridge.kt | 2 个 C ABI、runBlocking、MiniJson、像素直通 + 编码往返诊断、CA 注入 |
harmonyApp/.../cpp/napi_init.cpp | N-API 导出、参数检查、DisposeString |
harmonyApp/.../cpp/CMakeLists.txt | IMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套 |
harmonyApp/.../rawfile/cacert.pem | Mozilla CA 证书束(curl 静态 openssl 的 CA 来源) |
harmonyApp/.../ets/pages/Index.ets | ArkUI 验收页面(加载/再加载/双缓存清空/统计/日志) |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| 引擎 | ImageLoader { components { … } interceptor { … } } | 构建带组件集与缓存的真实加载器 |
| 引擎 | loader.async(ImageRequest(source)) | 挂起加载 + ImageEvent 事件流 |
| 引擎 | MemoryCache / DiskCache | 三级内存缓存 / okio 事务写磁盘缓存 |
| 组件 | setupKtorComponents(httpClient) | ktor 抓取器 + URL 映射/键器(引擎配置可注入) |
| 组件 | setupSkiaComponents / setupSvgComponents | Skia / SVG 解码器 |
| 桥 | ImageLoaderDispatch / ImageLoaderVersion | C ABI JSON 导出 |
| N-API | imageLoaderDispatch(req) / imageLoaderVersion() | 向 ArkTS 暴露能力 |
| ArkTS | doLoad() / 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 功能用例
- 启动后日志出现
imageLoaderVersion() → 1.10.0-ohos.1(来自真实 Native 调用,非静态文字); - init 响应含
MemoryKey equals ok(@Poko 代码生成在 ohos KLIB 生效的运行时证据)与TLS CA=cacert.pem; - 默认网络图加载返回
ok:true,cacheSource=fetch,图片渲染上屏,diskCacheBytes增长; - 再加载:
cacheSource=memory、耗时从千毫秒级降至百毫秒级以内、memoryImageKeys增长——内存缓存命中; - 清空内存后加载:
cacheSource=disk——磁盘缓存命中; - 杀掉应用重启后再加载:仍
cacheSource=disk——落盘持久化成立,不是内存缓存; - Base64 chip 加载 1×1 PNG:
resultType=OfBitmap、原始像素直通上屏; - 自检行显示
format=bgra8888 valid=true … codec check: png-ok——编码往返诊断与像素主通道同时可见; - 每步操作按时间戳滚动留痕,缓存统计随响应实时刷新。
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 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配,必须让 5 个库模块真正编译成
ohosArm64/ohosX64动态库; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;- 上游
build-logic约定插件不可用时,中间源集要手工重建;此工具链的 KGP 上dependsOn/freeCompilerArgs写法与上游文档有差异——sourceSets.create("…"){ dependsOn(common) }+ 扩展级compilerOptions {}(KSafe 同款)才是可用组合; - poko 是隐形地雷:
poko-annotations无 ohos 变体会让解析期直接失败,但只靠"补个注解"而不跑编译插件,编译全绿、内存缓存却永远不命中——MemoryKey的 equals/hashCode 是缓存键的命脉。插件选与 Kotlin 同代的 0.23.2,注解 vendored、制品 exclude,init 时运行时探测; Dispatchers.IO在鸿蒙 coroutines 线是 internal(FFRT 调度无独立 IO 池),ohos actual 映射Dispatchers.Default;- OHOS 系统编解码器与 ohos skiko 的 PNG 编码产物不互认:skia 整字节重解码通过的 PNG,
image.createImageSource返回 undefined 且不抛错(表现为Cannot read property createPixelMap of undefined)。显示通道因此改为原始像素直通,ImageSource必须 release(不释放连续加载几次后同样引发解码失败); - 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; - 静态 openssl 没有系统 CA store:百度等大陆必达站点也报
CURL_SSL_CONNECT_ERROR(error 35),不是网络问题而是证书库缺失——把 Mozilla CA 证书束打进 rawfile、解到沙箱后经CURLOPT_CAINFO注入即可; - Demo 默认 URL 要选设备网络可达的站点:
raw.githubusercontent.com在大陆网络 TLS 握手被 SNI 阻断,curl 报的同样是 error 35,极易与 CA 问题混淆——先用必达站点分辨,再看证书; BusinessError用JSON.stringify只会得到{},必须逐字段提取name/code/message,否则真机错误码全部隐形;commonTest的测试依赖(coroutines-test)没有 ohos 制品:测试统一 JVM 跑 + 根构建全局禁用Ohos*Test*任务,上游测试文件零改动;- KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free(); - 签名材料由 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 三条经验
- 制品线决定路线:qrose 时代只能隔离 Compose,本库适配当天 Compose ohos 线已齐备——真编译整库 + 补 2 个 actual 就够了;路线选择必须基于当天的制品可达性,而不是历史结论;
- 真机是唯一裁判:CIO 无 TLS、静态 openssl 缺 CA、OHOS 编解码器与 skiko 的分歧、ImageSource 泄漏,全部是桌面/编译期全绿、真机才暴露的问题;本轮四轮真机迭代(解码失败 → TLS 失败 → CA 缺失 → 缓存命中)每一轮都推翻了一个"应该没问题"的假设;
- 诚实信号设计让每层可证伪:
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。
参考文档
更多推荐

所有评论(0)