淺談HarmonyOS Glide組件的GIF能力
HarmonyOS Glide組件是一款非常優(yōu)秀的圖片處理工具,不僅支持多種格式圖片的加載,而且采用磁盤緩存和內(nèi)存緩存方式實現(xiàn)圖片的預加載,同時還能指定圖片緩存大小,節(jié)省內(nèi)存。本文將通過介紹Glide組件的GIF能力,來解讀Glide加載資源的過程。
通過以上GIF可以看到,一張網(wǎng)絡上的GIF圖片已經(jīng)被成功下載,并且展示到Image控件上了。
我們到底做了什么?實際上核心的代碼就只有這一段而已:
- Glide.with(classcontext)
- .asGif()
- .load(uri)
- .into(image);
雖說只有這簡簡單單的一段代碼,但大家可能不知道的是,Glide在背后幫我們默默執(zhí)行了成噸的工作。下面,我們將圍繞著這段簡單的代碼,來解讀Glide加載GIF的過程。
一、加載過程與數(shù)據(jù)轉(zhuǎn)換
在開始解讀Glide加載GIF的過程之前,先說明一下圖片的加載過程以及圖片加載過程中的數(shù)據(jù)轉(zhuǎn)換,便于后面對整個過程的理解。如下所示,是GIF的加載過程:

如下所示,是GIF加載過程中的數(shù)據(jù)轉(zhuǎn)換:

1、load狀態(tài)傳入的model類型
2、request狀態(tài)獲取的數(shù)據(jù)類型
3、原數(shù)據(jù)經(jīng)過decoder和transcode之后的數(shù)據(jù)類型
4、transformation變換
5、animation加載動畫實現(xiàn)
二、Glide.With()
with()方法是Glide類中的一組靜態(tài)方法,用于獲取RequestManager對象。Glide.with(Context)流程如下所示:

1.通過Glide.get(context)初始化Glide
2.通過GlideBuilder初始化各項配置
3.返回requestManagerRetriever對象
4.調(diào)用RequestManagerRetriever中的get方法,通過RequestManagerFactory中的build()方法創(chuàng)建并返回了RequestManager,用于管理Glide的請求。
三、Glide.asGif()
通過asGif()方法,規(guī)定了最后資源轉(zhuǎn)化類型為 GifDrawable。如果加載的資源不是GIF,則將操作失敗。
這里需要注意的是如果加載的是GIF文件,即使沒有使用asGif()方法,但只要配合DraweeView使用,最終解析還是會走GIF流程。如果用戶希望解析的GIF顯示為一張單幀圖片,那么一定要在asBitmap ()方法中聲明需求,讓Glide知道需要的僅僅是一張單幀圖片而非GIF。
四、Glide.load()
load()方法用于創(chuàng)建一個目標為Drawable的圖片加載請求,傳入需要加載的資源(String,URL,URI等)。由于with()方法返回的是一個RequestManager對象,那么很容易就能想到,load()方法是在RequestManager類當中。通過調(diào)用asDrawable()方法,創(chuàng)建一個目標為Drawable的圖片加載請求RequestBuilder。
load方法比較簡單,流程也比較清晰,主要是保存用戶傳入的參數(shù),包括load傳入的model和RequestOption構(gòu)建的參數(shù)都會被記錄保存,用于后續(xù)構(gòu)建Request使用。如下所示:

五、Glide.into()
如果說前面都是在準備開胃小菜的話,那么現(xiàn)在終于要進入主菜了,因為into()方法是整個Glide圖片加載流程中邏輯最復雜的地方,into()方法的作用是在子線程中網(wǎng)絡請求解析圖片,并回到主線程中繪制圖片。由于into()過程非常復雜,所以我們將這部分拆分為三個小節(jié)進行講解。
1.資源加載
Into()方法從load()創(chuàng)建的圖片加載請求RequestBuilder開始。資源加載過程中,通過onSizeReady()函數(shù)獲取image控件的寬和高。如果已知控件寬、高則直接進入onSizeReady函數(shù)執(zhí)行后續(xù)任務。如果控件寬、高未知,則會在ViewTarget中進行監(jiān)聽回調(diào),待控件擁有寬高之后再執(zhí)行onSizeReady函數(shù)和后續(xù)任務。

