1. 程式人生 > >Android圖片載入框架最全解析(四),玩轉Glide的回撥與監聽

Android圖片載入框架最全解析(四),玩轉Glide的回撥與監聽

本文同步發表於我的微信公眾號,掃一掃文章底部的二維碼或在微信搜尋 郭霖 即可關注,每天都有文章更新。

大家好,今天我們繼續學習Glide。

在上一篇文章當中,我帶著大家一起深入探究了Glide的快取機制,我們不光掌握了Glide快取的使用方法,還通過原始碼分析對快取的工作原理進行了瞭解。雖說上篇文章和本篇文章的內容關係並不是很大,不過感興趣的朋友還是可以去閱讀一下 Android圖片載入框架最全解析(三),深入探究Glide的快取機制

今天是這個Glide系列的第四篇文章,我們又要選取一個新的功能模組開始學習了,那麼就來研究一下Glide的回撥和監聽功能吧。今天的學習模式仍然是以基本用法和原始碼分析相結合的方式來進行的,當然,本文中的原始碼還是建在第二篇原始碼分析的基礎之上,還沒有看過這篇文章的朋友,建議先去閱讀

Android圖片載入框架最全解析(二),從原始碼的角度理解Glide的執行流程

回撥的原始碼實現

作為一名Glide老手,相信大家對於Glide的基本用法已經非常熟練了。我們都知道,使用Glide在介面上載入並展示一張圖片只需要一行程式碼:

Glide.with(this).load(url).into(imageView);

而在這一行程式碼的背後,Glide幫我們執行了成千上萬行的邏輯。其實在第二篇文章當中,我們已經分析了這一行程式碼背後的完整執行流程,但是這裡我準備再帶著大家單獨回顧一下回調這部分的原始碼,這將有助於我們今天這篇文章的學習。

首先來看一下into()方法,這裡我們將ImageView的例項傳入到into()方法當中,Glide將圖片載入完成之後,圖片就能顯示到ImageView上了。這是怎麼實現的呢?我們來看一下into()方法的原始碼:

public Target<TranscodeType> into(ImageView view) {
    Util.assertMainThread();
    if (view == null) {
        throw new IllegalArgumentException("You must pass in a non null View");
    }
    if (!isTransformationSet && view.getScaleType() != null) {
        switch (view.getScaleType()) {
            case
CENTER_CROP: applyCenterCrop(); break; case FIT_CENTER: case FIT_START: case FIT_END: applyFitCenter(); break; default: // Do nothing. } } return into(glide.buildImageViewTarget(view, transcodeClass)); }

可以看到,最後一行程式碼會呼叫glide.buildImageViewTarget()方法構建出一個Target物件,然後再把它傳入到另一個接收Target引數的into()方法中。Target物件則是用來最終展示圖片用的,如果我們跟進到glide.buildImageViewTarget()方法中,你會看到如下的原始碼:

public class ImageViewTargetFactory {

    @SuppressWarnings("unchecked")
    public <Z> Target<Z> buildTarget(ImageView view, Class<Z> clazz) {
        if (GlideDrawable.class.isAssignableFrom(clazz)) {
            return (Target<Z>) new GlideDrawableImageViewTarget(view);
        } else if (Bitmap.class.equals(clazz)) {
            return (Target<Z>) new BitmapImageViewTarget(view);
        } else if (Drawable.class.isAssignableFrom(clazz)) {
            return (Target<Z>) new DrawableImageViewTarget(view);
        } else {
            throw new IllegalArgumentException("Unhandled class: " + clazz
                    + ", try .as*(Class).transcode(ResourceTranscoder)");
        }
    }
}

buildTarget()方法會根據傳入的class引數來構建不同的Target物件,如果你在使用Glide載入圖片的時候呼叫了asBitmap()方法,那麼這裡就會構建出BitmapImageViewTarget物件,否則的話構建的都是GlideDrawableImageViewTarget物件。至於上述程式碼中的DrawableImageViewTarget物件,這個通常都是用不到的,我們可以暫時不用管它。

之後就會把這裡構建出來的Target物件傳入到GenericRequest當中,而Glide在圖片載入完成之後又會回撥GenericRequest的onResourceReady()方法,我們來看一下這部分原始碼:

public final class GenericRequest<A, T, Z, R> implements Request, SizeReadyCallback,
        ResourceCallback {

    private Target<R> target;
    ...

    private void onResourceReady(Resource<?> resource, R result) {
        boolean isFirstResource = isFirstReadyResource();
        status = Status.COMPLETE;
        this.resource = resource;
        if (requestListener == null || !requestListener.onResourceReady(result, model, target,
                loadedFromMemoryCache, isFirstResource)) {
            GlideAnimation<R> animation = animationFactory.build(loadedFromMemoryCache, isFirstResource);
            target.onResourceReady(result, animation);
        }
        notifyLoadSuccess();
    }
    ...
}

這裡在第14行呼叫了target.onResourceReady()方法,而剛才我們已經知道,這裡的target就是GlideDrawableImageViewTarget物件,那麼我們再來看一下它的原始碼:

public class GlideDrawableImageViewTarget extends ImageViewTarget<GlideDrawable> {
    ...

    @Override
    public void onResourceReady(GlideDrawable resource, GlideAnimation<? super GlideDrawable> animation) {
        if (!resource.isAnimated()) {
            float viewRatio = view.getWidth() / (float) view.getHeight();
            float drawableRatio = resource.getIntrinsicWidth() / (float) resource.getIntrinsicHeight();
            if (Math.abs(viewRatio - 1f) <= SQUARE_RATIO_MARGIN
                    && Math.abs(drawableRatio - 1f) <= SQUARE_RATIO_MARGIN) {
                resource = new SquaringDrawable(resource, view.getWidth());
            }
        }
        super.onResourceReady(resource, animation);
        this.resource = resource;
        resource.setLoopCount(maxLoopCount);
        resource.start();
    }

    @Override
    protected void setResource(GlideDrawable resource) {
        view.setImageDrawable(resource);
    }

    ...
}

可以看到,這裡在onResourceReady()方法中處理了圖片展示,還有GIF播放的邏輯,那麼一張圖片也就顯示出來了,這也就是Glide回撥的基本實現原理。

好的,那麼原理就先分析到這兒,接下來我們就來看一下在回撥和監聽方面還有哪些知識是可以擴充套件的。

into()方法

使用了這麼久的Glide,我們都知道into()方法中是可以傳入ImageView的。那麼into()方法還可以傳入別的引數嗎?我可以讓Glide加載出來的圖片不顯示到ImageView上嗎?答案是肯定的,這就需要用到自定義Target功能。

其實通過上面的分析,我們已經知道了,into()方法還有一個接收Target引數的過載。即使我們傳入的引數是ImageView,Glide也會在內部自動構建一個Target物件。而如果我們能夠掌握自定義Target技術的話,就可以更加隨心所欲地控制Glide的回調了。

我們先來看一下Glide中Target的繼承結構圖吧,如下所示:

可以看到,Target的繼承結構還是相當複雜的,實現Target介面的子類非常多。不過你不用被這麼多的子類所嚇到,這些大多數都是Glide已經實現好的具備完整功能的Target子類,如果我們要進行自定義的話,通常只需要在兩種Target的基礎上去自定義就可以了,一種是SimpleTarget,一種是ViewTarget。

接下來我就分別以這兩種Target來舉例,學習一下自定義Target的功能。

首先來看SimpleTarget,顧名思義,它是一種極為簡單的Target,我們使用它可以將Glide加載出來的圖片物件獲取到,而不是像之前那樣只能將圖片在ImageView上顯示出來。

那麼下面我們來看一下SimpleTarget的用法示例吧,其實非常簡單:

SimpleTarget<GlideDrawable> simpleTarget = new SimpleTarget<GlideDrawable>() {
    @Override
    public void onResourceReady(GlideDrawable resource, GlideAnimation glideAnimation) {
        imageView.setImageDrawable(resource);
    }
};

public void loadImage(View view) {
    String url = "http://cn.bing.com/az/hprichbg/rb/TOAD_ZH-CN7336795473_1920x1080.jpg";
    Glide.with(this)
         .load(url)
         .into(simpleTarget);
}

怎麼樣?不愧是SimpleTarget吧,短短几行程式碼就搞了。這裡我們建立了一個SimpleTarget的例項,並且指定它的泛型是GlideDrawable,然後重寫了onResourceReady()方法。在onResourceReady()方法中,我們就可以獲取到Glide加載出來的圖片物件了,也就是方法引數中傳過來的GlideDrawable物件。有了這個物件之後你可以使用它進行任意的邏輯操作,這裡我只是簡單地把它顯示到了ImageView上。

SimpleTarget的實現建立好了,那麼只需要在載入圖片的時候將它傳入到into()方法中就可以了,現在執行一下程式,效果如下圖所示。

雖然目前這個效果和直接在into()方法中傳入ImageView並沒有什麼區別,但是我們已經拿到了圖片物件的例項,然後就可以隨意做更多的事情了。

當然,SimpleTarget中的泛型並不一定只能是GlideDrawable,如果你能確定你正在載入的是一張靜態圖而不是GIF圖的話,我們還能直接拿到這張圖的Bitmap物件,如下所示:

SimpleTarget<Bitmap> simpleTarget = new SimpleTarget<Bitmap>() {
    @Override
    public void onResourceReady(Bitmap resource, GlideAnimation glideAnimation) {
        imageView.setImageBitmap(resource);
    }
};

public void loadImage(View view) {
    String url = "http://cn.bing.com/az/hprichbg/rb/TOAD_ZH-CN7336795473_1920x1080.jpg";
    Glide.with(this)
         .load(url)
         .asBitmap()
         .into(simpleTarget);
}

可以看到,這裡我們將SimpleTarget的泛型指定成Bitmap,然後在載入圖片的時候呼叫了asBitmap()方法強制指定這是一張靜態圖,這樣就能在onResourceReady()方法中獲取到這張圖的Bitmap物件了。

好了,SimpleTarget的用法就是這麼簡單,接下來我們學習一下ViewTarget的用法。

事實上,從剛才的繼承結構圖上就能看出,Glide在內部自動幫我們建立的GlideDrawableImageViewTarget就是ViewTarget的子類。只不過GlideDrawableImageViewTarget被限定只能作用在ImageView上,而ViewTarget的功能更加廣泛,它可以作用在任意的View上。

這裡我們還是通過一個例子來演示一下吧,比如我建立了一個自定義佈局MyLayout,如下所示:

public class MyLayout extends LinearLayout {

    private ViewTarget<MyLayout, GlideDrawable> viewTarget;

    public MyLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        viewTarget = new ViewTarget<MyLayout, GlideDrawable>(this) {
            @Override
            public void onResourceReady(GlideDrawable resource, GlideAnimation glideAnimation) {
                MyLayout myLayout = getView();
                myLayout.setImageAsBackground(resource);
            }
        };
    }

    public ViewTarget<MyLayout, GlideDrawable> getTarget() {
        return viewTarget;
    }

    public void setImageAsBackground(GlideDrawable resource) {
        setBackground(resource);
    }

}

在MyLayout的建構函式中,我們建立了一個ViewTarget的例項,並將Mylayout當前的例項this傳了進去。ViewTarget中需要指定兩個泛型,一個是View的型別,一個圖片的型別(GlideDrawable或Bitmap)。然後在onResourceReady()方法中,我們就可以通過getView()方法獲取到MyLayout的例項,並呼叫它的任意介面了。比如說這裡我們呼叫了setImageAsBackground()方法來將加載出來的圖片作為MyLayout佈局的背景圖。

接下來看一下怎麼使用這個Target吧,由於MyLayout中已經提供了getTarget()介面,我們只需要在載入圖片的地方這樣寫就可以了:

public class MainActivity extends AppCompatActivity {

    MyLayout myLayout;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        myLayout = (MyLayout) findViewById(R.id.background);
    }

    public void loadImage(View view) {
        String url = "http://cn.bing.com/az/hprichbg/rb/TOAD_ZH-CN7336795473_1920x1080.jpg";
        Glide.with(this)
             .load(url)
             .into(myLayout.getTarget());
    }

}

就是這麼簡單,在into()方法中傳入myLayout.getTarget()即可。現在重新執行一下程式,效果如下圖所示。

好的,關於自定義Target的功能我們就介紹這麼多,這些雖說都是自定義Target最基本的用法,但掌握了這些用法之後,你就能應對各種各樣複雜的邏輯了。

preload()方法

Glide載入圖片雖說非常智慧,它會自動判斷該圖片是否已經有快取了,如果有的話就直接從快取中讀取,沒有的話再從網路去下載。但是如果我希望提前對圖片進行一個預載入,等真正需要載入圖片的時候就直接從快取中讀取,不想再等待慢長的網路載入時間了,這該怎麼辦呢?

對於很多Glide新手來說這確實是一個煩惱的問題,因為在沒有學習本篇文章之前,into()方法中必須傳入一個ImageView呀,而傳了ImageView之後圖片就顯示出來了,這還怎麼預載入呢?

不過在學習了本篇文章之後,相信你已經能夠想到解決方案了。因為into()方法中除了傳入ImageView之後還可以傳入Target物件,如果我們在Target物件的onResourceReady()方法中做一個空實現,也就是不做任何邏輯處理,那麼圖片自然也就顯示不出來了,而Glide的快取機制卻仍然還會正常工作,這樣不就實現預載入功能了嗎?

沒錯,上述的做法完全可以實現預載入功能,不過有沒有感覺這種實現方式有點笨笨的。事實上,Glide專門給我們提供了預載入的介面,也就是preload()方法,我們只需要直接使用就可以了。

preload()方法有兩個方法過載,一個不帶引數,表示將會載入圖片的原始尺寸,另一個可以通過引數指定載入圖片的寬和高。

preload()方法的用法也非常簡單,直接使用它來替換into()方法即可,如下所示:

Glide.with(this)
     .load(url)
     .diskCacheStrategy(DiskCacheStrategy.SOURCE)
     .preload();

需要注意的是,我們如果使用了preload()方法,最好要將diskCacheStrategy的快取策略指定成DiskCacheStrategy.SOURCE。因為preload()方法預設是預載入的原始圖片大小,而into()方法則預設會根據ImageView控制元件的大小來動態決定載入圖片的大小。因此,如果不將diskCacheStrategy的快取策略指定成DiskCacheStrategy.SOURCE的話,很容易會造成我們在預載入完成之後再使用into()方法載入圖片,卻仍然還是要從網路上去請求圖片這種現象。

呼叫了預載入之後,我們以後想再去載入這張圖片就會非常快了,因為Glide會直接從快取當中去讀取圖片並顯示出來,程式碼如下所示:

Glide.with(this)
     .load(url)
     .diskCacheStrategy(DiskCacheStrategy.SOURCE)
     .into(imageView);

注意,這裡我們仍然需要使用diskCacheStrategy()方法將硬碟快取策略指定成DiskCacheStrategy.SOURCE,以保證Glide一定會去讀取剛才預載入的圖片快取。

preload()方法的用法大概就是這麼簡單,但是僅僅會使用顯然層次有些太低了,下面我們就滿足一下好奇心,看看它的原始碼是如何實現的。

和into()方法一樣,preload()方法也是在GenericRequestBuilder類當中的,程式碼如下所示:

public class GenericRequestBuilder<ModelType, DataType, ResourceType, TranscodeType> implements Cloneable {
    ...

    public Target<TranscodeType> preload(int width, int height) {
        final PreloadTarget<TranscodeType> target = PreloadTarget.obtain(width, height);
        return into(target);
    }

    public Target<TranscodeType> preload() {
        return preload(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL);
    }

    ...
}

正如剛才所說,preload()方法有兩個方法過載,你可以呼叫帶引數的preload()方法來明確指定圖片的寬和高,也可以呼叫不帶引數的preload()方法,它會在內部自動將圖片的寬和高都指定成Target.SIZE_ORIGINAL,也就是圖片的原始尺寸。

然後我們可以看到,這裡在第5行呼叫了PreloadTarget.obtain()方法獲取一個PreloadTarget的例項,並把它傳入到了into()方法當中。從剛才的繼承結構圖中可以看出,PreloadTarget是SimpleTarget的子類,因此它是可以直接傳入到into()方法中的。

那麼現在的問題就是,PreloadTarget具體的實現到底是什麼樣子的了,我們看一下它的原始碼,如下所示:

public final class PreloadTarget<Z> extends SimpleTarget<Z> {

    public static <Z> PreloadTarget<Z> obtain(int width, int height) {
        return new PreloadTarget<Z>(width, height);
    }

    private PreloadTarget(int width, int height) {
        super(width, height);
    }

    @Override
    public void onResourceReady(Z resource, GlideAnimation<? super Z> glideAnimation) {
        Glide.clear(this);
    }
}

PreloadTarget的原始碼非常簡單,obtain()方法中就是new了一個PreloadTarget的例項而已,而onResourceReady()方法中也沒做什麼事情,只是呼叫了Glide.clear()方法。

這裡的Glide.clear()並不是清空快取的意思,而是表示載入已完成,釋放資源的意思,因此不用在這裡產生疑惑。

其實PreloadTarget的思想和我們剛才提到設計思路是一樣的,就是什麼都不做就可以了。因為圖片載入完成之後只將它快取而不去顯示它,那不就相當於預載入了嘛。

preload()方法不管是在用法方面還是原始碼實現方面都還是非常簡單的,那麼關於這個方法我們就學到這裡。

downloadOnly()方法

一直以來,我們使用Glide都是為了將圖片顯示到介面上。雖然我們知道Glide會在圖片的載入過程中對圖片進行快取,但是快取檔案到底是存在哪裡的,以及如何去直接訪問這些快取檔案?我們都還不知道。

其實Glide將圖片載入介面設計成這樣也是希望我們使用起來更加的方便,不用過多去考慮底層的實現細節。但如果我現在就是想要去訪問圖片的快取檔案該怎麼辦呢?這就需要用到downloadOnly()方法了。

和preload()方法類似,downloadOnly()方法也是可以替換into()方法的,不過downloadOnly()方法的用法明顯要比preload()方法複雜不少。顧名思義,downloadOnly()方法表示只會下載圖片,而不會對圖片進行載入。當圖片下載完成之後,我們可以得到圖片的儲存路徑,以便後續進行操作。

那麼首先我們還是先來看下基本用法。downloadOnly()方法是定義在DrawableTypeRequest類當中的,它有兩個方法過載,一個接收圖片的寬度和高度,另一個接收一個泛型物件,如下所示:

  • downloadOnly(int width, int height)
  • downloadOnly(Y target)

這兩個方法各自有各自的應用場景,其中downloadOnly(int width, int height)是用於在子執行緒中下載圖片的,而downloadOnly(Y target)是用於在主執行緒中下載圖片的。

那麼我們先來看downloadOnly(int width, int height)的用法。當呼叫了downloadOnly(int width, int height)方法後會立即返回一個FutureTarget物件,然後Glide會在後臺開始下載圖片檔案。接下來我們呼叫FutureTarget的get()方法就可以去獲取下載好的圖片檔案了,如果此時圖片還沒有下載完,那麼get()方法就會阻塞住,一直等到圖片下載完成才會有值返回。

下面我們通過一個例子來演示一下吧,程式碼如下所示:

public void downloadImage(View view) {
    new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                String url = "http://cn.bing.com/az/hprichbg/rb/TOAD_ZH-CN7336795473_1920x1080.jpg";
                final Context context = getApplicationContext();
                FutureTarget<File> target = Glide.with(context)
                                                 .load(url)
                                                 .downloadOnly(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL);
                final File imageFile = target.get();
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        Toast.makeText(context, imageFile.getPath(), Toast.LENGTH_LONG).show();
                    }
                });
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }).start();
}

