Volley HTTP 緩存機制

Volley HTTP 緩存規則

在介紹Volley的HTTP緩存機制之前,我們首先來看一下HTTP HEADER中和緩存有關的字段有:

規則 字段 示例值 類型 作用
新鮮度 Expires Sat, 23 Jul 2016 03:34:17 GMT 響應 告訴客戶端在過期時間之前可以使用副本
Cache-Control no-cache 響應 告訴客戶端忽略資源的緩存副本,強制每次請求都訪問服務器
no-store 響應 強制緩存在任何情況下都不要保留任何副本
must-revalidate 響應 表示必須進行新鮮度的再驗證之後才能使用
max-age=[秒] 響應 指明緩存副本的有效時長,從請求時間到到期時間的秒數
Last-Modified Mon, 23 Jun 2014 08:43:26 GMT 響應 告訴客戶端當前資源的最後修改時間
If-Modified-Since Mon, 23 Jun 2014 08:43:26 GMT 請求 如果瀏覽器第一次請求時響應的Last-Modified非空,第二次請求同一資源時,會把它作爲該項的值發送給服務器
校驗值 ETag 53a7e8ae-1f79 響應 告知客戶端當前資源在服務器的唯一標識
If-None-Match 53a7e8ae-1f79 請求 如果瀏覽器第一次請求時響應中ETag非空,第二次請求同一資源時,會把它作爲該項的值發給服務器

Volley的緩存機制

Volley的緩存機制在對HTTP RESPONSE的解析中能夠明顯的看出來:

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

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

    long serverDate = 0;
    long lastModified = 0;
    long serverExpires = 0;
    long softExpire = 0;
    long finalExpire = 0;
    long maxAge = 0;
    long staleWhileRevalidate = 0;
    boolean hasCacheControl = false;
    boolean mustRevalidate = false;

    String serverEtag;
    String headerValue;

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

    // 獲取響應體的Cache緩存策略.
    headerValue = headers.get("Cache-Control");
    if (headerValue != null) {
        hasCacheControl = true;
        String[] tokens = headerValue.split(",");
        for (String token : tokens) {
            token = token.trim();
            if (token.equals("no-cache") || token.equals("no-store")) {
                // no-cache|no-store代表服務器禁止客戶端緩存,每次需要重新發送HTTP請求
                return null;
            } else if (token.startsWith("max-age=")) {
                // 獲取緩存的有效時間
                try {
                    maxAge = Long.parseLong(token.substring(8));
                } catch (Exception e) {
                    maxAge = 0;
                }
            } else if (token.startsWith("stale-while-revalidate=")) {
                try {
                    staleWhileRevalidate = Long.parseLong(token.substring(23));
                } catch (Exception e) {
                    staleWhileRevalidate = 0;
                }
            } else if (token.equals("must-revalidate") || token.equals("proxy-revalidate")) {
                // 需要進行新鮮度驗證
                mustRevalidate = true;
            }
        }
    }

    // 獲取服務器資源的過期時間
    headerValue = headers.get("Expires");
    if (headerValue != null) {
        serverExpires = parseDateAsEpoch(headerValue);
    }

    // 獲取服務器資源最後一次的修改時間
    headerValue = headers.get("Last-Modified");
    if (headerValue != null) {
        lastModified = parseDateAsEpoch(headerValue);
    }

    // 獲取服務器資源標識
    serverEtag = headers.get("ETag");

    // 計算緩存的ttl和softTtl
    if (hasCacheControl) {
        softExpire = now + maxAge * 1000;
        finalExpire = mustRevalidate
                ? softExpire
                : softExpire + staleWhileRevalidate * 1000;
    } else if (serverDate > 0 && serverExpires >= serverDate) {
        // Default semantic for Expire header in HTTP specification is softExpire.
        softExpire = now + (serverExpires - serverDate);
        finalExpire = softExpire;
    }

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

    return entry;
}

這個方法其實是實現了Volley的本地緩存的關鍵代碼.


L2級硬盤緩存的實現和緩存替換機制

