浅谈HarmonyOS Glide组件的GIF能力

系统 OpenHarmony
HarmonyOS Glide组件是一款非常优秀的图片处理工具,不仅支持多种格式图片的加载,而且采用磁盘缓存和内存缓存方式实现图片的预加载,本文将通过介绍Glide组件的GIF能力,来解读Glide加载资源的过程。

[[420674]]

想了解更多内容,请访问:

51CTO和华为官方合作共建的鸿蒙技术社区

https://harmonyos.51cto.com

HarmonyOS Glide组件是一款非常优秀的图片处理工具,不仅支持多种格式图片的加载,而且采用磁盘缓存和内存缓存方式实现图片的预加载,同时还能指定图片缓存大小,节省内存。本文将通过介绍Glide组件的GIF能力,来解读Glide加载资源的过程。

通过以上GIF可以看到,一张网络上的GIF图片已经被成功下载,并且展示到Image控件上了。

我们到底做了什么?实际上核心的代码就只有这一段而已:

  1. Glide.with(classcontext) 
  2.      .asGif() 
  3.      .load(uri) 
  4.      .into(image); 

虽说只有这简简单单的一段代码,但大家可能不知道的是,Glide在背后帮我们默默执行了成吨的工作。下面,我们将围绕着这段简单的代码,来解读Glide加载GIF的过程。

一、加载过程与数据转换

在开始解读Glide加载GIF的过程之前,先说明一下图片的加载过程以及图片加载过程中的数据转换,便于后面对整个过程的理解。如下所示,是GIF的加载过程:

浅谈HarmonyOS Glide组件的GIF能力-鸿蒙HarmonyOS技术社区

如下所示,是GIF加载过程中的数据转换:

浅谈HarmonyOS Glide组件的GIF能力-鸿蒙HarmonyOS技术社区

1、load状态传入的model类型

2、request状态获取的数据类型

3、原数据经过decoder和transcode之后的数据类型

4、transformation变换

5、animation加载动画实现

二、Glide.With()

with()方法是Glide类中的一组静态方法,用于获取RequestManager对象。Glide.with(Context)流程如下所示:

浅谈HarmonyOS Glide组件的GIF能力-鸿蒙HarmonyOS技术社区

 

1.通过Glide.get(context)初始化Glide

2.通过GlideBuilder初始化各项配置

3.返回requestManagerRetriever对象

4.调用RequestManagerRetriever中的get方法,通过RequestManagerFactory中的build()方法创建并返回了RequestManager,用于管理Glide的请求。

三、Glide.asGif()

通过asGif()方法,规定了最后资源转化类型为 GifDrawable。如果加载的资源不是GIF,则将操作失败。

这里需要注意的是如果加载的是GIF文件,即使没有使用asGif()方法,但只要配合DraweeView使用,最终解析还是会走GIF流程。如果用户希望解析的GIF显示为一张单帧图片,那么一定要在asBitmap ()方法中声明需求,让Glide知道需要的仅仅是一张单帧图片而非GIF。

四、Glide.load()

load()方法用于创建一个目标为Drawable的图片加载请求,传入需要加载的资源(String,URL,URI等)。由于with()方法返回的是一个RequestManager对象,那么很容易就能想到,load()方法是在RequestManager类当中。通过调用asDrawable()方法,创建一个目标为Drawable的图片加载请求RequestBuilder。

load方法比较简单,流程也比较清晰,主要是保存用户传入的参数,包括load传入的model和RequestOption构建的参数都会被记录保存,用于后续构建Request使用。如下所示:

浅谈HarmonyOS Glide组件的GIF能力-鸿蒙HarmonyOS技术社区

五、Glide.into()

如果说前面都是在准备开胃小菜的话,那么现在终于要进入主菜了,因为into()方法是整个Glide图片加载流程中逻辑最复杂的地方,into()方法的作用是在子线程中网络请求解析图片,并回到主线程中绘制图片。由于into()过程非常复杂,所以我们将这部分拆分为三个小节进行讲解。

1.资源加载

Into()方法从load()创建的图片加载请求RequestBuilder开始。资源加载过程中,通过onSizeReady()函数获取image控件的宽和高。如果已知控件宽、高则直接进入onSizeReady函数执行后续任务。如果控件宽、高未知,则会在ViewTarget中进行监听回调,待控件拥有宽高之后再执行onSizeReady函数和后续任务。

