Kotlin Multiplatform for OpenHarmony 实战:为 Kamel 实现图片加载适配
大家好,我是熊猫钓鱼!欢迎大家和我一起探讨技术。希望您能点赞关注,谢谢!

摘要
本文聚焦如何在 OpenHarmony 上为 Kotlin Multiplatform 图片加载库 Kamel 做适配落地。Kamel 用一套跨平台 API(ImageSource 数据源 / ImageRequest 请求 / ImageLoader 加载门面 + 内存缓存 + 平台解码)统一封装各平台图片加载。本文用 ArkTS 桥接 @kit.NetworkKit 的 @ohos.net.http(下载)与 @kit.ImageKit 的 @ohos.multimedia.image(解码),把「下载字节 → 解码位图 → 回填缓存 → 上屏」翻译成语义层的统一加载流水线,全程遵循本适配工程的三层架构:语义层 Kamel.ets(ImageSource 三源联合 / ImageRequest / ImageResult / DecodedImage / KamelEngine 平台接口 / MemoryCache LRU / ImageLoader 门面,不碰 @kit.*),引擎层 OhosKamelEngine.ets 真正接上系统网络与解码,验收页 KamelDemo.ets 提供「远程加载 / 示例图(bytes 源免网络)/ 清空缓存」三件套,并附内存缓存命中演示。
文章第二部分拆解 OpenHarmony 的图片加载真实基础(@ohos.net.http 的连接生命周期与 ARRAY_BUFFER 返回、@ohos.multimedia.image 的 ImageSource/createPixelMap、ArkUI Image 显示 PixelMap),第三至六节逐层对照本仓库代码讲语义层、引擎层与验收页,第七节总结 ArkTS 适配踩到的真实坑,第八节对照上游库讲本项目的 API 命名与类型设计决策(pixelMap 以 Object 形态跨层、Resource 源暂未实现、MemoryCache 简化为极简 LRU)。
本适配基于 HarmonyOS SDK 6.0.0(20) + KMP&CMP 鸿蒙社区工具链 v1.1.0(Kotlin 2.2.21 / CMP 1.9.2)开发,assembleHap 编译 BUILD SUCCESSFUL、零 ArkTS error,模拟器即可跑示例图解码与缓存演示。
目录
- 一、Kamel 是什么,以及为什么要在 OpenHarmony 上适配它
- Kamel 核心契约(ImageSource / ImageRequest / ImageLoader / 内存缓存)
- 适配目标:把图片加载语义还原成 ArkTS,引擎层真正接上系统网络与解码
- 二、OpenHarmony 的图片加载真实基础(适配的真实底座)
- 网络:
@ohos.net.http的 createHttp/destroy 生命周期、ARRAY_BUFFER 返回 - 解码:
@ohos.multimedia.image的 ImageSource.createImageSource / createPixelMap - 上屏:ArkUI
Image直接消费PixelMap
- 网络:
- 三、适配架构:语义层 / 引擎层 / 验收页三层
- 三层各自职责与文件落点
- 语义层只依赖自身接口,引擎层可替换
- 四、语义层:图片加载契约(对照原库)
ImageSource三源联合(Remote / Bytes / Resource)ImageRequest/ImageResult/DecodedImageKamelEngine平台接口 /MemoryCache极简 LRU /ImageLoader门面- 与原库差异:
pixelMap以Object形态跨层,避免语义层触碰@kit.*类型 - 分层与契约图
- 五、引擎层:OhosKamelEngine 桥接系统能力
fetch→http.createHttp()+ARRAY_BUFFERdecode→image.createImageSource+createPixelMap,独立ArrayBuffer切片,用完release()- 加载时序图
- 六、验收页:远程加载 / 示例图 / 缓存演示
- 远程加载(真实网络)/ 示例图(bytes 源免网络)/ 清空缓存
- 显示
PixelMap与fromCache标记 - 缓存命中图、运行时流程图
- 七、关于 API 命名与类型的一点设计说明(本项目的适配决策)
ImageSource三源对应原库;pixelMap以 Object 形态跨层Resource源暂未实现、MemoryCache简化为极简 LRU
- 八、版本与运行环境
- 适配平台、工具链、IDE、编译验证结果
- 总结
- 三层架构 + 真接口真实现在接入真实系统能力的价值
- 社区引导语与 AtomCode 专属邀请链接
一、Kamel 是什么,以及为什么要在 OpenHarmony 上适配它
Kamel 是 Kotlin Multiplatform 生态中一个轻量的图片加载库,它的设计目标与 Coil / Glide 类似,但天然跨平台:用一套 API 屏蔽各平台「下载 → 解码 → 缓存 → 上屏」的差异。本系列此前已经适配了 Decompose、Ktor、Notifier、kable、MVIKotlin、Reaktive、kotlin-inject、Moko Permissions 八个库。Kamel 与 Ktor / Notifier / kable / Moko Permissions 同属「真接口真实现」这一档——它本身就依赖平台提供网络下载与图片解码能力,我们的做法不是把上游 Kotlin 源码编译进 ohosArm64,而是用 ArkTS 把「网络下载」与「图像解码」这两件事真正接上 OpenHarmony 的系统能力,同时把加载编排逻辑用纯 ArkTS 复刻出来。
本文聚焦本仓库 kamel/ 目录下的真实代码,逐层对照讲清楚语义层、引擎层与验收页,并总结 ArkTS 严格模式下踩到的真实坑。
上游 Kamel 的核心契约可以概括成四块:
ImageSource:数据源,描述图片「从哪来」——远程 URL、内存字节、还是平台资源;ImageRequest:一次加载请求,包裹ImageSource并携带配置(是否走缓存等);ImageLoader:加载门面,对应getImage/load,内部编排「查缓存 → 取字节 → 解码 → 回填缓存」;- 平台解码与内存缓存:Kamel 在 Kotlin 端用 Ktor 下载、用平台图像解码器解码,并维护
MemoryCache。
适配目标很明确:把图片加载的语义还原成 ArkTS(语义层),并让引擎层真正调用系统网络栈与图像解码器(引擎层),最终在验收页用 ArkUI 把它显示出来。
二、OpenHarmony 的图片加载真实基础(适配的真实底座)
要让 Kamel 在鸿蒙上跑起来,必须先理解系统给了什么。本节拆解三个真实基础。
网络:@ohos.net.http
OpenHarmony 的 HTTP 能力由 @kit.NetworkKit 提供。最关键的两点:
- 连接生命周期:
http.createHttp()的产物必须用完destroy(),否则连接池不释放、长跑会泄漏(这一点在 Ktor 适配时已经踩过); - 返回类型声明:默认
request返回的是字符串,expectDataType设为http.HttpDataType.ARRAY_BUFFER时,response.result才是ArrayBuffer,我们才能拿到原始图片字节去解码。
解码:@ohos.multimedia.image
图片解码由 @kit.ImageKit 提供:image.createImageSource(arrayBuffer) 从一个 ArrayBuffer 创建 ImageSource,再 createPixelMap() 得到 PixelMap(鸿蒙里的「位图」)。解码完成后应调用 source.release() 释放底层句柄,避免泄漏。
上屏:ArkUI Image
ArkUI 的 Image 组件可以直接消费 PixelMap——Image(pixelMap) 即可把一个解码后的位图显示出来。这正是我们「引擎层解码、验收页上屏」的衔接点。
三、适配架构:语义层 / 引擎层 / 验收页三层
延续本工程一贯的三层架构:
| 层 | 文件 | 职责 |
|---|---|---|
| 语义层 | kamel/src/Kamel.ets | ImageSource 三源联合 / ImageRequest / ImageResult / DecodedImage / KamelEngine 平台接口 / MemoryCache LRU / ImageLoader 门面,不碰 @kit.* |
| 引擎层 | kamel/src/OhosKamelEngine.ets | 实现 KamelEngine:fetch(桥 @ohos.net.http)/ decode(桥 @ohos.multimedia.image) |
| 验收页 | kamel/src/KamelDemo.ets | URL 输入 / 远程加载 / 示例图 / 清空缓存 / Image(pixelMap) 上屏 / 事件日志 |
语义层只依赖自身的接口与类型,引擎层可替换——就像 Ktor 把 HttpClient 与引擎解耦一样,Kamel 的 ImageLoader 也只依赖 KamelEngine 接口,换一套引擎(比如换成缓存优先的变体)不影响语义层。
四、语义层:图片加载契约(对照原库)
语义层是本次适配里最「重」的一层——它不仅要定义契约,还自带内存缓存与加载编排。下面逐段对照 Kamel.ets 看。
ImageSource:三源联合
export type ImageSource =
| { kind: 'remote'; url: string }
| { kind: 'bytes'; data: Uint8Array }
| { kind: 'resource'; id: string };
用「可分辨联合(discriminated union)」来表达与原库 ImageSource 对应的三种来源。kind 作为判别字段,让 ArkTS 在 switch / if 里能精确收窄类型——这是 ArkTS 严格模式下比 any 安全得多的写法。
DecodedImage:解码结果,pixelMap 刻意用 Object 持有
export class DecodedImage {
readonly pixelMap: Object | null;
readonly bytes: Uint8Array | null;
readonly width: number;
readonly height: number;
constructor(pixelMap: Object | null, bytes: Uint8Array | null, width: number, height: number) {
this.pixelMap = pixelMap;
this.bytes = bytes;
this.width = width;
this.height = height;
}
}
注意 pixelMap 的类型是 Object | null,而不是 image.PixelMap。这是本次适配的一个刻意设计:语义层绝不引入 @kit.*,哪怕只是类型注解。解码出的位图在语义层只能被「当作对象持有」,真正要显示时,由验收页把它 as image.PixelMap 交给 ArkUI——守住边界,跨层才不会被具体平台类型绑架。
KamelEngine 平台接口 + MemoryCache + ImageLoader
export interface KamelEngine {
fetch(url: string): Promise<Uint8Array>;
decode(bytes: Uint8Array): Promise<DecodedImage>;
}
KamelEngine 是语义层与引擎层之间的契约,对应 Kamel 上游「平台解码/下载能力」的抽象。MemoryCache 是一个极简 LRU(Map 保持插入顺序,超容删最旧),对应上游的 MemoryCache 概念;ImageLoader 则是门面,编排整条流水线:
async load(request: ImageRequest): Promise<ImageResult> {
const key = cacheKeyOf(request.source);
if (request.useCache) {
const cached = this.cache.get(key);
if (cached !== undefined) return new ImageResult(cached, true, null); // 命中
}
try {
let bytes: Uint8Array;
if (request.source.kind === 'remote') bytes = await this.engine.fetch(request.source.url);
else if (request.source.kind === 'bytes') bytes = request.source.data;
else return new ImageResult(null, false, `resource 源暂未实现(id=${request.source.id})`);
const decoded = await this.engine.decode(bytes);
if (request.useCache) this.cache.put(key, decoded);
return new ImageResult(decoded, false, null);
} catch (e) {
return new ImageResult(null, false, String(e));
}
}
可以看到,load 本身完全没有网络与解码代码,只负责「取键 → 查缓存 → 取字节 → 解码 → 回填」的编排,具体能力全部委托给注入的 KamelEngine。

