Android圖片加載框架最全解析(六),探究Glide的自定義模塊功能

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

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

不知不覺中,我們的Glide系列教程已經到了第六篇了,距離第一篇Glide的基本用法發佈已經過去了半年的時間。在這半年中,我們通過用法講解和源碼分析配合學習的方式,將Glide的方方面面都研究了個遍,相信一直能看到這裏的朋友現在已經是一位Glide高手了。

整個Glide系列預計總共會有八篇文章,現在也是逐步進入尾聲了。不過,越是到後面,我們探究的內容也越是更加深入。那麼今天,我們就來一起探究一下Glide中一個比較深入,但同時也是非常重要的一個功能——自定義模塊。

自定義模塊的基本用法

學到這裏相信你已經知道,Glide的用法是非常非常簡單的,大多數情況下,我們想要實現的圖片加載效果只需要一行代碼就能解決了。但是Glide過於簡潔的API也造成了一個問題,就是如果我們想要更改Glide的某些默認配置項應該怎麼操作呢?很難想象如何將更改Glide配置項的操作串聯到一行經典的Glide圖片加載語句中當中吧?沒錯,這個時候就需要用到自定義模塊功能了。

自定義模塊功能可以將更改Glide配置,替換Glide組件等操作獨立出來,使得我們能輕鬆地對Glide的各種配置進行自定義,並且又和Glide的圖片加載邏輯沒有任何交集,這也是一種低耦合編程方式的體現。那麼接下來我們就學習一下自定義模塊的基本用法。

首先需要定義一個我們自己的模塊類,並讓它實現GlideModule接口,如下所示:

public class MyGlideModule implements GlideModule {
    @Override
    public void applyOptions(Context context, GlideBuilder builder) {
    }

    @Override
    public void registerComponents(Context context, Glide glide) {
    }
}

可以看到,在MyGlideModule類當中,我們重寫了applyOptions()和registerComponents()方法,這兩個方法分別就是用來更改Glide和配置以及替換Glide組件的。我們待會兒只需要在這兩個方法中加入具體的邏輯,就能實現更改Glide配置或者替換Glide組件的功能了。

不過,目前Glide還無法識別我們自定義的MyGlideModule,如果想要讓它生效,還得在AndroidManifest.xml文件當中加入如下配置才行:

<manifest>

    ...

    <application>

        <meta-data
            android:name="com.example.glidetest.MyGlideModule"
            android:value="GlideModule" />

        ...

    </application>
</manifest>  

在<application>標籤中加入一個meta-data配置項,其中android:name指定成我們自定義的MyGlideModule的完整路徑,android:value必須指定成GlideModule,這個是固定值。

這樣的話,我們就將Glide自定義模塊的功能完成了,是不是非常簡單?現在Glide已經能夠識別我們自定義的這個MyGlideModule了,但是在編寫具體的功能之前,我們還是按照老規矩閱讀一下源碼,從源碼的層面上來分析一下,Glide到底是如何識別出這個自定義的MyGlideModule的。

自定義模塊的原理

這裏我不會帶着大家從Glide代碼執行的第一步一行行重頭去解析Glide的源碼,而是隻分析和自定義模塊相關的部分。如果你想將Glide的源碼通讀一遍的話,可以去看本系列的第二篇文章 Android圖片加載框架最全解析(二),從源碼的角度理解Glide的執行流程

顯然我們已經用慣了Glide.with(context).load(url).into(imageView)這樣一行簡潔的Glide圖片加載語句,但是我們好像從來沒有注意過Glide這個類本身的實例。然而事實上,Glide類確實是有創建實例的,只不過是在內部由Glide自動幫我們創建和管理了,對於開發者而言,大多數情況下是不用關心它的,只需要調用它的靜態方法就可以了。

那麼Glide的實例到底是在哪裏創建的呢?我們來看下Glide類中的get()方法的源碼,如下所示:

public class Glide {

    private static volatile Glide glide;

    ...

