Load url with etag by glide
需求场景
通过固定的 url 加载图片, 在 url 不变的情况下, 服务器可能会更新 url 对应的资源, 客户端需要能够及时检测到图片的更新并加载最新资源.
服务器能够提供的交互
在正常情况下, 加载成功后, 返回 200 成功, Response Body 返回对应的图片资源, 同时 Response Header 中带上 etag 表示该 url 对应资源的特征值. 如:
1
etag:"bd6d0ff25778318b238ac530cb147247"
后续的请求, Request Header 带上
if-none-match
给服务器检查是否更新, 如果图片资源未更新, 直接返回 304, 否则正常返回.1
2
3
4
5
6
7// Request Header
if-none-match: "bd6d0ff25778318b238ac530cb147247"
//
// Response (未改变)
Statue Code : 304
//
客户端实现
客户端需要
- 本地缓存 url 对应的 etag, 同时在后续请求的时候带上.
- 加载图片的时候需要支持给 url 设置 Header
- 在图片加载成功后, 如果是远程 url 也需要读取 Response 的 Header.
由于客户端一般都有通用的图片加载库, 所以 2,3 点需要图片库支持
本地缓存 url 对应的 etag
这个使用通用的 Key-Value 缓存都能实现, 可以用 SharedPrference, sqlite, MMKV 或 DataStore 都能实现, 我比较推荐用 MMKV 的形式, 最主要的特点是不需要切线程, 同时也是属于能够接受丢失的数据.
图片库的支持
这里以 Glide 举例, 常见的写法是
1 | Glide.with() |
- 设置 Header, 这个比较简单, 可以通过自带的 GlideUrl 设置
1
2
3
4
5val header = LazyHeaders.Builder()
.addHeader("if-none-match", etag)
.build()
val glideUrl = GlideUrl(url, header)
Glide.load(glideUrl) - Glide 取出 Http Response Header 中的 etag.
我这边想到的办法比较麻烦:- 调整 Glide 的网络库实现为 OKHttp
- 给 Glide 设置 OKHttpClient 时添加自定义的 Interceptor , 然后就可以取出 Response 中的 etag.
但是这样有个问题, 就是 etag 无法与 Glide 的 Request 进行关联, 导致同一个 url 必定会有两次网络请求.
- 第一次加载, 由于没有 etag 缓存, signature 传入的 etag 为 null. 此时 Glide 图片以 url 作为缓存 key.
- 第一次加载后, response 中取出 etag , 记录与 url 的映射关系.
- 第二次加载, signature 传入 url 对应的 etag, 由于该 etag 缓存 key 未找到对应的缓存图片, 所以又会发起一次网络请求.
所以纯粹修改 Glide 图片库似乎不太好.
先发起一次网络请求
由于图片正常返回时, Http Body 就是所需加载的图片资源 byte, 所以可以先发起一次网络请求, 拿到 etag 后, 设置 signature 并使用 Glide.load(byte[])
方法直接加载 Http 的 Body, 这样只有一次网络请求.
1 | val request = Request.Builder() |
但是这样测试之后, 发现有新的问题, 即第二次请求虽然 etag 都匹配传入了, 但是依然发起了网络请求. 排查之后发现 Glide.load(byte[])
方法使用的 ByteArrayLoader
中定义的 DataSource 为 Local, 所以使用该方法就不会写入本地缓存, 第二次请求就不会命中缓存.
解决办法也比较简单, 可以仿照 ByteArrayLoader
写一个自定义的 Loader , 然后注册到 Glide 中, 注意替换 DataSource 为 Remote.
1 | /** |
然后注册到 Glide
1 |
|
然后加载的时候调用
1 | val eTag = response.header("etag") ?: url |
这样即可以实现 etag 更新, 也可以利用到 Glide 的缓存, 对项目修改比较小.
最终流程
- 首先检查是否有 url 对应的 etag 缓存
- 如果没有缓存, 先发起一次网络请求, 成功之后利用 Glide 设置 signature 加载 http body.
- 如果有缓存
- 先通过Glide 设置 signature 加载 url
- 在 Glide 的成功回调里面带上 if-none-match 的 Header 发起网络请求, 用于检查服务器是否更新了图片.
预加载与缓存的坑
如果使用 Glide 的 preload 方法并且缓存了 preload 返回的 Drawable, 需要注意 Drawable 中的 Bitmap 对象在 preload 完成后会释放掉, 此时 Bitmap 会被 Glide 回收后复用给其他请求.
如果自己缓存了该 Bitmap, 会导致两种结果:
- Bitmap 错误展示了其他地方请求的结果, 即展示的图片与实际的 url 对不上.
- Bitmap 复用 Crash解决办法是自己缓存的场景不要用 preload, preload 的实现也是 CustomTarget, 并且在 preload 成功后 clear 了资源. 所以我们使用 into CustomTarget 即可.
1
2
3
4
5
6
7
8
9java.lang.RuntimeException
Canvas: trying to use a recycled bitmap android.graphics.Bitmap@111bacc
android.graphics.BaseCanvas.throwIfCannotDraw(BaseCanvas.java:77)
android.graphics.MiuiCanvas.throwIfCannotDraw(MiuiCanvas.java:329)
android.graphics.RecordingCanvas.throwIfCannotDraw(RecordingCanvas.java:277)
android.graphics.BaseRecordingCanvas.drawBitmap(BaseRecordingCanvas.java:88)
android.graphics.drawable.BitmapDrawable.draw(BitmapDrawable.java:548)
android.widget.ImageView.onDraw(ImageView.java:1434)
...
1 | .into(object: CustomTarget<Drawable>() { |