之前介紹了用戶使用LruCache實現自定義的L1級緩存,而Volley本身利用了FIFO算法實現了L2級硬盤緩存.接下來,就詳細介紹一下硬盤緩存的實現和緩存替換機制.
這裏我們也是考慮如果自己實現硬盤緩存,需要實現哪幾個步驟:

  1. 抽象出存儲實體類.
  2. 定義抽象存儲接口,包括initialize,get,put,clear等具體緩存系統的操作.
  3. 對象的序列化.

存儲實體

存儲的實體肯定是響應的結果,響應結果分爲響應頭和響應體,抽象類代碼如下所示:

/** 真正HTTP請求緩存實體類. */
class Entry {
    /** HTTP響應Headers. */
    public Map<String, String> responseHeaders = Collections.emptyMap();

    /** HTTP響應體. */
    public byte[] data;

    /** 服務器資源標識ETag. */
    public String etag;

    /** HTTP響應時間. */
    public long serverDate;

    /** 緩存內容最後一次修改的時間. */
    public long lastModified;

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

    /** Request的緩存新鮮時間. */
    public long softTtl;

    /** 判斷緩存內容是否過期. */
    public boolean isExpired() {
        return this.ttl < System.currentTimeMillis();
    }

    /** 判斷緩存是否新鮮,不新鮮的緩存需要發到服務端做新鮮度的檢測. */
    public boolean refreshNeeded() {
        return this.softTtl < System.currentTimeMillis();
    }
}

抽象緩存系統類

public interface Cache {
    /** 通過key獲取請求的緩存實體. */
    Entry get(String key);

    /** 存入一個請求的緩存實體. */
    void put(String key, Entry entry);

    void initialize();

    void invalidate(String key, boolean fullExpire);

    /** 移除指定的緩存實體. */
    void remove(String key);

    /** 清空緩存. */
    void clear();
}

在Volley中,實現Cache接口的硬盤緩存類是DiskBasedCache.接下來,具體介紹每個方法的具體實現.

構造函數

我們先來看一下DiskBasedCache的構造函數實現:

public DiskBasedCache(File rootDirectory) {
    this(rootDirectory, DEFAULT_DISK_USAGE_BYTES);
}

public DiskBasedCache(File rootDirectory, int maxCacheSizeInBytes) {
    mRootDirectory = rootDirectory;
    mMaxCacheSizeInBytes = maxCacheSizeInBytes;
}

類似於LruCache,DiskBasedCache的構造函數做了兩件事:

  1. 指定硬盤緩存的目錄.
  2. 指定硬盤緩存的大小,默認爲5M.

initialize函數

在介紹put和get函數之前,先介紹一下硬盤緩存的初始化函數,這個函數主要是用來遍歷緩存的文件,從而獲取當前緩存大小,和構造

/** 
 * 初始化Disk緩存系統.
 * 作用是:遍歷Disk緩存系統,將緩存文件中的CacheHeader和key存儲到Map對象中. 
 */
public void initialize() {
    if (!mRootDirectory.exists() && !mRootDirectory.mkdirs()) {
        // 硬盤緩存目錄不存在直接返回即可
        return;
    }

    // 獲取硬盤緩存目錄所有文件集合.每個HTTP請求結果對應一個文件.
    File[] files = mRootDirectory.listFiles();
    if (files == null) {
        return;
    }

    for (File file : files) {
        BufferedInputStream fis = null;
        try {
            fis = new BufferedInputStream(new FileInputStream(file));
            // 進行對象反序列化
            CacheHeader entry = CacheHeader.readHeader(fis);
            // 將文件的大小賦值給entry.size,單位字節
            entry.size = file.length();
            // 在內存中維護一張硬盤<key,value>映射表
            putEntry(entry.key, entry);
        }catch (IOException e) {
            file.delete();
            e.printStackTrace();
        }finally {
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException ignored) {
                }
            }
        }
    }
}

/** 將key和CacheHeader存入到Map對象中.並更新當前佔用的總字節數. */
private void putEntry(String key, CacheHeader entry) {
    if (!mEntries.containsKey(key)) {
        mTotalSize += entry.size;
    } else {
        CacheHeader oldEntry = mEntries.get(key);
        mTotalSize += (entry.size - oldEntry.size);
    }

    mEntries.put(key, entry);
}

