はじめに
サーバ間からダウンロードしたデータをスマホに表示させたいと思い、キャッシュを使った効率的な方法を検索しました。しかし、いろいろな方法が書いてあり、古い情報もありそうで、どれが正しいかわからなかったので、実機で検証してみました。結論としては、サーバを正しく設定すれば、基本的には、iOSはデフォルトのURLSession
を利用できました。例外として、HTTP応答がbyte-range requests
の場合は、キャッシュを使わないようなので、このような場合には別の方法を見つける必要がありそうです。
今回、一番つまずいたところは、ネットワーク的には正しくキャッシュの動作(Status:304)をしているのに、アプリのログを見ると、キャッシュを使っていないように見える(Status:200)ところでした。同じように困る方もいると思うので、検証結果も最後に載せておきます。
やりたいこと
- サーバのデータをスマホにダウンロードして表示
- できるだけ最新のデータを利用する
- 同一のデータの場合は、ダウンロードせずに、スマホ内のキャッシュを使う
- 実装はできるだけ標準のものを使い、自分で実装する部分を減らす。
iOSでのキャッシュポリシーの実装
SWIFTでよく使うURLSessionのキャッシュポリシーは、設定しない場合のデフォルトポリシーは、useProtocolCachePolicy
です。この場合の動作は、[Apple社の資料]
(
https://developer.apple.com/documentation/foundation/nsurlrequest/cachepolicy/useprotocolcachepolicy)に書かれています。以下は、この図の引用です。今回実現したいことは、この図に書いた1〜3を実現することなので、URLSessionをそのまま使えば実現できそうです。
しかし、例外として、byte-range requests
の場合には、キャッシュポリシーは、reloadIgnoringLocalCacheData
になるようです。
URLSessionを利用すると簡単なコードでサーバからHTTP GETできます。以下は、検証に使ったコードです。
func getHttp() {
let urlString = "http://example.com/"
guard let url = URL(string: urlString) else { return }
let request = URLRequest(url: url)
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
debugPrint(response.debugDescription) // HTTPヘッダの表示
}
task.resume()
}
Webサーバの設定
上記のようにスマホ側にコンテンツをキャッシュさせるためには、Webサーバに設定が必要です。キャッシュが新しいかどうかを確認するためのHTTPヘッダは、RFC7232で2つ定義されています。Last-Modified
とETag
。Webサーバから少なくともどちらか1つのHTTPヘッダで応答する必要があります。
- Last-Modified (RFC7232 2.2節)
- ETag (RFC7232 2.3節)
また、キャッシュが最新かを常に検証(validation)するためには、WEBサーバで以下のHTTPヘッダのどちらかを追加する必要があります。Cache-Control: no-cache
とCache-Control: max-age=0
。no-cache
は、キャッシュをしない設定に見えますが、そうではなく、キャッシュがあ最新かを検証してからキャッシュを利用するという意味です。
-
Cache-Control: no-cache
(RFC7234 5.2.1.4) -
Cache-Control: max-age=0
(RFC7234 5.2.1.1)
検証環境
- Xcode : 11.2
- iOS: 13.2.2
検証結果概要
検証結果を以下の図で示します。構成要素は、以下の3つです。
- iOSアプリ
- iOS内のキャッシュ
- Webサーバ
検証は、上記の図で示した3つのパターンを試します。
- キャッシュが存在しない場合、サーバから取得
- キャッシュが新しい場合、キャッシュを利用
- キャッシュが古い場合、サーバから取得
1と3は、サーバからの応答と同じ内容をアプリで見れます。
しかし、2のキャッシュが新しい場合は、サーバから304 NOT MODIFIED
応答が来て、データは転送されません。このため、ネットワークの転送量は少なくなります。しかし、アプリから見る応答は、200 OK
と見えます。サーバからデータが転送されているように見えますが、実際にはキャッシュを使っています。
この図では、Cache-Control: no-cache
Last-Modified
を利用した例を図示していますが、Cache-Control: max-age=0
やETag
を利用した場合もキャッシュの利用は同じでしたので、図は省略します。
参考文献
-
HTTP キャッシュ (MDN Docs)
説明がわかりやすい -
HTTP/1.1: Caching (RFC7234)
キャッシュの仕様。RFC2616(旧仕様)の更新版。 -
HTTP/1.1: Conditional Requests (RFC7232)
条件付き要求の仕様。RFC2616(旧仕様)の更新版。 - URLSessionのデフォルトキャッシュ動作 (Apple社)
参考資料: 検証結果詳細
以下では、サーバのResponseにCache-Control: no-cache
を入れた場合を最初に載せます。動作は同じでしたが、以下の場合も載せておきます。
-
Cache-Control: no-cache
の場合 -
Cache-Control: max-age=0
の場合 -
Cache-Control: no-cache
ETag
の場合
HTTPヘッダ(Cache-Control: no-cache)の場合
初回アクセス
初回アクセス時のiOSからサーバへのHTTP Request
GET / HTTP/1.1
Host: example.com
初回アクセス時のサーバからiOSへのHTTP Response
HTTP/1.1 200 OK
Date: Tue, 12 Nov 2019 02:59:55 GMT
Last-Modifi![20191113-URLSession.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/404406/e1dbbef6-9589-4e80-a00d-babdff924194.png)
ed: Wed, 04 Sep 2019 00:00:00 GMT
Cache-Control: no-cache
アプリ(URLSession)で取得できるHTTP Response。このResponseデータがスマホのキャッシュに保存される。
HTTP/1.1 200 OK
Date: Tue, 12 Nov 2019 02:59:55 GMT
Last-Modified: Wed, 04 Sep 2019 00:00:00 GMT
Cache-Control: no-cache
2回目アクセス(スマホ内のキャッシュ利用)
2回目アクセス時のiOSからサーバへのHTTP Request。キャッシュにLast-Modified
がある場合は、If-Modified-Since
をRequestに含める。
GET / HTTP/1.1
Host: example.com
If-Modified-Since: Wed, 04 Sep 2019 00:00:00 GMT
2回目アクセス時のサーバからiOSへのHTTP Response。スマホ内のキャッシュを利用する。キャッシュからDateフィールドを更新してアプリに応答
![undefined]()
HTTP/1.1 304 NOT MODIFIED
Date: Tue, 12 Nov 2019 03:01:01 GMT
Last-Modified: Wed, 04 Sep 2019 00:00:00 GMT
Cache-Control: no-cache
アプリ(URLSession)で取得できるHTTP Response
HTTP/1.1 200 OK
Date: Tue, 12 Nov 2019 03:01:01 GMT
Last-Modified: Wed, 04 Sep 2019 00:00:00 GMT
Cache-Control: no-cache
HTTPヘッダ(Cache-Control: max-age=0)の場合
初回アクセス
初回アクセス時のiOSからサーバへのHTTP Request
GET / HTTP/1.1
Host: example.com
初回アクセス時のサーバからiOSへのHTTP Response
HTTP/1.1 200 OK
Date: Tue, 12 Nov 2019 03:14:14 GMT
Last-Modified: Wed, 04 Sep 2019 00:00:00 GMT
Cache-Control: max-age=0
アプリ(URLSession)で取得できるHTTP Response。このResponseデータがスマホのキャッシュに保存される。
HTTP/1.1 200 OK
Date: Tue, 12 Nov 2019 03:14:14 GMT
Last-Modified: Wed, 04 Sep 2019 00:00:00 GMT
Cache-Control: max-age=0
2回目アクセス(スマホ内のキャッシュ利用)
2回目アクセス時のiOSからサーバへのHTTP Request。キャッシュにLast-Modified
がある場合は、If-Modified-Since
をRequestに含める。
GET / HTTP/1.1
Host: example.com
If-Modified-Since: Wed, 04 Sep 2019 00:00:00 GMT
2回目アクセス時のサーバからiOSへのHTTP Response。スマホ内のキャッシュを利用する。キャッシュからDateフィールドを更新してアプリに応答
HTTP/1.1 304 NOT MODIFIED
Date: Tue, 12 Nov 2019 03:15:33 GMT
Last-Modified: Wed, 04 Sep 2019 00:00:00 GMT
Cache-Control: max-age=0
アプリ(URLSession)で取得できるHTTP Response
HTTP/1.1 200 OK
Date: Tue, 12 Nov 2019 03:15:33 GMT
Last-Modified: Wed, 04 Sep 2019 00:00:00 GMT
Cache-Control: max-age=0
HTTPヘッダ(ETag)の場合
初回アクセス
初回アクセス時のiOSからサーバへのHTTP Request
GET / HTTP/1.1
Host: example.com
初回アクセス時のサーバからiOSへのHTTP Response
HTTP/1.1 200 OK
Date: Tue, 12 Nov 2019 06:01:11 GMT
ETag: "7703193a5f4bc6aa199d7d2121684c3f"
Cache-Control: no-cache
アプリ(URLSession)で取得できるHTTP Response。このResponseデータがスマホのキャッシュに保存される。
HTTP/1.1 200 OK
Date: Tue, 12 Nov 2019 06:01:11 GMT
ETag: "7703193a5f4bc6aa199d7d2121684c3f"
Cache-Control: no-cache
2回目アクセス(スマホ内のキャッシュ利用)
2回目アクセス時のiOSからサーバへのHTTP Request。キャッシュにETag
がある場合は、If-None-Match
をRequestに含める。
GET / HTTP/1.1
Host: example.com
If-None-Match: "7703193a5f4bc6aa199d7d2121684c3f"
2回目アクセス時のサーバからiOSへのHTTP Response。スマホ内のキャッシュを利用する。キャッシュからDateフィールドを更新してアプリに応答
HTTP/1.1 304 NOT MODIFIED
Date: Tue, 12 Nov 2019 06:02:13 GMT
ETag: "7703193a5f4bc6aa199d7d2121684c3f"
Cache-Control: no-cache
アプリ(URLSession)で取得できるHTTP Response
HTTP/1.1 200 OK
Date: Tue, 12 Nov 2019 06:02:13 GMT
ETag: "7703193a5f4bc6aa199d7d2121684c3f"
Cache-Control: no-cache