深入理解Android中的快取機制(三)磁碟快取

wustor發表於2018-02-03

概述

磁碟儲存有兩種形式,一種是File儲存,一種是DB(DataBase)儲存。

File

File儲存比較常見,當我們資料量較小,資料的分類以及檢索沒有較大的要求的時候,可以採用File儲存

File存在的問題:

  • 檔案較大時,對檔案的讀取速度較慢
  • 定位,讀寫具體的資料較為困難

DataBase

對資料的併發性和檢索速度有高要求的時候,這個時候,DB就上場了,DB具有如下特點

  • 大資料訪問速度更快
  • 索引特定條件的資料較為方便

Http快取機制

相對於記憶體快取而言,磁碟時效性很低,所以通常單獨的磁碟快取沒有太大意義,每次去讀快取之前需要判斷一下懁促是否有效,必須要結合HTTP的快取機制來做一些處理,這樣快取才會比較有效,所以下面還是先介紹一下HTTP快取機制,將從快取儲存策略快取過期策略快取對比策略三個方面來分析一下Http的快取機制。

快取儲存策略

用來確定 Http 響應內容是否可以被客戶端快取,以及可以被哪些客戶端快取

對於 Cache-Control 頭裡的 Public、Private、no-cache、max-age 、no-store 他們都是用來指明響應內容是否可以被客戶端儲存的,其中前4個都會快取檔案資料(關於 no-cache 應理解為“不建議使用本地快取”,其仍然會快取資料到本地),後者 no-store 則不會在客戶端快取任何響應資料。另關於 no-cache 和 max-age 有點特別,我認為它是一種混合體,下面我會講到。

通過 Cache-Control:Public 設定我們可以將 Http 響應資料儲存到本地,但此時並不意味著後續瀏覽器會直接從快取中讀取資料並使用,為啥?因為它無法確定本地快取的資料是否可用(可能已經失效),還必須藉助一套鑑別機制來確認才行, 這就是我們下面要講到的“快取過期策略”。

快取過期策略

客戶端用來確認儲存在本地的快取資料是否已過期,進而決定是否要發請求到服務端獲取資料


剛上面我們已經闡述了資料快取到了本地後還需要經過判斷才能使用,那麼瀏覽器通過什麼條件來判斷呢? 答案是:Expires,Expires 指名了快取資料有效的絕對時間,告訴客戶端到了這個時間點(比照客戶端時間點)後本地快取就作廢了,在這個時間點內客戶端可以認為快取資料有效,可直接從快取中載入展示。

不過 Http 快取頭設計並沒有想象的那麼規矩,像上面提到的 Cache-Control(這個頭是在Http1.1里加進來的)頭裡的 no-cache 和 max-age 就是特例,它們既包含快取儲存策略也包含快取過期策略,以 max-age 為例,他實際上相當於:

Cache-Control:public/private
Expires:當前客戶端時間 + maxAge 。
複製程式碼

而 Cache-Control:no-cache 和 Cache-Control:max-age=0 (單位是秒)

這裡需要注意的是:

  1. Cache-Control 中指定的快取過期策略優先順序高於 Expires,當它們同時存在的時候,後者會被覆蓋掉。
  2. 快取資料標記為已過期只是告訴客戶端不能再直接從本地讀取快取了,需要再發一次請求到伺服器去確認,並不等同於本地快取資料從此就沒用了,有些情況下即使過期了還是會被再次用到,具體下面會講到。

快取對比策略

將快取在客戶端的資料標識發往服務端,服務端通過標識來判斷客戶端 快取資料是否仍有效,進而決定是否要重發資料。

客戶端檢測到資料過期或瀏覽器重新整理後,往往會重新發起一個 http 請求到伺服器,伺服器此時並不急於返回資料,而是看請求頭有沒有帶標識( If-Modified-Since、If-None-Match)過來,如果判斷標識仍然有效,則返回304告訴客戶端取本地快取資料來用即可(這裡要注意的是你必須要在首次響應時輸出相應的頭資訊(Last-Modified、ETags)到客戶端)。至此我們就明白了上面所說的本地快取資料即使被認為過期,並不等於資料從此就沒用了的道理了。

Android中的磁碟快取

很多時候我們都會說一些圖片載入框架使用了兩級或者三級快取,然後就會說先從記憶體中取,然後再從磁碟中取,最後再從網路中去取,我們現在按照這個思路來分析一下Picasso,Picasso一開始就從記憶體中去讀,然後就會去進行網路請求,如果記憶體中沒有讀取到,他就會去生成一個Request,去請求網路資料,他為什麼沒有直接去讀磁碟快取,這個時候你可能會說,Picasso預設沒有設定磁碟快取,只要當設定了OkHttp.Downloader之後才會進行磁碟快取,實際上不是這樣的,Picasso是有磁碟快取的,因為他的快取依賴於HTTP快取機制,所以每次是在請求之後根據Response的響應頭去看是否讀取記憶體快取,當Picasso在build的時候,如果沒有設定DownLoader,他會自己去設定一個Downloader

  public Picasso build() {
    Context context = this.context;
    if (downloader == null) {
      //建立一個預設的Downloader
      downloader = Utils.createDefaultDownloader(context);
    }
    return new Picasso(context, dispatcher, cache, listener, transformer, requestHandlers, stats,
        defaultBitmapConfig, indicatorsEnabled, loggingEnabled);
  }
}
複製程式碼

繼續檢視createDefaultDownloader,如果沒有OkhttpDownloader,那麼就會採用UrlConnectionDownloader

static Downloader createDefaultDownloader(Context context) {
  try {
    Class.forName("com.squareup.okhttp.OkHttpClient");
    return OkHttpLoaderCreator.create(context);
  } catch (ClassNotFoundException ignored) {
  }
  return new UrlConnectionDownloader(context);
}
複製程式碼

