写点什么

Glide 源码学习七:自定义模块功能,阿里、百度等大厂技术面试题汇总

作者:嘟嘟侠客
  • 2021 年 11 月 27 日
  • 本文字数:9336 字

    阅读完需:约 31 分钟

...


}


我们来仔细看一下上面这段代码。首先这里使用了一个单例模式来获取 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 priori


《Android 学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》

【docs.qq.com/doc/DSkNLaERkbnFoS0ZF】 完整内容开源分享


ty) 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());


}

最后

如果你看到了这里,觉得文章写得不错就给个赞呗?如果你觉得那里值得改进的,请给我留言。一定会认真查询,修正不足。谢谢。



欢迎大家一起交流讨论啊~


本文已被CODING开源项目:《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》收录

用户头像

嘟嘟侠客

关注

还未添加个人签名 2021.03.19 加入

还未添加个人简介

评论

发布
暂无评论
Glide源码学习七:自定义模块功能,阿里、百度等大厂技术面试题汇总