浅谈HarmonyOS Glide组件的GIF能力-鸿蒙HarmonyOS技术社区

进入engine.load函数后。首先通过loadFromMemory()函数,加载activeResource中的缓存资源,如果activeResource没有找到资源,则会通过loadFromLruCache()方法,到LruCache缓存中寻找资源。

如果通过以上方法都没有找到缓存资源,则会开启新的任务进行加载。在waitForExistingOrStartNewJob()方法中创建EngineJob和DecodeJob,然后通过EngineJob执行DecodeJob,解析任务。如下图所示:

浅谈HarmonyOS Glide组件的GIF能力-鸿蒙HarmonyOS技术社区

2.资源解析

完成资源加载之后,Glide会进入资源解析,通过decodeResourceWithList()方法获取对应的解析器。代码如下所示

  1. private Resource<ResourceType> decodeResourceWithList( DataRewinder<DataType> rewinder,int width,int height,Options options,List<Throwable> exceptions) throws GlideException { 
  2.   Resource<ResourceType> result = null
  3.   for (int i = 0, size = decoders.size(); i < size; i++) { 
  4.     // 循环去获取对应的解析器 
  5.     ResourceDecoder<DataType, ResourceType> decoder = decoders.get(i); 
  6.     try { 
  7.       DataType data = rewinder.rewindAndGet(); 
  8.       if (decoder.handles(data, options)) { 
  9.         data = rewinder.rewindAndGet(); 
  10.         result = decoder.decode(data, width, height, options); 
  11.       } 
  12.     } catch (IOException | RuntimeException | OutOfMemoryError e) { 
  13.     } 
  14.   } 
  15.   return result; 

然后通过DataType、ResourceType来寻找具体实现类,发现byteBufferGifDecoder的decode才是真正的执行者。

  1. /* GIFs */ 
  2. .append( 
  3.         Registry.BUCKET_GIF, 
  4.         InputStream.class, 
  5.         GifDrawable.class, 
  6.         new StreamGifDecoder(imageHeaderParsers, byteBufferGifDecoder, arrayPool)) 
  7. ByteBufferGifDecoder byteBufferGifDecoder = 
  8.         new ByteBufferGifDecoder(context, imageHeaderParsers, bitmapPool, arrayPool); 

 下面是ByteBufferGifDecoder的资源解析过程,解析完成后会生成一个GifDrawable回调资源。

  1. // 生成GifDecoder GIF的解析工作是GifDecoder承担的       
  2. GifDecoder gifDecoder = gifDecoderFactory.build(provider, header, byteBuffer, sampleSize); 
  3.         gifDecoder.setDefaultBitmapConfig(config); 
  4.         gifDecoder.advance(); 
  5.         PixelMap firstFrame = gifDecoder.getNextFrame(); 
  6.       // 此处生成 gifDrawable 
  7.       GifDrawable gifDrawable = 
  8.           new GifDrawable(context, gifDecoder, unitTransformation, width, height, firstFrame); 
  9.       return new GifDrawableResource(gifDrawable); 

如果成功获取resource就执行回调通知,onResourceReady()用于将图片显示到DraweeView上。

  1. public void onResourceReady(@NotNull Z resource, @Nullable Transition<? super Z> transition) { 
  2.     if (transition == null || !transition.transition(resource, this)) { 
  3.         setResourceInternal(resource); 
  4.     } else {         
  5.         maybeUpdateAnimatable(resource); 
  6.     } 

如果resource继承了Animatable,就会触发animatable.start()进行GIF的加载和绘制。

  1. private void maybeUpdateAnimatable(@Nullable Z resource) { 
  2.     if (resource instanceof Animatable) { 
  3.         animatable = (Animatable) resource; 
  4.         // GIFDrawable继承了Animatable所以接下来GIF流程查看GIFDrawable.java 
  5.         animatable.start(); 
  6.     } else { 
  7.         animatable = null
  8.     } 

3.GIF加载和绘制

GIF的加载和绘制就是通过将GIF解析成一张张的单帧图片,然后再将单帧图片循环不停地绘制到canvas上,从而实现动画效果。

GIF加载和绘制的序列图如下:

浅谈HarmonyOS Glide组件的GIF能力-鸿蒙HarmonyOS技术社区

3.1GIF加载

Glide 加载 GIF 的原理就是将GIF 解码成多张图片进行无限轮播,每帧切换都是一次图片加载请求,当加载到新的一帧数据时会对旧的一帧数据进行清除,然后再继续下一帧数据的加载请求,以此类推。

在GIF加载和绘制的序列图中可以看到,ImageViewTarget中的onResourceReady触发onStart() =>realStart()=>startRunning()。当GIF为单张图片的时候就直接绘制。当GIF为多张图片就先加载第一张,然后注册frameLoader的回调。

  1. private void startRunning() { 
  2.     if (state.frameLoader.getFrameCount() == 1) { 
  3.       invalidateSelf(); 
  4.     } else if (!isRunning) { 
  5.       isRunning = true
  6.       state.frameLoader.subscribe(this); 
  7.       invalidateSelf(); 
  8.     }else
  9.     } 
  10.   } 
  11.  // 注册frameLoader的回调 
  12.  void subscribe(FrameCallback frameCallback) { 
  13.     boolean start = callbacks.isEmpty(); 
  14.     callbacks.add(frameCallback); 
  15.     if (start) { 
  16.       start(); 
  17.     } 
  18.   } 

到这里,就是整个GIF加载的关键了,通过loadNextFrame加载GIF的下一帧。

  1. private void loadNextFrame() { 
  2.     isLoadPending = true
  3.     // 获取解析器当前帧到下一帧的延迟时间 
  4.     int delay = gifDecoder.getNextDelay(); 
  5.     // 获取系统当前时间+延时时间 
  6.     long targetTime = SystemClock.uptimeMillis() + delay; 
  7.     // 将GIF的当前帧往后+1 
  8.     gifDecoder.advance(); 
  9.     // 创建出DelayTarget任务 
  10.     next = new DelayTarget(handler, gifDecoder.getCurrentFrameIndex(), targetTime);  
  11.     // 启动DelayTarget 
  12.     requestBuilder.apply(signatureOf(getFrameSignature())).load(gifDecoder).into(next); 
  13.   } 

 然后进入DelayTarget类中执行onSourceReady()方法,使用EventHandler将PixelMap的resource传到主线程上,用于定时发送解析好的资源。

  1. public void onResourceReady( 
  2.         PixelMap resource, @Nullable Transition<? super PixelMap> transition) { 
  3.       this.resource = resource; 
  4.       InnerEvent innerEvent = InnerEvent.get(FrameLoaderCallback.MSG_DELAY, this); 
  5.       // 使用handler发送消息,此处会将解析好的资源定时发送FrameLoaderCallback 
  6.       handler.sendTimingEvent(innerEvent, targetTime); 
  7.     } 

FrameLoaderCallback是EventHandler的实现类,用于接收EventHandler发送过来的任务,并触发onFrameReady函数。

  1. private class FrameLoaderCallback extends EventHandler{ 
  2.     static final int MSG_DELAY = 1; 
  3.     static final int MSG_CLEAR = 2; 
  4.     @Synthetic 
  5.     FrameLoaderCallback() { 
  6.       super(EventRunner.getMainEventRunner()); 
  7.     } 
  8.     @Override 
  9.     protected void processEvent(InnerEvent event) { 
  10.       if (event.eventId == MSG_DELAY) { 
  11.         DelayTarget target = (DelayTarget) event.object 
  12.         // 接收到消息,触发onFrameReady函数 
  13.         onFrameReady(target); 
  14.         return
  15.       } else if (event.eventId == MSG_CLEAR) { 
  16.         DelayTarget target = (DelayTarget) event.object; 
  17.         requestManager.clear(target); 
  18.       } 
  19.       return
  20.     } 
  21.     } 

当上一帧加载完成后, GifFrameLoader类中的onFrameReady(target)方法触发绘制的回调操作,然后进入加载GIF的下一帧。同时,会通过FrameLoaderCallback.MSG_CLEAR对旧的一帧数据进行清除。清除完后再次通过loadNextFrame()加载下一帧,实现了GIF循环不停去加载下一帧的这个流程,直到加载完整个GIF。

  1. void onFrameReady(DelayTarget delayTarget) { 
  2.     // 触发了 GifDrawable.java的绘制回调操作 
  3.     if (delayTarget.getResource() != null) { 
  4.       recycleFirstFrame(); 
  5.       DelayTarget previous = current
  6.       current = delayTarget; 
  7.       for (int i = callbacks.size() - 1; i >= 0; i--) { 
  8.         FrameCallback cb = callbacks.get(i); 
  9.         // 注册在GifFrameLoader的GifDrawable会接收onFrameReady回调通知 
  10.         cb.onFrameReady(); 
  11.       } 
  12.       if (previous != null) { 
  13.        // 这里将上一个target给清理了 
  14.         InnerEvent innerEvent = InnerEvent.get(FrameLoaderCallback.MSG_CLEAR, previous);  
  15.        handler.sendEvent(innerEvent); 
  16.       } 
  17.     } 
  18.     // 加载下一帧,构成了gif的循环不停的地去执行这个流程 
  19.     loadNextFrame(); 
  20.   } 

3.2GIF绘制

GIF绘制,就是将解析后的图片通过invalidateSelf()方法通知DraweeView进行重绘。

在绘制过程中invalideDraweeView通过调用GifDrawable的drawToCanvas()方法将图片绘制到Canvas上。 

GifDrawable类中的onFrameReady()调用的invalidateSelf()函数用于执行绘制任务

  1. public void onFrameReady() { 
  2.     // 如果没有找到Callback的实现控件就停止绘制最后一帧 
  3.     if (findCallback() == null) { 
  4.       stop(); 
  5.       invalidateSelf(); 
  6.       return
  7.     } 
  8.     // 执行绘制流程 
  9.     invalidateSelf(); 
  10.     if (getFrameIndex() == getFrameCount() - 1) { 
  11.     // 循环次数计数 
  12.       loopCount++; 
  13.     } 
  14.     // 非无限循环并且达到设置最大值停止gif 
  15.     if (maxLoopCount != LOOP_FOREVER && loopCount >= maxLoopCount) { 
  16.       stop(); 
  17.     } 
  18.   } 
  19. public void invalidateSelf(){ 
  20.         final Callback callback = getHmCallback(); 
  21.         if(callback!=null){ 
  22.             // 这里的callback就是注册Callback函数的组件,此处是DraweeView   
  23.             callback.invalidateDrawable(this); 
  24.         }else
  25.         } 

通过调用setImageElement(((RootShapeElement) resource))方法,实现Callback接口。

  1. protected void setResource(@Nullable Element resource) { 
  2.     if(resource instanceof PixelMapElement) { 
  3.       view.setPixelMap(((PixelMapElement) resource).getPixelMap()); 
  4.     }else if(resource instanceof RootShapeElement){ 
  5.       view.setImageElement(((RootShapeElement) resource)); 
  6.     } 
  7.   }public void setImageElement(Element element) { 
  8.         if(element == null){ 
  9.             // 如果设置的内容为null 则去刷新图片并且清空之前的东西 
  10.             invalidate(); 
  11.             return
  12.         } 
  13.         super.setImageElement(element); 
  14.         element.setCallback(this::onChange); 
  15.         if(element instanceof RootShapeElement){ 
  16.             // 将组件注册到RootShapeElement中 
  17.             ((RootShapeElement) element).setHmCallback(this); 
  18.         } 
  19.     } 

最后通过drawToCanvas()方法生成空白PixelMap交给GifDrawable绘制,并根据scaleMode()方法重新设置最后生成图像的位置。

  1. private void init(Context context) { 
  2.     setBindStateChangedListener(this); 
  3.     addDrawTask(this::drawToCanvas); 
  4.     setTouchEventListener(this::onTouchEvent); 
  5.     } 
  6.     private void drawToCanvas(Component component, Canvas canvas) { 
  7.         if(getImageElement() instanceof RootShapeElement){ 
  8.          RootShapeElement rootShapeElement = (RootShapeElement) getImageElement(); 
  9.          int rw = rootShapeElement.getIntrinsicWidth(); 
  10.          int rh = rootShapeElement.getIntrinsicHeight(); 
  11.          int cw = component.getWidth(); 
  12.          int ch = component.getHeight(); 
  13.          PixelMap.InitializationOptions opts = new PixelMap.InitializationOptions(); 
  14.             opts.size = new Size(rw, rh); 
  15.             opts.pixelFormat = PixelFormat.ARGB_8888; 
  16.             opts.editable = true
  17.             PixelMap gifmap = PixelMap.create(opts); 
  18.             // 生成空白PixelMap交给GifDrawable绘制 
  19.             applyDrawToCanvas(gifmap); 
  20.             RectFloat src = new RectFloat(0,0,cw,ch); 
  21.             // 根据scaleMode重新设置最后生成图像的位置 
  22.             RectFloat dst = scaleTypeFixed(gifmap,component); 
  23.             PixelMapHolder pixelMapHolder = new PixelMapHolder(gifmap); 
  24.             canvas.drawPixelMapHolderRect(pixelMapHolder, src, dst, getGifDrawPaint()); 
  25.         } 
  26.     } 
  27. private void applyDrawToCanvas(PixelMap targetBitmap){ 
  28.         BITMAP_DRAWABLE_LOCK.lock(); 
  29.         try { 
  30.             Canvas canvasRootShape = new Canvas(new Texture(targetBitmap)); 
  31.             // 将canvas交给RootShapeElement,gifDrawable会调用RootShapeElement的drawToCanvas 进行绘制 
  32.             getImageElement().drawToCanvas(canvasRootShape); 
  33.             clear(canvasRootShape); 
  34.         } finally { 
  35.             BITMAP_DRAWABLE_LOCK.unlock(); 
  36.         } 
  37.     } 

至此,整个GIF的流程就走了一遍。

六、课题延伸

因为GIF加载过程其实是无限循环加载单张图片的过程,其实对系统的性能消耗还是非常大的。所以在使用GIF的时候,一定要坚持用完之后及时释放资源。在这里因为HarmonyOS的生命周期和Android有所不同,所以在DraweeView开放了stopGif()方法,当你的GIF不打算用之后,请务必先调用stopGif(),防止内存泄露。

重要提示:

1、目前必须配合DraweeView使用GIF。

2、如果Glide使用了生命周期较长的上下文,例如applicationContext,则在GIF页面结束时调用绘制视图的stopGif方法停止Glide,以减少资源浪费。

3.如果您想使用Glid的GIF能力,但原生Image不支持此功能,因为Image和Element是独立的,不能使用Element重绘。要支持GIF,您需要自定义Image。具体可以参考DraweeView的实现

源码地址:https://gitee.com/openharmony-tpc/glide

想了解更多内容,请访问:

51CTO和华为官方合作共建的鸿蒙技术社区

https://harmonyos.51cto.com

 

责任编辑:jianghua 来源: 鸿蒙社区
相关推荐

2021-03-19 17:42:01

鸿蒙HarmonyOS应用开发

2014-10-15 14:07:21

AndroidGlide组件

2009-06-25 13:03:48

JSF的UI组件

2022-05-19 15:59:23

组件焦点鸿蒙

2021-06-23 09:25:57

鸿蒙HarmonyOS应用

2024-01-23 11:16:08

操作系统鸿蒙HarmonyOS

2024-02-19 15:46:24

操作系统鸿蒙SDK

2022-03-04 06:36:35

数据能力数据分析

2021-08-12 14:59:15

鸿蒙HarmonyOS应用

2009-07-15 13:06:38

Swing组件

2023-02-27 09:10:57

前端组件设计

2021-06-22 09:44:56

鸿蒙HarmonyOS应用

2021-09-13 15:17:28

鸿蒙HarmonyOS应用

2021-09-29 10:15:00

鸿蒙HarmonyOS应用

2021-09-06 15:31:01

鸿蒙HarmonyOS应用

2021-03-30 09:45:07

鸿蒙HarmonyOS应用开发

2021-10-26 15:22:52

鸿蒙HarmonyOS应用

2021-03-17 09:35:09

鸿蒙HarmonyOS应用开发

2021-03-26 09:35:35

鸿蒙HarmonyOS应用开发

2021-03-31 15:49:34

鸿蒙HarmonyOS应用
点赞
收藏

51CTO技术栈公众号