【问题标题】:URLresponse is not retrieved after storing in cache using storeCachedResponse使用 storeCachedResponse 存储在缓存中后未检索 URLresponse
【发布时间】:2019-03-27 00:59:36
【问题描述】:

目标

我正在尝试将来自 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-age31536000
  • 如果是全新安装,那么存储到缓存之前,我的 lizardURLString 的cachedResponsenil。但是存储之后,它不再是nil。结果,我得出结论,我已成功将某些内容存储到缓存中!
  • 我还怀疑 URLCache 将 URLRequest 视为键。所以我打印了我的lizardURLString 的 hashValue。它与我存储的密钥相同。结合以上几点,我得出的结论是缓存中存在确切的键!
  • 我还可以看到,当我将其存储在缓存中时,我的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:

所以我取得了一些进展。或者也许退后一步,前进一步。

我尝试查看是否可以存储 same 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 创建缓存之间有什么区别。

这很重要,因为我开始怀疑是否还有某种形式的internal bugs in URLCache。这让我有理由相信它可能按预期工作。

现在我知道了存储到缓存中的过程。我知道我的 URLResponse 很好。我只需要通过映射 URLRequest 来工作

EDIT3:

Guy Kogus 建议我的网址必须来自同一来源。 因此,一旦我下载了他提到的 BearImage,我的 lizardImage 就通过了。瞧!

作为我学到的非常重要的调试说明:即使您在问题的某些部分(它正在为自己缓存风景图像)取得成功,更改变量(这里更改初始 URL)总是可以改变整个测试结果。

他怀疑这是因为 Server in 标头在 shared 中,这对于查找 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 本身。它不适用于任何 other URL。

所以问题的当前状态是:需要包含哪些标头才能使其正常工作?

【问题讨论】:

  • Bounty 旨在提供给 dgatwood 的答案。但由于我的一些错误/延迟,它不是。所以把他的答案当作最重要的答案。尽管盖伊关于标题的注释对于解决问题至关重要。 FWIW 我为他的不同答案提供了赏金:)

标签: ios http caching nsurlsession nsurlcache


【解决方案1】:

欢迎来到异步缓存的美妙世界。 NSURLCache 是高度异步的。仅仅因为您将数据推入其中并不意味着它可以用于检索。您必须让主运行循环在它可用之前返回,甚至可能等待一段时间。存储后未能立即返回响应并不少见。尝试在大约 5 秒后调度它。

其次,您的缓存可能有点小,无法存储数兆字节的图像。尝试提高它,看看是否有帮助。

最后,您说“关闭互联网”是什么意思?你说你正在超时。通常,如果您在禁用所有连接的情况下将设备置于飞行模式,则在失败并显示没有连接的错误之前,它不应该在那里停留很长时间)。如果这没有发生,那么就会发生一些奇怪的事情,就像在会话上设置了 waitsForConnectivity 或其他东西一样。 (您不是在后台发出网络请求,是吗?如果是这样,请尝试将 waitsForConnectivity 显式设置为 NO,这样他们就不会等待连接可用。)

此外,对于这种用法,您可能必须去掉 Vary: Accept-Encoding 标头或提供一致的用户代理字符串。该标头导致缓存基本上是每个浏览器的。这可能会导致缓存以意想不到的方式运行,并且可能是您看到的奇怪现象的原因。

请注意,去掉 Vary 标头有点小技巧,而且可能不是解决问题的最正确方法;理想情况下,您应该调整必须调整的任何传出标头字段,以便即使存在该标头也能正常工作。但是你必须研究它并确切地弄清楚需要哪些领域,因为我不知道。 :-)