    public static Glide get(Context context) {
        if (glide == null) {
            synchronized (Glide.class) {
                if (glide == null) {
                    Context applicationContext = context.getApplicationContext();
                    List<GlideModule> modules = new ManifestParser(applicationContext).parse();
                    GlideBuilder builder = new GlideBuilder(applicationContext);
                    for (GlideModule module : modules) {
                        module.applyOptions(applicationContext, builder);
                    }
                    glide = builder.createGlide();
                    for (GlideModule module : modules) {
                        module.registerComponents(applicationContext, glide);
                    }
                }
            }
        }
        return glide;
    }

    ...
}

我們來仔細看一下上面這段代碼。首先這裏使用了一個單例模式來獲取Glide對象的實例,可以看到,這是一個非常典型的雙重鎖模式。然後在第12行,調用ManifestParser的parse()方法去解析AndroidManifest.xml文件中的配置,實際上就是將AndroidManifest中所有值爲GlideModule的meta-data配置讀取出來,並將相應的自定義模塊實例化。由於你可以自定義任意多個模塊,因此這裏我們將會得到一個GlideModule的List集合。

接下來在第13行創建了一個GlideBuilder對象,並通過一個循環調用了每一個GlideModule的applyOptions()方法,同時也把GlideBuilder對象作爲參數傳入到這個方法中。而applyOptions()方法就是我們可以加入自己的邏輯的地方了,雖然目前爲止我們還沒有編寫任何邏輯。

再往下的一步就非常關鍵了,這裏調用了GlideBuilder的createGlide()方法,並返回了一個Glide對象。也就是說,Glide對象的實例就是在這裏創建的了,那麼我們跟到這個方法當中瞧一瞧:

public class GlideBuilder {
    private final Context context;

    private Engine engine;
    private BitmapPool bitmapPool;
    private MemoryCache memoryCache;
    private ExecutorService sourceService;
    private ExecutorService diskCacheService;
    private DecodeFormat decodeFormat;
    private DiskCache.Factory diskCacheFactory;

    ...

    Glide createGlide() {
        if (sourceService == null) {
            final int cores = Math.max(1, Runtime.getRuntime().availableProcessors());
            sourceService = new FifoPriorityThreadPoolExecutor(cores);
        }
        if (diskCacheService == null) {
            diskCacheService = new FifoPriorityThreadPoolExecutor(1);
        }
        MemorySizeCalculator calculator = new MemorySizeCalculator(context);
        if (bitmapPool == null) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
                int size = calculator.getBitmapPoolSize();
                bitmapPool = new LruBitmapPool(size);
            } else {
                bitmapPool = new BitmapPoolAdapter();
            }
        }
        if (memoryCache == null) {
            memoryCache = new LruResourceCache(calculator.getMemoryCacheSize());
        }
        if (diskCacheFactory == null) {
            diskCacheFactory = new InternalCacheDiskCacheFactory(context);
        }
        if (engine == null) {
            engine = new Engine(memoryCache, diskCacheFactory, diskCacheService, sourceService);
        }
        if (decodeFormat == null) {
            decodeFormat = DecodeFormat.DEFAULT;
        }
        return new Glide(engine, memoryCache, bitmapPool, context, decodeFormat);
    }
}

這個方法中會創建BitmapPool、MemoryCache、DiskCache、DecodeFormat等對象的實例,並在最後一行創建一個Glide對象的實例,然後將前面創建的這些實例傳入到Glide對象當中,以供後續的圖片加載操作使用。

但是大家有沒有注意到一個細節,createGlide()方法中創建任何對象的時候都做了一個空檢查,只有在對象爲空的時候纔會去創建它的實例。也就是說,如果我們可以在applyOptions()方法中提前就給這些對象初始化並賦值,那麼在createGlide()方法中就不會再去重新創建它們的實例了,從而也就實現了更改Glide配置的功能。關於這個功能我們待會兒會進行具體的演示。

現在繼續回到Glide的get()方法中,得到了Glide對象的實例之後,接下來又通過一個循環調用了每一個GlideModule的registerComponents()方法,在這裏我們可以加入替換Glide的組件的邏輯。

好了,這就是Glide自定義模塊的全部工作原理。瞭解了它的工作原理之後,接下來所有的問題就集中在我們到底如何在applyOptions()和registerComponents()這兩個方法中加入具體的邏輯了,下面我們馬上就來學習一下。