這段程式碼稍微有一點點長,我帶著大家解讀一下。首先剛才說了,downloadOnly(int width, int height)方法必須要用在子執行緒當中,因此這裡的第一步就是new了一個Thread。在子執行緒當中,我們先獲取了一個Application Context,這個時候不能再用Activity作為Context了,因為會有Activity銷燬了但子執行緒還沒執行完這種可能出現。

接下來就是Glide的基本用法,只不過將into()方法替換成了downloadOnly()方法。downloadOnly()方法會返回一個FutureTarget物件,這個時候其實Glide已經開始在後臺下載圖片了,我們隨時都可以呼叫FutureTarget的get()方法來獲取下載的圖片檔案,只不過如果圖片還沒下載好執行緒會暫時阻塞住,等下載完成了才會把圖片的File物件返回。

最後,我們使用runOnUiThread()切回到主執行緒,然後使用Toast將下載好的圖片檔案路徑顯示出來。

現在重新執行一下程式碼,效果如下圖所示。

這樣我們就能清晰地看出來圖片完整的快取路徑是什麼了。

之後我們可以使用如下程式碼去載入這張圖片,圖片就會立即顯示出來,而不用再去網路上請求了:

public void loadImage(View view) {
    String url = "http://cn.bing.com/az/hprichbg/rb/TOAD_ZH-CN7336795473_1920x1080.jpg";
    Glide.with(this)
            .load(url)
            .diskCacheStrategy(DiskCacheStrategy.SOURCE)
            .into(imageView);
}

