在云端 Mac 上验收 iOS 图片 HTTP 缓存

在云端 Mac 上验收 iOS 图片 HTTP 缓存

一张列表缩略图首次打开正常,第二次几乎瞬间出现,开发者很容易判定“缓存已生效”。但它也可能只是视图还保留着图片对象。要验收 iOS 的 HTTP 缓存,得让服务端说出自己收到了什么:首次应返回图片正文;内容过期后,客户端应带着 ETag 重新询问;离线读取则要单独测试,不能拿一次快速刷新代替。

先固定测试边界

在云端 Mac 上同时运行测试服务和 iOS 模拟器。用模拟器访问 localhost,避免把跨地区链路波动混进缓存判断。测试素材选一张很小的 PNG,放在当前工作目录并命名为 thumb.png;不要在这一轮更换 URL、账户或图片解码器。

这次只验证三个可观察结果:

场景 服务端应看到什么 应用应得到什么
首次请求 无 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 连续请求两次,每次都等任务完成,并确认得到的是可解码的 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")!

让应用的两次图片请求共用这里的 session,不要每次新建配置。模拟器测试目标若被传输安全策略拦下本地 HTTP,可仅在测试目标的配置中启用本地网络例外;不要把开发用例外带进正式应用。若服务日志根本没有第二笔请求,先检查是否复用了内存里的图片、是否真的发起了第二次加载,再排查缓存策略。只看请求完成时间,分不清这些路径。

改变图片时检查 ETag

把服务端的 TAG 改为 '"thumb-v2"',同时替换 thumb.png,重启服务后再请求。旧 ETag 不匹配,新响应应为 200,应用也应拿到新图片。实际项目的 ETag 必须跟随内容版本变化;文件换了却沿用旧标识,会让重新验证错误地保留旧图。

把离线读取当成另一项测试

先在线完成一次 200 请求,确认该响应允许存储。随后停止测试服务,为同一个 URL创建采用 .returnCacheDataDontLoad 策略的请求,并仍通过持有原缓存的 session 发起任务。成功意味着本次从缓存读取;失败也需要被记录为缓存未命中,而不是自动宣称服务端异常。测试完成后检查服务日志:停止服务前后不应出现由离线请求产生的新记录。

这里的离线策略是应用明确选择的行为,不等于默认请求在断网时一定会回退到旧图。缓存容量、系统清理和响应头都会影响命中率。实际界面应为未命中准备占位状态,并在网络恢复后允许重试,而不是假设“曾经打开过”就一定可读。

排查误判并留下验收记录

每轮记录测试 URL、ETag、响应缓存头、服务日志中的状态序列,以及应用最终显示的图片版本。若出现 200→200,优先查响应是否可缓存、两次请求是否共用 URL 与缓存;若服务端显示 304 但 UI 仍是旧图,检查是否在响应处理后又被视图层的旧对象覆盖。若更新图片后仍返回 304,先核对服务端标识是否真的更新。

最后用全新安装的模拟器应用重复首轮测试,避免前一次实验留下的缓存影响结论。这套记录把“图片看起来加载很快”改成可复核的 200、304、离线命中与未命中四种结果,也便于团队在调整图片服务或客户端缓存策略后复测。

常见问题

客户端拿到图片时,怎样确认服务端确实返回了 304?

查看服务端请求日志中的 If-None-Match 和响应状态;URLSession 可能将重新验证后的缓存内容交给应用,仅看图片是否显示不足以判断。

缓存过的图片为什么断网后仍可能打不开?

缓存可能已被清理、响应禁止存储,或请求仍采用默认网络策略。离线场景应明确选择缓存读取策略,并准备缓存未命中时的界面状态。

独享物理机

给下一次远程开发准备一台 Mac

按用途选机型与节点,使用屏幕共享、VNC 或 SSH 连接。价格与实际可用性以控制台实时返回为准。

配置远程 Mac