【讨论】:

  • 非常感谢。 1) 我等了超过 5 秒。 2)我的图像是 1mb。我的缓存大小是500mb! 3) 你的预期结果是什么?它应该失败还是应该从缓存中读取?因为显然现在它失败了,就好像它没有从缓存中读取一样。你说的背景是什么意思?当应用程序处于前台时,我正在尝试检索缓存。显然网络请求是在后台执行的......大卫,只要你有时间,你可以复制我的代码并尝试一下吗?代码已完成,可以重现我的问题。有所有的链接和一切......
  • FWIW 如果我 1. 下载横向图像。 2. 杀死应用程序 3. 关闭设备的互联网并加载 landscape 图像,然后立即从缓存中加载。当我为我的 lizard 请求强制将其注入缓存时,它不会加载...
  • 我编辑了我的缓存大小。在我的测试中,它以 500mb 运行。我的图像大小为 1mb,因此它遵循 Apple 的指导方针,即小于缓存大小的 5%。
  • 您是否尝试过创建一个 new NSHTTPURLResponse 对象,A. 尚未在缓存中,而 B. 是基于蜥蜴 URL 而不是基于不同的网址?
  • 我不明白你的建议。 是基于 lizard URL 创建的 lizardURL 永远不会到达服务器!因此,我无法基于此创建响应。
【解决方案2】:

这不是一个完整的答案,但它应该能让你朝着正确的方向前进。

问题与您的代码无关,我相信它基本上没问题。问题在于您从 landscapeURLString 获得的响应,因为图像存储在 Cloudflare 中。如果您使用来自同一来源的 2 张图片(例如,尝试使用 this bear from wikipedia 而不是来自 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 与导致缓存未命中的响应字段匹配,但我没有足够的知识知道它是什么。其他人可能会为您找到它(因此我说这个答案不完整)。

【讨论】:

  • 瞧!确实使用熊图像使其工作。它也可以是URL 或其主机的第一个 URLSegment。但是Server 似乎也很合理。我不明白的是:这个熊图像的cache-control 设置为max-age: 0。然而,如果我打电话给session.configuration.urlCache?.cachedResponse(for: self!.bearURLRequest),它不会变成nil。我还尝试强制退出应用程序并在离线时下载bearImage。令人惊讶的是它奏效了。所以我打印了所有的标题,并在Strict-Transport-Security 键下找到了“max-age”。该键控制缓存吗?
  • 请问您如何知道这个问题?你遇到过类似的问题吗?
  • 第二个想法。我几乎可以肯定它是基于 URLPath 的。因为我的 lizardRequest 永远不会到达互联网,即它永远不会得到任何 Server 标头进行比较。当它离线时,它唯一可以查看的是URLRequest 本身。现在唯一不同的是 URLPath。我已经让其他一切都一样了。这有意义吗?
  • 我只是四处玩耍和试验,直到我意识到响应和标题非常不同。要做的一件有用的事情是,我没有使用session.dataTask(with:completionHandler),而是使用了session.dataTask(with:),并检查了URLSessionDataDelegate 回调以深入挖掘。
  • 似乎也与URLPath无关。它与标题有关。我已经编辑了这个问题。你能再看看吗? (在我编辑问题后看到你的评论......)
【解决方案3】:

我解决了这个问题,将 dataTask completionHandler 中的 first guard 替换为:

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
}

如果请求失败,它将对该请求进行缓存响应

【讨论】:

  • 我错误地对此表示赞同。如果我理解正确,您的最终方法是聪明的。但是您当前的流程是不正确的。 if let cr = session.configuration.urlCache?.cachedResponse(for: urlRequest){ 应该进行任何网络调用之前。这意味着如果它在缓存中,那么您应该从那里读取它,如果有错误则不要检索。这仍然不能解释为什么正常过程不起作用。如果我能够通过调用cachedResponse 而不是直接通过网络调用来访问缓存,那么就大错特错了。行为不应有所不同。
  • 然后我误解了如果网络请求图像失败,从缓存中获取响应应该是后备。
  • 我可以编辑。但我不确定你想让我编辑什么?
猜你喜欢
  • 1970-01-01
  • 2019-08-29
  • 2020-12-15
  • 2018-04-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-10-21
  • 1970-01-01
相关资源
最近更新 更多