iOSにおける通信キャッシュについて

iOSにおける通信キャッシュについて

今回はiOSの通信キャッシュについていくつか調べてみました。

検証環境

  • iOS 9.0

iOSではキャッシュポリシーを設定することで透過的に通信キャッシュを扱うことができます。

キャッシュポリシーの種類

NSURLRequestCachePolicy

設定 内容 キャッシュ
UseProtocolCachePolicy プロトコルの規約に従う(デフォルト) 使う
ReloadIgnoringLocalCacheData キャッシュを使わない 使わない
ReloadIgnoringLocalAndRemoteCacheData 未実装
ReloadIgnoringCacheData NSURLRequestReloadIgnoringLocalCacheDataと同じ
ReturnCacheDataElseLoad キャッシュがある場合はキャッシュを使い、無い場合は通信する 使う
ReturnCacheDataDontLoad 常にキャッシュを使い、キャッシュがなければエラーになる 使う
ReloadRevalidatingCacheData 未実装

未実装と書いてあるところはヘッダファイルでは以下のようにUnimplementedとなっているものです。

{
    NSURLRequestUseProtocolCachePolicy = 0,

    NSURLRequestReloadIgnoringLocalCacheData = 1,
    NSURLRequestReloadIgnoringLocalAndRemoteCacheData = 4, // Unimplemented
    NSURLRequestReloadIgnoringCacheData = NSURLRequestReloadIgnoringLocalCacheData,

    NSURLRequestReturnCacheDataElseLoad = 2,
    NSURLRequestReturnCacheDataDontLoad = 3,

    NSURLRequestReloadRevalidatingCacheData = 5, // Unimplemented
};

キャッシュの保存場所

通信関連のキャッシュはNSURLRequest(NSMutableURLRequest)を使っている限りは/Library/Caches/Cache.dbに自動的に保存されます。
Cache.dbには、WebViewのキャッシュなども保存されます。

キャッシュポリシーの変更方法

以下のコードは、キャッシュポリシーを設定した上でリクエストを非同期で実行する実装例です。

request = NSURLRequest(URL: NSURL(string: "https://example.co.jp/path/to")!, cachePolicy: NSURLRequestCachePolicy.ReturnCacheDataElseLoad, timeoutInterval: 10.0)

let session = NSURLSession.sharedSession()
let task = session.dataTaskWithRequest(self.request!) { (dat: NSData?, res: NSURLResponse?, err: NSError?) in {
    // コールバックの処理
}
task!.resume()

UseProtocolCachePolicyの挙動

UseProtocolCachePolicy, NSURLRequestReturnCacheDataElseLoad, NSURLRequestReturnCacheDataDontLoad はそれぞれキャッシュを使うポリシーですが、デフォルトになっているUseProtocolCachePolicy だけ挙動が理解しにくいです。

UseProtocolCachePolicyを使った場合、iOS側はETagをリクエストヘッダに含めてリクエストをします。
サーバサイドがETagに対応していた場合、前回の通信からレスポンスに変更が無ければ通常はステータスコード304が返却されてレスポンスボディは空になるはずですが、そのあたりはiOS側のライブラリが透過的に処理をしているようで、レスポンスに変更が無くてもステータスコード200とレスポンスボディが返却されてきます。
そのため、更新の有無を確認する場合はステータスコードではなくキャッシュから取得したETagとレスポンスヘッダのETagを比較する必要があります。

ちなみに、iOS側からHEADでリクエストを送った場合でもキャッシュが無い場合はボディは空になりますが、キャッシュがある場合はレスポンスボディが返ってきます

ETagを使って更新の有無を確認する方法

UseProtocolCachePolicyでは通信に成功した場合はコンテンツの変更の有無にかかわらずステータスコード200が返却されるのでステータスコードで更新の有無を確認することはできませんが、ETagを比較することで更新の有無を確認できます。
HEADリクエストでETagを取得して比較すれば、ネットワークのトラフィックを軽減できます。

ETagの比較実装例

以下のようにリクエストからキャッシュレスポンスを取得してヘッダからETagを取得し、HEADでリクエストして取得したallHeaderFieldsのeTagを比較します。
※以下の実装例はイメージなので動くことを保証しません

request = NSMutableURLRequest(URL: NSURL(string: "https://example.co.jp/path/to")!, cachePolicy: NSURLRequestCachePolicy.ReturnCacheDataElseLoad, timeoutInterval: 10.0)
request!.HTTPMethod = "HEAD"

let session = NSURLSession.sharedSession()
let task = session.dataTaskWithRequest(self.request!) { (dat: NSData?, res: NSURLResponse?, err: NSError?) in {
    
    if let cacheResponse = NSURLCache.sharedURLCache().cachedResponseForRequest(request!) {
        let httpResponse = cacheResponse.response as! NSHTTPURLResponse
        let cacheResponse = cacheResponse.response as! NSHTTPURLResponse
        if let eTag == cacheResponse.allHeaderFields["Etag"] as? String {
            // eTagとリクエストのレスポンスのallHeaderFieldsから取得したeTagを比較
        }
    }
}
task!.resume()

余談

Alamofire(3.1.5で確認)ではポリシーをUseProtocolCachePolicyにしていてもETagをつけてくれないようです。

TAG

  • このエントリーをはてなブックマークに追加
金子 将範
エンジニア 金子 将範 rubyist

新しいことや難しい課題に挑戦することにやりがいを感じ、安定やぬるい事は退屈だと感じます。 考えるより先に手が動く、肉体派エンジニアで座右の銘は諸行無常。 大事なのは感性、プログラミングにおいても感覚で理解し、感覚で書きます。