簡述
基于項目需求,用戶更換新頭像后,iOS、Android、web 端三端需要能更新到最新的頭像。由于各種原因,用戶頭像的URL始終是不變的。而一般App端的圖片加載框架都會把 URL 作為 key 對圖片進行多級緩存,用戶更改了新頭像,此時 URL 不變就會導致圖片框架始終都只會加載本地的緩存。
梳理以上需求,有以下問題需要解決:
1、在 URL 不變的情況下,如何得知服務端的圖片是否已經更改
2、在知道服務端圖片已經更改的情況下,如何讓圖片請求框架去請求服務端的最新圖片,而不是加載本地緩存
- 針對第一個問題,Http協議提供了 ETag 或者 Last-Modified 來判斷當前請求資源是否改變,具體可以查看鏈接了解,通俗解釋為第一次請求資源 A,會返回一個請求頭 H,第二次請求 A 時帶上該請求頭 H,返回的響應碼為 304 代表資源沒有變(不會返回資源 A ),為 200 代表資源有更新(會返回資源 A )。
注意:
ETag 對比 Last-Modified 的優勢
1、一些文件也許會周期性的更改,但是他的內容并不改變( 僅僅改變的修改時間),這個時候我們并不希望客戶端認為這個文件被修改了,而重新GET;
2、某些文件修改非常頻繁,比如在秒以下的時間內進行修改,(比方說 1s 內修改了 N 次),If-Modified-Since 能檢查到的粒度是 s 級的,這種修改無法判斷(或者說 UNIX 記錄 MTIME 只能精確到秒);
3、某些服務器不能精確的得到文件的最后修改時間。
- 針對第二個問題,項目中使用 Glide 作為圖片請求,設置 memery 和 disk 緩存都忽略是可以讓 glide 直接去請求服務端圖片的,但是沒有了緩存,體驗會比較差,這里使用的是 Glide 的 signature,通過 ObjectKey 來作為圖片的標識,ObjectKey 大家可以看一下源碼,通過所傳參數的 hashCode 來區分,結合第一個問題的回答,我們可以把 ETag 或者 Last-Modified 作為 ObjectKey 的參數
/**
* 使用Glide加載圖片
* @param context 上下文
* @param key Last-Modified或Etag
* @param url 圖片url
* @param imageView 圖片控件
*/
private fun glideLoadImg(context: Context, key: String, url: String, imageView: ImageView) {
Glide.with(context)
.setDefaultRequestOptions(RequestOptions.circleCropTransform()
//圖片簽名信息,相同url下如果需要刷新圖片,signature不同則會加載網絡端的圖片資源
.signature(ObjectKey(key)).placeholder(imageView.drawable))
.load(url)
.into(imageView)
}
- 綜合上面的,一個簡單的方案就有了,接下來就是代碼實現,首先是獲取資源的 ETag 或者 Last-Modified,這兩個都是存在于響應頭里面,為了性能和流量( HEAD 請求只會返回響應頭,不會響應體),我們使用了 HEAD 請求,代碼是用 Retrofit 實現
/**
* 基礎api方法,包括POST、GET、UPLOAD、DOWNLOAD等
* @version 2.2.0
* @date 2017/5/16 16:49
*/
interface BaseApiService {
/** HEAD請求,只會返回響應頭,沒有返回響應體,節省流量 */
/** HEAD請求,帶上Last-Modified或Etag的請求頭 */
@HEAD
fun getImg(@Url url: String, @Header(IF_NONE_MATCH) lastModify: String): Observable<Response<Void>>
}
/**
* 圖片相關工具類
*
* @date 2018/2/27 18:27
* @version v4.0.0
*/
const val ETAG = "ETag"
const val IF_NONE_MATCH = "If-None-Match"
const val LAST_MODIFIED = "Last-Modified"
const val IF_MODIFIED_SINCE = "If-Modified-Since"
/**
* 加載頭像
* @param url 圖片url
*/
fun ImageView.loaderHeadImgWithHead(url: String) {
//當url為空時,不請求網絡,加載默認圖片
if (url.isEmpty()) {
Glide.with(context).load(R.drawable.default_head).into(this)
} else {
//獲取url對應存儲在sp中的Last-Modified或Etag
val key = SharedPreferencesUtils[context, SP_FILE_COMMON, url, ""]
if (key.isNotEmpty()) {
//key非空,即本地存在緩存,先加載本地緩存
glideLoadImg(context, key, url, this)
} else {
//key為空,不存在緩存,加載默認圖片
Glide.with(context).load(R.drawable.default_head).into(this)
}
RetrofitClient.getInstance(context).getApiService().getImg(url, key)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
//獲取響應頭中的Last-Modified或Etag
val head = it.headers().get(ETAG)
if (it.code() == 304) {
//304的時候返回的lastModified或者Etag為null,此時使用上次存儲的key來加載圖片
glideLoadImg(context, key, url, this)
} else if (it.code() == 200) {
//保存最新的lastModified或者Etag
SharedPreferencesUtils.save(context, SP_FILE_COMMON, url, head!!)
glideLoadImg(context, head, url, this)
}
},{it.printStackTrace()})
}
}
總結上面的實現,大概就是 Retrofit 攜帶 ETag 或者 Last-Modified 作為請求頭發起 HEAD 請求,根據返回的請求碼( 304 或者 200 )來判斷服務端的圖片是否更改,如果有更改,再將服務端返回的 ETag 或者 Last-Modified 作為 Signature 讓 Glide 去加載新圖片。上面的代碼更細致一點,還加了本地是否已經存在緩存圖片的校驗,體驗稍微好一些。
上面的實現可優化的地方還有很多,正常的圖片加載,本地緩存存在的情況下根本不需要進行網絡請求,上面的實現會先進行一次 HEAD 請求,是為了判斷服務端圖片是否有更改,下圖是 Android studio 監測的流量,HEAD 請求是黃色,加載圖片的是藍色,雖然流量消耗很少,但是增加一次網絡請求,圖片多的情況下還是會有影響的。如果大家仔細了解了 ETag 或者 Last-Modified,會發現,如果資源有更新,返回 200 時,同時資源也會返回回來,這里返回的應該是圖片的二進制,此時已經拿到圖片的二進制了,本來可以不用 Glide 再次發起請求,只需要讓 Glide 加載二進制流即可,但是這里存在一個問題,直接加載二進制流,下次需要加載緩存的時候就沒辦法加載了,因為一般都是用 url 作為加載圖片的路徑,這里直接給流,那么 url 跟圖片之間就沒有關聯了,如果有更好的方案,歡迎拍磚。
本地搞了個 Tomcat,把圖片放在 webapps\ROOT 路徑下,直接就可以測試訪問。