リモートMacでiOS画像のHTTPキャッシュを検証する

リモートMacでiOS画像のHTTPキャッシュを検証する

一覧のサムネイルが初回は普通に表示され、2回目はほぼ一瞬で表示されると、「キャッシュが効いている」と判断したくなります。しかし、ビューが画像オブジェクトを保持しているだけかもしれません。iOSのHTTPキャッシュを検証するには、サーバーが受け取ったリクエストを確認する必要があります。初回は画像本体が返ること、保存した応答の有効期限が切れた後はクライアントがETagを添えて再検証すること、オフラインでも読み出せるかは別にテストすること。この3つを、一度の高速な再表示だけで判断しないようにします。

テスト条件を先に固定する

リモートMac上でテスト用サーバーとiOSシミュレータを同時に動かします。シミュレータからlocalhostにアクセスすれば、地域間の通信経路の変動をキャッシュの判定に持ち込まずに済みます。テスト画像には小さなPNGを1枚選び、カレントディレクトリにthumb.pngという名前で置いてください。このテスト中にURL、アカウント、画像デコーダーは変更しません。

今回は、観測できる次の3つの結果だけを検証します。

場面 サーバーで確認すること アプリが取得するもの
初回リクエスト If-None-Matchなし、200を返す 画像のバイト列
再リクエスト 一致するETagを受け取り、304を返す キャッシュを再検証して得た画像
明示的なオフライン読み出し 新しいリクエストなし 保存済みの画像、または明確なキャッシュミス

304には画像本体が含まれません。それでもアプリに画像が表示されるのは、クライアントが保存済みの内容を再検証の結果と組み合わせて使うためです。「UIに画像が渡された」ことを「サーバーが画像をもう一度送った」と記録しないでください。

条件付きリクエストを記録するサーバーを起動する

画像のあるディレクトリに、次のスクリプトをfixture.pyとして保存します。待ち受けるのはローカルのループバックアドレスだけで、テスト用ポートを外部には公開しません。max-age=0により、その後の通常のリクエストでは再検証が必要になります。ETagを固定しておくと、リクエストの流れを追いやすくなります。

from http.server import BaseHTTPRequestHandler, HTTPServer
from pathlib import Path

BODY = Path("thumb.png").read_bytes()
TAG = '"thumb-v1"'

class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path != "/thumb":
            self.send_error(404)
            return
        matched = self.headers.get("If-None-Match") == TAG
        status = 304 if matched else 200
        print(f"If-None-Match={self.headers.get('If-None-Match')!r} status={status}", flush=True)
        self.send_response(status)
        self.send_header("ETag", TAG)
        self.send_header("Cache-Control", "public, max-age=0")
        if not matched:
            self.send_header("Content-Type", "image/png")
            self.send_header("Content-Length", str(len(BODY)))
        self.end_headers()
        if not matched:
            self.wfile.write(BODY)

HTTPServer(("127.0.0.1", 8765), Handler).serve_forever()

python3 fixture.pyを実行したら、別のターミナルでcurl -si http://127.0.0.1:8765/thumb -o /dev/nullを実行して200が返ることを確かめ、続けて次を実行します。

curl -si -H 'If-None-Match: "thumb-v1"' \
  http://127.0.0.1:8765/thumb -o /dev/null

ログには順に200と304が記録されるはずです。ここで確認できるのはテスト用サーバーが期待どおりに動くことだけで、iOSが条件付きリクエストを自動送信した証拠にはなりません。

同じセッションでシミュレータのリクエストを検証する

テスト対象専用のURLSessionConfiguration.defaultを用意し、十分なメモリ容量を持つURLCacheを設定します。リクエストポリシーは.useProtocolCachePolicyのままにします。同じURLと同じsessionを使って連続して2回リクエストし、それぞれのタスクが完了するまで待ち、デコード可能なPNGが得られたことを確認します。サーバーログでは、1回目は200になるはずです。2回目が実際にサーバーへ届いた場合は、If-None-Matchが付き、304が返るはずです。