put函數

接下來我們講解put函數,是因爲一個緩存系統最爲關鍵的操作就是put,這其中還設計到緩存替換策略的實現.

首先是緩存替換策略.

private void pruneIfNeeded(int neededSpace) {
    if ((mTotalSize + neededSpace) < mMaxCacheSizeInBytes) {
        return;
    }

    Iterator<Map.Entry<String, CacheHeader>> iterator = mEntries.entrySet().iterator();
    while (iterator.hasNext()) {
        Map.Entry<String, CacheHeader> entry = iterator.next();
        CacheHeader e = entry.getValue();
        // 這裏的替換策略不太好,其實可以按照serverDate排序,從而實現FIFO的緩存替換策略.
        boolean deleted = getFileForKey(e.key).delete();
        if (deleted) {
            mTotalSize -= e.size;
        }
        iterator.remove();

        // 當硬盤大小滿足可以存放新的HTTP請求結果時,停止刪除操作
        if ((mTotalSize + neededSpace) < mMaxCacheSizeInBytes * HYSTERESIS_FACTOR) {
            break;
        }
    }
}

接下來,是硬盤緩存的插入操作,準備是對象序列化的一些內容.

/** 將Cache.Entry存入到指定的緩存文件中. 並在Map中記錄<key,CacheHeader>. */
@Override
public synchronized void put(String key, Entry entry) {
    pruneIfNeeded(entry.data.length);
    // 根據HTTP的url生成緩存文件(ps:根據hash值生成文件名)
    File file = getFileForKey(key);
    try {
        BufferedOutputStream fos = new BufferedOutputStream(new FileOutputStream(file));
        // 這裏有個bug,插入時的size只計算響應體,沒有考慮響應頭部緩存字段的大小.
        CacheHeader e = new CacheHeader(key, entry);
        boolean success = e.writeHeader(fos);
        if (!success) {
            fos.close();
            throw new IOException();
        }
        fos.write(entry.data);
        fos.close();
        putEntry(key, e);
        return;
    } catch (IOException e) {
        e.printStackTrace();
    }
    file.delete();
}

get函數

get函數比較簡單了,源碼如下:

/** 從Disk中根據key獲取並構造HTTP響應體Cache.Entry. */
@Override
public synchronized Entry get(String key) {
    CacheHeader entry = mEntries.get(key);
    if (entry == null) {
        return null;
    }

    File file = getFileForKey(key);
    CountingInputStream cis = null;
    try {
        cis = new CountingInputStream(new BufferedInputStream(new FileInputStream(file)));
        // 讀完CacheHeader部分,並通過CountingInputStream的bytesRead成員記錄已經讀取的字節數.
        CacheHeader.readHeader(cis);
        // 讀取緩存文件存儲的HTTP響應體內容.
        byte[] data = streamToBytes(cis, (int)(file.length() - cis.bytesRead));
        return entry.toCacheEntry(data);
    } catch (IOException e) {
        remove(key);
        return null;
    } finally {
        if (cis != null) {
            try {
                cis.close();
            } catch (IOException ignored) {
            }
        }
    }
}

clear函數

clear顧名思義,就是清空硬盤緩存的操作:

public synchronized void clear() {
    File[] files = mRootDirectory.listFiles();
    if (files != null) {
        for (File file : files) {
            file.delete();
        }
    }
    mEntries.clear();
    mTotalSize = 0;
}

所做的事情也比較簡單,包括:

  1. 情況緩存文件.
  2. 將使用size置爲0.
  3. 清空內存中維護的硬盤

remove函數

remove函數也就是刪除指定key對應的硬盤緩存,代碼很簡單:

@Override
public synchronized void remove(String key) {
    boolean deleted = getFileForKey(key).delete();
    removeEntry(key);
    if (!deleted) {
        Log.e("Volley", "沒能刪除key=" + key + ", 文件名=" + getFilenameForKey(key) + "緩存.");
    }
}

/** 從Map對象中刪除key對應的鍵值對. */
private void removeEntry(String key) {
    CacheHeader entry = mEntries.get(key);
    if (entry != null) {
        mTotalSize -= entry.size;
        mEntries.remove(key);
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章