0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

通过Glide组件的GIF能力解读Glide加载资源的过程

DRXU_gh_019562b 来源:HarmonyOS开发者 作者: HarmonyOS 2021-08-20 10:17 次阅读

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

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

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

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

一、加载过程与数据转换

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

10fddf72-00ed-11ec-9bcf-12bb97331649.png

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

113d68ae-00ed-11ec-9bcf-12bb97331649.png

1、load状态传入的model类型

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

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

4、transformation变换

5、animation加载动画实现

二、Glide.With()

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

115d9a2a-00ed-11ec-9bcf-12bb97331649.png

1.通过Glide.get(context)初始化Glide2.通过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使用。如下所示:

116cf5e2-00ed-11ec-9bcf-12bb97331649.png

五、Glide.into()

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

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

119dac0a-00ed-11ec-9bcf-12bb97331649.png

进入engine.load函数后。首先通过loadFromMemory()函数,加载activeResource中的缓存资源,如果activeResource没有找到资源,则会通过loadFromLruCache()方法,到LruCache缓存中寻找资源。如果通过以上方法都没有找到缓存资源,则会开启新的任务进行加载。在waitForExistingOrStartNewJob()方法中创建EngineJob和DecodeJob,然后通过EngineJob执行DecodeJob,解析任务。如下图所示:

11e7551c-00ed-11ec-9bcf-12bb97331649.png

2.资源解析

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

