• Lv9
    粉丝21

积分1226 / 贡献0

提问0答案被采纳67文章10

[经验分享] 【闲谈OpenHarmony三方库】图片库@ohos/imageknife 原创

马迪 显示全部楼层 发表于 2025-2-13 09:06:17

在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<PixelMapTransformation>(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为主要入口,内部实现了图片的加载。 image1.png

2.核心加载流程

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

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 动态预加载
1.gif 2.gif 3.gif
数据设置 首屏加载 滑动过程滑块数量
cachedCount=5 首屏加载快(首屏加载可视区 +5 张)。 滑动过程中白块很多。
cachedCount=30 首屏概率加载慢(首屏加载可视区 +30 张)。 滑动过程中没有白块或很少。
动态预加载 首屏加载快(首屏加载可视区 +5 张;再加载不可视区域图片)。 滑动过程中没有白块或很少。

局限性

1.因为该库完全使用arkts语言编写,且存在跨语言调用的性能损耗,所以虽然自定义能力比系统image强,但图片加载性能较系统Image有所下降。因此该库正在使用c++重构实现,详见image-knife-c。 2.当前该库设计没有较好地提供扩展方式,让开发者自定义缓存实现,解码能力。 3.由于底层系统网络Network Kit没有提供取消请求的功能,导致销毁的组件无法及时取消网络请求。这样在快速下滑的图片的场景时,前面的图片虽然被销毁了,但后台仍然在下载,影响当前图片的下载速度。

结束语

随着越来越多的鸿蒙应用上线,各种开源的闭源的,开发者自发的和大厂贡献的库也正在逐步涌现出来,同时也有一些早期的库已被淘汰。今年我将抽空基于HarmonyOS 5.0+试试水,根据实际效果和下载量筛选在awesome-harmony-library,帮助大家快速找到合适的三方库,也请大家也帮忙Star下这个仓库:)

©著作权归作者所有,转载或内容合作请联系作者

您尚未登录,无法参与评论,登录后可以:
参与开源共建问题交流
认同或收藏高质量问答
获取积分成为开源共建先驱

Copyright   ©2025  OpenHarmony开发者论坛  京ICP备2020036654号-3 |技术支持 Discuz!

返回顶部