在遠端 Mac 上驗收 iOS 圖片 HTTP 快取

在遠端 Mac 上驗收 iOS 圖片 HTTP 快取

列表縮圖第一次開啟時正常載入,第二次卻幾乎立刻出現,開發者很容易認定「快取已生效」。但也可能只是檢視仍保留著圖片物件。要驗收 iOS 的 HTTP 快取,必須從伺服器確認它實際收到的請求:首次請求應回傳圖片內容;內容過期後,客戶端應帶著 ETag 再次詢問;離線讀取則要另外測試,不能用一次快速重新整理代替。

先界定測試範圍

在雲端 Mac 上同時執行測試伺服器和 iOS 模擬器。從模擬器存取 localhost,避免跨地區網路連線的波動干擾快取判斷。測試素材選用一張很小的 PNG,放在目前工作目錄並命名為 thumb.png;這一輪不要更換 URL、帳號或圖片解碼器。

這次只驗證三種可觀察的結果:

情境 伺服器應看到什麼 App 應取得什麼
首次請求 沒有 If-None-Match,回傳 200 圖片位元組資料
再次請求 帶有相符的 ETag,回傳 304 經快取重新驗證的圖片
明確指定離線讀取 沒有新請求 已儲存的圖片,或明確的快取未命中結果

304 不含圖片內容。App 仍能顯示圖片,是因為客戶端結合了已儲存的內容與重新驗證結果;不要把「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 連續請求兩次,每次都等任務完成,並確認取得的 PNG 可以解碼。觀察伺服器記錄:第一筆應為 200;若第二筆確實送達伺服器,應帶有 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")!

讓 App 的兩次圖片請求共用這個 session,不要每次都建立新設定。如果模擬器測試目標因傳輸安全性政策而無法存取本機 HTTP,可以只在測試目標的設定中允許本機網路例外;不要把開發用例外帶進正式 App。若伺服器記錄中完全沒有第二筆請求,先檢查是否重用了記憶體中的圖片、是否真的發起第二次載入,再排查快取策略。只看請求完成時間,無法區分這些情況。

更換圖片時檢查 ETag

將伺服器的 TAG 改為 '"thumb-v2"',同時替換 thumb.png,重新啟動伺服器後再發出請求。舊 ETag 不相符,新回應應為 200,App 也應取得新圖片。實際專案的 ETag 必須隨內容版本變更;檔案換了卻沿用舊識別碼,會讓重新驗證錯誤地保留舊圖。

將離線讀取視為另一項測試

先在線上完成一次 200 請求,確認該回應允許儲存。接著停止測試伺服器,針對同一個 URL建立採用 .returnCacheDataDontLoad 策略的請求,並仍透過持有原快取的 session 執行任務。成功代表這次從快取讀取;失敗也應記錄為快取未命中,而不是直接認定伺服器異常。測試完成後檢查伺服器記錄:停止服務前後,都不應出現由離線請求產生的新紀錄。

這裡的離線策略是 App 明確選擇的行為,不代表預設請求在斷網時一定會回退使用舊圖。快取容量、系統清理與回應標頭都會影響命中率。實際介面應為未命中準備佔位狀態,並允許在網路恢復後重試,而不是假設「曾經開啟過」就一定能讀取。

排查誤判並保留驗收記錄

每一輪都記錄測試 URL、ETag、回應的快取標頭、伺服器記錄中的狀態序列,以及 App 最後顯示的圖片版本。若出現 200→200,優先檢查回應是否可快取、兩次請求是否共用 URL 與快取;若伺服器顯示 304,但 UI 仍是舊圖,檢查回應處理後是否又被檢視層的舊物件覆蓋。若更新圖片後仍回傳 304,先核對伺服器的識別碼是否真的更新。

最後,使用全新安裝的模擬器 App 重複首輪測試,避免前一次實驗留下的快取影響結論。這份記錄將「圖片看起來載入得很快」轉化為可複核的 200、304、離線命中與未命中四種結果,也方便團隊在調整圖片服務或客戶端快取策略後重新測試。

常見問題

如何證明伺服器確實回傳 304?

檢查伺服器紀錄中的 If-None-Match 與回應狀態;圖片顯示正常並不能單獨證明發生了重新驗證。

曾經載入的圖片為何仍可能無法離線開啟?

快取可能已遭清除、回應不允許儲存,或請求仍沿用一般網路策略;應另外處理快取未命中的情況。

獨享實體主機

為下一次遠端開發準備一台 Mac

依照使用需求選擇機型與節點,透過螢幕共享、VNC 或 SSH 連線。價格與實際供應狀況以控制台即時顯示為準。

設定遠端 Mac