[iOS] mitmproxy を使用して iOS アプリ側の HTTP キャッシュの動作を確認する

2015.09.01

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

サーバー (データを提供する側) が HTTP の仕様にそってキャッシュの仕組みを実装すれば、iOS アプリ側は独自の実装を追加しなくてもキャッシュの恩恵を受けられるのかを調査しました。

環境

  • OS X 10.10.5(14F27)
  • Xcode Version 6.4 (6E35b) / iOS SDK 8.4
  • development target: iOS 8.0

目次

実現したい動作

今回実現したい動作は以下の通りです。

  • iOS アプリは一定のタイミングで JSON データの取得を試みる (アプリがフォアグラウンドになったタイミングなど)
  • 初めてデータの取得を試みた場合は、サーバーから JSON データを取得する
  • データが更新されていれば、サーバーから JSON データを取得する
  • 更新されていなければ、アプリ上に保存されたキャッシュデータを使う

JSON データのサイズがある程度大きい場合を想定しており、HTTP キャッシュの仕組みを利用してデータのやり取りを削減します。

iOS の HTTP キャッシュ周りの動作

URL loading system

まずは iOS の内部でのキャッシュ周りの動作を調べてみました。iOS の場合、「URL loading system」と呼ばれる機構が HTTP レスポンスをキャッシュする機能を提供しているので、通常は独自の実装を追加する必要はありません。

また、キャッシュの動作設定は NSURLRequest の cachePolicy に値を設定して行いますが、デフォルトが NSURLRequestUseProtocolCachePolicy (プロトコルのキャッシュ方針に従う) になっているので、値を変更する必要もありません。

  • Understanding Cache Access

The URL loading system provides a composite on-disk and in-memory cache of responses to requests. This cache allows an application to reduce its dependency on a network connection and increase its performance.

  • Using the Cache for a Request

An NSURLRequest instance specifies how the local cache is used by setting the cache policy to one of the NSURLRequestCachePolicy values: NSURLRequestUseProtocolCachePolicy, NSURLRequestReloadIgnoringCacheData, NSURLRequestReturnCacheDataElseLoad, or NSURLRequestReturnCacheDataDontLoad.

The default cache policy for an NSURLRequest instance is NSURLRequestUseProtocolCachePolicy. The NSURLRequestUseProtocolCachePolicy behavior is protocol specific and is defined as being the best conforming policy for the protocol.

Understanding Cache Access - URL Loading System Programming Guide より引用

公式以外のページでも「通常は独自の実装を追加する必要はないよ!」って書いてあります。

If you are trying to implement your own HTTP caching methodology you are probably doing it wrong.

Image Caching Guide - DFImageManager より引用

iOS の HTTP キャッシュの動作に関する記事の中には日本語の記事もあります。

キャッシュ周りの動作は隠蔽されている

URL loading system と呼ばれる機構によって HTTP レスポンスが適切にキャッシュされることはわかりました。

しかし、キャッシュ周りの動作は隠蔽されているので、アプリ側から動作を確認することができません。例えば、データの更新がない場合にサーバーがステータスコード 304 を返しても、NSHTTPURLResponse の statusCode の値は 200 になります。

It's somewhat maddening because NSURLConnection handles it completely transparently -- you cannot see the header in the request at all and if a 304 response is returned by the server you will only see the 200 response that it transparently loaded from the cache.

Adding If-Modified-Since HTTP Header when available. より引用

動作確認用の環境を準備する

オリジナルのリクエスト/レスポンスヘッダをアプリ側から確認することができないことがわかったので、通信を監視するツールを使ってリクエスト/レスポンスヘッダを確認することにしました。

mitmproxy

今回は mitmproxy というコンソール・ツールを使用しました。

ios-http-cache-mitmproxy-01

mitmproxy is a console tool that allows interactive examination and modification of HTTP traffic. It differs from mitmdump in that all flows are kept in memory, which means that it's intended for taking and manipulating small-ish samples. Use the ? shortcut key to view, context-sensitive documentation from any mitmproxy screen.

mitmproxy より引用

ネットワーク周りに詳しい人に聞くと「Wireshark」というツールが定番らしいんですが、インストールで躓いてしまったので今回は mitmproxy を使いました。同様のツールは他にもあるようです。

環境を準備する

以下のそれぞれについて準備します。

  1. サーバー上にデータを準備
  2. mitmproxy をインストールして、起動する
  3. iOS 端末に証明書をインストール
  4. iOS 端末の Wifi 設定画面でプロキシの設定を行う

2〜4 の項目、mitmproxy の基本操作については「モバイルアプリ開発者のための mitmproxy 入門 - Qiita」に詳しく書いてありますので、必要に応じて参照してください。

