從源碼帶看Volley的緩存機制

轉載請註明出處:http://blog.csdn.net/asdzheng/article/details/45955653

磁盤緩存DiskBasedCache

如果你還不知道volley有磁盤緩存的話,請看一下我的另一篇博客請注意,Volley已默認使用磁盤緩存

DiskBasedCache內部結構

它由兩部分組成,一部分是頭部,一部分是內容;先得從它的內部靜態類CacheHeader(緩存的頭部信息)講起,先看它的內部結構:

static class CacheHeader {
    /** 緩存文件的大小 */
    public long size;

    /** 緩存文件的唯一標識 */
    public String key;

    /** 這個是與與http請求緩存相關的標籤 */
    public String etag;

    /** 服務器的返回來數據的時間 */
    public long serverDate;

    /** TTL 緩存過期時間. */
    public long ttl;

    /** Soft TTL 緩存新鮮度時間. */
    public long softTtl;

    /** 服務器還回來的頭部信息. */
    public Map<String, String> responseHeaders;
}

 //可以看到,頭部類裏包含的都是一些基本信息。再來看一下內容部分,父類Cache裏面的的Entry:
 public static class Entry {
    /** 服務端返回數據的主要內容. */
    public byte[] data;

    public String etag;

    public long serverDate;

    public long ttl;

    public long softTtl;
}

可以看到,Entry裏面和CacheHeader裏有四個參數是一樣的,只是Entry裏多了data[],data[]就是用來保存主要數據的。看到這你可以有點迷糊,Entry和CacheHeader裏爲什麼要有四個參數一樣,先簡單說一下原因:volley框架裏都用到接口編程,所以實際代碼中除了初始化,你只看到cache,而DiskBasedCache是看不到的,所以必須在Entry裏先把那些緩存需要用到的參數保留起來,然後具體實現和封裝放在DiskBasedCache裏。

DiskBasedCache的使用流程

  • 初始化

    DiskBasedCache的初始化時在RequestQueue新建時就發生的,可以看Volley.newRequestQueue()的源碼:

public static RequestQueue newRequestQueue(Context context, HttpStack stack, int maxDiskCacheBytes) {
    File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR);

    ....

    //maxDiskCacheBytes爲緩存的最大容量,不傳就默認爲5M
    if (maxDiskCacheBytes <= -1)
    {
        // No maximum size specified
        queue = new RequestQueue(new DiskBasedCache(cacheDir), network);
    }
    else
    {
        // Disk cache size specified
        queue = new RequestQueue(new DiskBasedCache(cacheDir, maxDiskCacheBytes), network);
    }

    queue.start();
    return queue;
}

可以看到,磁盤緩存的路徑爲:context.getCacheDir(),如果maxDiskCacheBytes有傳入,就以傳入的爲準,如果爲空:

/** 默認的磁盤存放的最大byte */
private static final int DEFAULT_DISK_USAGE_BYTES = 5 * 1024 * 1024;
...
public DiskBasedCache(File rootDirectory) {
    this(rootDirectory, DEFAULT_DISK_USAGE_BYTES);
}

磁盤默認爲5M。所以如果你想設置最大的磁盤緩存值,那麼就不能直接向下面那樣這樣初始化了:

    queue = Volley.newRequestQueue(context);

而是需要這樣:

    queue = Volley.newRequestQueue(context, 10 * 1024 * 1024);
  • 存放緩存數據

    第一次緩存的數據是從哪來的呢,當然是從網上來,看NetWorkDispatcher的run方法裏:

@Override
public void run() {
    while (true) {
        ...
        try {
            ....
            請求解析http的返回信息
            ....
            if (request.shouldCache() && response.cacheEntry != null) {
                mCache.put(request.getCacheKey(), response.cacheEntry);
                request.addMarker("network-cache-written");
            }

            ....
    }
}

其中request.getCacheKey()默認爲請求的url,response.cacheEntry是Cache.Entry,裏面已存放好解析完的httpResponse數據,request.shouldCache()默認是需要緩存,如果不需要可調用request.setShouldCache(false)來去掉緩存功能。

我們把請求和處理的http的返回略過,留下幾行關鍵代碼,如果這個請求需要緩存(默認需要)和緩存信息不爲空,那麼就保存緩存信息。接下來看,DiskBaseCache是怎麼保存緩存的:

 /**
 * 把緩存數據Entry寫進磁盤裏
 */
@Override
public synchronized void put(String key, Entry entry) {
    //判斷是否有足夠的緩存空間來緩存新的數據
    pruneIfNeeded(entry.data.length);

    File file = getFileForKey(key);
    try {
        FileOutputStream fos = new FileOutputStream(file);
        //用enry裏面的數據,再封裝成一個CacheHeader
        CacheHeader e = new CacheHeader(key, entry);
        //先寫頭部緩存信息
        boolean success = e.writeHeader(fos);
        if (!success) {
            fos.close();
            VolleyLog.d("Failed to write header for %s", file.getAbsolutePath());
            throw new IOException();
        }
        //成功後再寫緩存內容
        fos.write(entry.data);
        fos.close();
        //把頭部信息先暫時保存在一個容器裏
        putEntry(key, e);
        return;
    } catch (IOException e) {
    }
    boolean deleted = file.delete();
    if (!deleted) {
        VolleyLog.d("Could not clean up file %s", file.getAbsolutePath());
    }
}

可以看到,每次寫入緩存之前,都先調用pruneIfNeeded()檢查對象的大小,當緩衝區空間足夠新對象的加入時就直接添加進來,否則會刪除部分對象,一直到新對象添加進來後還會有10%的空間剩餘時爲止,文件引用以LinkHashMap保存。添加時,首先以URL爲key,經過個文本轉換後,以轉換後的文本爲名稱,獲取一個file對象。首先向這個對象寫入緩存的頭文件,然後是真正有用的網絡返回數據。最後是當前內存佔有量數值的更新,這裏需要注意的是真實數據被寫入磁盤文件後,在內存中維護的應用,存的只是數據的相關屬性。

  • 從緩存數據裏取緩存

我們知道隊列創建後就會有一個緩存線程在後臺一直運行等待着緩存請求進來,但在等待線程前,會先調用mCache.initialize(),把緩存數據的頭部信息放進一個Map類型mEntries裏,這樣以後要用到就先用mEntries判斷,速度更快。

如果請求進來即調用Cache.Entry entry = mCache.get(request.getCacheKey()),那我們就看DiskBaseCache。get方法裏做了什麼:

 @Override
public synchronized Entry get(String key) {
    CacheHeader entry = mEntries.get(key);
    // 如果entry不爲空,就直接返回
    if (entry == null) {
        return null;
    }

    File file = getFileForKey(key);
    CountingInputStream cis = null;
    try {
        cis = new CountingInputStream(new FileInputStream(file));
        CacheHeader.readHeader(cis); // eat header
        byte[] data = streamToBytes(cis, (int) (file.length() - cis.bytesRead));
        return entry.toCacheEntry(data);
    } catch (IOException e) {
        VolleyLog.d("%s: %s", file.getAbsolutePath(), e.toString());
        remove(key);
        return null;
    } finally {
        if (cis != null) {
            try {
                cis.close();
            } catch (IOException ioe) {
                return null;
            }
        }
    }
}

從方法裏可以看到,先從文件裏獲得字節數輸入流,從中減去頭部文件的字節數,最後把真正內容的data[]數據拿到再組裝成一個Cache.Entry返回。不得不說,Volley這真是精打細算啊。

從上面的分析可見,cache在做一些基礎判斷時都會先用到緩存的頭部數據,如果確定頭部信息沒問題了,再真正讀寫內容,原因是頭部數據比較小,放在內存中也不佔地方,但處理速度會快很多。而真正的數據內容,可能會比較大,處理的開銷也大,只在真正需要的地方讀寫。

Volley對304的處理

http的304狀態碼的含義是:

如果服務器端的資源沒有變化,則自動返回 HTTP 304 (Not Changed.)狀態碼,內容爲空,這樣就節省了傳輸數據量。當服務器端代碼發生改變或者重啓服務器時,則重新發出資源,返回和第一次請求時類似。從而 保證不向客戶端重複發出資源,也保證當服務器有變化時,客戶端能夠得到最新的資源。

完整的過程如下:

  1. 客戶端請求一個頁面(A)。
  2. 服務器返回頁面A,並在給A加上一個Last-Modified/ETag。(Last-Modified爲標記此文件在服務期端最後被修改的時間,ETag是這個請求的token)
  3. 客戶端展現該頁面,並將頁面連同Last-Modified/ETag一起緩存。
  4. 客戶再次請求頁面A,並將上次請求時服務器返回的Last-Modified/ETag一起傳遞給服務器。
  5. 服務器檢查該Last-Modified或ETag,並判斷出該頁面自上次客戶端請求之後還未被修改,直接返 迴響應304和一個空的響應體。

介紹完304,我們接下來來看看volley是怎麼運用304來重用緩存的。

Volley對於頭部的解析

首先我們來看一下對於response.header的處理,在每一個request裏,都必須繼承 parseNetworkResponse(NetworkResponse response)方法,然後在裏面用 HttpHeaderParser.parseCacheHeaders()解析類來解析頭部數據,具體如下:

public static Cache.Entry parseCacheHeaders(NetworkResponse response) {
    long now = System.currentTimeMillis();

    Map<String, String> headers = response.headers;

    long serverDate = 0;
    long serverExpires = 0;
    long softExpire = 0;
    long maxAge = 0;
    boolean hasCacheControl = false;

    String serverEtag = null;
    String headerValue;

    headerValue = headers.get("Date");
    if (headerValue != null) {
        serverDate = parseDateAsEpoch(headerValue);
    }

    headerValue = headers.get("Cache-Control");
    if (headerValue != null) {
        hasCacheControl = true;
        String[] tokens = headerValue.split(",");
        for (int i = 0; i < tokens.length; i++) {
            String token = tokens[i].trim();
            //如果Cache-Control裏爲no-cache和no-store則表示不需要緩存,返回null
            if (token.equals("no-cache") || token.equals("no-store")) {
                return null;
            } else if (token.startsWith("max-age=")) {
                try {
                    maxAge = Long.parseLong(token.substring(8));
                } catch (Exception e) {
                }
            } else if (token.equals("must-revalidate") || token.equals("proxy-revalidate")) {
                maxAge = 0;
            }
        }
    }

    headerValue = headers.get("Expires");
    if (headerValue != null) {
        serverExpires = parseDateAsEpoch(headerValue);
    }
    serverEtag = headers.get("ETag");
    // Cache-Control takes precedence over an Expires header, even if both exist and Expires
    // is more restrictive.
    if (hasCacheControl) {
        softExpire = now + maxAge * 1000;
    } else if (serverDate > 0 && serverExpires >= serverDate) {
        // Default semantic for Expire header in HTTP specification is softExpire.
        softExpire = now + (serverExpires - serverDate);
    }

    Cache.Entry entry = new Cache.Entry();
    entry.data = response.data;
    entry.etag = serverEtag;
    entry.softTtl = softExpire;
    entry.ttl = entry.softTtl;
    entry.serverDate = serverDate;
    entry.responseHeaders = headers;
    return entry;
}

從上面代碼可以看出緩存頭部是根據 Cache-Control 和 Expires 首部,計算出緩存的過期時間(ttl),和緩存的新鮮度時間(softTtl,默認softTtl和ttl相同),如果有Cache-Control標籤以它爲準,沒有就以Expires標籤裏的內容爲準。

需要注意的是:Volley沒有處理Last-Modify首部,而是處理存儲了Date首部,並在後續的新鮮度驗證時,使用Date來構建If-Modified-Since。 這與 Http 1.1 的語義有些違背。

Volley對於新鮮度和過期的驗證

在使用緩存數據前,Volley會先對驗證緩存數據是否過期,是否需要更新等屬性,然後一一處理,代碼在CacheDispatcher的run方法裏:

 @Override
public void run() {
    if (DEBUG) VolleyLog.v("start new dispatcher");
    Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
    // 初始化緩存,裏面會先把磁盤緩存裏的頭部數據緩存進內存裏,增加處理速度
    mCache.initialize();

    while (true) {
        try {
            // 阻塞線程直到有請求加入,纔開始運行
            final Request request = mCacheQueue.take();
            request.addMarker("cache-queue-take");

            //請求是否取消
            if (request.isCanceled()) {
                request.finish("cache-discard-canceled");
                continue;
            }

            // 得到緩存數據entry
            Cache.Entry entry = mCache.get(request.getCacheKey());
            //如果緩存不存在,就把請求交給網絡隊列取處理
            if (entry == null) {
                request.addMarker("cache-miss");
                mNetworkQueue.put(request);
                continue;
            }

            // 如果請求過期,也需要到網絡重新獲取數據
            if (entry.isExpired()) {
                request.addMarker("cache-hit-expired");
                request.setCacheEntry(entry);
                mNetworkQueue.put(request);
                continue;
            }

            // 到這裏就表明緩存數據是可用的,解析緩存
            request.addMarker("cache-hit");
            Response<?> response = request.parseNetworkResponse(
                    new NetworkResponse(entry.data, entry.responseHeaders));
            request.addMarker("cache-hit-parsed");

            //驗證緩存的新鮮度
            if (!entry.refreshNeeded()) {
               //新鮮的
                mDelivery.postResponse(request, response);
            } else {
                // 不新鮮,雖然把緩存數據分發出去,但還是需要到網絡上驗證緩存是否需要更新
                request.addMarker("cache-hit-refresh-needed");
                //請求帶上緩存屬性
                request.setCacheEntry(entry);

                response.intermediate = true;

                // 分發完緩存數據後,將請求加入網絡請求隊列,判斷是否需要更新緩存數據
                mDelivery.postResponse(request, response, new Runnable() {
                    @Override
                    public void run() {
                        try {
                            mNetworkQueue.put(request);
                        } catch (InterruptedException e) {
                            // Not much we can do about this.
                        }
                    }
                });
            }

        } catch (InterruptedException e) {
            // We may have been interrupted because it was time to quit.
            if (mQuit) {
                return;
            }
            continue;
        }
    }
}

上面代碼都已經加了註釋,相信不難理解,那我們繼續看,網絡請求是怎麼判斷是否需要更新緩存的,在BasicNetwork.performRequest()裏:

@Override
public NetworkResponse performRequest(Request<?> request) throws VolleyError {
....
while (true) {
    ...
    try {
        Map<String, String> headers = new HashMap<String, String>();
        //如果請求有帶屬性,就將etag和If-Modified-Since屬性加上
        addCacheHeaders(headers, request.getCacheEntry());
        httpResponse = mHttpStack.performRequest(request, headers);
        StatusLine statusLine = httpResponse.getStatusLine();
        int statusCode = statusLine.getStatusCode();

        responseHeaders = convertHeaders(httpResponse.getAllHeaders());
        //如果304就直接用緩存數據返回
        if (statusCode == HttpStatus.SC_NOT_MODIFIED) {
            return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED,
                    request.getCacheEntry().data, responseHeaders, true);
        }
        .....
        return new NetworkResponse(statusCode, responseContents, responseHeaders, false);
    } catch (SocketTimeoutException e) {
      ...
    }
}
}

