查看: 196|回复: 2

【闲谈OpenHarmony三方库】图片库@ohos/imageknife

[复制链接]

1

主题

1

回帖

14

积分

新手上路

积分
14
发表于 2025-4-13 08:08:34 | 显示全部楼层 |阅读模式
在OpenHarmony/HarmonyOS应用开发场景中,使用系统原生的 image 存在诸多局限性:一方面,无法自定义实现网络下载和图片解码;另一方面,无法实现自定义缓存与图片预加载功能,并且在图片加载过程中还会出现白块现象,严重影响用户体验。在安卓开发领域,通常借助 Glide 来解决上述类似问题;而在 iOS 开发中,SDWebImage 则是常用的解决方案。而在鸿蒙中,大家可以使用ImageKnife来解决上述问题。
ImageKnife是一个OpenHarmony/HarmonyOS的网络图片加载库。它主要对标安卓glide/fresco,提供内存和文件缓存功能,提升网络图片加载的流畅性。
其主要特性包括:
    支持自定义内存缓存策略,支持设置内存缓存的大小(默认LRU策略)。支持磁盘二级缓存,支持设置文件缓存的大小和路径。支持自定义实现图片获取/网络下载。支持占位图和错误图。支持传入http请求头,证书,超时时间等。支持监听网络下载回调进度。支持图片加载数据回调,获取图片加载过程数据。支持自定义缓存key。支持自定义http网络请求头。支持图形变换,如模糊,高亮等。支持对已销毁和复用的图片,不再发起请求。支持预加载图片。
开源代码地址:https://gitee.com/openharmony-tpc/ImageKnife
快速上手方法

1.下载安装
  1. ohpm install @ohos/imageknife
复制代码
当前稳定版本3.2.0
2.权限配置

因核心功能涉及网络下载,需要添加ohos.permission.INTERNET的权限。
3.初始化

如果需要用文件缓存,需要提前初始化文件缓存。
  1. await ImageKnife.getInstance().initFileCache(context, 256, 256 * 1024 * 1024)
复制代码
更多功能展示

示例1 简单加载网络图片

由于是鸿蒙原生arkTs库,该库采用自定义组件的方式进行使用,即在页面的build方法中,声明式使用ImageKnifeComponent。其参数imageKnifeOption里的必备属性包含loadSrc,即主图的url地址。此外,可选参数placeholderSrc和errorholderSrc分别表示占位图和错误图,其类型可以是url地址,也可以是Resource本地图片资源。
  1. build() {
  2. Column() {
  3.     ImageKnifeComponent({
  4.     imageKnifeOption: {
  5.         loadSrc: imageUrl,
  6.         placeholderSrc: $r('app.media.loading'),
  7.         errorholderSrc: $r('app.media.failed'),
  8.     }
  9.     })
  10. }
  11. .height('100%')
  12. .width('100%')
  13. }
复制代码
示例2 设置边框,圆角

通过imageKnifeOption的border属性,设置图片的边框和圆角。
  1. ImageKnifeComponent({
  2. imageKnifeOption: {
  3.     loadSrc: imageUrl,
  4.     border: { width: 1, color: Color.Blue, radius: 20 }
  5. }
  6. }).width(100).height(100)
复制代码
示例3 设置图片填充效果

通过imageKnifeOption的objectFit属性,设置图片的填充效果。其效果与系统ImageFit的完全一致,主要效果包括:
名称描述
Contain(缺省)保持宽高比进行缩小或者放大,使得图片完全显示在显示边界内。
Cover保持宽高比进行缩小或者放大,使得图片两边都大于或等于显示边界。
Auto图像会根据其自身尺寸和组件的尺寸进行适当缩放,以在保持比例的同时填充视图。
Fill不保持宽高比进行放大缩小,使得图片充满显示边界。
ScaleDown保持宽高比显示,图片缩小或者保持不变。
None保持原有尺寸显示。
除objectFit属性外,还可以通过placeholderObjectFit和errorholderObjectFit分别设置占位图填充效果和错误图填充效果。
  1. ImageKnifeComponent({
  2. imageKnifeOption: {
  3.     loadSrc: imageUrl,
  4.     objectFit: ImageFit.Fill
  5. }
  6. }).width(100).height(100)
复制代码
示例4 监听图片加载成功与失败,获取加载数据

通过imageKnifeOption的onLoadListener回调,监听图片加载请求的结果,并获取图片宽高等信息。
  1. ImageKnifeComponent({
  2. imageKnifeOption: {
  3.     loadSrc: imageUrl,
  4.     onLoadListener: {
  5.     onLoadStart: () => {
  6.     },
  7.     onLoadSuccess: (pixelmap, data) => {
  8.         console.info("LoadSuccess width=%d,height=%d", data.imageWidth, data.imageHeight);
  9.     },
  10.     onLoadFailed: () => {
  11.     },
  12.     onLoadCancel: () => {
  13.     }
  14.     }
  15. }
  16. }).width(100).height(100)
复制代码
示例5 监听网络下载进度