1. サーバー上にデータを準備

今回は Amazon S3 に JSON ファイルをアップロードして検証しました。ファイルのメタデータは以下のように設定しました。

  • JSON ファイルのメタデータの設定
  • Content-Type
  • application/json
  • Cache-Control
  • public, max-age=0

2. mitmproxy をインストールして、起動する

$ brew install python // Python をインストール
$ easy_install pip // Python のパッケージ管理ツール pip をインストール
$ pip install mitmproxy // pip で mitmproxy をインストール
$ mitmproxy // mitmproxy を起動

3. iOS 端末に証明書インストール

mitmproxy をインストールすると、以下のディレクトリに「mitmproxy-ca-cert.pem」ファイルがコピーされます。 このファイルをメール等の方法で実機に移してインストールします。今回は標準のメールアプリを使いました。

  • ~/.mitmproxy/

ios-http-cache-mitmproxy-02

ios-http-cache-mitmproxy-03

ios-http-cache-mitmproxy-04

ios-http-cache-mitmproxy-05

ios-http-cache-mitmproxy-06

4. iOS 端末の Wifi 設定画面でプロキシの設定を行う

今回は、「mitmproxy をインストールした Mac」と「iOS 端末」を同じネットワークに接続して検証しました。

設定アプリの Wifi 設定画面でプロキシの設定を行います。

ios-http-cache-mitmproxy-07

ios-http-cache-mitmproxy-08

「サーバ」項目には「mitmproxy をインストールした Mac」の IP アドレスを入力します。IP アドレスは「システム環境設定」の「ネットワーク」を開くと確認できます。

ios-http-cache-mitmproxy-09

環境の準備は以上です。

準備が終わった状態で iOS 端末側で通信を行うと、mitmproxy 側に以下のようなログが表示されます。

ios-http-cache-mitmproxy-10

動作を検証する

mitmproxy のログを見ながら操作を行いました。「実現したい動作」通りの動作を確認することが出来ました!

サーバーから JSON データを取得する処理が実行されるパターン (初回)

モバイルアプリのリクエスト

ヘッダに If-None-Match 項目はありません。

  • ヘッダ
  • (If-None-Match の指定なし)

Amazon S3 のレスポンス

ヘッダに ETag、ボディに JSON データが入っています。

  • ステータスコード: 200
  • ヘッダ
  • ETag: "406c10aa7c967b5859d03df6ab62c49d"
  • Cache-Control: public, max-age=0
  • ボディ:
  • (JSONデータ)

サーバーから JSON データを取得する処理が実行されないパターン (2回目以降、データが更新されていない場合)

モバイルアプリのリクエスト

ヘッダに If-None-Match 項目が入っています。値は前回取得した ETag です。

  • ヘッダ
  • If-None-Match: "406c10aa7c967b5859d03df6ab62c49d"

Amazon S3 のレスポンス

ステータスコードは 304 で、レスポンスボディはありません。

  • ステータスコード: 304 Not Modified
  • ヘッダ
  • ETag: "406c10aa7c967b5859d03df6ab62c49d"
  • Cache-Control: public, max-age=0
  • ボディ
  • (レスポンスボディなし)

サーバーから JSON データを取得する処理が実行されるパターン (2回目以降、データが更新されている場合)

モバイルアプリのリクエスト

ヘッダに If-None-Match 項目が入っています。

  • ヘッダ
  • If-None-Match: "406c10aa7c967b5859d03df6ab62c49d"

Amazon S3 のレスポンス

ヘッダに 新しい Etag、ボディに更新されたデータが入っています。

  • ステータスコード: 200
  • ヘッダ
  • Cache-Control: public, max-age=0
  • ETag: "5d2a1cda06b91f3a143f13df62f04bd0"
  • ボディ:
  • (JSONデータ)

動作検証用のサンプルアプリ

Amazon S3 に対して GET を叩くだけの簡単なサンプルアプリを作って検証しました。

ios-http-cache-mitmproxy-11

  • サンプルアプリで使用したクラス (キャッシュに関する動作はどちらも同じ)
  • AFHTTPSessionManager
  • NSURLSession
  • キャッシュポリシーの設定
  • NSURLRequestUseProtocolCachePolicy (デフォルトの設定)

まとめ

本記事では mitmproxy を使用してオリジナルのリクエスト・レスポンスヘッダを確認し、iOS のキャッシュ周りの動作を確認しました。独自の実装を追加しなくても HTTP のキャッシュの仕組みの恩恵を受けられることを検証できました。

iOS のキャッシュ周りを調査している方のためになれば幸いです。

参考記事

HTTP の仕様に関する記事

iOS に関する記事

URL Loading System
NSURLCache
その他

mitmproxy に関する記事