從上面的註釋可以看到,如果是返回304就直接用緩存數據返回。那來看NetworkDispatcher的run()裏:

public void run() {
    ...
    NetworkResponse networkResponse = mNetwork.performRequest(request);
    request.addMarker("network-http-complete");
    // 如果是304並且已經將緩存分發出去裏,就直接結束這個請求
    if (networkResponse.notModified && request.hasHadResponseDelivered()) {
        request.finish("not-modified");
        continue;
    }
    ...
    }
}

現在流程比較清晰了,在有緩存的情況下,如果已經過期,但是返回304,就複用緩存。如果不新鮮了,就先將緩存分發出去,然後再進行網絡請求,看是否需要更新緩存。

不過眼尖的讀者一定有個疑惑,在解析頭部數據時,默認不是新鮮度和過期事件是一樣的嗎?那新鮮度不是一定運行不到嗎?確實是這樣,我也有這個疑惑,網上也找不到確切的資料來解釋這一點。不過按照正常的邏輯,新鮮度時間一定比過期時間短,這樣我們就可以根據實際需要更改Volley的源碼。例如,我們可以直接把新鮮度的驗證時間設爲3分鐘,而過期時間設爲一天,代碼如下:

public static Cache.Entry parseIgnoreCacheHeaders(NetworkResponse response) {
    long now = System.currentTimeMillis();
    Map<String, String> headers = response.headers;
    long serverDate = 0;
    String serverEtag = null;
    String headerValue;
    headerValue = headers.get("Date");

    if (headerValue != null) {
       serverDate = HttpHeaderParser.parseDateAsEpoch(headerValue);
    }

    serverEtag = headers.get("ETag");
    final long cacheHitButRefreshed = 3 * 60 * 1000; 
    final long cacheExpired = 24 * 60 * 60 * 1000; 
    final long softExpire = now + cacheHitButRefreshed;
    final long ttl = now + cacheExpired;

    Cache.Entry entry = new Cache.Entry();
    entry.data = response.data;
    entry.etag = serverEtag;
    entry.softTtl = softExpire;
    entry.ttl = ttl;
    entry.serverDate = serverDate;
    entry.responseHeaders = headers;
    return entry;
}

