簡(jiǎn)述
基于項(xiàng)目需求郁妈,用戶更換新頭像后,iOS绍申、Android噩咪、web 端三端需要能更新到最新的頭像。由于各種原因极阅,用戶頭像的URL始終是不變的胃碾。而一般App端的圖片加載框架都會(huì)把 URL 作為 key 對(duì)圖片進(jìn)行多級(jí)緩存,用戶更改了新頭像涂屁,此時(shí) URL 不變就會(huì)導(dǎo)致圖片框架始終都只會(huì)加載本地的緩存书在。
梳理以上需求,有以下問題需要解決:
1拆又、在 URL 不變的情況下儒旬,如何得知服務(wù)端的圖片是否已經(jīng)更改
2、在知道服務(wù)端圖片已經(jīng)更改的情況下帖族,如何讓圖片請(qǐng)求框架去請(qǐng)求服務(wù)端的最新圖片栈源,而不是加載本地緩存
- 針對(duì)第一個(gè)問題,Http協(xié)議提供了 ETag 或者 Last-Modified 來(lái)判斷當(dāng)前請(qǐng)求資源是否改變竖般,具體可以查看鏈接了解甚垦,通俗解釋為第一次請(qǐng)求資源 A,會(huì)返回一個(gè)請(qǐng)求頭 H,第二次請(qǐng)求 A 時(shí)帶上該請(qǐng)求頭 H艰亮,返回的響應(yīng)碼為 304 代表資源沒有變(不會(huì)返回資源 A )闭翩,為 200 代表資源有更新(會(huì)返回資源 A )。
注意:
ETag 對(duì)比 Last-Modified 的優(yōu)勢(shì)
1迄埃、一些文件也許會(huì)周期性的更改疗韵,但是他的內(nèi)容并不改變( 僅僅改變的修改時(shí)間),這個(gè)時(shí)候我們并不希望客戶端認(rèn)為這個(gè)文件被修改了侄非,而重新GET;
2蕉汪、某些文件修改非常頻繁,比如在秒以下的時(shí)間內(nèi)進(jìn)行修改逞怨,(比方說 1s 內(nèi)修改了 N 次)者疤,If-Modified-Since 能檢查到的粒度是 s 級(jí)的,這種修改無(wú)法判斷(或者說 UNIX 記錄 MTIME 只能精確到秒)叠赦;
3驹马、某些服務(wù)器不能精確的得到文件的最后修改時(shí)間。
- 針對(duì)第二個(gè)問題眯搭,項(xiàng)目中使用 Glide 作為圖片請(qǐng)求窥翩,設(shè)置 memery 和 disk 緩存都忽略是可以讓 glide 直接去請(qǐng)求服務(wù)端圖片的,但是沒有了緩存鳞仙,體驗(yàn)會(huì)比較差寇蚊,這里使用的是 Glide 的 signature,通過 ObjectKey 來(lái)作為圖片的標(biāo)識(shí)棍好,ObjectKey 大家可以看一下源碼仗岸,通過所傳參數(shù)的 hashCode 來(lái)區(qū)分,結(jié)合第一個(gè)問題的回答借笙,我們可以把 ETag 或者 Last-Modified 作為 ObjectKey 的參數(shù)
/**
* 使用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不同則會(huì)加載網(wǎng)絡(luò)端的圖片資源
.signature(ObjectKey(key)).placeholder(imageView.drawable))
.load(url)
.into(imageView)
}
- 綜合上面的业稼,一個(gè)簡(jiǎn)單的方案就有了盗痒,接下來(lái)就是代碼實(shí)現(xiàn),首先是獲取資源的 ETag 或者 Last-Modified低散,這兩個(gè)都是存在于響應(yīng)頭里面俯邓,為了性能和流量( HEAD 請(qǐng)求只會(huì)返回響應(yīng)頭,不會(huì)響應(yīng)體)熔号,我們使用了 HEAD 請(qǐng)求稽鞭,代碼是用 Retrofit 實(shí)現(xiàn)
/**
* 基礎(chǔ)api方法,包括POST引镊、GET朦蕴、UPLOAD篮条、DOWNLOAD等
* @version 2.2.0
* @date 2017/5/16 16:49
*/
interface BaseApiService {
/** HEAD請(qǐng)求,只會(huì)返回響應(yīng)頭吩抓,沒有返回響應(yīng)體涉茧,節(jié)省流量 */
/** HEAD請(qǐng)求琴拧,帶上Last-Modified或Etag的請(qǐng)求頭 */
@HEAD
fun getImg(@Url url: String, @Header(IF_NONE_MATCH) lastModify: String): Observable<Response<Void>>
}
/**
* 圖片相關(guān)工具類
*
* @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) {
//當(dāng)url為空時(shí)降瞳,不請(qǐng)求網(wǎng)絡(luò),加載默認(rèn)圖片
if (url.isEmpty()) {
Glide.with(context).load(R.drawable.default_head).into(this)
} else {
//獲取url對(duì)應(yīng)存儲(chǔ)在sp中的Last-Modified或Etag
val key = SharedPreferencesUtils[context, SP_FILE_COMMON, url, ""]
if (key.isNotEmpty()) {
//key非空嘱支,即本地存在緩存蚓胸,先加載本地緩存
glideLoadImg(context, key, url, this)
} else {
//key為空,不存在緩存除师,加載默認(rèn)圖片
Glide.with(context).load(R.drawable.default_head).into(this)
}
RetrofitClient.getInstance(context).getApiService().getImg(url, key)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
//獲取響應(yīng)頭中的Last-Modified或Etag
val head = it.headers().get(ETAG)
if (it.code() == 304) {
//304的時(shí)候返回的lastModified或者Etag為null沛膳,此時(shí)使用上次存儲(chǔ)的key來(lái)加載圖片
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()})
}
}
總結(jié)上面的實(shí)現(xiàn),大概就是 Retrofit 攜帶 ETag 或者 Last-Modified 作為請(qǐng)求頭發(fā)起 HEAD 請(qǐng)求汛聚,根據(jù)返回的請(qǐng)求碼( 304 或者 200 )來(lái)判斷服務(wù)端的圖片是否更改锹安,如果有更改,再將服務(wù)端返回的 ETag 或者 Last-Modified 作為 Signature 讓 Glide 去加載新圖片倚舀。上面的代碼更細(xì)致一點(diǎn)叹哭,還加了本地是否已經(jīng)存在緩存圖片的校驗(yàn),體驗(yàn)稍微好一些痕貌。
上面的實(shí)現(xiàn)可優(yōu)化的地方還有很多风罩,正常的圖片加載,本地緩存存在的情況下根本不需要進(jìn)行網(wǎng)絡(luò)請(qǐng)求舵稠,上面的實(shí)現(xiàn)會(huì)先進(jìn)行一次 HEAD 請(qǐng)求超升,是為了判斷服務(wù)端圖片是否有更改,下圖是 Android studio 監(jiān)測(cè)的流量哺徊,HEAD 請(qǐng)求是黃色室琢,加載圖片的是藍(lán)色,雖然流量消耗很少落追,但是增加一次網(wǎng)絡(luò)請(qǐng)求盈滴,圖片多的情況下還是會(huì)有影響的。如果大家仔細(xì)了解了 ETag 或者 Last-Modified轿钠,會(huì)發(fā)現(xiàn)巢钓,如果資源有更新,返回 200 時(shí)谣膳,同時(shí)資源也會(huì)返回回來(lái)竿报,這里返回的應(yīng)該是圖片的二進(jìn)制,此時(shí)已經(jīng)拿到圖片的二進(jìn)制了继谚,本來(lái)可以不用 Glide 再次發(fā)起請(qǐng)求烈菌,只需要讓 Glide 加載二進(jìn)制流即可,但是這里存在一個(gè)問題,直接加載二進(jìn)制流芽世,下次需要加載緩存的時(shí)候就沒辦法加載了挚赊,因?yàn)橐话愣际怯?url 作為加載圖片的路徑,這里直接給流济瓢,那么 url 跟圖片之間就沒有關(guān)聯(lián)了荠割,如果有更好的方案,歡迎拍磚旺矾。
本地搞了個(gè) Tomcat蔑鹦,把圖片放在 webapps\ROOT 路徑下,直接就可以測(cè)試訪問箕宙。