更改Glide配置

剛纔在分析自定義模式工作原理的時候其實就已經提到了,如果想要更改Glide的默認配置,其實只需要在applyOptions()方法中提前將Glide的配置項進行初始化就可以了。那麼Glide一共有哪些配置項呢?這裏我給大家做了一個列舉:

  • setMemoryCache()
    用於配置Glide的內存緩存策略,默認配置是LruResourceCache。

  • setBitmapPool()
    用於配置Glide的Bitmap緩存池,默認配置是LruBitmapPool。

  • setDiskCache()
    用於配置Glide的硬盤緩存策略,默認配置是InternalCacheDiskCacheFactory。

  • setDiskCacheService()
    用於配置Glide讀取緩存中圖片的異步執行器,默認配置是FifoPriorityThreadPoolExecutor,也就是先入先出原則。

  • setResizeService()
    用於配置Glide讀取非緩存中圖片的異步執行器,默認配置也是FifoPriorityThreadPoolExecutor。

  • setDecodeFormat()
    用於配置Glide加載圖片的解碼模式,默認配置是RGB_565。

其實Glide的這些默認配置都非常科學且合理,使用的緩存算法也都是效率極高的,因此在絕大多數情況下我們並不需要去修改這些默認配置,這也是Glide用法能如此簡潔的一個原因。

但是Glide科學的默認配置並不影響我們去學習自定義Glide模塊的功能,因此總有某些情況下,默認的配置可能將無法滿足你,這個時候就需要我們自己動手來修改默認配置了。

下面就通過具體的實例來看一下吧。剛纔說到,Glide默認的硬盤緩存策略使用的是InternalCacheDiskCacheFactory,這種緩存會將所有Glide加載的圖片都存儲到當前應用的私有目錄下。這是一種非常安全的做法,但同時這種做法也造成了一些不便,因爲私有目錄下即使是開發者自己也是無法查看的,如果我想要去驗證一下圖片到底有沒有成功緩存下來,這就有點不太好辦了。

這種情況下,就非常適合使用自定義模塊來更改Glide的默認配置。我們完全可以自己去實現DiskCache.Factory接口來自定義一個硬盤緩存策略,不過卻大大沒有必要這麼做,因爲Glide本身就內置了一個ExternalCacheDiskCacheFactory,可以允許將加載的圖片都緩存到SD卡。

那麼接下來,我們就嘗試使用這個ExternalCacheDiskCacheFactory來替換默認的InternalCacheDiskCacheFactory,從而將所有Glide加載的圖片都緩存到SD卡上。

由於在前面我們已經創建好了一個自定義模塊MyGlideModule,那麼現在就可以直接在這裏編寫邏輯了,代碼如下所示:

public class MyGlideModule implements GlideModule {

    @Override
    public void applyOptions(Context context, GlideBuilder builder) {
        builder.setDiskCache(new ExternalCacheDiskCacheFactory(context));
    }

    @Override
    public void registerComponents(Context context, Glide glide) {

    }

}

沒錯,就是這麼簡單,現在所有Glide加載的圖片都會緩存到SD卡上了。

另外,InternalCacheDiskCacheFactory和ExternalCacheDiskCacheFactory的默認硬盤緩存大小都是250M。也就是說,如果你的應用緩存的圖片總大小超出了250M,那麼Glide就會按照DiskLruCache算法的原則來清理緩存的圖片。

當然,我們是可以對這個默認的緩存大小進行修改的,而且修改方式非常簡單,如下所示:

public class MyGlideModule implements GlideModule {

    public static final int DISK_CACHE_SIZE = 500 * 1024 * 1024;

    @Override
    public void applyOptions(Context context, GlideBuilder builder) {
        builder.setDiskCache(new ExternalCacheDiskCacheFactory(context, DISK_CACHE_SIZE));
    }

    @Override
    public void registerComponents(Context context, Glide glide) {

    }

}

只需要向ExternalCacheDiskCacheFactory或者InternalCacheDiskCacheFactory再傳入一個參數就可以了,現在我們就將Glide硬盤緩存的大小調整成了500M。