通过imageKnifeOption的progressListener回调,获取图片的下载进度。
  1. ImageKnifeComponent({
  2. imageKnifeOption: {
  3.     loadSrc: imageUrl,
  4.     progressListener: (progress: number) => {
  5.     console.info("progress %d", progress);
  6.     }
  7. }
  8. }).width(100).height(100)
复制代码
示例6 自定义下载图片

通过imageKnifeOption的customGetImage,传入一个@Concurrent修饰的方法,并发这个方法中自定义实现图片的获取方法,通常接入其它的网络库,比如Remote Communication Kit。之所以需要用@Concurrent修饰,是因为该方法将通过taskpool在子线程中执行。
  1. ImageKnifeComponent({
  2. imageKnifeOption: {
  3.     loadSrc: imageUrl,
  4.     customGetImage: custom
  5. }
  6. }).width(100).height(100)
  7. // 自定义实现图片获取方法,如自定义网络下载
  8. @Concurrent
  9. async function custom(context: Context, src: string | PixelMap | Resource): Promise {
  10.   console.info("ImageKnife::  custom download:" + src)
  11.   // 自定义从本地文件获取
  12.   return context.resourceManager.getMediaContentSync($r("app.media.startIcon").id).buffer as ArrayBuffer
  13. }
复制代码
示例7 设置图片变换

通过imageKnifeOption的transformation属性设置设置图片变化。transformation的类型是PixelMapTransformation的抽象类,如果只需要一种图形变化,则传入该图形变化的实例。
  1. ImageKnifeComponent({
  2. imageKnifeOption: {
  3.     loadSrc: imageUrl,
  4.     transformation: new BrightnessTransformation(0.2)
  5. }
  6. }).width(100).height(100)
复制代码
如果需要多种图形变化,则实例化一个MultiTransTransformation对象,其构造方法可以传入一个PixelMapTransformation数组。
  1. ImageKnifeComponent({
  2. imageKnifeOption: {
  3.     loadSrc: imageUrl,
  4.     transformation: new MultiTransTransformation(new collections.Array(new BrightnessTransformation(0.2),
  5.     new BlurTransformation(5)))
  6. }
  7. }).width(100).height(100)
复制代码
更多图形变化,需要依赖GPUImage这个三方库
  1. ohpm install @ohos/gpu_transform
复制代码
示例8 图片降采样

降采样(Downsampling)是一种常用的技术,用于减小图片的分辨率,从而降低内存占用,避免内存溢出(OOM)等问题。通过imageKnifeOption的downsampleOf属性设置图片的降采样策略。
  1. ImageKnifeComponent({
  2. imageKnifeOption: {
  3.     loadSrc: imageUrl,
  4.     objectFit: ImageFit.Fill,
  5.     downsampleOf: DownsampleStrategy.FIT_CENTER_MEMORY
  6. }
  7. }).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方法,改变图片的缩放比例。实际场景多监听手指的缩放事件,来调整组件的缩放比例。
  1. @Component
  2. struct Transform {
  3.   private customScale: number = 1
  4.   @State matrix: object = matrix4.identity().scale({ x: 1, y: 1 })
  5.   build() {
  6.     Column() {
  7.       ImageKnifeComponent({
  8.         imageKnifeOption: {
  9.           loadSrc: imageUrl,
  10.         }
  11.       }).width(100).height(100).transform(this.matrix)
  12.       Button('放大').onClick(() => {
  13.         this.customScale = this.customScale * 2
  14.         this.matrix = matrix4.identity().scale({ x: this.customScale, y: this.customScale })
  15.       })
  16.       Button('缩小').onClick(() => {
  17.         this.customScale = this.customScale / 2
  18.         this.matrix = matrix4.identity().scale({ x: this.customScale, y: this.customScale })
  19.       })
  20.     }.height('100%').width('100%')
  21.   }
  22. }
复制代码
示例10 网络请求header设置

通过imageKnifeOption的headerOption,可以设置网络请求头,便于访问某些Header鉴权的网络图片资源。
  1. ImageKnifeComponent({
  2. imageKnifeOption: {
  3.     loadSrc: imageUrl,
  4.     headerOption: [
  5.     {
  6.         key: 'Content-Type',
  7.         value: 'text/html; charset=utf-8'
  8.     },
  9.     {
  10.         key: 'Accept',
  11.         value: ["text/html", "application/xhtml+xml", "application/xml"]
  12.     }
  13.     ],
  14. }
  15. }).width(100).height(100)
复制代码
示例11 动图展示与控制

