Android:图片加载框架 Glide 高级用法
环境:Android 11
一、自定义模块
自定义模块功能可以将更改Glide配置,替换Glide组件等操作独立出来,使得我们能轻松地对Glide的各种配置进行自定义,并且又和Glide的图片加载逻辑没有任何交集,这也是一种低耦合编程方式的体现。
首先定义一个我们自己的模块类,并让它继承自AppGlideModule,如下所示:
@GlideModule
public class MyAppGlideModule extends AppGlideModule {
//更改Glide配置
@Override
public void applyOptions(@NonNull Context context, @NonNull GlideBuilder builder) {
super.applyOptions(context, builder);
}
//替换Glide组件
@Override
public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) {
super.registerComponents(context, glide, registry);
}
}
可以看到,在MyAppGlideModule类当中,我们重写了applyOptions()和registerComponents()方法,这两个方法分别就是用来更改Glide配置以及替换Glide组件的。
注意在MyAppGlideModule类在上面,我们加入了一个@GlideModule的注解,有了@GlideModule注解Glide才能够识别这个自定义模块。
这样的话,我们就将Glide自定义模块的功能完成了。后面只需要在applyOptions()和registerComponents()这两个方法中加入具体的逻辑,就能实现更改Glide配置或者替换Glide组件的功能了。
1、更改Glide配置
刚才在分析自定义模式工作原理的时候其实就已经提到了,如果想要更改Glide的默认配置,其实只需要在applyOptions()方法中提前将Glide的配置项进行初始化就可以了。如下:
- setMemoryCache():用于配置Glide的内存缓存策略,默认配置是LruResourceCache。
- setBitmapPool():用于配置Glide的Bitmap缓存池,默认配置是LruBitmapPool。
- setDiskCache():用于配置Glide的硬盘缓存策略,默认配置是InternalCacheDiskCacheFactory。
- setDiskCacheExecutor():用于配置Glide读取缓存中图片的异步执行器,默认配置是FifoPriorityThreadPoolExecutor,也就是先入先出原则。
- setResizeService():用于配置Glide读取非缓存中图片的异步执行器,默认配置也是FifoPriorityThreadPoolExecutor。
- setDefaultRequestOptions():用于配置Glide加载图片的默认请求选项,其中解码模式的配置就包含在里面,默认配置是RGB_565。
其实Glide的这些默认配置都非常科学且合理,使用的缓存算法也都是效率极高的,因此在绝大多数情况下我们并不需要去修改这些默认配置,这也是Glide用法能如此简洁的一个原因。
2、将加载的图片缓存到SD卡
Glide默认的硬盘缓存策略使用的是 InternalCacheDiskCacheFactory,这种缓存会将所有Glide加载的图片都存储到当前应用的私有目录下。这是一种非常安全的做法,但同时这种做法也造成了一些不便,因为私有目录下即使是开发者自己也是无法查看的。这种情况下,就非常适合使用自定义模块来更改Glide的默认配置。我们完全可以自己去实现DiskCache.Factory接口来自定义一个硬盘缓存策略,不过没有必要这么做,因为Glide本身就内置了一个ExternalCacheDiskCacheFactory,可以允许将加载的图片都缓存到SD卡。
使用这个ExternalPreferredCacheDiskCacheFactory来替换默认的InternalCacheDiskCacheFactory,就可以将所有Glide加载的图片都缓存到SD卡上。
使用前面自定义的模块 MyGlideModule,直接在这里编写逻辑,代码如下所示:
//更改Glide配置
@Override
public void applyOptions(@NonNull Context context, @NonNull GlideBuilder builder) {
super.applyOptions(context, builder);
builder.setDiskCache(new ExternalPreferredCacheDiskCacheFactory(context));
}
现在所有Glide加载的图片都会缓存到SD卡上了。
3、修改Glide默认的缓存大小
InternalCacheDiskCacheFactory和ExternalPreferredCacheDiskCacheFactory的默认硬盘缓存大小都是250M。也就是说,如果你的应用缓存的图片总大小超出了250M,那么Glide就会按照DiskLruCache算法的原则来清理缓存的图片。
可以对这个默认的缓存大小进行修改的,如下所示:
private static final long DISK_CACHE_SIZE = 500*1024*1024;
//更改Glide配置
@Override
public void applyOptions(@NonNull Context context, @NonNull GlideBuilder builder) {
super.applyOptions(context, builder);
builder.setDiskCache(new ExternalPreferredCacheDiskCacheFactory(context,DISK_CACHE_SIZE));
}
只需要向ExternalPreferredCacheDiskCacheFactory或者InternalCacheDiskCacheFactory再传入一个参数就可以了,现在我们就将Glide硬盘缓存的大小调整成了500M。测试加载一张网络图片,如下:
String url = "http://guolin.tech/book.png";
Glide.with(this)
.load(url)
.into(imageView);
运行一下程序,图片加载出来之后我们去找找它的缓存吧。ExternalPreferredCacheDiskCacheFactory的默认缓存路径是在Android/data/包名/cache/image_manager_disk_cache目录当中。可以看到,这里有两个文件,其中journal文件是DiskLruCache算法的日志文件,这个文件必不可少,且只会有一个。而另外一个文件就是那张缓存的图片了,它的文件名虽然看上去很奇怪,但是我们只需要把这个文件的后缀改成.png,然后用图片浏览器打开。
4、修改Glide加载图片的格式为ARGB_8888
我们都知道Glide和Picasso的用法是非常相似的,但是有一点差别却很大。Glide加载图片的默认格式是RGB_565,而Picasso加载图片的默认格式是ARGB_8888。ARGB_8888格式的图片效果会更加细腻,但是内存开销会比较大。而RGB_565格式的图片则更加节省内存,但是图片效果上会差一些。
Glide和Picasso各自采取的默认图片格式谈不上熟优熟劣,只能说各自的取舍不一样。但是如果你希望Glide也能使用ARGB_8888的图片格式,这当然也是可以的。我们只需要在MyGlideModule中更改一下默认配置即可,如下所示:
private static final long DISK_CACHE_SIZE = 500*1024*1024;
//更改Glide配置
@Override
public void applyOptions(@NonNull Context context, @NonNull GlideBuilder builder) {
super.applyOptions(context, builder);
builder.setDiskCache(new ExternalPreferredCacheDiskCacheFactory(context,DISK_CACHE_SIZE));
builder.setDefaultRequestOptions(
new RequestOptions()
.format(DecodeFormat.PREFER_ARGB_8888)
);
}
通过这样配置之后,使用Glide加载的所有图片都将会使用ARGB_8888的格式,虽然图片质量变好了,但同时内存开销也会明显增大。
5、替换Glide组件(替换HTTP通讯组件)
替换Glide组件功能需要在自定义模块的registerComponents()方法中加入具体的替换逻辑。相比于更改Glide配置,替换Glide组件这个功能的难度就明显大了不少。Glide中的组件非常繁多,也非常复杂,但其实大多数情况下并不需要我们去做什么替换。不过,有一个组件却有着比较大的替换需求,那就是Glide的HTTP通讯组件。
默认情况下,Glide使用的是基于原生HttpURLConnection进行订制的HTTP通讯组件,但是现在大多数的Android开发者都更喜欢使用OkHttp,因此将Glide中的HTTP通讯组件修改成OkHttp的这个需求比较常见,那么我们就来实现下这个功能,基本就是将HTTP的通讯部分代码用到的通讯组件HttpURLConnection替换成我们需要的OkHttp。
首先将OkHttp的库引入到当前项目中,如下所示:
dependencies {
implementation 'com.squareup.okhttp3:okhttp:3.11.0'
}
新建一个OkHttpFetcher类,并且实现DataFetcher接口,代码如下所示:
public class OkHttpFetcher implements DataFetcher<InputStream>, okhttp3.Callback {
private static final String TAG = "OkHttpFetcher";
private final Call.Factory client;
private final GlideUrl url;
private InputStream stream;
private ResponseBody responseBody;
private DataFetcher.DataCallback<? super InputStream> callback;
private volatile Call call;
@SuppressWarnings("WeakerAccess")
public OkHttpFetcher(Call.Factory client, GlideUrl url) {
this.client = client;
this.url = url;
}
@Override
public void loadData(@NonNull Priority priority,
@NonNull final DataCallback<? super InputStream> callback) {
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());
}
Request request = requestBuilder.build();
this.callback = callback;
call = client.newCall(request);
call.enqueue(this);
}
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "OkHttp failed to obtain result", e);
}
callback.onLoadFailed(e);
}
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) {
responseBody = response.body();
if (response.isSuccessful()) {
long contentLength = Preconditions.checkNotNull(responseBody).contentLength();
stream = ContentLengthInputStream.obtain(responseBody.byteStream(), contentLength);
callback.onDataReady(stream);
} else {
callback.onLoadFailed(new HttpException(response.message(), response.code()));
}
}
@Override
public void cleanup() {
try {
if (stream != null) {
stream.close();
}
} catch (IOException e) {
// Ignored
}
if (responseBody != null) {
responseBody.close();
}
callback = null;
}
@Override
public void cancel() {
Call local = call;
if (local != null) {
local.cancel();
}
}
@NonNull
@Override
public Class<InputStream> getDataClass() {
return InputStream.class;
}
@NonNull
@Override
public DataSource getDataSource() {
return DataSource.REMOTE;
}
}
然后新建一个OkHttpGlideUrlLoader类,并且实现ModelLoader
public class OkHttpGlideUrlLoader implements ModelLoader<GlideUrl, InputStream> {
private final Call.Factory client;
@SuppressWarnings("WeakerAccess")
public OkHttpGlideUrlLoader(@NonNull Call.Factory client) {
this.client = client;
}
@Override
public boolean handles(@NonNull GlideUrl url) {
return true;
}
@Override
public LoadData<InputStream> buildLoadData(@NonNull GlideUrl model, int width, int height,
@NonNull Options options) {
return new LoadData<>(model, new OkHttpFetcher(client, model));
}
@SuppressWarnings("WeakerAccess")
public static class Factory implements ModelLoaderFactory<GlideUrl, InputStream> {
private static volatile Call.Factory internalClient;
private final Call.Factory client;
private static Call.Factory getInternalClient() {
if (internalClient == null) {
synchronized (OkHttpGlideUrlLoader.Factory.class) {
if (internalClient == null) {
internalClient = new OkHttpClient();
}
}
}
return internalClient;
}
public Factory() {
this(getInternalClient());
}
public Factory(@NonNull Call.Factory client) {
this.client = client;
}
@NonNull
@Override
public ModelLoader<GlideUrl, InputStream> build(MultiModelLoaderFactory multiFactory) {
return new OkHttpGlideUrlLoader(client);
}
@Override
public void teardown() {}
}
}
接下来,新建一个MyGlideModule类并实现GlideModule接口,然后在registerComponents()方法中将我们刚刚创建的OkHttpGlideUrlLoader和OkHttpFetcher注册到Glide当中,将原来的HTTP通讯组件给替换掉,如下所示:
@GlideModule
public class MyAppGlideModule extends AppGlideModule {
private static final long DISK_CACHE_SIZE = 500*1024*1024;
//更改Glide配置
@Override
public void applyOptions(@NonNull Context context, @NonNull GlideBuilder builder) {
super.applyOptions(context, builder);
builder.setDiskCache(new ExternalPreferredCacheDiskCacheFactory(context,DISK_CACHE_SIZE));
builder.setDefaultRequestOptions(
new RequestOptions()
.format(DecodeFormat.PREFER_ARGB_8888)
);
}
//替换Glide组件
@Override
public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) {
super.registerComponents(context, glide, registry);
registry.replace(GlideUrl.class, InputStream.class, new OkHttpGlideUrlLoader.Factory());
}
}
可以看到,这里是调用了registry的replace()方法来替换组件的。
6、更简单的组件替换
上述方法是我们纯手工地将Glide的HTTP通讯组件进行了替换,如果你不想这么麻烦也是可以的,Glide官方给我们提供了非常简便的HTTP组件替换方式。并且除了支持OkHttp3之外,还支持Volley。我们只需要在gradle当中添加几行库的配置就行了。比如使用OkHttp3来作为HTTP通讯组件的配置如下:
implementation "com.github.bumptech.glide:okhttp3-integration:4.7.1"
implementation 'com.squareup.okhttp3:okhttp:3.11.0'
使用Volley来作为HTTP通讯组件的配置如下:
implementation "com.github.bumptech.glide:volley-integration:4.7.1"
implementation 'com.mcxiaoke.volley:library:1.0.19'
当然了,这些库背后的工作原理和我们刚才自己手动实现替换HTTP组件的原理是一模一样的。而学会了手动替换组件的原理我们就能更加轻松地扩展更多丰富的功能,因此掌握这一技能还是非常重要的。
7、实现下载进度监听
使用Glide来加载一张网络上的图片是非常简单的,如果加载比较大的图片时,用户耐心等了很久结果图片还没显示出来,这个时候有下载进度功能是十分有必要的。
将HTTP通讯组件替换成OkHttp之后,这就要依靠OkHttp强大的拦截器机制了。我们只要向OkHttp中添加一个自定义的拦截器,就可以在拦截器中捕获到整个HTTP的通讯过程,然后加入一些自己的逻辑来计算下载进度,这样就可以实现下载进度监听的功能了。
首先创建一个没有任何逻辑的空拦截器,新建ProgressInterceptor类并实现Interceptor接口,代码如下所示:
public class ProgressInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Response response = chain.proceed(request);
return response;
}
}
这个拦截器中我们可以说是什么都没有做。就是拦截到了OkHttp的请求,然后调用proceed()方法去处理这个请求,最终将服务器响应的Response返回。
接下来我们需要启用这个拦截器,修改MyGlideModule中的代码,如下所示:
@GlideModule
public class MyAppGlideModule extends AppGlideModule {
private static final long DISK_CACHE_SIZE = 500*1024*1024;
//更改Glide配置
@Override
public void applyOptions(@NonNull Context context, @NonNull GlideBuilder builder) {
super.applyOptions(context, builder);
}
//替换Glide组件
@Override
public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) {
super.registerComponents(context, glide, registry);
OkHttpClient.Builder builder = new OkHttpClient.Builder();
builder.addInterceptor(new ProgressInterceptor());
OkHttpClient okHttpClient = builder.build();
registry.replace(GlideUrl.class, InputStream.class, new OkHttpGlideUrlLoader.Factory(okHttpClient));
}
}
这里我们创建了一个OkHttpClient.Builder,然后调用addInterceptor()方法将刚才创建的ProgressInterceptor添加进去,最后将构建出来的新OkHttpClient对象传入到OkHttpGlideUrlLoader.Factory中。
现在自定义的拦截器已经启用了,接下来就可以开始去实现下载进度监听逻辑了。首先新建一个ProgressListener接口,用于作为进度监听回调的工具,如下所示:
public interface ProgressListener {
void onProgress(int progress);
}
然后我们在上面空的ProgressInterceptor拦截器类中加入注册下载监听和取消注册下载监听的方法。修改ProgressInterceptor中的代码,如下所示:
public class ProgressInterceptor implements Interceptor {
static final Map<String, ProgressListener> LISTENER_MAP = new HashMap<>();
public static void addListener(String url, ProgressListener listener) {
LISTENER_MAP.put(url, listener);
}
public static void removeListener(String url) {
LISTENER_MAP.remove(url);
}
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Response response = chain.proceed(request);
return response;
}
}
可以看到,这里使用了一个Map来保存注册的监听器,Map的键是一个URL地址。之所以要这么做,是因为你可能会使用Glide同时加载很多张图片,而这种情况下,必须要能区分出来每个下载进度的回调到底是对应哪个图片URL地址的。
接下来就要到最复杂的部分了,也就是下载进度的具体计算。我们需要新建一个ProgressResponseBody类,并让它继承自OkHttp的ResponseBody,然后在这个类当中去编写具体的监听下载进度的逻辑,代码如下所示:
public class ProgressResponseBody extends ResponseBody {
private static final String TAG = "ProgressResponseBody";
private BufferedSource bufferedSource;
private ResponseBody responseBody;
private ProgressListener listener;
public ProgressResponseBody(String url, ResponseBody responseBody) {
this.responseBody = responseBody;
listener = ProgressInterceptor.LISTENER_MAP.get(url);
}
@Override
public MediaType contentType() {
return responseBody.contentType();
}
@Override
public long contentLength() {
return responseBody.contentLength();
}
@Override
public BufferedSource source() {
if (bufferedSource == null) {
bufferedSource = Okio.buffer(new ProgressSource(responseBody.source()));
}
return bufferedSource;
}
private class ProgressSource extends ForwardingSource {
long totalBytesRead = 0;
int currentProgress;
ProgressSource(Source source) {
super(source);
}
@Override
public long read(Buffer sink, long byteCount) throws IOException {
long bytesRead = super.read(sink, byteCount);
long fullLength = responseBody.contentLength();
if (bytesRead == -1) {
totalBytesRead = fullLength;
} else {
totalBytesRead += bytesRead;
}
int progress = (int) (100f * totalBytesRead / fullLength);
Log.d(TAG, "download progress is " + progress);
if (listener != null && progress != currentProgress) {
listener.onProgress(progress);
}
if (listener != null && totalBytesRead == fullLength) {
listener = null;
}
currentProgress = progress;
return bytesRead;
}
}
}
首先,我们定义了一个ProgressResponseBody的构造方法,该构造方法中要求传入一个url参数和一个ResponseBody参数。那么很显然,url参数就是图片的url地址了,而ResponseBody参数则是OkHttp拦截到的原始的ResponseBody对象。然后在构造方法中,我们调用了ProgressInterceptor中的LISTENER_MAP来去获取该url对应的监听器回调对象,有了这个对象,待会就可以回调计算出来的下载进度了。
由于继承了ResponseBody类之后一定要重写contentType()、contentLength()和source()这三个方法,我们在contentType()和contentLength()方法中直接就调用传入的原始ResponseBody的contentType()和contentLength()方法即可,这相当于一种委托模式。但是在source()方法中,我们就必须加入点自己的逻辑了,因为这里要涉及到具体的下载进度计算。
那么我们具体看一下source()方法,这里先是调用了原始ResponseBody的source()方法来去获取Source对象,接下来将这个Source对象封装到了一个ProgressSource对象当中,最终再用Okio的buffer()方法封装成BufferedSource对象返回。
那么这个ProgressSource是什么呢?它是一个我们自定义的继承自ForwardingSource的实现类。ForwardingSource也是一个使用委托模式的工具,它不处理任何具体的逻辑,只是负责将传入的原始Source对象进行中转。但是,我们使用ProgressSource继承自ForwardingSource,那么就可以在中转的过程中加入自己的逻辑了。
可以看到,在ProgressSource中我们重写了read()方法,然后在read()方法中获取该次读取到的字节数以及下载文件的总字节数,并进行一些简单的数学计算就能算出当前的下载进度了。这里我先使用Log工具将算出的结果打印了一下,再通过前面获取到的回调监听器对象将结果进行回调。修改ProgressInterceptor中的代码,如下所示:
public class ProgressInterceptor implements Interceptor {
...
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Response response = chain.proceed(request);
String url = request.url().toString();
ResponseBody body = response.body();
Response newResponse = response.newBuilder().body(new ProgressResponseBody(url, body)).build();
return newResponse;
}
}
这里也都是一些OkHttp的简单用法。我们通过Response的newBuilder()方法来创建一个新的Response对象,并把它的body替换成刚才实现的ProgressResponseBody,最终将新的Response对象进行返回,这样计算下载进度的逻辑就能生效了。
四、使用Generated API
Generated API是Glide 4中全新引入的一个功能,它的工作原理是使用注解处理器 (Annotation Processor) 来生成出一个API,在自定义模块中可使用该流式API一次性调用到RequestBuilder,RequestOptions和集成库中所有的选项。简单点说,就是Glide 4仍然给我们提供了一套和Glide 3一模一样的流式API接口。
对于熟悉Glide 3的朋友来说那是再简单不过了,基本上就是和Glide 3一模一样的用法,只不过需要把Glide关键字替换成GlideApp关键字,如下所示:
GlideApp.with(this)
.load(url)
.placeholder(R.drawable.loading)
.error(R.drawable.error)
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.override(Target.SIZE_ORIGINAL)
.circleCrop()
.into(imageView);
不过,有可能你的IDE中会提示找不到GlideApp这个类。这个类是通过编译时注解自动生成的,首先确保你的代码中有一个自定义的模块,并且给它加上了@GlideModule注解,也就是我们在上面所讲的内容。然后在Android Studio中点击菜单栏Build -> Rebuild Project,GlideApp这个类就会自动生成了。
对现有的API进行扩展,定制出任何属于你自己的API
下面具体举个例子,比如说我们要求项目中所有图片的缓存策略全部都要缓存原始图片,那么每次在使用Glide加载图片的时候,都去指定diskCacheStrategy(DiskCacheStrategy.DATA)这么长长的一串代码,确实是让人比较心烦。这种情况我们就可以去定制一个自己的API了。
定制自己的API需要借助@GlideExtension和@GlideOption这两个注解。创建一个我们自定义的扩展类,代码如下所示:
@GlideExtension
public class MyGlideExtension {
private MyGlideExtension() {
}
@GlideOption
public static void cacheSource(RequestOptions options) {
options.diskCacheStrategy(DiskCacheStrategy.DATA);
}
}
这里我们定义了一个MyGlideExtension类,并且给加上了一个@GlideExtension注解,然后要将这个类的构造函数声明成private,这都是必须要求的写法。
接下来就可以开始自定义API了,这里我们定义了一个cacheSource()方法,表示只缓存原始图片,并给这个方法加上了@GlideOption注解。注意自定义API的方法都必须是静态方法,而且第一个参数必须是RequestOptions,后面你可以加入任意多个你想自定义的参数。
在cacheSource()方法中,我们仍然还是调用的diskCacheStrategy(DiskCacheStrategy.DATA)方法,所以说cacheSource()就是一层简化API的封装而已。
然后在Android Studio中点击菜单栏Build -> Rebuild Project,神奇的事情就会发生了,你会发现你已经可以使用这样的语句来加载图片了:
GlideApp.with(this)
.load(url)
.cacheSource()
.into(imageView);
有了这个强大的功能之后,我们使用Glide就能变得更加灵活了。