好了,更改Glide配置的功能就是這麼簡單,那麼接下來我們就來驗證一下更改的配置到底有沒有生效吧。

這裏還是使用最基本的Glide加載語句來去加載一張網絡圖片:

String url = "http://guolin.tech/book.png";
Glide.with(this)
     .load(url)
     .into(imageView);

運行一下程序,效果如下圖所示:

OK,現在圖片已經加載出現了,那麼我們去找一找它的緩存吧。

ExternalCacheDiskCacheFactory的默認緩存路徑是在sdcard/Android/包名/cache/image_manager_disk_cache目錄當中,我們使用文件瀏覽器進入到這個目錄,結果如下圖所示。

可以看到,這裏有兩個文件,其中journal文件是DiskLruCache算法的日誌文件,這個文件必不可少,且只會有一個。想了解更多關於DiskLruCache算法的朋友,可以去閱讀我的這篇博客 Android DiskLruCache完全解析,硬盤緩存的最佳方案

而另外一個文件就是那張緩存的圖片了,它的文件名雖然看上去很奇怪,但是我們只需要把這個文件的後綴改成.png,然後用圖片瀏覽器打開,結果就一目瞭然了,如下圖所示。

由此證明,我們已經成功將Glide的硬盤緩存路徑修改到SD卡上了。

另外這裏再提一點,我們都知道Glide和Picasso的用法是非常相似的,但是有一點差別卻很大。Glide加載圖片的默認格式是RGB_565,而Picasso加載圖片的默認格式是ARGB_8888。ARGB_8888格式的圖片效果會更加細膩,但是內存開銷會比較大。而RGB_565格式的圖片則更加節省內存,但是圖片效果上會差一些。

Glide和Picasso各自採取的默認圖片格式談不上熟優熟劣,只能說各自的取捨不一樣。但是如果你希望Glide也能使用ARGB_8888的圖片格式,這當然也是可以的。我們只需要在MyGlideModule中更改一下默認配置即可,如下所示:

public class MyGlideModule implements GlideModule {

    public static final int DISK_CACHE_SIZE = 500 * 1024 * 1024;

    @Override
    public void applyOptions(Context context, GlideBuilder builder) {
        builder.setDiskCache(new ExternalCacheDiskCacheFactory(context, DISK_CACHE_SIZE));
        builder.setDecodeFormat(DecodeFormat.PREFER_ARGB_8888);
    }

    @Override
    public void registerComponents(Context context, Glide glide) {

    }

}

通過這樣配置之後,使用Glide加載的所有圖片都將會使用ARGB_8888的格式,雖然圖片質量變好了,但同時內存開銷也會明顯增大,所以你要做好心理準備哦。

好了,關於更改Glide配置的內容就介紹這麼多,接下來就讓我們進入到下一個非常重要的主題,替換Glide組件。

替換Glide組件

替換Glide組件功能需要在自定義模塊的registerComponents()方法中加入具體的替換邏輯。相比於更改Glide配置,替換Glide組件這個功能的難度就明顯大了不少。Glide中的組件非常繁多,也非常複雜,但其實大多數情況下並不需要我們去做什麼替換。不過,有一個組件卻有着比較大的替換需求,那就是Glide的HTTP通訊組件。

默認情況下,Glide使用的是基於原生HttpURLConnection進行訂製的HTTP通訊組件,但是現在大多數的Android開發者都更喜歡使用OkHttp,因此將Glide中的HTTP通訊組件修改成OkHttp的這個需求比較常見,那麼今天我們也會以這個功能來作爲例子進行講解。

首先來看一下Glide中目前有哪些組件吧,在Glide類的構造方法當中,如下所示:

public class Glide {