let configuration = URLSessionConfiguration.default
configuration.urlCache = URLCache(
    memoryCapacity: 8 * 1024 * 1024,
    diskCapacity: 0
)
configuration.requestCachePolicy = .useProtocolCachePolicy
let session = URLSession(configuration: configuration)
let url = URL(string: "http://localhost:8765/thumb")!

アプリの2回の画像リクエストには、このsessionを共用してください。毎回新しい設定を作らないようにします。シミュレータ上のテスト対象でローカルHTTP通信がトランスポートセキュリティポリシーに遮断される場合は、テスト対象の設定に限ってローカルネットワークの例外を有効にできます。開発用の例外を本番アプリに持ち込まないでください。サーバーログに2回目のリクエストがまったくなければ、まずメモリ内の画像を再利用していないか、2回目の読み込みを本当に開始したかを確認し、その後にキャッシュポリシーを調べます。完了までの時間だけでは、これらを区別できません。

画像を変更するときはETagも確認する

サーバー側のTAGを'"thumb-v2"'に変更し、同時にthumb.pngを差し替えます。サーバーを再起動してリクエストすると、古いETagは一致しないため、新しい応答は200となり、アプリにも新しい画像が渡るはずです。実際のプロジェクトでも、ETagはコンテンツのバージョンに合わせて変える必要があります。ファイルを差し替えても古い識別子を使い続けると、再検証によって古い画像が誤って保持されます。

オフライン読み出しは別のテストとして扱う

まずオンラインで200のリクエストを1回完了させ、その応答が保存可能であることを確認します。その後テスト用サーバーを停止し、同じURLに対して.returnCacheDataDontLoadポリシーのリクエストを作成します。タスクの実行には、元のキャッシュを保持するsessionを引き続き使います。成功すれば、このリクエストではキャッシュから読み出せたことになります。失敗した場合も、サーバーの異常と決めつけず、キャッシュミスとして記録してください。テスト後にサーバーログを確認し、停止前後を通じてオフラインリクエストによる新しい記録がないことを確かめます。

ここでのオフラインポリシーは、アプリが明示的に選ぶ動作です。デフォルトのリクエストがネットワーク切断時に必ず古い画像へフォールバックする、という意味ではありません。キャッシュ容量、システムによる削除、応答ヘッダーはいずれもヒット率に影響します。実際の画面では、キャッシュミス時のプレースホルダーを用意し、ネットワーク復旧後に再試行できるようにしてください。「以前開いたことがある」だけで必ず読み出せるとは想定しないようにします。

誤判定を調べ、検証結果を記録する

各回のテストURL、ETag、応答のキャッシュヘッダー、サーバーログに記録されたステータスの順序、アプリが最終的に表示した画像のバージョンを記録します。200→200となった場合は、まず応答がキャッシュ可能か、2回のリクエストで同じURLとキャッシュを使っているかを調べます。サーバーが304を返したのにUIが古い画像のままなら、応答の処理後にビュー層が保持する古いオブジェクトで上書きしていないか確認します。画像を更新しても304が返る場合は、最初にサーバー側の識別子が本当に更新されたかを確認してください。

最後に、シミュレータへアプリを新規インストールして初回のテストを繰り返し、前の実験で残ったキャッシュが結論に影響しないようにします。この記録があれば、「画像が速く表示された」という印象を、200、304、オフラインでのキャッシュヒットとキャッシュミスという再確認可能な4種類の結果に置き換えられます。画像配信サービスやクライアントのキャッシュポリシーを変更した後も、チームで再テストしやすくなります。

よくある質問

サーバーが304を返したと確認する方法は?

サーバー側でIf-None-Matchと応答ステータスを記録します。アプリに画像が表示されたことだけでは判別できません。

取得済みの画像をオフラインで開けないのはなぜ?

キャッシュが削除されたか、応答が保存不可だった可能性があります。キャッシュを明示的に読む処理と、未命中時の表示を用意します。

専有物理サーバー

次回のリモート開発に備えてMacを用意

用途に合わせてモデルとロケーションを選び、画面共有、VNC、SSHで接続できます。価格と空き状況はコンソールでリアルタイムにご確認ください。

リモートMacを設定する