OpenHarmony开发者论坛
标题:
【闲谈OpenHarmony三方库】图片库@ohos/imageknife
[打印本页]
作者:
马迪
时间:
2025-2-13 09:06
标题:
【闲谈OpenHarmony三方库】图片库@ohos/imageknife
[md]在OpenHarmony/HarmonyOS应用开发场景中,使用系统原生的 image 存在诸多局限性:一方面,无法自定义实现网络下载和图片解码;另一方面,无法实现自定义缓存与图片预加载功能,并且在图片加载过程中还会出现白块现象,严重影响用户体验。
在安卓开发领域,通常借助 Glide 来解决上述类似问题;而在 iOS 开发中,SDWebImage 则是常用的解决方案。而在鸿蒙中,大家可以使用ImageKnife来解决上述问题。
ImageKnife是一个OpenHarmony/HarmonyOS的网络图片加载库。它主要对标安卓glide/fresco,提供内存和文件缓存功能,提升网络图片加载的流畅性。
其主要特性包括:
- 支持自定义内存缓存策略,支持设置内存缓存的大小(默认LRU策略)。
- 支持磁盘二级缓存,支持设置文件缓存的大小和路径。
- 支持自定义实现图片获取/网络下载。
- 支持占位图和错误图。
- 支持传入http请求头,证书,超时时间等。
- 支持监听网络下载回调进度。
- 支持图片加载数据回调,获取图片加载过程数据。
- 支持自定义缓存key。
- 支持自定义http网络请求头。
- 支持图形变换,如模糊,高亮等。
- 支持对已销毁和复用的图片,不再发起请求。
- 支持预加载图片。
开源代码地址:
https://gitee.com/openharmony-tpc/ImageKnife
## 快速上手方法
#### 1.下载安装
```
ohpm install @ohos/imageknife
```
当前稳定版本3.2.0
#### 2.权限配置
因核心功能涉及网络下载,需要添加ohos.permission.INTERNET的权限。
#### 3.初始化
如果需要用文件缓存,需要提前初始化文件缓存。
```
await ImageKnife.getInstance().initFileCache(context, 256, 256 * 1024 * 1024)
```
## 更多功能展示
#### 示例1 简单加载网络图片
由于是鸿蒙原生arkTs库,该库采用自定义组件的方式进行使用,即在页面的build方法中,声明式使用ImageKnifeComponent。其参数imageKnifeOption里的必备属性包含loadSrc,即主图的url地址。此外,可选参数placeholderSrc和errorholderSrc分别表示占位图和错误图,其类型可以是url地址,也可以是Resource本地图片资源。
```
build() {
Column() {
ImageKnifeComponent({
imageKnifeOption: {
loadSrc: imageUrl,
placeholderSrc: $r('app.media.loading'),
errorholderSrc: $r('app.media.failed'),
}
})
}
.height('100%')
.width('100%')
}
```
#### 示例2 设置边框,圆角
通过imageKnifeOption的border属性,设置图片的边框和圆角。
```
ImageKnifeComponent({
imageKnifeOption: {
loadSrc: imageUrl,
border: { width: 1, color: Color.Blue, radius: 20 }
}
}).width(100).height(100)
```
#### 示例3 设置图片填充效果
通过imageKnifeOption的objectFit属性,设置图片的填充效果。其效果与系统ImageFit的完全一致,主要效果包括:
| 名称 | 描述 |
| ------------- | -------------------------------------------------------------------------- |
| Contain(缺省) | 保持宽高比进行缩小或者放大,使得图片完全显示在显示边界内。 |
| Cover | 保持宽高比进行缩小或者放大,使得图片两边都大于或等于显示边界。 |
| Auto | 图像会根据其自身尺寸和组件的尺寸进行适当缩放,以在保持比例的同时填充视图。 |
| Fill | 不保持宽高比进行放大缩小,使得图片充满显示边界。 |
| ScaleDown | 保持宽高比显示,图片缩小或者保持不变。 |
| None | 保持原有尺寸显示。 |
除objectFit属性外,还可以通过placeholderObjectFit和errorholderObjectFit分别设置占位图填充效果和错误图填充效果。
```
ImageKnifeComponent({
imageKnifeOption: {
loadSrc: imageUrl,
objectFit: ImageFit.Fill
}
}).width(100).height(100)
```
#### 示例4 监听图片加载成功与失败,获取加载数据
通过imageKnifeOption的onLoadListener回调,监听图片加载请求的结果,并获取图片宽高等信息。
```
ImageKnifeComponent({
imageKnifeOption: {
loadSrc: imageUrl,
onLoadListener: {
onLoadStart: () => {
},
onLoadSuccess: (pixelmap, data) => {
console.info("LoadSuccess width=%d,height=%d", data.imageWidth, data.imageHeight);
},
onLoadFailed: () => {
},
onLoadCancel: () => {
}
}
}
}).width(100).height(100)
```
#### 示例5 监听网络下载进度
通过imageKnifeOption的progressListener回调,获取图片的下载进度。
```
ImageKnifeComponent({
imageKnifeOption: {
loadSrc: imageUrl,
progressListener: (progress: number) => {
console.info("progress %d", progress);
}
}
}).width(100).height(100)
```
#### 示例6 自定义下载图片
通过imageKnifeOption的customGetImage,传入一个@Concurrent修饰的方法,并发这个方法中自定义实现图片的获取方法,通常接入其它的网络库,比如Remote Communication Kit
。之所以需要用@Concurrent修饰,是因为该方法将通过taskpool在子线程中执行。
```
ImageKnifeComponent({
imageKnifeOption: {
loadSrc: imageUrl,
customGetImage: custom
}
}).width(100).height(100)
// 自定义实现图片获取方法,如自定义网络下载
@Concurrent
async function custom(context: Context, src: string | PixelMap | Resource): Promise<ArrayBuffer | undefined> {
console.info("ImageKnife:: custom download:" + src)
// 自定义从本地文件获取
return context.resourceManager.getMediaContentSync($r("app.media.startIcon").id).buffer as ArrayBuffer
}
```
#### 示例7 设置图片变换
通过imageKnifeOption的transformation属性设置设置图片变化。transformation的类型是PixelMapTransformation的抽象类,如果只需要一种图形变化,则传入该图形变化的实例。
```
ImageKnifeComponent({
imageKnifeOption: {
loadSrc: imageUrl,
transformation: new BrightnessTransformation(0.2)
}
}).width(100).height(100)
```
如果需要多种图形变化,则实例化一个MultiTransTransformation对象,其构造方法可以传入一个PixelMapTransformation数组。
```
ImageKnifeComponent({
imageKnifeOption: {
loadSrc: imageUrl,
transformation: new MultiTransTransformation(new collections.Array<
ixelMapTransformation>(new BrightnessTransformation(0.2),
new BlurTransformation(5)))
}
}).width(100).height(100)
```
更多图形变化,需要依赖GPUImage这个三方库
```
ohpm install @ohos/gpu_transform
```
#### 示例8 图片降采样
降采样(Downsampling)是一种常用的技术,用于减小图片的分辨率,从而降低内存占用,避免内存溢出(OOM)等问题。通过imageKnifeOption的downsampleOf属性设置图片的降采样策略。
```
ImageKnifeComponent({
imageKnifeOption: {
loadSrc: imageUrl,
objectFit: ImageFit.Fill,
downsampleOf: DownsampleStrategy.FIT_CENTER_MEMORY
}
}).width(100).height(100)
```
| 名称 | 描述 |
| --------------------- | -------------------------------------------------------------------------- |
| DEFAULT(缺省) | 默认值,图片分辨率超过上限7680 * 4320,宽高等比降为7680 * 4320。 |
| NONE | 不进行降采样。 |
| AT_MOST | 请求尺寸大于实际尺寸不进行放大。 |
| FIT_CENTER_MEMORY | 两边自适应内存优先。 |
| FIT_CENTER_QUALITY | 两边自适应质量优先。 |
| CENTER_INSIDE_QUALITY | 按照宽高比的最大比进行适配内存优先。 |
| CENTER_INSIDE_MEMORY | 按照宽高比的最大比进行适配质量优先。 |
| AT_LEAST | 宽高进行等比缩放宽高里面最小的比例先放进去,然后再根据原图的缩放比去适配。 |
#### 示例9 图片缩放
通过ImageKnifeComponent的transform方法,改变图片的缩放比例。实际场景多监听手指的缩放事件,来调整组件的缩放比例。
```
@Component
struct Transform {
private customScale: number = 1
@State matrix: object = matrix4.identity().scale({ x: 1, y: 1 })
build() {
Column() {
ImageKnifeComponent({
imageKnifeOption: {
loadSrc: imageUrl,
}
}).width(100).height(100).transform(this.matrix)
Button('放大').onClick(() => {
this.customScale = this.customScale * 2
this.matrix = matrix4.identity().scale({ x: this.customScale, y: this.customScale })
})
Button('缩小').onClick(() => {
this.customScale = this.customScale / 2
this.matrix = matrix4.identity().scale({ x: this.customScale, y: this.customScale })
})
}.height('100%').width('100%')
}
}
```
#### 示例10 网络请求header设置
通过imageKnifeOption的headerOption,可以设置网络请求头,便于访问某些Header鉴权的网络图片资源。
```
ImageKnifeComponent({
imageKnifeOption: {
loadSrc: imageUrl,
headerOption: [
{
key: 'Content-Type',
value: 'text/html; charset=utf-8'
},
{
key: 'Accept',
value: ["text/html", "application/xhtml+xml", "application/xml"]
}
],
}
}).width(100).height(100)
```
#### 示例11 动图展示与控制
因为ImageKnifeComponent底部封装系统Image,所以只能展示动图(gif/webp等),而不能控制动图的播放与暂停。如需要控制动图,则可使用该三方库提供的另一个ImageKnifeAnimatorComponent组件。
```
ImageKnifeAnimatorComponent({
imageKnifeOption: {
loadSrc: imageUrl,
},
animatorOption: {
state: AnimationStatus.Running,
iterations: -1,
onFinish: () => {},
onStart: () => {},
onPause: () => {},
onCancel: () => {},
onRepeat: () => {}
}
}).width(200).height(200)
```
通过animatorOption的state属性,可以修改动画的播放状态。通过iterations属性,可以设置循环播放的次数,其中-1是无限循环。此外还可以监听动画开始,完成,取消等事件。
## 实现原理
#### 1.组件依赖关系
ImageKnifeComponent和ImageKnifeAnimatorComponent是ImageKnife三方库提供的两个自定义组件,内部以Imageknife为主要入口,内部实现了图片的加载。