    Glide(Engine engine, MemoryCache memoryCache, BitmapPool bitmapPool, Context context, DecodeFormat decodeFormat) {
        ...

        register(File.class, ParcelFileDescriptor.class, new FileDescriptorFileLoader.Factory());
        register(File.class, InputStream.class, new StreamFileLoader.Factory());
        register(int.class, ParcelFileDescriptor.class, new FileDescriptorResourceLoader.Factory());
        register(int.class, InputStream.class, new StreamResourceLoader.Factory());
        register(Integer.class, ParcelFileDescriptor.class, new FileDescriptorResourceLoader.Factory());
        register(Integer.class, InputStream.class, new StreamResourceLoader.Factory());
        register(String.class, ParcelFileDescriptor.class, new FileDescriptorStringLoader.Factory());
        register(String.class, InputStream.class, new StreamStringLoader.Factory());
        register(Uri.class, ParcelFileDescriptor.class, new FileDescriptorUriLoader.Factory());
        register(Uri.class, InputStream.class, new StreamUriLoader.Factory());
        register(URL.class, InputStream.class, new StreamUrlLoader.Factory());
        register(GlideUrl.class, InputStream.class, new HttpUrlGlideUrlLoader.Factory());
        register(byte[].class, InputStream.class, new StreamByteArrayLoader.Factory());

        ...
    }

}

可以看到,這裏都是以調用register()方法的方式來註冊一個組件,register()方法中傳入的參數表示Glide支持使用哪種參數類型來加載圖片,以及如何去處理這種類型的圖片加載。舉個例子:

register(GlideUrl.class, InputStream.class, new HttpUrlGlideUrlLoader.Factory());

這句代碼就表示,我們可以使用Glide.with(context).load(new GlideUrl("url...")).into(imageView)的方式來加載圖片,而HttpUrlGlideUrlLoader.Factory則是要負責處理具體的網絡通訊邏輯。如果我們想要將Glide的HTTP通訊組件替換成OkHttp的話,那麼只需要在自定義模塊當中重新註冊一個GlideUrl類型的組件就行了。

說到這裏有的朋友可能會疑問了,我們平時使用Glide加載圖片時,大多數情況下都是直接將圖片的URL字符串傳入到load()方法當中的,很少會將它封裝成GlideUrl對象之後再傳入到load()方法當中,那爲什麼只需要重新註冊一個GlideUrl類型的組件,而不需要去重新註冊一個String類型的組件呢?其實道理很簡單,因爲load(String)方法只是Glide給我們提供一種簡易的API封裝而已,它的底層仍然還是調用的GlideUrl組件,因此我們在替換組件的時候只需要直接替換最底層的,這樣就一步到位了。

那麼接下來我們就開始學習到底如何將Glide的HTTP通訊組件替換成OkHttp。

首先第一步,不用多說,肯定是要先將OkHttp的庫引入到當前項目中,如下所示:

dependencies {
    compile 'com.squareup.okhttp3:okhttp:3.9.0'
}

然後接下來該怎麼做呢?我們只要依葫蘆畫瓢就可以了。剛纔不是說Glide的網絡通訊邏輯是由HttpUrlGlideUrlLoader.Factory來負責的嗎,那麼我們就來看一下它的源碼:

public class HttpUrlGlideUrlLoader implements ModelLoader<GlideUrl, InputStream> {

    private final ModelCache<GlideUrl, GlideUrl> modelCache;

    public static class Factory implements ModelLoaderFactory<GlideUrl, InputStream> {
        private final ModelCache<GlideUrl, GlideUrl> modelCache = new ModelCache<GlideUrl, GlideUrl>(500);

        @Override
        public ModelLoader<GlideUrl, InputStream> build(Context context, GenericLoaderFactory factories) {
            return new HttpUrlGlideUrlLoader(modelCache);
        }

        @Override
        public void teardown() {
        }
    }

    public HttpUrlGlideUrlLoader() {
        this(null);
    }

    public HttpUrlGlideUrlLoader(ModelCache<GlideUrl, GlideUrl> modelCache) {
        this.modelCache = modelCache;
    }

    @Override
    public DataFetcher<InputStream> getResourceFetcher(GlideUrl model, int width, int height) {
        GlideUrl url = model;
        if (modelCache != null) {
            url = modelCache.get(model, 0, 0);
            if (url == null) {
                modelCache.put(model, 0, 0, model);
                url = model;
            }
        }
        return new HttpUrlFetcher(url);
    }
}

