APP圖片快取與Glide之signature的分析
1.圖片快取遇到的問題
在快取網路圖片的過程中,有一種情況是圖片的地址不變,但圖片發生了變化,如果只按照圖片的地址進行快取,在載入快取中的圖片時就會發生圖片一直顯示為舊圖的現象。 在App中修改使用者頭像的功能中,如果伺服器儲存頭像的地址保持不變,載入快取就會出現上述的情況。馬上想到,當修改頭像之後,馬上將本地原頭像的快取清除,並載入新頭像,此方法只是暫時解決了APP端頭像的顯示效果,如果在其他終端進行了頭像修改,手機上並不能同步顯示最新圖片。 那麼該如何獲取最新的網路圖片呢?顯然不使用快取是肯定可以顯示最新的圖片,但要使用快取圖片功能,又希望可以獲取最新的圖片,我們需要記錄圖片是否發生了變化,根據變化與否,選擇是否更新快取中的內容。 有人說下載圖片之後,判斷其SHA值是否相同,即可得知圖片是否相同。然而,每次都要下載圖片,再判斷SHA值,還用快取做什麼,已經完全背道而馳。 好的做法是在伺服器上加上圖片是否改變的標識,在APP端儲存該值,在載入快取內容之前判斷是否有改變,需要更新快取內容。該標識可以使用時間戳,來記錄圖片更新時間,或使用累加數來記錄標識。 當然,以上說的是處理自己的伺服器上,資料可以增加欄位的情況。如果只是單純的載入網路上的圖片,可以在圖片下載之後,在APP中做標識,一段時間之內不更新,在一天或固定時間後檢測標識並更新網路圖片。比如一天更新一次,則可將日期作為標識。2.Glide簡析
也可以通過.signature(Key signature)方法傳入標識,來實現預期功能。傳入引數為實現Key介面的類物件,Glide中有三個類StringSignature,MediaStoreSignature,EmptySignature. 構造方法StringSignature(String signature)中傳入String,可以簡單傳入剛才說的時間戳進行標識。 構造方法MediaStoreSignature(String mimeType, long dateModified, intorientation)可以傳入圖片的mimeType,修改時間,圖片或視訊的方向 當然也可以自定義類實現Key介面並複寫其equals方法判斷標識相同,及updateDiskCacheKey方法去更新本地快取檔案。 現在說說Glide載入圖片的過程。 在into()方法(GenericRequestBuilder類中的方法)中建立了Request:private final boolean cacheSource; private final boolean cacheResult; //四個列舉值 /** Caches with both {@link #SOURCE} and {@link #RESULT}. */ ALL(true, true), //快取原檔案和處理後(如尺寸變化、型別轉換) 的資料 /** Saves no data to cache. */ NONE(false, false), //不快取 /** Saves just the original data to cache. */ SOURCE(true, false), //只快取原檔案 /** Saves the media item after all transformations to cache. */ RESULT(false, true); //只快取處理後的資料
而在begin()方法中會呼叫onSizeReady(int width,int height)方法 此方法中呼叫engine.load方法開始載入圖片 Engine類中load方法部分如下:Request request = buildRequest(target);//建立GenericRequest例項 requestTracker.runRequest(request);//呼叫GenericRequest的begin方法
final String id = fetcher.getId();//當載入url圖片時,id為圖片地址
EngineKey key = keyFactory.buildKey(id, signature, width, height, loadProvider.getCacheDecoder(),
loadProvider.getSourceDecoder(), transformation, loadProvider.getEncoder(),
transcoder, loadProvider.getSourceEncoder());//根據id,signature以及其他的資訊生成key,用於快取檔案的key
EngineResource<?> cached = loadFromCache(key, isMemoryCacheable);
EngineJob engineJob = engineJobFactory.build(key, isMemoryCacheable);
DecodeJob<T, Z, R> decodeJob = new DecodeJob<T, Z, R>(key, width, height, fetcher, loadProvider, transformation,
transcoder, diskCacheProvider, diskCacheStrategy, priority);
EngineRunnable runnable = new EngineRunnable(engineJob, decodeJob, priority);
jobs.put(key, engineJob);
engineJob.addCallback(cb);
engineJob.start(runnable);
在runnable的run方法中呼叫decode()方法
private Resource<?> decode() throws Exception {
if (isDecodingFromCache()) {
return decodeFromCache();
} else {
return decodeFromSource();
}
}
private boolean isDecodingFromCache() {
return stage == Stage.CACHE;
}
而EngineRunnable的初始化中
this.stage = Stage.CACHE;
故首先執行decodeFromCache();首次載入返回空,呼叫:
private void onLoadFailed(Exception e) {
if (isDecodingFromCache()) {
stage = Stage.SOURCE;//改變值
manager.submitForSource(this);//重新調起run()方法,之後進入decodeFromSource()方法
} else {
manager.onException(e);
}
}
兩個方法長這樣:
private Resource<?> decodeFromCache() throws Exception {
Resource<?> result = null;
try {
result = decodeJob.decodeResultFromCache();
} catch (Exception e) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Exception decoding result from cache: " + e);
}
}
if (result == null) {
result = decodeJob.decodeSourceFromCache();
}
return result;
}
public Resource<Z> decodeResultFromCache() throws Exception {
if (!diskCacheStrategy.cacheResult()) {
//cacheResult ()判斷列舉類DiskCacheStrategy的型別,由初始化時的型別決定布林值,詳見文章開頭處
return null;//如果沒有快取資料,返回null
}
long startTime = LogTime.getLogTime();
Resource<T> transformed = loadFromCache(resultKey);//根據resultKey獲取transform後的資料
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Decoded transformed from cache", startTime);
}
startTime = LogTime.getLogTime();
Resource<Z> result = transcode(transformed);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Transcoded transformed from cache", startTime);
}
return result;
}
loadFromCache方法載入快取檔案:
private Resource<T> loadFromCache(Key key) throws IOException {
File cacheFile = diskCacheProvider.getDiskCache().get(key);//根據key獲取快取檔案
if (cacheFile == null) {
return null;
}
Resource<T> result = null;
try {
result = loadProvider.getCacheDecoder().decode(cacheFile, width, height);//將檔案解碼為Resource
} finally {
if (result == null) {
diskCacheProvider.getDiskCache().delete(key);//若Resource為空則key無效,刪除
}
}
return result;
}
public Resource<Z> decodeSourceFromCache() throws Exception {
if (!diskCacheStrategy.cacheSource()) {
return null;
}
long startTime = LogTime.getLogTime();
Resource<T> decoded = loadFromCache(resultKey.getOriginalKey());//根據OriginalKey獲取原檔案
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Decoded source from cache", startTime);
}
return transformEncodeAndTranscode(decoded);
}
OriginalKey的構造方法如下,只含有id和signature兩個屬性
public OriginalKey(String id, Key signature) {
this.id = id;
this.signature = signature;
}
上述decodeFromSource()的原始碼如下:
public Resource<Z> decodeFromSource() throws Exception {
Resource<T> decoded = decodeSource();
return transformEncodeAndTranscode(decoded);
}
decodeSource()方法最後調到了cacheAndDecodeSourceData方法:
private Resource<T> cacheAndDecodeSourceData(A data) throws IOException {
long startTime = LogTime.getLogTime();
SourceWriter<A> writer = new SourceWriter<A>(loadProvider.getSourceEncoder(), data);
diskCacheProvider.getDiskCache().put(resultKey.getOriginalKey(), writer);//根據OriginalKey儲存圖片原檔案
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Wrote source to cache", startTime);
}
startTime = LogTime.getLogTime();
Resource<T> result = loadFromCache(resultKey.getOriginalKey());
if (Log.isLoggable(TAG, Log.VERBOSE) && result != null) {
logWithTimeAndKey("Decoded source from cache", startTime);
}
return result;
}
也就是說,decodeFromCache()和 decodeFromSource()最後都呼叫了transformEncodeAndTranscode方法,將原檔案進行轉換transform:
private Resource<Z> transformEncodeAndTranscode(Resource<T> decoded) {
long startTime = LogTime.getLogTime();
Resource<T> transformed = transform(decoded);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Transformed resource from source", startTime);
}
writeTransformedToCache(transformed);//會判斷是否需要快取轉換後的資料,根據diskCacheStrategy.cacheResult()結果決定
startTime = LogTime.getLogTime();
Resource<Z> result = transcode(transformed);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Transcoded transformed from source", startTime);
}
return result;
}
如果diskCacheStrategy.cacheResult() 為true,此方法中的writeTransformedToCache方法會將資料快取起來:
diskCacheProvider.getDiskCache().put(resultKey, writer);//根據resultKey儲存transform之後的資料
這裡的diskCacheProvider又從哪裡來的呢?追蹤到Engine裡, this.diskCacheProvider = new LazyDiskCacheProvider(diskCacheFactory); 而此類中getDiskCache()方法如下:
public DiskCache getDiskCache() {
if (diskCache == null) {
synchronized (this) {
if (diskCache == null) {
diskCache = factory.build();
}
if (diskCache == null) {
diskCache = new DiskCacheAdapter();
}
}
}
return diskCache;
}
繼續找factory發現,上面這些方法中的get,put方法都是介面方法,具體factory從Glide.with(context)時傳入: with方法context不同時,呼叫方法相同,以activity為例:
public static RequestManager with(Activity activity) {
RequestManagerRetriever retriever = RequestManagerRetriever.get();
return retriever.get(activity);
}
上述方法最終建立了RequestManager物件,在其構造方法中
this.glide = Glide.get(context);
get方法呼叫
glide = builder.createGlide();
GlideBuilder類中createGlide()方法有這麼幾行:
memoryCache = new LruResourceCache(calculator.getMemoryCacheSize());
diskCacheFactory = new InternalCacheDiskCacheFactory(context);
engine = new Engine(memoryCache, diskCacheFactory, diskCacheService, sourceService);
return new Glide(engine, memoryCache, bitmapPool, context, decodeFormat);
InternalCacheDiskCacheFactory 類繼承自DiskLruCacheFactory 這裡就是factory的build方法了
@Override
public DiskCache build() {
File cacheDir = cacheDirectoryGetter.getCacheDirectory();
if (cacheDir == null) {
return null;
}
if (!cacheDir.mkdirs() && (!cacheDir.exists() || !cacheDir.isDirectory())) {
return null;
}
return DiskLruCacheWrapper.get(cacheDir, diskCacheSize);
}
最後的get方法返回一個DiskLruCacheWrapper物件,也就是getDiskCache()返回的物件了,用來快取資料的方法都在這裡了:
public static synchronized DiskCache get(File directory, int maxSize) {
// TODO calling twice with different arguments makes it return the cache for the same directory, it's public!
if (wrapper == null) {
wrapper = new DiskLruCacheWrapper(directory, maxSize);
}
return wrapper;
}
終於,上邊獲取快取檔案和儲存快取的方法都在DiskLruCacheWrapper類裡了:
private synchronized DiskLruCache getDiskCache() throws IOException {
if (diskLruCache == null) {
diskLruCache = DiskLruCache.open(directory, APP_VERSION, VALUE_COUNT, maxSize);//返回一個DiskLruCache物件
}
return diskLruCache;
}
@Override
public File get(Key key) {
String safeKey = safeKeyGenerator.getSafeKey(key);
File result = null;
try {
final DiskLruCache.Value value = getDiskCache().get(safeKey);
if (value != null) {
result = value.getFile(0);
}
} catch (IOException e) {
if (Log.isLoggable(TAG, Log.WARN)) {
Log.w(TAG, "Unable to get from disk cache", e);
}
}
return result;
}
@Override
public void put(Key key, Writer writer) {
String safeKey = safeKeyGenerator.getSafeKey(key);
writeLocker.acquire(key);
try {
DiskLruCache.Editor editor = getDiskCache().edit(safeKey);
// Editor will be null if there are two concurrent puts. In the worst case we will just silently fail.
if (editor != null) {
try {
File file = editor.getFile(0);
if (writer.write(file)) {
editor.commit();
}
} finally {
editor.abortUnlessCommitted();
}
}
} catch (IOException e) {
if (Log.isLoggable(TAG, Log.WARN)) {
Log.w(TAG, "Unable to put to disk cache", e);
}
} finally {
writeLocker.release(key);
}
}
@Override
public void delete(Key key) {
String safeKey = safeKeyGenerator.getSafeKey(key);
try {
getDiskCache().remove(safeKey);
} catch (IOException e) {
if (Log.isLoggable(TAG, Log.WARN)) {
Log.w(TAG, "Unable to delete from disk cache", e);
}
}
}
@Override
public synchronized void clear() {
try {
getDiskCache().delete();
resetDiskCache();
} catch (IOException e) {
if (Log.isLoggable(TAG, Log.WARN)) {
Log.w(TAG, "Unable to clear disk cache", e);
}
}
}
這些get,put的方法又是呼叫的DiskLruCache中的get,put方法,方法返回Value物件 其中用到了Entry類來儲存檔案file, Entry的構造方法如下:
private Entry(String key) {
this.key = key;
this.lengths = new long[valueCount];
cleanFiles = new File[valueCount];
dirtyFiles = new File[valueCount];
// The names are repetitive so re-use the same builder to avoid allocations.
StringBuilder fileBuilder = new StringBuilder(key).append('.');
int truncateTo = fileBuilder.length();
for (int i = 0; i < valueCount; i++) {
fileBuilder.append(i);
cleanFiles[i] = new File(directory, fileBuilder.toString());
fileBuilder.append(".tmp");
dirtyFiles[i] = new File(directory, fileBuilder.toString());
fileBuilder.setLength(truncateTo);
}
}
get/put方法中,生成key的方法如下:
class SafeKeyGenerator {
private final LruCache<Key, String> loadIdToSafeHash = new LruCache<Key, String>(1000);
public String getSafeKey(Key key) {
String safeKey;
synchronized (loadIdToSafeHash) {
safeKey = loadIdToSafeHash.get(key);
}
if (safeKey == null) {
try {
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
key.updateDiskCacheKey(messageDigest);
safeKey = Util.sha256BytesToHex(messageDigest.digest());
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
synchronized (loadIdToSafeHash) {
loadIdToSafeHash.put(key, safeKey);
}
}
return safeKey;
}
}
其中,LruCache用了一個Map儲存key及其對應的加密後的字串, LruCache的程式碼不多,都放上吧:
public class LruCache<T, Y> {
private final LinkedHashMap<T, Y> cache = new LinkedHashMap<T, Y>(100, 0.75f, true);//最後的最後,這裡是用一個LinkedHashMap對key和快取進行了儲存。
private int maxSize;
private final int initialMaxSize;
private int currentSize = 0;
public LruCache(int size) {
this.initialMaxSize = size;
this.maxSize = size;
}
public void setSizeMultiplier(float multiplier) {
if (multiplier < 0) {
throw new IllegalArgumentException("Multiplier must be >= 0");
}
maxSize = Math.round(initialMaxSize * multiplier);
evict();
}
protected int getSize(Y item) {
return 1;
}
protected void onItemEvicted(T key, Y item) {
// optional override
}
public int getMaxSize() {
return maxSize;
}
public int getCurrentSize() {
return currentSize;
}
public boolean contains(T key) {
return cache.containsKey(key);
}
public Y get(T key) {
return cache.get(key);
}
public Y put(T key, Y item) {
final int itemSize = getSize(item);
if (itemSize >= maxSize) {
onItemEvicted(key, item);
return null;
}
final Y result = cache.put(key, item);
if (item != null) {
currentSize += getSize(item);
}
if (result != null) {
// TODO: should we call onItemEvicted here?
currentSize -= getSize(result);
}
evict();
return result;
}
public Y remove(T key) {
final Y value = cache.remove(key);
if (value != null) {
currentSize -= getSize(value);
}
return value;
}
public void clearMemory() {
trimToSize(0);
}
protected void trimToSize(int size) {
Map.Entry<T, Y> last;
while (currentSize > size) {
last = cache.entrySet().iterator().next();
final Y toRemove = last.getValue();
currentSize -= getSize(toRemove);
final T key = last.getKey();
cache.remove(key);
onItemEvicted(key, toRemove);
}
}
private void evict() {
trimToSize(maxSize);
}
}
另外:
public static String sha1BytesToHex(byte[] bytes) {
synchronized (SHA_1_CHARS) {
return bytesToHex(bytes, SHA_1_CHARS);
}
}
private static String bytesToHex(byte[] bytes, char[] hexChars) {
int v;
for (int j = 0; j < bytes.length; j++) {
v = bytes[j] & 0xFF;
hexChars[j * 2] = HEX_CHAR_ARRAY[v >>> 4];
hexChars[j * 2 + 1] = HEX_CHAR_ARRAY[v & 0x0F];
}
return new String(hexChars);
}
總結: Glide的signature方法有效地添加了標識,通過簡單的傳入StringSignature即可實現判斷圖片資訊是否為最新,從而載入最新的圖片。 在自行開發過程中,可以更好的處理舊圖片問題,把標識資訊和圖片分開存放,如有圖片更新,可刪除舊圖片,只快取新圖片。當然,主要看是否需要儲存舊圖片。 P.S.僅對Glide一部分功能進行了簡要分析,有興趣的讀者可以參見https://github.com/bumptech/glide進行進一步研究,如上述內容有紕漏,多謝指正。
相關推薦
APP圖片快取與Glide之signature的分析
1.圖片快取遇到的問題 在快取網路圖片的過程中,有一種情況是圖片的地址不變,但圖片發生了變化,如果只按照圖片的地址進行快取,在載入快取中的圖片時就會發生圖片一直顯示為舊圖的現象。 在App中修改使用者頭像的功能中,如果伺服器儲存頭像的地址保持不變,載入快取就會出現上述的情
Android圖片載入框架Glide之探究Glide的快取機制
轉載自:http://blog.csdn.net/guolin_blog/article/details/54895665 在本系列的上一篇文章中,我帶著大家一起閱讀了一遍Glide的原始碼,初步瞭解了這個強大的圖片載入框架的基本執行流程。 不過,上一篇文
網站運維技術與實踐之資料分析與報警
對於日益積累的監控資料,顯然需要有規劃地進行儲存和分析,做到“故障沒來時有預防,故障來臨時有提示,故障到來時有解決方案”。 一、時間序列儲存 對於大多數監控資料,都有一個天然的類似資料庫主鍵的屬性,那就是時間。所以,通常情況下,各類監控系統的後臺資料庫都可以認為是時間序列的資
Android實現圖片快取與非同步載入
ImageManager2這個類具有非同步從網路下載圖片,從sd讀取本地圖片,記憶體快取,硬碟快取,圖片使用動畫漸現等功能,已經將其應用在包含大量圖片的應用中一年多,沒有出現oom。 Android程式常常會記憶體溢位,網上也有很多解決方案,如軟引用,手動呼叫recycle
Android開發圖片快取框架Glide的總結
前言 前段時間寫過一篇圖片快取框架Picasso的用法,對於Picasso有些同學也比較熟悉,採用Lru最近最少快取策略,並且自帶記憶體和硬碟快取機制,在圖片載入尤其是多圖載入著實為大夥省了不少力,在此之前同樣也相識有Afinal、Xutil、Univer
Android 圖片快取與載入方式
開場白 從開始開發Android到現在使用的好多載入圖片的框架,剛開始什麼都不懂就看第三方封裝的框架是如何載入的,然後照搬過來使用,只要能加載出圖片就算工作完成,我才不考慮什麼好不好?對不對?因為我自
ImageLoader圖片快取之簡單配置與詳細配置
ImageLoader依賴 implementation 'com.nostra13.universalimageloader:universal-image-loader:1.9.5' 1.簡單配置 public class App extends Applicati
Glide圖片載入神器----官方庫wiki說明文件的翻譯(快取與快取校驗)
快取校驗是一個相對複雜的話題和概念,應該儘量少去考慮。本篇幅將給出一個在Glide中如何生成cache key大致的方案,並且如何讓快取更好的為你工作給一些建議提醒。 Cache Keys: 在Glide中Cache Keys用於DiskCacheS
Android圖片快取分析與優化
protected int sizeOf(String key, Drawable value) { if(value!=null) { if (value instanceof BitmapDrawable) { Bitmap bitmap = ((BitmapDraw
[轉載]熱血傳奇之資源文件與地圖的讀取分析
thead open pda exc height 保留字 img 單位 累加 Mr.Johness阿何的程序人生JMir——Java版熱血傳奇2之資源文件與地圖 我雖然是90後,但是也很喜歡熱血傳奇2(以下簡稱“傳奇”)這款遊戲。 進入程序員行業後自己也對傳奇客戶端實
01 整合IDEA+Maven+SSM框架的高並發的商品秒殺項目之業務分析與DAO層
初始 lob 可能 很多 ont 配置 支持 個數 base 作者:nnngu 項目源代碼:https://github.com/nnngu/nguSeckill 這是一個整合IDEA+Maven+SSM框架的高並發的商品秒殺項目。我們將分為以下幾篇文章來進行詳細的講解:
BLE4.0教程二 藍牙協議之服務與特征值分析
cli rac info onf eric ack 而已 訪問 搭建 1.關於服務與特征值的簡述 之前說到藍牙的連接過程,那藍牙連接之後具體是如何傳數據的呢。這裏做一下簡要說明。 藍牙4.0是以參數來進行數據傳輸的,即服務端定好一個參數,客戶端可以對這個參數進行
Android框架之Volley與Glide
cat name 圖片緩存 上傳文件 bsp 怎麽 每一個 ons exc PS:在看到這個題目的同時,你們估計會想,Volley與Glide怎麽拿來一塊說呢,他們雖然不是一個框架,但有著相同功能,那就是圖片處理方面。首先我們先來看一下什麽volley,又什麽是glide。
數據結構與算法學習筆記之如何分析一個排序算法?
編號 height href eight 代碼 [] www. 價值 它的 前言 現在IT這塊找工作,不會幾個算法都不好意思出門,排序算法恰巧是其中最簡單的,我接觸的第一個算法就是它,但是你知道怎麽分析一個排序算法麽?有很多時間復雜度相同的排序算法,在實際編碼中,那又如何
Android與JS之JsBridge使用與原始碼分析
在Android開發中,由於Native開發的成本較高,H5頁面的開發更靈活,修改成本更低,因此前端網頁JavaScript(下面簡稱JS)與Java之間的互相呼叫越來越常見。 JsBridge就是一個簡化Android與JS通訊的框架,原始碼:https://github.com/lzyzsd
演算法分析與設計之多處最優服務次序問題
#include <iostream> #include <algorithm> #include <cstring> #include <cstdio> using namespace std; int main() { int i,n,j,k
(待續)科學計算與MATLAB語言之資料分析
MATLAB資料分析專題 主要內容: 資料統計分析 多項式計算 資料插值 曲線擬合 第一節 資料統計分析 1)最值 求矩陣的最大和最小元素 max( ): 求向量或矩陣的最大元素 min( ): 求向
演算法分析與設計之多處最優服務次序問題2
¢ 設有n個顧客同時等待一項服務,顧客i需要的服務時間為ti,1≤i≤n,共有s處可以提供此項服務。應如何安排n個顧客的服務次序才能使平均等待時間達到最小?平均等待時間是n個顧客等待服務時間的總和除以n。 ¢ 給定的n個顧客需要的服務時間和s的值,程式設計計算最優服務次序。 ¢ 輸入 第一行
算法分析與設計之多處最優服務次序問題2
循環 sin bsp 一行 print include 對比 進行 ios ¢ 設有n個顧客同時等待一項服務,顧客i需要的服務時間為ti,1≤i≤n,共有s處可以提供此項服務。應如何安排n個顧客的服務次序才能使平均等待時間達到最小?平均等待時間是n個顧客等待服務時間的總和
統計分析之ROC曲線與多指標聯合分析——附SPSS繪製ROC曲線指南
在進行某診斷方法的評估是,我們常常要用到ROC曲線。這篇博文將簡要介紹ROC曲線以及用SPSS及medcal繪製ROC曲線的方法。 定義 ROC受試者工作特徵曲線 (receive