#### 2.核心加载流程
ImageKnifeComponent和ImageKnifeAnimatorComponent在初始化后,通过onSizeChange获取组件宽高后,每个图片生成一个ImageKnifeRequest请求,下发到排队队列。ImageKnifeDispacher管理了请求并发数,并利用taskpool在非UI线程里异步下载图片,写缓存,解码成pixelmap,以及图形变化后,最后交给系统Image和ImageAnimator展示。

#### 3.核心设计点
- 外观封装:通过三方库自定义组件方式提供给应用,内部封装了图片下载,解码,缓存等细节,并使用系统Image显示图片。
- 异步加载:内部使用taskpool线程池管理图片加载,避免在主线程上进行耗时的网络请求和磁盘IO操作,保障应用的流畅性。
- 单例接口:对应用全局提供核心接口,避免频繁地创建和销毁对象。
- 合并相同请求:自动合并相同的图片请求,减少重复网络和解码开销。
- 生命周期感知:组件还未加载就被复用或销毁时,自动取消改图片的加载。
- 解码拓展:通过实现PixelMapTransformation,可拓展图片解码。
- 下载拓展:通过传入customGetImage方法,可自实现网络下载。
## 拓展进阶
#### 1. ImageKnife动态预加载方案最佳实践
**背景**
列表是应用开发中最常见的一类开发场景,它可以将杂乱的信息整理成有规律、易于理解和操作的形式,便于用户查找和获取所需要的信息。应用程序中常见的列表场景有新闻列表、购物车列表、各类排行榜等。随着信息数据的累积,特别是一些新闻应用、购物应用、聊天应用,列表数据往往会达到上万条,针对这类大量数据加载的长列表应用,如何对长列表的性能进行优化是非常重要的。一个正确、高性能的长列表应用能明显降低列表渲染时间、提升页面的滑动帧率、降低应用内存占用,大幅提升用户体验。
针对长列表加载这一场景,通常会使用 5 种优化手段,通过这些优化手段的单个使用或组合使用,可以对列表渲染时间、页面滑动帧率、应用内存占用等方面带来优化,提升性能和用户体验:
- 懒加载:提供列表数据按需加载能力,解决一次性加载长列表数据耗时长、占用过多资源的问题,可以提升页面响应速度。
- 缓存列表项:提供屏幕可视区域外列表项长度的自定义调节能力,配合懒加载设置可缓存列表项参数,通过预加载数据提升列表滑动体验。
- 动态预加载:根据历史任务加载耗时情况,动态调整屏幕可视区域外数据预取数量,配合懒加载设置,可在列表不断滑动时,屏幕可视区外实时更新列表数据,通过预取和预渲染数据提升列表滑动体验。
- 组件复用:提供可复用组件对象的缓存资源池,通过重复使用已经创建过并缓存的组件对象,降低相同组件短时间内频繁创建和销毁的开销,提升组件渲染效率。
- 布局优化:使用扁平化布局方案,减少视图嵌套层级和组件数,避免过度绘制,提升页面渲染效率。
**预加载方案**
三方库ImageKnife自带图片缓存特性,结合懒加载的使用,可以有效提升图片加载的效率,解决白块问题。此外通过ImageKnife的preload方法可以预加载图片,结合列表LazyForEach懒加载机制,可以更好地实现提前预加载,进一步减少图片加载白块问题:
- 首先需要实现 DataSourcePrefetchingImageKnife 类,继承 IDataSourcePrefetching 接口,并通过imageknife的能力实现 prefetch 和 cancel 方法。
其中 prefetch 做图片的请求和缓存处理。cancel 在组件不可视区域取消请求。
```
const IMADE_UNAVAILABLE = $r('app.media.failed')
export interface InfoItem {
albumUrl: string | Resource
}
export default class DataSourcePrefetchingImageKnife implements IDataSourcePrefetching {
private dataArray: Array<InfoItem>
private listeners: DataChangeListener[] = [];
constructor(dataArray: Array<InfoItem>) {
this.dataArray = dataArray;
}
public getData(index: number) {
return this.dataArray[index]
}
public totalCount(): number {
return this.dataArray.length
}
public addData(index: number, data: InfoItem[]): void {
this.dataArray = this.dataArray.concat(data)
this.notifyDataAdd(index)
}
unregisterDataChangeListener(listener: DataChangeListener): void {
const pos = this.listeners.indexOf(listener);
if (pos >= 0) {
this.listeners.splice(pos, 1);
}
}
registerDataChangeListener(listener: DataChangeListener): void {
if (this.listeners.indexOf(listener) < 0) {
this.listeners.push(listener)
}
}
notifyDataAdd(index: number): void {
this.listeners.forEach((listener: DataChangeListener) => {
listener.onDataAdd(index)
})
}
async prefetch(index: number): Promise<void> {
let item = this.dataArray[index]
try {
if (typeof item.albumUrl == "string") {
// 图片预加载
console.info("prefetch:" + item.albumUrl);
await ImageKnife.getInstance().preLoadCache(item.albumUrl);
}
} catch (err) {
// 预加载失败,展示错误图
item.albumUrl = IMADE_UNAVAILABLE
}
}
// 取消请求处理
cancel(index: number) {
// ImageKnifeComponent 内部触发aboutToDisAppear生命周期会将请求取消
}
}
```
- 在应用列表界面,首先创建 DataSourcePrefetchingImageKnife、BasicPrefetcher 对象,然后在 List 的 onScrollIndex 回调中调用 BasicPrefetcher 的 visibleAreaChanged 方法,传入 List 的可见区域起始坐标。这样在下滑过程中,会在图片尚未加载前,自动触发DataSourcePrefetchingImageKnife的prefetch 和 cancel 接口动态预加载。这样当图片真正加载时,仅需要从缓存加载图片,从而大幅度缩短图片加载的时间。
```
@Entry
@Component
export struct PrefetchAndPreload {
// 创建DataSourcePrefetchingImageKnife对象,具备任务预取、取消能力的数据源
private readonly dataSource = new DataSourcePrefetchingImageKnife(PageViewModel.getItems());
// 创建BasicPrefetcher对象,默认的动态预取算法实现
private readonly prefetcher = new BasicPrefetcher(this.dataSource);
build() {
Column() {
List({ space: 16 }) {
LazyForEach(this.dataSource, (item: InfoItem, index: number) => {
ListItem() {
Column({ space: 12 }) {
ImageKnifeComponent({
imageKnifeOption: {
loadSrc: item.albumUrl,
placeholderSrc: $r('app.media.loading')
}
}).width(100).height(100)
Text(`${index}`)
}.border({ width: 5, color: "#000000" })
}
.reuseId('imageKnife')
})
}
.cachedCount(5)
.onScrollIndex((start: number, end: number) => {
// 列表滚动触发visibleAreaChanged,实时更新预取范围,触发调用prefetch、cancel接口
this.prefetcher.visibleAreaChanged(start, end)
})
.width("100%")
.height("100%")
.margin({ left: 10, right: 10 })
.layoutWeight(1)
}
}
}
```
**预加载效果**
对懒惰加载场景时,cachedCount=5、cachedCount=30 和动态预加载的数据:
| cachedCount=5 | cachedCount=30 | 动态预加载 |
| ----------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
|  |  |  |
| 数据设置 | 首屏加载 | 滑动过程滑块数量 |
| -------------- | ---------------------------------------------------------- | -------------------------- |
| cachedCount=5 | 首屏加载快(首屏加载可视区 +5 张)。 | 滑动过程中白块很多。 |
| cachedCount=30 | 首屏概率加载慢(首屏加载可视区 +30 张)。 | 滑动过程中没有白块或很少。 |
| 动态预加载 | 首屏加载快(首屏加载可视区 +5 张;再加载不可视区域图片)。 | 滑动过程中没有白块或很少。 |
## 局限性
1.因为该库完全使用arkts语言编写,且存在跨语言调用的性能损耗,所以虽然自定义能力比系统image强,但图片加载性能较系统Image有所下降。因此该库正在使用c++重构实现,详见[image-knife-c](
https://gitee.com/openharmony-tpc-incubate/image-knife-c
)。
2.当前该库设计没有较好地提供扩展方式,让开发者自定义缓存实现,解码能力。
3.由于底层系统网络Network Kit没有提供取消请求的功能,导致销毁的组件无法及时取消网络请求。这样在快速下滑的图片的场景时,前面的图片虽然被销毁了,但后台仍然在下载,影响当前图片的下载速度。
## 结束语
随着越来越多的鸿蒙应用上线,各种开源的闭源的,开发者自发的和大厂贡献的库也正在逐步涌现出来,同时也有一些早期的库已被淘汰。今年我将抽空基于HarmonyOS 5.0+试试水,根据实际效果和下载量筛选在[awesome-harmony-library](
https://github.com/Madixin/awesome-harmony-library
),帮助大家快速找到合适的三方库,也请大家也帮忙Star下这个仓库
[/md]
欢迎光临 OpenHarmony开发者论坛 (https://forums.openharmony.cn/)
Powered by Discuz! X3.5