Миниатюра в списке при первом открытии загружается как обычно, а во второй раз появляется почти мгновенно. Легко решить, что «кеш заработал». Но, возможно, представление просто сохранило объект изображения. Чтобы проверить HTTP-кеш в iOS, нужно видеть, какие запросы получает сервер: при первом запросе он должен вернуть само изображение, после истечения срока действия сохранённого ответа клиент должен повторно обратиться с ETag, а чтение без сети следует проверять отдельно. Быстрое повторное отображение не заменяет эти проверки.
Сначала зафиксируем условия теста
Запустите тестовый сервер и симулятор iOS на одном удалённом Mac. Обращайтесь из симулятора к 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 и код ответа. Видимая в приложении картинка не доказывает повторную проверку.
Почему ранее загруженная картинка недоступна без сети?
Запись могла быть удалена из кеша или запрещена к сохранению. Обрабатывайте отсутствие записи при явном чтении кеша.
Подготовьте Mac для следующей удалённой разработки
Выберите модель и регион под свои задачи, а затем подключитесь через общий доступ к экрану, VNC или SSH. Актуальные цены и доступность смотрите в консоли.