然後使用的時候:

public class MyRequest extends com.android.volley.Request<MyResponse> {
    ...
    @Override
     protected Response<MyResponse> parseNetworkResponse(NetworkResponse response) {
         String jsonString = new String(response.data);
         MyResponse MyResponse = gson.fromJson(jsonString, MyResponse.class);
         return Response.success(MyResponse, HttpHeaderParser.parseIgnoreCacheHeaders(response));
     }
}

這樣的話,在3分鐘後就不新鮮,24小時後就會過期。

圖片的自定義內存緩存

我們使用ImageLoader時會傳入一個ImageCache,它是個接口,裏面定義了兩個方法:

public interface ImageCache {
        public Bitmap getBitmap(String url);
        public void putBitmap(String url, Bitmap bitmap);
}

那他們是什麼時候使用的呢,可以從開始請求數據ImageLoader.get()方法看起:

 public ImageContainer get(String requestUrl, ImageListener imageListener,
            int maxWidth, int maxHeight) {
        //請求只能在主線程裏,不然會報錯
        throwIfNotOnMainThread();
        //用url和寬高組成key
        final String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight);

        //從內存緩存裏獲取數據
        Bitmap cachedBitmap = mCache.getBitmap(cacheKey);
        if (cachedBitmap != null) {
            // 如果內存不爲空,直接返回圖片信息
            ImageContainer container = new ImageContainer(cachedBitmap, requestUrl, null, null);
            imageListener.onResponse(container, true);
            return container;
        }

        ...
        // 如果爲空,就正常請求網絡數據,下面用的是ImageRequest取請求網絡數據
        Request<?> newRequest =
            new ImageRequest(requestUrl, new Listener<Bitmap>() {
                @Override
                public void onResponse(Bitmap response) {
                   //請求成功後,在這個方法裏,把圖片放進內存緩存中
                    onGetImageSuccess(cacheKey, response);
                }
            }, maxWidth, maxHeight,
            Config.RGB_565, new ErrorListener() {
                @Override
                public void onErrorResponse(VolleyError error) {
                    onGetImageError(cacheKey, error);
                }
            });
     ...
    }

    private void onGetImageSuccess(String cacheKey, Bitmap response) {
        //把圖片放進內存裏
        mCache.putBitmap(cacheKey, response);
        ...
    }

從上面的代碼註釋中已經能比較清晰的看出,每次調用ImageLoader.get()方法,會先從內存緩存裏先看有沒有數據,有就直接返回,沒有就走正常的網絡流程,先查看磁盤緩存,不存在或過期再去請求網絡。圖片比普通數據多一層緩存的原因也很簡單,因爲圖片較大,讀取和網絡成本都大,能用緩存就用緩存,能省一點是一點。

以上就是Volley框架所使用到的所有緩存機制,如有遺漏請留言指出,多謝閱讀。

參考鏈接:
Volley網絡請求源碼解析——擊潰6大疑慮
Last-Modified和If-Modified-Since
Volley 源碼解析

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章