可以看到,HttpUrlGlideUrlLoader.Factory是一個內部類,外層的HttpUrlGlideUrlLoader類實現了ModelLoader<GlideUrl, InputStream>這個接口,並重寫了getResourceFetcher()方法。而在getResourceFetcher()方法中,又創建了一個HttpUrlFetcher的實例,在這裏纔是真正處理具體網絡通訊邏輯的地方,代碼如下所示:

public class HttpUrlFetcher implements DataFetcher<InputStream> {
    private static final String TAG = "HttpUrlFetcher";
    private static final int MAXIMUM_REDIRECTS = 5;
    private static final HttpUrlConnectionFactory DEFAULT_CONNECTION_FACTORY = new DefaultHttpUrlConnectionFactory();

    private final GlideUrl glideUrl;
    private final HttpUrlConnectionFactory connectionFactory;

    private HttpURLConnection urlConnection;
    private InputStream stream;
    private volatile boolean isCancelled;

    public HttpUrlFetcher(GlideUrl glideUrl) {
        this(glideUrl, DEFAULT_CONNECTION_FACTORY);
    }

    HttpUrlFetcher(GlideUrl glideUrl, HttpUrlConnectionFactory connectionFactory) {
        this.glideUrl = glideUrl;
        this.connectionFactory = connectionFactory;
    }

    @Override
    public InputStream loadData(Priority priority) throws Exception {
        return loadDataWithRedirects(glideUrl.toURL(), 0 , null , glideUrl.getHeaders());
    }

    private InputStream loadDataWithRedirects(URL url, int redirects, URL lastUrl, Map<String, String> headers)
            throws IOException {
        if (redirects >= MAXIMUM_REDIRECTS) {
            throw new IOException("Too many (> " + MAXIMUM_REDIRECTS + ") redirects!");
        } else {
            try {
                if (lastUrl != null && url.toURI().equals(lastUrl.toURI())) {
                    throw new IOException("In re-direct loop");
                }
            } catch (URISyntaxException e) {
            }
        }
        urlConnection = connectionFactory.build(url);
        for (Map.Entry<String, String> headerEntry : headers.entrySet()) {
          urlConnection.addRequestProperty(headerEntry.getKey(), headerEntry.getValue());
        }
        urlConnection.setConnectTimeout(2500);
        urlConnection.setReadTimeout(2500);
        urlConnection.setUseCaches(false);
        urlConnection.connect();
        if (isCancelled) {
            return null;
        }
        final int statusCode = urlConnection.getResponseCode();
        if (statusCode / 100 == 2) {
            return getStreamForSuccessfulRequest(urlConnection);
        } else if (statusCode / 100 == 3) {
            String redirectUrlString = urlConnection.getHeaderField("Location");
            if (TextUtils.isEmpty(redirectUrlString)) {
                throw new IOException("Received empty or null redirect url");
            }
            URL redirectUrl = new URL(url, redirectUrlString);
            return loadDataWithRedirects(redirectUrl, redirects + 1, url, headers);
        } else {
            if (statusCode == -1) {
                throw new IOException("Unable to retrieve response code from HttpUrlConnection.");
            }
            throw new IOException("Request failed " + statusCode + ": " + urlConnection.getResponseMessage());
        }
    }

    private InputStream getStreamForSuccessfulRequest(HttpURLConnection urlConnection)
            throws IOException {
        if (TextUtils.isEmpty(urlConnection.getContentEncoding())) {
            int contentLength = urlConnection.getContentLength();
            stream = ContentLengthInputStream.obtain(urlConnection.getInputStream(), contentLength);
        } else {
            stream = urlConnection.getInputStream();
        }
        return stream;
    }

    @Override
    public void cleanup() {
        if (stream != null) {
            try {
                stream.close();
            } catch (IOException e) {
            }
        }
        if (urlConnection != null) {
            urlConnection.disconnect();
        }
    }

    @Override
    public String getId() {
        return glideUrl.getCacheKey();
    }

    @Override
    public void cancel() {
        isCancelled = true;
    }

    interface HttpUrlConnectionFactory {
        HttpURLConnection build(URL url) throws IOException;
    }