然後我們就分開檢視,因為OkHttpLoader是在UrlConnectionDownloader的基礎上進行改良的,所以我們先檢視一下UrlConnectionDownloader,也就是load方法

UrlConnectionDownloader

@Override 
public Response load(Uri uri, int networkPolicy) throws IOException {
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
    installCacheIfNeeded(context);
  }
  HttpURLConnection connection = openConnection(uri);
  connection.setUseCaches(true);
  if (networkPolicy != 0) {
    String headerValue;
    if (NetworkPolicy.isOfflineOnly(networkPolicy)) {
      headerValue = FORCE_CACHE;
    } else {
      StringBuilder builder = CACHE_HEADER_BUILDER.get();
      builder.setLength(0);
      if (!NetworkPolicy.shouldReadFromDiskCache(networkPolicy)) {
        builder.append("no-cache");
      }
      if (!NetworkPolicy.shouldWriteToDiskCache(networkPolicy)) {
        if (builder.length() > 0) {
          builder.append(',');
        }
        builder.append("no-store");
      }
      headerValue = builder.toString();
    }
    //設定快取策略
    connection.setRequestProperty("Cache-Control", headerValue);
  }

  int responseCode = connection.getResponseCode();
  if (responseCode >= 300) {
    connection.disconnect();
    throw new ResponseException(responseCode + " " + connection.getResponseMessage(),
        networkPolicy, responseCode);
  }
long contentLength = connection.getHeaderFieldInt("Content-Length", -1);
  //我們根據服務端返回的Response的Header來判斷是走快取還是重新取資料
boolean fromCache = parseResponseSourceHeader(connection.getHeaderField(RESPONSE_SOURCE));
  return new Response(connection.getInputStream(), fromCache, contentLength);
}
複製程式碼

緊接著看一下UrlConnectionDownloader

OkHttpDownloader

@Override 
public Response load(Uri uri, int networkPolicy) throws IOException {
  CacheControl cacheControl = null;
  if (networkPolicy != 0) {
    if (NetworkPolicy.isOfflineOnly(networkPolicy)) {
      cacheControl = CacheControl.FORCE_CACHE;
    } else {
      CacheControl.Builder builder = new CacheControl.Builder();
      if (!NetworkPolicy.shouldReadFromDiskCache(networkPolicy)) {
        builder.noCache();
      }
      if (!NetworkPolicy.shouldWriteToDiskCache(networkPolicy)) {
        builder.noStore();
      }
      cacheControl = builder.build();
    }
  }

  Request.Builder builder = new Request.Builder().url(uri.toString());
  if (cacheControl != null) {
    //設定快取策略
    builder.cacheControl(cacheControl);
  }
  com.squareup.okhttp.Response response = client.newCall(builder.build()).execute();
  int responseCode = response.code();
  if (responseCode >= 300) {
    response.body().close();
    throw new ResponseException(responseCode + " " + response.message(), networkPolicy,
        responseCode);
  }
 //是否讀取快取的標誌
  boolean fromCache = response.cacheResponse() != null;
  ResponseBody responseBody = response.body();
  return new Response(responseBody.byteStream(), fromCache, responseBody.contentLength());
}
複製程式碼

儲存目錄

在開發Android的過程中,也會涉及到很多的IO操作,比如說網路請求,下載圖片等,由於很多框架平時已經幫我們封裝好了,所以平時容易忽略,下面簡單分析一下Android下的儲存目錄:

Android平臺的儲存目錄

內部儲存

data資料夾就是我們常說的內部儲存,對於沒有root的手機來說,我們是沒有許可權開啟這個資料夾的但是可以訪問到,

外部儲存

外部儲存才是我們平時操作最多的,外部儲存一般就是我們上面看到的storage資料夾,當然也有可能是mnt資料夾,這個名稱不影響我們運算元據。

路徑獲取

兩種儲存方式都是通過Context類來進行獲取的

內部儲存

   getFilesDir();//獲取內部儲存的File路徑
   getCacheDir();//獲取內部儲存的Cache路徑
   getDatabasePath("demo.db");//獲取database路徑
   getSharedPreferences("demo",MODE_PRIVATE);//獲取SP
複製程式碼

外部儲存

   getExternalCacheDir();//獲取外部儲存私有目錄
   getExternalFilesDir(Environment.DIRECTORY_DCIM);//獲取外部儲存公有目錄
      
複製程式碼

清除快取/清除資料

清除快取:快取是程式執行時的臨時儲存空間,它可以存放從網路下載的臨時圖片,從使用者的角度出發清除快取對使用者並沒有太大的影響,但是清除快取後使用者再次使用該APP時,由於本地快取已經被清理,所有的資料需要重新從網路上獲取,注意:為了在清除快取的時候能夠正常清除與應用相關的快取,請將快取檔案存放在getCacheDir()或者 getExternalCacheDir()路徑下。 清除資料:清除使用者配置,比如SharedPreferences、資料庫等等,這些資料都是在程式執行過程中儲存的使用者配置資訊,清除資料後,下次進入程式就和第一次進入程式時一樣

關於許可權

Android6.0以後,谷歌加強了對使用者許可權的控制,但是這個許可權只是針對於外部儲存的公有目錄,對於私有目錄的,仍然可以正常訪問。所以當遇到有些手機許可權很難適配的時候可以把檔案儲存在外部儲存的私有目錄。

總結

磁碟快取在Android中需要注意訪問外部儲存時候需要許可權,注意各個不同路徑下的區別,同時需要結合Http快取注意快取的時效性。

相關文章