URLresponse не извлекается после сохранения в кеше с использованием storeCachedResponse

Цель

Я пытаюсь вставить данные / ответ из URLRequest в другой URLRequest в моем кэше.

Настроить

Это просто пример кода. Это готово быть сброшенным в проект.

Я пытаюсь использовать ответ + данные, полученные из моего landscapeURLString сетевой запрос... сохранить в кеше моего сеанса для моего запроса lizardURLString.

import UIKit

class ViewController: UIViewController {

    lazy var defaultSession : URLSession = {
        let urlCache = URLCache(memoryCapacity: 500 * 1024 * 1024, diskCapacity: 500 * 1024 * 1024, diskPath: "something")
        let configuration = URLSessionConfiguration.default
        configuration.urlCache = urlCache
        let session = URLSession(configuration: configuration)

        return session
    }()
    lazy var downloadLizzardbutton : UIButton = {
        let btn = UIButton()
        btn.translatesAutoresizingMaskIntoConstraints = false
        btn.setTitle("download lizard image OFFLINE", for: .normal)
        btn.backgroundColor = .blue
        btn.addTarget(self, action: #selector(downloadLizardAction), for: .touchUpInside)
        return btn
    }()

    let imageView : UIImageView = {
        let imageView = UIImageView()
        imageView.translatesAutoresizingMaskIntoConstraints = false
        imageView.contentMode = .scaleAspectFill
        return imageView
    }()

    // I make sure my internet is set to OFF so that it forces this to be read from cache...
    @objc func downloadLizardAction() {
        downloadImage(from: lizardURLString, from: defaultSession)
    }
    let lizardURLString = "https://upload.wikimedia.org/wikipedia/commons/e/e0/Large_Scaled_Forest_Lizard.jpg"
    let landscapeURLString = "https://images.pexels.com/photos/414171/pexels-photo-414171.jpeg"        

    override func viewDidLoad() {
        super.viewDidLoad()
        view.addSubview(imageView)
        view.addSubview(downloadLizzardbutton)
        imageView.pinToAllEdges(of: view)

        downloadImage(from: landscapeURLString, from: defaultSession)
    }
    private func downloadImage(from urlString: String, from session : URLSession){
        guard let url = URL(string: urlString) else{
            fatalError("bad String we got!")
        }

        let urlRequest = URLRequest(url: url, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 15)
        print("url.hashValue: \(urlRequest.hashValue)")

        let task = session.dataTask(with: urlRequest) { [weak self] (data, response, error) in

            guard error == nil else {
                print(error)
                return
            }
            guard let httpResponse = response as? HTTPURLResponse,
                (200...299).contains(httpResponse.statusCode) else {
                    print("response NOT 2xx: \(response)")
                    return
            }

            for header in httpResponse.allHeaderFields{
                if let key = header.key as? String, key == "Cache-Control"{
                    print("found Cache-Control: \(httpResponse.allHeaderFields["Cache-Control"])")
                }
            }

            if let data = data,
                let image = UIImage(data: data){
                let lizardURL = URL(string: self!.lizardURLString)
                let lizardURLRequest = URLRequest(url: lizardURL!)

                let landscapeCachedURLPResponse : CachedURLResponse = CachedURLResponse(response: response!, data: data, userInfo:nil, storagePolicy: .allowed)
                print("before storing into cache: \(String(describing: session.configuration.urlCache?.cachedResponse(for: lizardURLRequest)))")

                session.configuration.urlCache?.storeCachedResponse(landscapeCachedURLPResponse, for: lizardURLRequest)    

                print("after storing into cache: \(String(describing: session.configuration.urlCache?.cachedResponse(for: lizardURLRequest)))")
                print("lizardRequest.hashValue: \(lizardURLRequest.hashValue)")

                DispatchQueue.main.async {
                    self?.imageView.image = image
                }
            }
        }
        task.resume()
    }        
}


extension UIView{

    func pinToAllEdges(of view: UIView){
        let leading = leadingAnchor.constraint(equalTo: view.leadingAnchor)
        let top = topAnchor.constraint(equalTo: view.topAnchor)
        let trailing = trailingAnchor.constraint(equalTo: view.trailingAnchor)
        let bottom = bottomAnchor.constraint(equalTo: view.bottomAnchor)

        NSLayoutConstraint.activate([leading, top, trailing, bottom])
    }
}

Вещи, которые я уже проверил:

  • мой landscapeURLString имеет cache-control заголовок с max-age из 31536000
  • Если это новая установка, то перед сохранением в кеш мой cachedResponse для lizardURLString есть nil, Но после хранения его уже нет nil, В результате я делаю вывод, что я успешно что-то храню в кеше!
  • Я также подозреваю, что URLCache рассматривает URLRequest в качестве ключа. Итак, я напечатал hashValue моего lizardURLString, Это то же самое, что ключ, который я сохранил. Объединяя это с пунктом выше, я пришел к выводу, что точный ключ существует в кеше!
  • Я также вижу, что когда я храню его в кеше, мой currentMemoryUsage увеличивается.

Как я тестирую и что вижу:

  1. Я просто загружаю изображение ландшафта.
  2. Выключи мой интернет
  3. Нажмите кнопку, чтобы загрузить изображение ящерицы.

Очевидно, что это не в сети. Я ожидаю, что он будет использовать из кэша, но это не так. Все, что я получаю, это время!

Я также пытался изменить cachePolicy в returnCacheDataElseLoad но это тоже не помогло

EDIT1:

Я также попытался сделать то, что сказал Дэвид и сделать:

let landscapeHTTPResponse : HTTPURLResponse = HTTPURLResponse(url: self!.lizardURL, statusCode: 200, httpVersion: "HTTP/1.1", headerFields: (httpResponse.allHeaderFields as! [String : String]))!
let landscapedCachedURLPResponse : CachedURLResponse = CachedURLResponse(response: landscapeHTTPResponse, data: data, userInfo:nil, storagePolicy: .allowed)

и хранится landscapedCachedURLPResponse в кеш. Это тоже не сработало. Это также истекает - не все заглядывают в кеш.

EDIT2:

Так что я добился определенного прогресса. Или, возможно, сделал один шаг назад и один шаг вперед.

Я попытался проверить, смогу ли я сохранить ответ для того же URL-адреса, и посмотреть, смогу ли я получить ответ после очистки кеша. Я не смог.

Я создавал свой кэшированный ответ, как это:

let cachedResponse = CachedURLResponse(response: response!, data: data, userInfo:nil, storagePolicy: .allowed)

или просто так:

let cachedResponse = CachedURLResponse(response: response!, data: data)

Что заставило эту часть работать?

let cachedResponseFromCache = session.configuration.urlCache?.cachedResponse(for: self!.landscapeURLRequest)
self._cachedResponse = cachedResponseFromCache

Затем я:

  1. очистил кеш
  2. выключил интернет
  3. попытался загрузить изображение, но безуспешно, что хорошо. Это ожидаемое поведение
  4. хранится cachedResponseFromCache свойство в кеш.
  5. удалось извлечь из кеша!

Я не уверен, в чем разница между извлечением из самого кеша и созданием кеша из Response + Data,

Это важно, потому что я начал сомневаться, есть ли еще какие-то внутренние ошибки в URLCache. Это дало мне основание полагать, что это может работать как ожидалось.

Теперь я знаю процесс сохранения в кеш работ. Я знаю, что мой URLResponse хорош. Мне просто нужно пройти через отображение URLRequest

EDIT3:

Гай Когус предположил, что мои URL должны быть из того же источника. Поэтому, как только я скачал упомянутое им изображение медведя, мое изображение ящерицы проходило. VOILA!

Как очень важное замечание по отладке, которое я узнал: даже если вы добились успеха в какой-то части (то, что она кешировала ландшафтное изображение для себя) проблемы, изменение переменных (здесь изменение исходного URL) всегда может изменить результаты всего тестирования.

Он подозревал, что это потому, что Server в заголовке в общем, и это важно для поиска cachedResponse.

Я опроверг это утверждение, сказав, что мой lizardURLRequest сделан, когда он находится в сети, поэтому сравнивать его не с чем, но он работает! Поэтому следующая идея заключалась в том, что он может иметь какое-то отношение к некоторой части URL, например, к первому сегменту или к чему-то еще.

Тогда я пошел и изменил lizardURL из:

https://upload.wikimedia.org/wikipedia/commons/e/e0/Large_Scaled_Forest_Lizard.jpg

на что-то вроде: https://skdhfsupload.qwiklkjlkjimedia.com/qwikipehkjdia/eeeeeecommons/sdalfjkdse/aldskfjae0/extraParam/anotherextraparam/asasdLarge_Scaled_Forest_Lizard.jpeg

Я добавил тупые символы в URL. Я также добавил дополнительные сегменты в него. Я изменил тип файла в конце.

Все еще это работало. Таким образом, единственное, что я могу сделать вывод, это то, что что-то из Заголовков принимает решения.

Заголовки для моего landscapeURL: (кэширование для другого URL не работает для этого)

Content-Length : 997361
x-cache : HIT, MISS
cf-ray : 472793e93ce39574-IAD
x-served-by : cache-lax8621-LAX, cache-iad2132-IAD
cf-cache-status : HIT
Last-Modified : Sun, 14 Oct 2018 2:10:05 GMT
Accept-Ranges : bytes
Vary : Accept-Encoding
x-content-type-options : nosniff
Content-Type : image/jpeg
expect-ct : max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"
Set-Cookie : __cfduid=d5f5fd59ce5ff9ac86e42f8c008708ae61541004176; expires=Thu, 31-Oct-19 16:42:56 GMT; path=/; domain=.pexels.com; HttpOnly
Expires : Thu, 31 Oct 2019 16:42:56 GMT
Server : cloudflare
Cache-Control : public, max-age=31536000
Date : Wed, 31 Oct 2018 16:42:56 GMT

Заголовки для моего BearURL: (для этого работает кэширование другого URL)

Date : Wed, 31 Oct 2018 16:46:38 GMT
Content-Length : 215104
x-client-ip : 2001:558:1400:4e:808c:2738:43e:36f5
access-control-expose-headers : Age, Date, Content-Length, Content-Range, X-Content-Duration, X-Cache, X-Varnish
x-cache : cp1076 miss, cp1088 hit/21
Age : 27646
Etag : 00e21950bf432476c91b811bb685b6af
Strict-Transport-Security : max-age=106384710; includeSubDomains; preload
x-analytics : https=1;nocookies=1
Accept-Ranges : bytes
x-object-meta-sha1base36 : 42tq5grg9rq1ydmqd4z5hmmqj6h2309
x-varnish : 48388488, 503119619 458396839
x-cache-status : hit-front
Content-Type : image/jpeg
x-trans-id : tx08ed43bbcc1946269a9a3-005bd97070
Last-Modified : Fri, 04 Oct 2013 23:30:08 GMT
Access-Control-Allow-Origin : *
timing-allow-origin : *
x-timestamp : 1380929407.39127
Via : 1.1 varnish (Varnish/5.1), 1.1 varnish (Varnish/5.1)

Важная заметка:

Для BearURL работает кэширование для BearURL и lizardURL или любого другого URL. Для landscapeURL кэширование работает только для самого landscapeURL. Это не работает для любого другого URL.

Итак, текущее состояние вопроса таково: какие заголовки нужно включить, чтобы это работало?

3 ответа

Решение

Добро пожаловать в удивительный мир асинхронных кешей. NSURLCache очень асинхронный. То, что вы поместили данные в них, не означает, что они доступны для поиска. Вы должны позволить основному циклу выполнения вернуться до того, как он станет доступен, и, возможно, даже немного подождать. Неспособность вернуть ответ сразу после его хранения не является чем-то необычным. Попробуйте отправить его через пять секунд или около того.

Во-вторых, ваш кэш может быть небольшого размера для хранения мультимегабайтных изображений. Попробуйте увеличить это и посмотреть, поможет ли это.

Наконец, что вы имеете в виду, когда говорите, что "выключаете Интернет"? Вы говорите, что получаете тайм-аут. Обычно, если вы переводите устройство в режим "В самолете" с отключенным подключением, оно не должно находиться там в течение значительного периода времени, прежде чем произойдет сбой с ошибкой, указывающей на отсутствие подключения). Если этого не происходит, происходит что-то странное, почти так, как если бы в сеансе устанавливалось значение waitsForConnectivity или что-то в этом роде. (Вы не делаете сетевые запросы в фоновом режиме, не так ли? Если это так, попробуйте явно установить для waitsForConnectivity значение NO, чтобы они не ожидали, пока соединение будет доступно.)