    private static class DefaultHttpUrlConnectionFactory implements HttpUrlConnectionFactory {
        @Override
        public HttpURLConnection build(URL url) throws IOException {
            return (HttpURLConnection) url.openConnection();
        }
    }
}

上面這段代碼看上去應該不費力吧?其實就是一些HttpURLConnection的用法而已。那麼我們只需要仿照着HttpUrlFetcher的代碼來寫,並且把HTTP的通訊組件替換成OkHttp就可以了。

現在新建一個OkHttpFetcher類,並且同樣實現DataFetcher<InputStream>接口,代碼如下所示:

public class OkHttpFetcher implements DataFetcher<InputStream> {

    private final OkHttpClient client;
    private final GlideUrl url;
    private InputStream stream;
    private ResponseBody responseBody;
    private volatile boolean isCancelled;

    public OkHttpFetcher(OkHttpClient client, GlideUrl url) {
        this.client = client;
        this.url = url;
    }

    @Override
    public InputStream loadData(Priority priority) throws Exception {
        Request.Builder requestBuilder = new Request.Builder()
                .url(url.toStringUrl());
        for (Map.Entry<String, String> headerEntry : url.getHeaders().entrySet()) {
            String key = headerEntry.getKey();
            requestBuilder.addHeader(key, headerEntry.getValue());
        }
        requestBuilder.addHeader("httplib", "OkHttp");
        Request request = requestBuilder.build();
        if (isCancelled) {
            return null;
        }
        Response response = client.newCall(request).execute();
        responseBody = response.body();
        if (!response.isSuccessful() || responseBody == null) {
            throw new IOException("Request failed with code: " + response.code());
        }
        stream = ContentLengthInputStream.obtain(responseBody.byteStream(),
                responseBody.contentLength());
        return stream;
    }