因为ImageKnifeComponent底部封装系统Image,所以只能展示动图(gif/webp等),而不能控制动图的播放与暂停。如需要控制动图,则可使用该三方库提供的另一个ImageKnifeAnimatorComponent组件。
  1.       ImageKnifeAnimatorComponent({
  2.         imageKnifeOption: {
  3.           loadSrc: imageUrl,
  4.         },
  5.         animatorOption: {
  6.           state: AnimationStatus.Running,
  7.           iterations: -1,
  8.           onFinish: () => {},
  9.           onStart: () => {},
  10.           onPause: () => {},
  11.           onCancel: () => {},
  12.           onRepeat: () => {}
  13.         }
  14.       }).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 在组件不可视区域取消请求。
  1. const IMADE_UNAVAILABLE = $r('app.media.failed')
  2. export interface InfoItem {
  3.   albumUrl: string | Resource
  4. }
  5. export default class DataSourcePrefetchingImageKnife implements IDataSourcePrefetching {
  6.   private dataArray: Array
  7.   private listeners: DataChangeListener[] = [];
  8.   constructor(dataArray: Array) {
  9.     this.dataArray = dataArray;
  10.   }
  11.   public getData(index: number) {
  12.     return this.dataArray[index]
  13.   }
  14.   public totalCount(): number {
  15.     return this.dataArray.length
  16.   }
  17.   public addData(index: number, data: InfoItem[]): void {
  18.     this.dataArray = this.dataArray.concat(data)
  19.     this.notifyDataAdd(index)
  20.   }
  21.   unregisterDataChangeListener(listener: DataChangeListener): void {
  22.     const pos = this.listeners.indexOf(listener);
  23.     if (pos >= 0) {
  24.       this.listeners.splice(pos, 1);
  25.     }
  26.   }
  27.   registerDataChangeListener(listener: DataChangeListener): void {
  28.     if (this.listeners.indexOf(listener) < 0) {
  29.       this.listeners.push(listener)
  30.     }
  31.   }
  32.   notifyDataAdd(index: number): void {
  33.     this.listeners.forEach((listener: DataChangeListener) => {
  34.       listener.onDataAdd(index)
  35.     })
  36.   }
  37.   async prefetch(index: number): Promise {
  38.     let item = this.dataArray[index]
  39.     try {
  40.       if (typeof item.albumUrl == "string") {
  41.         // 图片预加载
  42.         console.info("prefetch:" + item.albumUrl);
  43.         await ImageKnife.getInstance().preLoadCache(item.albumUrl);
  44.       }
  45.     } catch (err) {
  46.       // 预加载失败,展示错误图
  47.       item.albumUrl = IMADE_UNAVAILABLE
  48.     }
  49.   }
  50.   // 取消请求处理
  51.   cancel(index: number) {
  52.     //   ImageKnifeComponent 内部触发aboutToDisAppear生命周期会将请求取消
  53.   }
  54. }
复制代码
    在应用列表界面,首先创建 DataSourcePrefetchingImageKnife、BasicPrefetcher 对象,然后在 List 的 onScrollIndex 回调中调用 BasicPrefetcher 的 visibleAreaChanged 方法,传入 List 的可见区域起始坐标。这样在下滑过程中,会在图片尚未加载前,自动触发DataSourcePrefetchingImageKnife的prefetch 和 cancel 接口动态预加载。这样当图片真正加载时,仅需要从缓存加载图片,从而大幅度缩短图片加载的时间。
  1. @Entry
  2. @Component
  3. export struct PrefetchAndPreload {
  4.   // 创建DataSourcePrefetchingImageKnife对象,具备任务预取、取消能力的数据源
  5.   private readonly dataSource = new DataSourcePrefetchingImageKnife(PageViewModel.getItems());
  6.   // 创建BasicPrefetcher对象,默认的动态预取算法实现
  7.   private readonly prefetcher = new BasicPrefetcher(this.dataSource);
  8.   build() {
  9.     Column() {
  10.       List({ space: 16 }) {
  11.         LazyForEach(this.dataSource, (item: InfoItem, index: number) => {
  12.           ListItem() {
  13.             Column({ space: 12 }) {
  14.               ImageKnifeComponent({
  15.                 imageKnifeOption: {
  16.                   loadSrc: item.albumUrl,
  17.                   placeholderSrc: $r('app.media.loading')
  18.                 }
  19.               }).width(100).height(100)
  20.               Text(`${index}`)
  21.             }.border({ width: 5, color: "#000000" })
  22.           }
  23.           .reuseId('imageKnife')
  24.         })
  25.       }
  26.       .cachedCount(5)
  27.       .onScrollIndex((start: number, end: number) => {
  28.         // 列表滚动触发visibleAreaChanged,实时更新预取范围,触发调用prefetch、cancel接口
  29.         this.prefetcher.visibleAreaChanged(start, end)
  30.       })
  31.       .width("100%")
  32.       .height("100%")
  33.       .margin({ left: 10, right: 10 })
  34.       .layoutWeight(1)
  35.     }
  36.   }
  37. }
复制代码
预加载效果对懒惰加载场景时,cachedCount=5、cachedCount=30 和动态预加载的数据:
cachedCount=5cachedCount=30动态预加载
数据设置首屏加载滑动过程滑块数量
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下这个仓库:)

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x

0

主题

391

回帖

810

积分

高级会员

积分
810
发表于 2025-8-4 16:40:22 | 显示全部楼层
сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт  
сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт  
сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт  
сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт  
сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт  
сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт  
сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт  
сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт сайт

0

主题

391

回帖

810

积分

高级会员

积分
810
发表于 2025-8-7 14:16:23 | 显示全部楼层
http://tuchkas.ru/
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表