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

轉載請註明出處:http://blog.csdn.net/guolin_blog/article/details/70215985

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

大家好,今天我們繼續學習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強大的圖片變換功能

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

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

        

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章