    @Override
    public void cleanup() {
        try {
            if (stream != null) {
                stream.close();
            }
            if (responseBody != null) {
                responseBody.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public String getId() {
        return url.getCacheKey();
    }

    @Override
    public void cancel() {
        isCancelled = true;
    }
}

上面這段代碼完全就是我照着HttpUrlFetcher依葫蘆畫瓢寫出來的,用的也都是一些OkHttp的基本用法,相信不需要再做什麼解釋了吧。可以看到,使用OkHttp來編寫網絡通訊的代碼要比使用HttpURLConnection簡單很多,代碼行數也少了很多。注意在第22行,我添加了一個httplib: OkHttp的請求頭,這個是待會兒我們用來進行測試驗證的,大家實際項目中的代碼無須添加這個請求頭。

那麼我們就繼續發揮依葫蘆畫瓢的精神,仿照着HttpUrlGlideUrlLoader再寫一個OkHttpGlideUrlLoader吧。新建一個OkHttpGlideUrlLoader類,並且實現ModelLoader<GlideUrl, InputStream>接口,代碼如下所示:

public class OkHttpGlideUrlLoader implements ModelLoader<GlideUrl, InputStream> {

    private OkHttpClient okHttpClient;

    public static class Factory implements ModelLoaderFactory<GlideUrl, InputStream> {

        private OkHttpClient client;

        public Factory() {
        }

        public Factory(OkHttpClient client) {
            this.client = client;
        }

        private synchronized OkHttpClient getOkHttpClient() {
            if (client == null) {
                client = new OkHttpClient();
            }
            return client;
        }

        @Override
        public ModelLoader<GlideUrl, InputStream> build(Context context, GenericLoaderFactory factories) {
            return new OkHttpGlideUrlLoader(getOkHttpClient());
        }

        @Override
        public void teardown() {
        }
    }

    public OkHttpGlideUrlLoader(OkHttpClient client) {
        this.okHttpClient = client;
    }

    @Override
    public DataFetcher<InputStream> getResourceFetcher(GlideUrl model, int width, int height) {
        return new OkHttpFetcher(okHttpClient, model);
    }
}

注意這裏的Factory我提供了兩個構造方法,一個是不帶任何參數的,一個是帶OkHttpClient參數的。如果對OkHttp不需要進行任何自定義的配置,那麼就調用無參的Factory構造函數即可,這樣會在內部自動創建一個OkHttpClient實例。但如果你需要想添加攔截器,或者修改OkHttp的默認超時等等配置,那麼就自己創建一個OkHttpClient的實例,然後傳入到Factory的構造方法當中就行了。

好了,現在就只差最後一步,將我們剛剛創建的OkHttpGlideUrlLoader和OkHttpFetcher註冊到Glide當中,將原來的HTTP通訊組件給替換掉,如下所示:

public class MyGlideModule implements GlideModule {

    ...

    @Override
    public void registerComponents(Context context, Glide glide) {
        glide.register(GlideUrl.class, InputStream.class, new OkHttpGlideUrlLoader.Factory());
    }

}

可以看到,這裏也是調用了Glide的register()方法來註冊組件的。register()方法中使用的Map類型來存儲已註冊的組件,因此我們這裏重新註冊了一遍GlideUrl.class類型的組件,就把原來的組件給替換掉了。

理論上來說,現在我們已經成功將Glide的HTTP通訊組件替換成OkHttp了,現在唯一的問題就是我們該如何去驗證一下到底有沒有替換成功呢?

驗證的方式我倒是想了很多種,比如添加OkHttp攔截器,或者自己架設一個測試用的服務器都是可以的。不過爲了讓大家最直接地看到驗證結果,這裏我準備使用Fiddler這個抓包工具來進行驗證。這個工具的用法非常簡單,但是限於篇幅我就不在本篇文章中介紹這個工具的用法了,還沒用過這個工具的朋友們可以通過 這篇文章 瞭解一下。

在開始驗證之前,我們還得要再修改一下Glide加載圖片的代碼才行,如下所示:

String url = "http://guolin.tech/book.png";
Glide.with(this)
     .load(url)
     .skipMemoryCache(true)
     .diskCacheStrategy(DiskCacheStrategy.NONE)
     .into(imageView);

這裏我把Glide的內存緩存和硬盤緩存都禁用掉了,不然的話,Glide可能會直接讀取剛纔緩存的圖片,而不會再重新發起網終請求。

好的,現在我們重新使用Glide加載一下圖片,然後觀察Fiddler中的抓包情況,如下圖所示。

可以看到,在HTTP請求頭中確實有我們剛纔自己添加的httplib: OkHttp。也就說明,Glide的HTTP通訊組件的確被替換成功了。

更簡單的組件替換

上述方法是我們純手工地將Glide的HTTP通訊組件進行了替換,如果你不想這麼麻煩也是可以的,Glide官方給我們提供了非常簡便的HTTP組件替換方式。並且除了支持OkHttp3之外,還支持OkHttp2和Volley。

我們只需要在gradle當中添加幾行庫的配置就行了。比如使用OkHttp3來作爲HTTP通訊組件的配置如下:

dependencies {
    compile 'com.squareup.okhttp3:okhttp:3.9.0'
    compile 'com.github.bumptech.glide:okhttp3-integration:1.5.0@aar'
}

使用OkHttp2來作爲HTTP通訊組件的配置如下:

dependencies {
    compile 'com.github.bumptech.glide:okhttp-integration:1.5.0@aar'
    compile 'com.squareup.okhttp:okhttp:2.7.5'
}

使用Volley來作爲HTTP通訊組件的配置如下:

dependencies {
    compile 'com.github.bumptech.glide:volley-integration:1.5.0@aar'  
    compile 'com.mcxiaoke.volley:library:1.0.19'  
}

當然了,這些庫背後的工作原理和我們剛纔自己手動實現替換HTTP組件的原理是一模一樣的。而學會了手動替換組件的原理我們就能更加輕鬆地擴展更多豐富的功能,因此掌握這一技能還是非常重要的。

好了,那麼今天的文章就到這裏了。下篇文章中,我們將會利用本篇文章中學到的知識,對Glide進行一個高級的功能擴展,感興趣的朋友請繼續閱讀 Android圖片加載框架最全解析(七),實現帶進度的Glide圖片加載功能

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

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

        

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