需要注意的是,這裡必須將硬碟快取策略指定成DiskCacheStrategy.SOURCE或者DiskCacheStrategy.ALL,否則Glide將無法使用我們剛才下載好的圖片快取檔案。

現在重新執行一下程式碼,效果如下圖所示。

可以看到,圖片的載入和顯示是非常快的,因為Glide直接使用的是剛才下載好的快取檔案。

那麼這個downloadOnly(int width, int height)方法的工作原理到底是什麼樣的呢?我們來簡單快速地看一下它的原始碼吧。

首先在DrawableTypeRequest類當中可以找到定義這個方法的地方,如下所示:

public class DrawableTypeRequest<ModelType> extends DrawableRequestBuilder<ModelType>
        implements DownloadOptions {
    ...

    public FutureTarget<File> downloadOnly(int width, int height) {
        return getDownloadOnlyRequest().downloadOnly(width, height);
    }

    private GenericTranscodeRequest<ModelType, InputStream, File> getDownloadOnlyRequest() {
        return optionsApplier.apply(new GenericTranscodeRequest<ModelType, InputStream, File>(
            File.class, this, streamModelLoader, InputStream.class, File.class, optionsApplier));
    }
}

這裡會先呼叫getDownloadOnlyRequest()方法得到一個GenericTranscodeRequest物件,然後再呼叫它的downloadOnly()方法,程式碼如下所示:

public class GenericTranscodeRequest<ModelType, DataType, ResourceType>
    implements DownloadOptions {
    ...

    public FutureTarget<File> downloadOnly(int width, int height) {
        return getDownloadOnlyRequest().into(width, height);
    }

    private GenericRequestBuilder<ModelType, DataType, File, File> getDownloadOnlyRequest() {
        ResourceTranscoder<File, File> transcoder = UnitTranscoder.get();
        DataLoadProvider<DataType, File> dataLoadProvider = glide.buildDataProvider(dataClass, File.class);
        FixedLoadProvider<ModelType, DataType, File, File> fixedLoadProvider =
            new FixedLoadProvider<ModelType, DataType, File, File>(modelLoader, transcoder, dataLoadProvider);
        return optionsApplier.apply(
                new GenericRequestBuilder<ModelType, DataType, File, File>(fixedLoadProvider,
                File.class, this))
                .priority(Priority.LOW)
                .diskCacheStrategy(DiskCacheStrategy.SOURCE)
                .skipMemoryCache(true);
    }
}

這裡又是呼叫了一個getDownloadOnlyRequest()方法來構建了一個圖片下載的請求,getDownloadOnlyRequest()方法會返回一個GenericRequestBuilder物件,接著呼叫它的into(width, height)方法,我們繼續跟進去瞧一瞧:

public FutureTarget<TranscodeType> into(int width, int height) {
    final RequestFutureTarget<ModelType, TranscodeType> target =
            new RequestFutureTarget<ModelType, TranscodeType>(glide.getMainHandler(), width, height);
    glide.getMainHandler().post(new Runnable() {
        @Override
        public void run() {
            if (!target.isCancelled()) {
                into(target);
            }
        }
    });
    return target;
}

可以看到,這裡首先是new出了一個RequestFutureTarget物件,RequestFutureTarget也是Target的子類之一。然後通過Handler將執行緒切回到主執行緒當中,再將這個RequestFutureTarget傳入到into()方法當中。

那麼也就是說,其實這裡就是呼叫了接收Target引數的into()方法,然後Glide就開始執行正常的圖片載入邏輯了。那麼現在剩下的問題就是,這個RequestFutureTarget中到底處理了些什麼邏輯?我們開啟它的原始碼看一看:

public class RequestFutureTarget<T, R> implements FutureTarget<R>, Runnable {
    ...

    @Override
    public R get() throws InterruptedException, ExecutionException {
        try {
            return doGet(null);
        } catch (TimeoutException e) {
            throw new AssertionError(e);
        }
    }

    @Override
    public R get(long time, TimeUnit timeUnit) throws InterruptedException, ExecutionException, 
        TimeoutException {
        return doGet(timeUnit.toMillis(time));
    }

    @Override
    public void getSize(SizeReadyCallback cb) {
        cb.onSizeReady(width, height);
    }

    @Override
    public synchronized void onLoadFailed(Exception e, Drawable errorDrawable) {
        exceptionReceived = true;
        this.exception = e;
        waiter.notifyAll(this);
    }

    @Override
    public synchronized void onResourceReady(R resource, GlideAnimation<? super R> glideAnimation) {
        resultReceived = true;
        this.resource = resource;
        waiter.notifyAll(this);
    }

    private synchronized R doGet(Long timeoutMillis) throws ExecutionException, InterruptedException, 
        TimeoutException {
        if (assertBackgroundThread) {
            Util.assertBackgroundThread();
        }

        if (isCancelled) {
            throw new CancellationException();
        } else if (exceptionReceived) {
            throw new ExecutionException(exception);
        } else if (resultReceived) {
            return resource;
        }

        if (timeoutMillis == null) {
            waiter.waitForTimeout(this, 0);
        } else if (timeoutMillis > 0) {
            waiter.waitForTimeout(this, timeoutMillis);
        }

        if (Thread.interrupted()) {
            throw new InterruptedException();
        } else if (exceptionReceived) {
            throw new ExecutionException(exception);
        } else if (isCancelled) {
            throw new CancellationException();
        } else if (!resultReceived) {
            throw new TimeoutException();
        }

        return resource;
    }

    static class Waiter {

        public void waitForTimeout(Object toWaitOn, long timeoutMillis) throws InterruptedException {
            toWaitOn.wait(timeoutMillis);
        }

        public void notifyAll(Object toNotify) {
            toNotify.notifyAll();
        }
    }

    ...
}

這裡我對RequestFutureTarget的原始碼做了一些精簡,我們只看最主要的邏輯就可以了。

剛才我們已經學習過了downloadOnly()方法的基本用法,在呼叫了downloadOnly()方法之後,再呼叫FutureTarget的get()方法,就能獲取到下載的圖片檔案了。而downloadOnly()方法返回的FutureTarget物件其實就是這個RequestFutureTarget,因此我們直接來看它的get()方法就行了。

RequestFutureTarget的get()方法中又呼叫了一個doGet()方法,而doGet()方法才是真正處理具體邏輯的地方。首先在doGet()方法中會判斷當前是否是在子執行緒當中,如果不是的話會直接丟擲一個異常。然後下面會判斷下載是否已取消、或者已失敗,如果是已取消或者已失敗的話都會直接丟擲一個異常。接下來會根據resultReceived這個變數來判斷下載是否已完成,如果這個變數為true的話,就直接把結果進行返回。

那麼如果下載還沒有完成呢?我們繼續往下看,接下來就進入到一個wait()當中,把當前執行緒給阻塞住,從而阻止程式碼繼續往下執行。這也是為什麼downloadOnly(int width, int height)方法要求必須在子執行緒當中使用,因為它會對當前執行緒進行阻塞,如果在主執行緒當中使用的話,那麼就會讓主執行緒卡死,從而使用者無法進行任何其他操作。