pixelMap以Object形态跨层:上游ImageBitmap是平台富类型,本项目为了让语义层零@kit,选择用Object持有,显示侧再强转;Resource源暂未实现:本适配聚焦remote+bytes两条主链路(也是图片加载最常见场景),resource留作引擎层扩展点;MemoryCache简化:上游还有磁盘缓存与尺寸权重,本项目只做极简 LRU,足以演示缓存命中语义。
五、引擎层:OhosKamelEngine 桥接系统能力
引擎层 OhosKamelEngine 是「真接口真实现」的落点,它把 KamelEngine 的 fetch / decode 真接上系统能力。
fetch:桥 @ohos.net.http
async fetch(url: string): Promise<Uint8Array> {
const request = http.createHttp();
try {
const options: http.HttpRequestOptions = {
method: http.RequestMethod.GET,
expectDataType: http.HttpDataType.ARRAY_BUFFER
};
const response = await request.request(url, options);
const result = response.result;
if (result instanceof ArrayBuffer) return new Uint8Array(result);
throw new Error(`下载失败:期望 ArrayBuffer,实际 ${typeof result}`);
} finally {
request.destroy(); // 无论成败都必须释放
}
}
两点要强调:expectDataType 必须声明 ARRAY_BUFFER 才能拿到 ArrayBuffer;http.createHttp() 出的实例必须在 finally 里 destroy(),否则连接泄漏。
decode:桥 @ohos.multimedia.image
async decode(bytes: Uint8Array): Promise<DecodedImage> {
const buffer = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
const source = image.createImageSource(buffer);
// ImageSource.createPixelMap 接收可选的 DecodingOptions;不传则按原图全量解码。
// 注意别误用 InitializationOptions(其 size 字段必填),否则 ArkTS 报缺 size。
const pixelMap = await source.createPixelMap();
const info = await pixelMap.getImageInfo();
source.release(); // 解码完即释放 ImageSource,避免句柄泄漏
// ImageInfo 的尺寸在 info.size(Size 接口)上,不是 info.width / info.height
return new DecodedImage(pixelMap, bytes, info.size.width, info.size.height);
}
时序图如下:

这里有一个容易踩的坑:ArkTS 的 Uint8Array 可能只是某个大 ArrayBuffer 上的视图,byteOffset / byteLength 未必从 0 开始。解码需要「完整且独立的 ArrayBuffer」,所以先用 bytes.buffer.slice(...) 切出一份独立副本再交给 createImageSource。解码完成后 source.release() 释放底层句柄。
适配中踩到的真实坑:
http.createHttp()必须destroy():不释放会连接泄漏,Ktor 适配时也踩过同一处;- 构造函数参数属性被禁:ArkTS 严格模式不允许
constructor(private readonly x)这种参数即属性的写法,所有字段必须显式声明再赋值; Uint8Array可能是视图:解码前要用bytes.buffer.slice(byteOffset, ...)拿独立ArrayBuffer,否则字节错位;Image显示PixelMap需条件渲染:@State pixelMap为null时不能Image(null),否则报错,必须if (this.pixelMap !== null)包住;@State不可变更新:日志数组用[...this.logLines, item]展开生成新数组,直接push不会触发刷新;expectDataType决定返回形态:不声明ARRAY_BUFFER,response.result是字符串而非ArrayBuffer;ImageSource.createPixelMap别传InitializationOptions:它期望DecodingOptions(可选),误传InitializationOptions会因size必填而报「缺少 size」;不传即按原图全量解码;ImageInfo尺寸在info.size上:取宽高要用info.size.width/info.size.height,直接用info.width/info.height在 ArkTS 下不存在该属性;Base64Helper解码是实例方法decodeSync:util.Base64Helper没有静态decode,必须先new util.Base64Helper()再调decodeSync(str)拿到字节;ImageFit是全局枚举:objectFit(ImageFit.Contain)不需要从@kit.ArkUIimport,误 import 会报「未导出」。
开发代码界面如下:

遇到报错信息如下:

调试解决!
对应 DevEco Studio 实测 assembleHap 报出的 12 个 ArkTS ERROR。
所有修复均对照本机 SDK 类型定义核实(@ohos.multimedia.image.d.ts 与 @ohos.util.d.ts),非凭记忆推断。
修复清单(12 处 ERROR → 5 类根因)
| # | 文件:行 | 报错码 | 根因 | 修法 |
|---|---|---|---|---|
| 1–3 | Kamel.ets:36-38 | arkts-no-obj-literals-as-types | ImageSource 用了内联对象字面量联合类型 | 拆成 3 个具名接口 RemoteSource / BytesSource / ResourceSource + type ImageSource = … 联合 |
| 4–5 | KamelDemo.ets:58,66 | arkts-no-untyped-obj-literals | { kind: 'remote', url } 字面量无对应具名类型 | 上面的接口化一并对消,字面量现能匹配 RemoteSource / BytesSource |
| 6 | OhosKamelEngine.ets:41 | InitializationOptions 缺 size | 误把 InitializationOptions(size 必填)当 createPixelMap 参数 | ImageSource.createPixelMap 实际收可选 DecodingOptions,直接 source.createPixelMap() 无参全量解码 |
| 7–8 | OhosKamelEngine.ets:46 | ImageInfo 无 width/height | 尺寸在 info.size: Size 上 | 改 info.size.width / info.size.height |
| 9/11 | KamelDemo.ets:10 | ImageFit 未从 @kit.ArkUI 导出 | ImageFit 是 ArkUI 全局枚举,不能 import | 移除 import,ImageFit.Contain 仍可直接用 |
| 10 | KamelDemo.ets:65 | Base64Helper 无 decode | base64 解码是实例方法 decodeSync | new util.Base64Helper().decodeSync(SAMPLE_PNG_BASE64) |
改动文件
kamel/src/Kamel.ets+ demoets/kamel/Kamel.ets(语义层:ImageSource接口化)kamel/src/OhosKamelEngine.ets+ demoets/kamel/OhosKamelEngine.ets(引擎层:createPixelMap无参 +info.size)kamel/src/KamelDemo.ets+ demoets/pages/KamelDemo.ets(验收页:移除ImageFitimport +decodeSync)kamel/docs/11-Kamel技术博客.md+11-Kamel完整版.md(同步代码样例,并在「踩坑」节新增 7/8/9/10 四条,正是本次踩到的雷)
关键经验(桥接型适配常见雷,后续库直接复用)
InitializationOptions(size 必填) vsDecodingOptions(可选) 易混:ImageSource.createPixelMap收的是DecodingOptions,不传即按原图全量解码;误传InitializationOptions会因size必填报缺字段。ImageInfo尺寸一律走info.size:取宽高用info.size.width/info.size.height,直接用info.width/info.height在 ArkTS 下不存在该属性。- base64 解码用
new util.Base64Helper().decodeSync:util.Base64Helper没有静态decode,必须先实例化再调decodeSync(str)拿字节。 - ArkUI 枚举是全局:
ImageFit、FontWeight、Alignment、BarState等均为全局枚举,不要从@kit.ArkUIimport,误 import 会报「未导出」。 - 对象字面量类型要具名:ArkTS 严格模式禁止内联对象字面量作为类型声明(
arkts-no-obj-literals-as-types),也禁止字面量无对应具名类/接口(arkts-no-untyped-obj-literals);可分辨联合统一用具名接口表达。
修复状态
- 12 个 ERROR 已全部修复


六、验收页:远程加载 / 示例图 / 缓存演示
验收页 KamelDemo.ets 把三层串起来。与 Moko Permissions 不同,Kamel 的引擎不需要 UIAbilityContext——http 与 image 都是全局系统能力,所以页面里直接 new ImageLoader(new OhosKamelEngine()) 即可。
三个核心动作:
- 加载远程图:把输入框的 URL 包成
ImageRequest({ kind: 'remote' })交给loader.load,成功后得到DecodedImage,this.pixelMap = result.image.pixelMap as image.PixelMap,再用Image(this.pixelMap)上屏; - 加载示例图(bytes 源,免网络):内置一段 96×96 橙黄渐变 PNG 的 base64,
new util.Base64Helper().decodeSync(...)转成字节后包成ImageRequest({ kind: 'bytes' })。这一步完全不经过网络,专门给「模拟器无稳定网络」的场景做演示与截图; - 清空缓存:
loader.clearCache()把MemoryCache清空,页面上cacheSize归零。
ImageResult.fromCache 是缓存命中的直观证据:同一 URL 第二次 load,会跳过 fetch 与 decode,直接返回 fromCache = true。


运行界面如下:

点击加载远超图如下:


七、关于 API 命名与类型的一点设计说明(本项目的适配决策)
对照上游 Kamel 库,本项目的命名与类型做了几处务实收敛:
ImageSource三源命名沿用上游:remote/bytes/resource直接对应 Kamel 的数据源语义,不另造词;但resource在本适配中暂未实现(留作引擎层扩展点),聚焦remote+bytes主链路;pixelMap以Object形态跨层:上游ImageBitmap是平台富类型,本项目为了守住「语义层零@kit」的边界,只用Object持有,显示侧再as image.PixelMap;MemoryCache简化为极简 LRU:不实现上游的磁盘缓存与尺寸权重,只保留「命中 / 淘汰」语义,足以演示缓存价值;KamelEngine用接口而非具体类:保留「换引擎」的扩展点,与 Ktor 把 HttpClient 与引擎解耦是同一思路。
八、版本与运行环境
本适配基于 HarmonyOS SDK 6.0.0(20)(API 20)+ KMP&CMP 鸿蒙社区工具链 v1.1.0(Kotlin 2.2.21 / CMP 1.9.2)开发,使用 DevEco Studio 26.0.0 Release。模拟器即可跑「示例图解码」与「缓存命中」演示;远程加载需在真机或带网络的模拟器上验证(INTERNET 权限已在 module.json5 声明)。
总结
Kamel 的适配再次印证了本系列的方法论:语义层用纯 ArkTS 还原跨平台契约(不碰 @kit.*),引擎层把真接口真实现接上系统能力,验收页用真实 UI 验证能力。图片加载看似简单,却完整覆盖了「网络下载 + 图像解码 + 内存缓存 + 上屏」四件事,是检验「桥接型适配」成色的很好样本。
欢迎加入 KMP&CMP 鸿蒙社区: https://atomgit.com/CPF-KMP-CMP
如果你也想体验鸿蒙原生 AI 开发,可通过 AtomCode 专属邀请链接加入:
https://developer.huaweicloud.com/codeartsco.html?source=dmzntgwatomgit1&sourcead=dmzntgwatomgiths
更多推荐

所有评论(0)