Кроме того, для этого использования может потребоваться удалить заголовок Vary: Accept-Encoding или предоставить согласованную строку пользовательского агента. Этот заголовок приводит к тому, что кеш в основном для каждого браузера. Это может привести к тому, что кеш будет вести себя неожиданным образом, и, вероятно, является причиной странности, которую вы видите.

Обратите внимание, что удаление заголовка Vary - это нечто вроде хака, и, вероятно, это не самый правильный способ исправить проблему; в идеале вы должны настроить любые исходящие поля заголовка, которые нужно настроить, чтобы они работали даже при наличии этого заголовка. Но вам придется исследовать это и выяснить, какие именно поля необходимы, потому что я понятия не имею.:-)

Это не полный ответ, но он должен направить вас в правильном направлении.

Проблема не в вашем коде, я думаю, что это в основном нормально. Вопрос касается ответа, который вы получаете от landscapeURLString потому что изображение хранится в Cloudflare. Если вы используете 2 изображения из одного источника (например, попробуйте использовать этого медведя из википедии вместо изображения из images.pexels.com), оно должно работать.

Я попытался распечатать ответ и заголовки загрузки изображения images.pexels.com, и вот что я увидел:

response: <NSHTTPURLResponse: 0x600002bf65c0> { URL: https://upload.wikimedia.org/wikipedia/commons/e/e0/Large_Scaled_Forest_Lizard.jpg } { Status Code: 200, Headers {
    "Accept-Ranges" =     (
        bytes
    );
    "Cache-Control" =     (
        "public, max-age=31536000"
    );
    "Content-Length" =     (
        997361
    );
    "Content-Type" =     (
        "image/jpeg"
    );
    Date =     (
        "Wed, 31 Oct 2018 11:38:52 GMT"
    );
    Expires =     (
        "Thu, 31 Oct 2019 11:38:52 GMT"
    );
    "Last-Modified" =     (
        "Fri, 26 Oct 2018 6:31:56 GMT"
    );
    Server =     (
        cloudflare
    );
    Vary =     (
        "Accept-Encoding"
    );
    "cf-cache-status" =     (
        HIT
    );
    "cf-ray" =     (
        "4725d67b0ae461bd-BCN"
    );
    "expect-ct" =     (
        "max-age=604800, report-uri=\"https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct\""
    );
    "x-cache" =     (
        "HIT, MISS"
    );
    "x-content-type-options" =     (
        nosniff
    );
    "x-served-by" =     (
        "cache-lax8643-LAX, cache-mad9437-MAD"
    );
} }
headers: ["Accept-Ranges": "bytes", "Content-Type": "image/jpeg", "Last-Modified": "Fri, 26 Oct 2018 6:31:56 GMT", "Vary": "Accept-Encoding", "cf-ray": "4725d67b0ae461bd-BCN", "Date": "Wed, 31 Oct 2018 11:38:52 GMT", "Server": "cloudflare", "Expires": "Thu, 31 Oct 2019 11:38:52 GMT", "x-content-type-options": "nosniff", "expect-ct": "max-age=604800, report-uri=\"https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct\"", "x-cache": "HIT, MISS", "x-served-by": "cache-lax8643-LAX, cache-mad9437-MAD", "cf-cache-status": "HIT", "Content-Length": "997361", "Cache-Control": "public, max-age=31536000"]

Там, вероятно, есть что-то, что пытается сопоставить URL-адрес запроса с полем ответа, которое вызывает пропадание кэша, но я недостаточно осведомлен, чтобы понять, что это такое. Кто-то другой, вероятно, может поймать его для вас (поэтому я и сказал, что этот ответ неполон).

Я решил эту проблему, заменив первую охрану в dataTask завершении Handler:

guard error == nil else {
  print(error)

  if let cr = session.configuration.urlCache?.cachedResponse(for: urlRequest){
     let image = UIImage(data: cr.data)
     DispatchQueue.main.async {
       self?.imageView.image = image
     }
   }

   return
}

Если запрос не выполнен, он примет кешированный ответ на этот запрос

Другие вопросы по тегам