private Resource《ResourceType》 decodeResourceWithList( DataRewinder《DataType》 rewinder,int width,int height,Options options,List《Throwable》 exceptions) throws GlideException { Resource《ResourceType》 result = null; for (int i = 0, size = decoders.size(); i 《 size; i++) { // 循环去获取对应的解析器 ResourceDecoder《DataType, ResourceType》 decoder = decoders.get(i); try { DataType data = rewinder.rewindAndGet(); if (decoder.handles(data, options)) { data = rewinder.rewindAndGet(); result = decoder.decode(data, width, height, options); } } catch (IOException | RuntimeException | OutOfMemoryError e) { } } return result;}

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

/* GIFs */.append( Registry.BUCKET_GIF, InputStream.class, GifDrawable.class, new StreamGifDecoder(imageHeaderParsers, byteBufferGifDecoder, arrayPool))ByteBufferGifDecoder byteBufferGifDecoder = new ByteBufferGifDecoder(context, imageHeaderParsers, bitmapPool, arrayPool);

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

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

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

public void onResourceReady(@NotNull Z resource, @Nullable Transition《? super Z》 transition) { if (transition == null || !transition.transition(resource, this)) { setResourceInternal(resource); } else { maybeUpdateAnimatable(resource); }}

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

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

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

11f461ee-00ed-11ec-9bcf-12bb97331649.png

3.1GIF加载Glide 加载 GIF 的原理就是将GIF 解码成多张图片进行无限轮播,每帧切换都是一次图片加载请求,当加载到新的一帧数据时会对旧的一帧数据进行清除,然后再继续下一帧数据的加载请求,以此类推。在GIF加载和绘制的序列图中可以看到,ImageViewTarget中的onResourceReady触发onStart() =》realStart()=》startRunning()。当GIF为单张图片的时候就直接绘制。当GIF为多张图片就先加载第一张,然后注册frameLoader的回调。

private void startRunning() { if (state.frameLoader.getFrameCount() == 1) { invalidateSelf(); } else if (!isRunning) { isRunning = true; state.frameLoader.subscribe(this); invalidateSelf(); }else{ } } // 注册frameLoader的回调 void subscribe(FrameCallback frameCallback) { boolean start = callbacks.isEmpty(); callbacks.add(frameCallback); if (start) { start(); } }到这里,就是整个GIF加载的关键了,通过loadNextFrame加载GIF的下一帧。

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

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

public void onResourceReady( PixelMap resource, @Nullable Transition《? super PixelMap》 transition) { this.resource = resource; InnerEvent innerEvent = InnerEvent.get(FrameLoaderCallback.MSG_DELAY, this); // 使用handler发送消息,此处会将解析好的资源定时发送FrameLoaderCallback handler.sendTimingEvent(innerEvent, targetTime); }FrameLoaderCallback是EventHandler的实现类,用于接收EventHandler发送过来的任务,并触发onFrameReady函数。

private class FrameLoaderCallback extends EventHandler{ static final int MSG_DELAY = 1; static final int MSG_CLEAR = 2; @Synthetic FrameLoaderCallback() { super(EventRunner.getMainEventRunner()); } @Override protected void processEvent(InnerEvent event) { if (event.eventId == MSG_DELAY) { DelayTarget target = (DelayTarget) event.object // 接收到消息,触发onFrameReady函数 onFrameReady(target); return; } else if (event.eventId == MSG_CLEAR) { DelayTarget target = (DelayTarget) event.object; requestManager.clear(target); } return; } }当上一帧加载完成后, GifFrameLoader类中的onFrameReady(target)方法触发绘制的回调操作,然后进入加载GIF的下一帧。同时,会通过FrameLoaderCallback.MSG_CLEAR对旧的一帧数据进行清除。清除完后再次通过loadNextFrame()加载下一帧,实现了GIF循环不停去加载下一帧的这个流程,直到加载完整个GIF。

void onFrameReady(DelayTarget delayTarget) { // 触发了 GifDrawable.java的绘制回调操作 if (delayTarget.getResource() != null) { recycleFirstFrame(); DelayTarget previous = current; current = delayTarget; for (int i = callbacks.size() - 1; i 》= 0; i--) { FrameCallback cb = callbacks.get(i); // 注册在GifFrameLoader的GifDrawable会接收onFrameReady回调通知 cb.onFrameReady(); } if (previous != null) { // 这里将上一个target给清理了 InnerEvent innerEvent = InnerEvent.get(FrameLoaderCallback.MSG_CLEAR, previous); handler.sendEvent(innerEvent); } } // 加载下一帧,构成了gif的循环不停的地去执行这个流程 loadNextFrame(); }3.2GIF绘制GIF绘制,就是将解析后的图片通过invalidateSelf()方法通知DraweeView进行重绘。在绘制过程中invalideDraweeView通过调用GifDrawable的drawToCanvas()方法将图片绘制到Canvas上。GifDrawable类中的onFrameReady()调用的invalidateSelf()函数用于执行绘制任务

public void onFrameReady() { // 如果没有找到Callback的实现控件就停止绘制最后一帧 if (findCallback() == null) { stop(); invalidateSelf(); return; } // 执行绘制流程 invalidateSelf(); if (getFrameIndex() == getFrameCount() - 1) { // 循环次数计数 loopCount++; } // 非无限循环并且达到设置最大值停止gif if (maxLoopCount != LOOP_FOREVER && loopCount 》= maxLoopCount) { stop(); } }public void invalidateSelf(){ final Callback callback = getHmCallback(); if(callback!=null){ // 这里的callback就是注册Callback函数的组件,此处是DraweeView callback.invalidateDrawable(this); }else{ }}通过调用setImageElement(((RootShapeElement) resource))方法,实现Callback接口

protected void setResource(@Nullable Element resource) { if(resource instanceof PixelMapElement) { view.setPixelMap(((PixelMapElement) resource).getPixelMap()); }else if(resource instanceof RootShapeElement){ view.setImageElement(((RootShapeElement) resource)); } }public void setImageElement(Element element) { if(element == null){ // 如果设置的内容为null 则去刷新图片并且清空之前的东西 invalidate(); return; } super.setImageElement(element); element.setCallback(this::onChange); if(element instanceof RootShapeElement){ // 将组件注册到RootShapeElement中 ((RootShapeElement) element).setHmCallback(this); } }最后通过drawToCanvas()方法生成空白PixelMap交给GifDrawable绘制,并根据scaleMode()方法重新设置最后生成图像的位置。

private void init(Context context) { setBindStateChangedListener(this); addDrawTask(this::drawToCanvas); setTouchEventListener(this::onTouchEvent); } private void drawToCanvas(Component component, Canvas canvas) { if(getImageElement() instanceof RootShapeElement){ RootShapeElement rootShapeElement = (RootShapeElement) getImageElement(); int rw = rootShapeElement.getIntrinsicWidth(); int rh = rootShapeElement.getIntrinsicHeight(); int cw = component.getWidth(); int ch = component.getHeight(); PixelMap.InitializationOptions opts = new PixelMap.InitializationOptions(); opts.size = new Size(rw, rh); opts.pixelFormat = PixelFormat.ARGB_8888; opts.editable = true; PixelMap gifmap = PixelMap.create(opts); // 生成空白PixelMap交给GifDrawable绘制 applyDrawToCanvas(gifmap); RectFloat src = new RectFloat(0,0,cw,ch); // 根据scaleMode重新设置最后生成图像的位置 RectFloat dst = scaleTypeFixed(gifmap,component); PixelMapHolder pixelMapHolder = new PixelMapHolder(gifmap); canvas.drawPixelMapHolderRect(pixelMapHolder, src, dst, getGifDrawPaint()); } }private void applyDrawToCanvas(PixelMap targetBitmap){ BITMAP_DRAWABLE_LOCK.lock(); try { Canvas canvasRootShape = new Canvas(new Texture(targetBitmap)); // 将canvas交给RootShapeElement,gifDrawable会调用RootShapeElement的drawToCanvas 进行绘制 getImageElement().drawToCanvas(canvasRootShape); clear(canvasRootShape); } finally { BITMAP_DRAWABLE_LOCK.unlock(); } }至此,整个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的实现

编辑:jq

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 内存
    +关注

    关注

    8

    文章

    2763

    浏览量

    72749
  • GIF
    GIF
    +关注

    关注

    0

    文章

    23

    浏览量

    6465
  • HarmonyOS
    +关注

    关注

    79

    文章

    1806

    浏览量

    29254

原文标题:浅谈HarmonyOS Glide组件的GIF能力

文章出处:【微信号:gh_019562b5fb4b,微信公众号:gh_019562b5fb4b】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    在PSoC4000的UART Bootloader中不能将引导加载程序时钟资源设置为extCLK吗?

    的 CY8C4024AZI-S403 设备。 为了提高 UART 通信的时钟精度,系统时钟使用 extCLK 运行。 收到来自 UART 的固件更新命令时,启动加载程序将通过调用 Bootloadable_Load () API
    发表于 01-22 06:52

    如何使用NVTFAT显示GIF

    如何使用NVTFAT显示GIF
    发表于 01-18 06:48

    鸿蒙开发-ArkUI 组件基础

    ;, \"value\": \"#1890ff\" } ] } 然后在Button组件通过“$r(\'app.type.name\')”的形式引用应用资源。app代表应用内
    发表于 01-17 19:31

    编程语言的加载和执行过程

    上一章着重讲了指令,我们用汇编、C语言等工具编写的代码最终也被转化成机器指令被执行。本章进一步阐述编程语言如何被加载和执行的过程
    的头像 发表于 10-31 10:13 338次阅读
    编程语言的<b class='flag-5'>加载</b>和执行<b class='flag-5'>过程</b>

    解读旁路二极管的基本特性和其在光伏组件中的运用

    对整个电力系统的安全产生直接影响。随着电池片尺寸的增大带来电流的提升,电气安全风险也在增加,严重时可能导致接线盒、背板等材料烧毁,甚至引发火灾,给光伏电站带来损失。本文旨在解读旁路二极管的基本特性和其在光伏组件中的运用。
    的头像 发表于 10-25 10:45 2303次阅读
    <b class='flag-5'>解读</b>旁路二极管的基本特性和其在光伏<b class='flag-5'>组件</b>中的运用

    广州大彩VisualTFT组态控件教程(七)GIF控件

    电子发烧友网站提供《广州大彩VisualTFT组态控件教程(七)GIF控件.pdf》资料免费下载
    发表于 10-13 17:45 1次下载

    类隔离实现之自定义类加载器的扩展

    机制。 2、类加载是什么? 类加载是一种过程,是将class文件加载到jvm内存的过程。当代码逻辑中需要引用类时,
    的头像 发表于 10-08 15:17 242次阅读

    HarmonyOS 应用开发 Web 组件基本属性应用和事件

    页面 ,并且加载完成后可通过调用​​loadUrl()​​接口变更当前Web组件的页面。 在下面的示例中展示加载本地页面文件的方法: ● 将资源
    发表于 09-15 15:28

    HarmonyOS—使用Web组件加载页面

    页面加载是Web组件的基本功能。根据页面加载数据来源可以分为三种常用场景,包括加载网络页面、加载本地页面、
    发表于 08-31 17:51

    HarmonyOS/OpenHarmony应用开发-ArkTSAPI组件总体分类与说明(下)

    进度条组件,用于显示内容加载或操作处理等进度。 QRCode 用于显示单个二维码的组件。 TextClock 通过文本将当前系统时间显示在设备上。支持不同时区的时间显示,最高精度到秒
    发表于 08-17 09:56

    Dynamsoft--如何在Glide应用程序中扫描条形码

    Glide是一款无代码应用程序构建器,可让您轻松创建应用程序。Dynamsoft是Glide的合作伙伴,基于其条码读取SDK为其提供条码扫描功能。
    的头像 发表于 07-19 15:24 500次阅读
    Dynamsoft--如何在<b class='flag-5'>Glide</b>应用程序中扫描条形码

    HarmonyOS/OpenHarmony应用开发-Stage模型UIAbility组件使用(一)

    Create状态为在应用加载过程中,UIAbility实例创建完成时触发,系统会调用onCreate()回调。可以在该回调中进行应用初始化操作,例如变量定义资源加载等,用于后续的UI
    发表于 06-28 10:32

    HarmonyOS/OpenHarmony应用开发-Stage模型UIAbility组件使用(一)

    Create状态为在应用加载过程中,UIAbility实例创建完成时触发,系统会调用onCreate()回调。可以在该回调中进行应用初始化操作,例如变量定义资源加载等,用于后续的UI
    发表于 06-28 10:32

    Linux内核模块加载过程解析(1)

    `insmod_main()`函数是 insmod 命令的入口函数,该函数首先通过函数参数获取被加载模块的名字并存入局部指针变量 filename,然后调用`bb_init_module()`函数进行后续操作。
    发表于 06-27 15:42 377次阅读

    USB设备在Linux系统中的识别和加载过程

    不知道大家在学习Linux的时候有没有过这样的疑问,为什么我们通过同一个接口接入的不同USB设备,我们的电脑都可以识别到呢?为什么Linux电脑不会把鼠标识别成键盘呢?带着这些疑问我们一起来看一下USB的识别和加载过程
    的头像 发表于 05-10 11:53 2694次阅读