進入engine.load函數(shù)后。首先通過loadFromMemory()函數(shù),加載activeResource中的緩存資源,如果activeResource沒有找到資源,則會通過loadFromLruCache()方法,到LruCache緩存中尋找資源。
如果通過以上方法都沒有找到緩存資源,則會開啟新的任務進行加載。在waitForExistingOrStartNewJob()方法中創(chuàng)建EngineJob和DecodeJob,然后通過EngineJob執(zhí)行DecodeJob,解析任務。如下圖所示:

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++) {
- // 循環(huán)去獲取對應的解析器
- 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來尋找具體實現(xiàn)類,發(fā)現(xiàn)byteBufferGifDecoder的decode才是真正的執(zhí)行者。
- /* GIFs */
- .append(
- Registry.BUCKET_GIF,
- InputStream.class,
- GifDrawable.class,
- new StreamGifDecoder(imageHeaderParsers, byteBufferGifDecoder, arrayPool))
- ByteBufferGifDecoder byteBufferGifDecoder =
- new ByteBufferGifDecoder(context, imageHeaderParsers, bitmapPool, arrayPool);
下面是ByteBufferGifDecoder的資源解析過程,解析完成后會生成一個GifDrawable回調(diào)資源。
- // 生成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就執(zhí)行回調(diào)通知,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,就會觸發(fā)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解析成一張張的單幀圖片,然后再將單幀圖片循環(huán)不停地繪制到canvas上,從而實現(xiàn)動畫效果。
GIF加載和繪制的序列圖如下:

3.1GIF加載
Glide 加載 GIF 的原理就是將GIF 解碼成多張圖片進行無限輪播,每幀切換都是一次圖片加載請求,當加載到新的一幀數(shù)據(jù)時會對舊的一幀數(shù)據(jù)進行清除,然后再繼續(xù)下一幀數(shù)據(jù)的加載請求,以此類推。
在GIF加載和繪制的序列圖中可以看到,ImageViewTarget中的onResourceReady觸發(fā)onStart() =>realStart()=>startRunning()。當GIF為單張圖片的時候就直接繪制。當GIF為多張圖片就先加載第一張,然后注冊frameLoader的回調(diào)。
- private void startRunning() {
- if (state.frameLoader.getFrameCount() == 1) {
- invalidateSelf();
- } else if (!isRunning) {
- isRunning = true;
- state.frameLoader.subscribe(this);
- invalidateSelf();
- }else{
- }
- }
- // 注冊frameLoader的回調(diào)
- 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();
- // 獲取系統(tǒng)當前時間+延時時間
- long targetTime = SystemClock.uptimeMillis() + delay;
- // 將GIF的當前幀往后+1
- gifDecoder.advance();
- // 創(chuàng)建出DelayTarget任務
- next = new DelayTarget(handler, gifDecoder.getCurrentFrameIndex(), targetTime);
- // 啟動DelayTarget
- requestBuilder.apply(signatureOf(getFrameSignature())).load(gifDecoder).into(next);
- }
然后進入DelayTarget類中執(zhí)行onSourceReady()方法,使用EventHandler將PixelMap的resource傳到主線程上,用于定時發(fā)送解析好的資源。
- public void onResourceReady(
- PixelMap resource, @Nullable Transition<? super PixelMap> transition) {
- this.resource = resource;
- InnerEvent innerEvent = InnerEvent.get(FrameLoaderCallback.MSG_DELAY, this);
- // 使用handler發(fā)送消息,此處會將解析好的資源定時發(fā)送FrameLoaderCallback
- handler.sendTimingEvent(innerEvent, targetTime);
- }
FrameLoaderCallback是EventHandler的實現(xiàn)類,用于接收EventHandler發(fā)送過來的任務,并觸發(fā)onFrameReady函數(shù)。
- 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
- // 接收到消息,觸發(fā)onFrameReady函數(shù)
- onFrameReady(target);
- return;
- } else if (event.eventId == MSG_CLEAR) {
- DelayTarget target = (DelayTarget) event.object;
- requestManager.clear(target);
- }
- return;
- }
- }
當上一幀加載完成后, GifFrameLoader類中的onFrameReady(target)方法觸發(fā)繪制的回調(diào)操作,然后進入加載GIF的下一幀。同時,會通過FrameLoaderCallback.MSG_CLEAR對舊的一幀數(shù)據(jù)進行清除。清除完后再次通過loadNextFrame()加載下一幀,實現(xiàn)了GIF循環(huán)不停去加載下一幀的這個流程,直到加載完整個GIF。
- void onFrameReady(DelayTarget delayTarget) {
- // 觸發(fā)了 GifDrawable.java的繪制回調(diào)操作
- 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回調(diào)通知
- cb.onFrameReady();
- }
- if (previous != null) {
- // 這里將上一個target給清理了
- InnerEvent innerEvent = InnerEvent.get(FrameLoaderCallback.MSG_CLEAR, previous);
- handler.sendEvent(innerEvent);
- }
- }
- // 加載下一幀,構(gòu)成了gif的循環(huán)不停的地去執(zhí)行這個流程
- loadNextFrame();
- }
3.2GIF繪制
GIF繪制,就是將解析后的圖片通過invalidateSelf()方法通知DraweeView進行重繪。
在繪制過程中invalideDraweeView通過調(diào)用GifDrawable的drawToCanvas()方法將圖片繪制到Canvas上。
GifDrawable類中的onFrameReady()調(diào)用的invalidateSelf()函數(shù)用于執(zhí)行繪制任務
- public void onFrameReady() {
- // 如果沒有找到Callback的實現(xiàn)控件就停止繪制最后一幀
- if (findCallback() == null) {
- stop();
- invalidateSelf();
- return;
- }
- // 執(zhí)行繪制流程
- invalidateSelf();
- if (getFrameIndex() == getFrameCount() - 1) {
- // 循環(huán)次數(shù)計數(shù)
- loopCount++;
- }
- // 非無限循環(huán)并且達到設置最大值停止gif
- if (maxLoopCount != LOOP_FOREVER && loopCount >= maxLoopCount) {
- stop();
- }
- }
- public void invalidateSelf(){
- final Callback callback = getHmCallback();
- if(callback!=null){
- // 這里的callback就是注冊Callback函數(shù)的組件,此處是DraweeView
- callback.invalidateDrawable(this);
- }else{
- }
- }
通過調(diào)用setImageElement(((RootShapeElement) resource))方法,實現(xiàn)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){
- // 如果設置的內(nèi)容為null 則去刷新圖片并且清空之前的東西
- invalidate();
- return;
- }
- super.setImageElement(element);
- element.setCallback(this::onChange);
- if(element instanceof RootShapeElement){
- // 將組件注冊到RootShapeElement中
- ((RootShapeElement) element).setHmCallback(this);
- }
- }
最后通過drawToCanvas()方法生成空白PixelMap交給GifDrawable繪制,并根據(jù)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);
- // 根據(jù)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會調(diào)用RootShapeElement的drawToCanvas 進行繪制
- getImageElement().drawToCanvas(canvasRootShape);
- clear(canvasRootShape);
- } finally {
- BITMAP_DRAWABLE_LOCK.unlock();
- }
- }
至此,整個GIF的流程就走了一遍。
六、課題延伸
因為GIF加載過程其實是無限循環(huán)加載單張圖片的過程,其實對系統(tǒng)的性能消耗還是非常大的。所以在使用GIF的時候,一定要堅持用完之后及時釋放資源。在這里因為HarmonyOS的生命周期和Android有所不同,所以在DraweeView開放了stopGif()方法,當你的GIF不打算用之后,請務必先調(diào)用stopGif(),防止內(nèi)存泄露。
重要提示:
1、目前必須配合DraweeView使用GIF。
2、如果Glide使用了生命周期較長的上下文,例如applicationContext,則在GIF頁面結(jié)束時調(diào)用繪制視圖的stopGif方法停止Glide,以減少資源浪費。
3.如果您想使用Glid的GIF能力,但原生Image不支持此功能,因為Image和Element是獨立的,不能使用Element重繪。要支持GIF,您需要自定義Image。具體可以參考DraweeView的實現(xiàn)
源碼地址:https://gitee.com/openharmony-tpc/glide