那麼現線上程被阻塞住了,什麼時候才能恢復呢?答案在onResourceReady()方法中。可以看到,onResourceReady()方法中只有三行程式碼,第一行把resultReceived賦值成true,說明圖片檔案已經下載好了,這樣下次再呼叫get()方法時就不會再阻塞執行緒,而是可以直接將結果返回。第二行把下載好的圖片檔案賦值到一個全域性的resource變數上面,這樣doGet()方法就也可以訪問到它。第三行notifyAll一下,通知所有wait的執行緒取消阻塞,這個時候圖片檔案已經下載好了,因此doGet()方法也就可以返回結果了。

好的,這就是downloadOnly(int width, int height)方法的基本用法和實現原理,那麼下面我們來看一下downloadOnly(Y target)方法。

回想一下,其實downloadOnly(int width, int height)方法必須使用在子執行緒當中,最主要還是因為它在內部幫我們自動建立了一個RequestFutureTarget,是這個RequestFutureTarget要求必須在子執行緒當中執行。而downloadOnly(Y target)方法則要求我們傳入一個自己建立的Target,因此就不受RequestFutureTarget的限制了。

但是downloadOnly(Y target)方法的用法也會相對更復雜一些,因為我們又要自己建立一個Target了,而且這次必須直接去實現最頂層的Target介面,比之前的SimpleTarget和ViewTarget都要複雜不少。

那麼下面我們就來實現一個最簡單的DownloadImageTarget吧,注意Target介面的泛型必須指定成File物件,這是downloadOnly(Y target)方法要求的,程式碼如下所示:

public class DownloadImageTarget implements Target<File> {

    private static final String TAG = "DownloadImageTarget";

    @Override
    public void onStart() {
    }

    @Override
    public void onStop() {
    }

    @Override
    public void onDestroy() {
    }

    @Override
    public void onLoadStarted(Drawable placeholder) {
    }

    @Override
    public void onLoadFailed(Exception e, Drawable errorDrawable) {
    }

    @Override
    public void onResourceReady(File resource, GlideAnimation<? super File> glideAnimation) {
        Log.d(TAG, resource.getPath());
    }

    @Override
    public void onLoadCleared(Drawable placeholder) {
    }

    @Override
    public void getSize(SizeReadyCallback cb) {
        cb.onSizeReady(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL);
    }

    @Override
    public void setRequest(Request request) {
    }

    @Override
    public Request getRequest() {
        return null;
    }
}

由於是要直接實現Target介面,因此需要重寫的方法非常多。這些方法大多是數Glide載入圖片生命週期的一些回撥,我們可以不用管它們,其中只有兩個方法是必須實現的,一個是getSize()方法,一個是onResourceReady()方法。

在第二篇Glide原始碼解析的時候,我帶著大家一起分析過,Glide在開始載入圖片之前會先計算圖片的大小,然後回撥到onSizeReady()方法當中,之後才會開始執行圖片載入。而這裡,計算圖片大小的任務就交給我們了。只不過這是一個最簡單的Target實現,我在getSize()方法中就直接回調了Target.SIZE_ORIGINAL,表示圖片的原始尺寸。

然後onResourceReady()方法我們就非常熟悉了,圖片下載完成之後就會回撥到這裡,我在這個方法中只是列印了一下下載的圖片檔案的路徑。

這樣一個最簡單的DownloadImageTarget就定義好了,使用它也非常的簡單,我們不用再考慮什麼執行緒的問題了,而是直接把它的例項傳入downloadOnly(Y target)方法中即可,如下所示:

public void downloadImage(View view) {
    String url = "http://cn.bing.com/az/hprichbg/rb/TOAD_ZH-CN7336795473_1920x1080.jpg";
    Glide.with(this)
            .load(url)
            .downloadOnly(new DownloadImageTarget());
}

現在重新執行一下程式碼並點選Download Image按鈕,然後觀察控制檯日誌的輸出,結果如下圖所示。

這樣我們就使用了downloadOnly(Y target)方法同樣獲取到下載的圖片檔案的快取路徑了。

好的,那麼關於downloadOnly()方法我們就學到這裡。

listener()方法

今天學習的內容已經夠多了,下面我們就以一個簡單的知識點結尾吧,Glide回撥與監聽的最後一部分——listener()方法。

其實listener()方法的作用非常普遍,它可以用來監聽Glide載入圖片的狀態。舉個例子,比如說我們剛才使用了preload()方法來對圖片進行預載入,但是我怎樣確定預載入有沒有完成呢?還有如果Glide載入圖片失敗了,我該怎樣除錯錯誤的原因呢?答案都在listener()方法當中。

首先來看下listener()方法的基本用法吧,不同於剛才幾個方法都是要替換into()方法的,listener()是結合into()方法一起使用的,當然也可以結合preload()方法一起使用。最基本的用法如下所示:

public void loadImage(View view) {
    String url = "http://cn.bing.com/az/hprichbg/rb/TOAD_ZH-CN7336795473_1920x1080.jpg";
    Glide.with(this)
            .load(url)
            .listener(new RequestListener<String, GlideDrawable>() {
                @Override
                public boolean onException(Exception e, String model, Target<GlideDrawable> target,
                    boolean isFirstResource) {
                    return false;
                }

                @Override
                public boolean onResourceReady(GlideDrawable resource, String model,
                    Target<GlideDrawable> target, boolean isFromMemoryCache, boolean isFirstResource) {
                    return false;
                }
            })
            .into(imageView);
}

這裡我們在into()方法之前串接了一個listener()方法,然後實現了一個RequestListener的例項。其中RequestListener需要實現兩個方法,一個onResourceReady()方法,一個onException()方法。從方法名上就可以看出來了,當圖片載入完成的時候就會回撥onResourceReady()方法,而當圖片載入失敗的時候就會回撥onException()方法,onException()方法中會將失敗的Exception引數傳進來,這樣我們就可以定位具體失敗的原因了。

沒錯,listener()方法就是這麼簡單。不過還有一點需要處理,onResourceReady()方法和onException()方法都有一個布林值的返回值,返回false就表示這個事件沒有被處理,還會繼續向下傳遞,返回true就表示這個事件已經被處理掉了,從而不會再繼續向下傳遞。舉個簡單點的例子,如果我們在RequestListener的onResourceReady()方法中返回了true,那麼就不會再回調Target的onResourceReady()方法了。

關於listener()方法的用法就講這麼多,不過還是老規矩,我們再來看一下它的原始碼是怎麼實現的吧。

首先,listener()方法是定義在GenericRequestBuilder類當中的,而我們傳入到listener()方法中的例項則會賦值到一個requestListener變數當中,如下所示:

public class GenericRequestBuilder<ModelType, DataType, ResourceType, TranscodeType> implements Cloneable {

    private RequestListener<? super ModelType, TranscodeType> requestListener;
    ...

    public GenericRequestBuilder<ModelType, DataType, ResourceType, TranscodeType> listener(
            RequestListener<? super ModelType, TranscodeType> requestListener) {
        this.requestListener = requestListener;
        return this;
    }

    ...
}

接下來在構建GenericRequest的時候這個變數也會被一起傳進去,最後在圖片載入完成的時候,我們會看到如下邏輯:

public final class GenericRequest<A, T, Z, R> implements Request, SizeReadyCallback,
        ResourceCallback {

    private RequestListener<? super A, R> requestListener;
    ...

    private void onResourceReady(Resource<?> resource, R result) {
        boolean isFirstResource = isFirstReadyResource();
        status = Status.COMPLETE;
        this.resource = resource;
        if (requestListener == null || !requestListener.onResourceReady(result, model, target,
                loadedFromMemoryCache, isFirstResource)) {
            GlideAnimation<R> animation = animationFactory.build(loadedFromMemoryCache, isFirstResource);
            target.onResourceReady(result, animation);
        }
        notifyLoadSuccess();
    }
    ...
}

可以看到,這裡在第11行會先回調requestListener的onResourceReady()方法,只有當這個onResourceReady()方法返回false的時候,才會繼續呼叫Target的onResourceReady()方法,這也就是listener()方法的實現原理。

另外一個onException()方法的實現機制也是一模一樣的,程式碼同樣是在GenericRequest類中,如下所示:

public final class GenericRequest<A, T, Z, R> implements Request, SizeReadyCallback,
        ResourceCallback {
    ...

    @Override
    public void onException(Exception e) {
        status = Status.FAILED;
        if (requestListener == null || 
                !requestListener.onException(e, model, target, isFirstReadyResource())) {
            setErrorPlaceholder(e);
        }
    }

    ...
}

可以看到,這裡會在第9行回撥requestListener的onException()方法,只有在onException()方法返回false的情況下才會繼續呼叫setErrorPlaceholder()方法。也就是說,如果我們在onException()方法中返回了true,那麼Glide請求中使用error(int resourceId)方法設定的異常佔位圖就失效了。

這樣我們也就將listener()方法的全部實現原理都分析完了。

好了,關於Glide回撥與監聽方面的內容今天就講到這裡,這一篇文章的內容非常充實,希望大家都能好好掌握。下一篇文章當中,我會繼續帶著大家深入分析Glide的其他功能模組,講一講圖片變換方面的知識,感興趣的朋友請繼續閱讀 Android圖片載入框架最全解析(五),Glide強大的圖片變換功能

關注我的技術公眾號,每天都有優質技術文章推送。關注我的娛樂公眾號,工作、學習累了的時候放鬆一下自己。

微信掃一掃下方二維碼即可關注: