iOS App离线缓存max-stale失效:超期仍返回缓存数据
iOS NSURLRequest 缓存策略问题:max-stale 限制不生效
需求背景
设备在线时,每次请求直接发起网络请求;设备离线时,仅使用最长4小时内的缓存响应。
离线场景下的处理代码:
if isOffline && request.isOfflineCompatible { urlRequest.cachePolicy = .returnCacheDataDontLoad urlRequest.setValue("max-stale=14400", forHTTPHeaderField: "cache-control") } else { // 恢复请求创建时设置的缓存策略,默认值为 useProtocolCachePolicy urlRequest.cachePolicy = request.cachePolicy }
问题描述
无论设置的max-stale值为多少(测试时设为120秒),超过该时长后离线请求仍会返回成功(预期应返回失败),相同逻辑在Android平台运行正常。
离线请求对象信息
- url : 保密API地址
- cachePolicy : 3(对应
.returnCacheDataDontLoad)- timeoutInterval : 30.0
- mainDocumentURL : nil
- networkServiceType : __C.NSURLRequestNetworkServiceType
- allowsCellularAccess : true
- httpMethod : "GET"
- allHTTPHeaderFields :
- key : "Cache-Control",value : "max-stale=120"(测试用短时长)
- key : "Content-Type",value : "application/json"
- key : "APIKey",value : "abcdxyz"
- httpBody : nil
- httpBodyStream : nil
- httpShouldHandleCookies : true
- httpShouldUsePipelining : false
对应响应信息
return (<NSHTTPURLResponse: 0x281dc61a0> { URL: 保密地址 } { Status Code: 200, Headers { "Access-Control-Allow-Origin" = ( "*" ); "Cache-Control" = ( "max-age=30" ); "Content-Encoding" = ( gzip ); "Content-Length" = ( 9797 ); "Content-Type" = ( "application/json" ); Date = ( "Sun, 16 Apr 2023 18:33:23 GMT" ); Etag = ( 0x8DB33C6C0E756AD ); Expires = ( "Sun, 16 Apr 2023 18:33:53 GMT" ); "Last-Modified" = ( "Sun, 02 Apr 2023 22:08:13 GMT" ); Vary = ( "Accept-Encoding" ); "content-md5" = ( "ozV+j+j89923XIxSCSjFoA==" ); "request-context" = ( "appId=cid-v1:79144720-54fb-43e6-964e-37c4f997a8ec" ); "x-ms-blob-type" = ( BlockBlob ); "x-ms-lease-status" = ( unlocked ); "x-ms-request-id" = ( "154aae09-301e-004e-201f-6e69d9000000" ); "x-ms-version" = ( "2009-09-19" ); } } )
问题分析与解决方案
iOS的NSURLCache对max-stale的处理逻辑和Android的HttpURLCache存在差异:当设置cachePolicy为.returnCacheDataDontLoad时,系统会直接返回缓存数据,忽略Cache-Control头中的max-stale限制。
可行解决方法
- 手动校验缓存有效期
获取缓存响应后,自行计算缓存是否超出max-stale限制:
- 从响应头的
Date、Expires或Cache-Control的max-age字段,计算缓存的基础过期时间。 - 叠加
max-stale允许的时长,判断当前时间是否超出该范围,超出则手动返回失败。
自定义NSURLCache
继承NSURLCache并重写cachedResponseForRequest:方法,在该方法中加入max-stale校验逻辑,不符合条件时返回nil,触发请求失败。替换缓存策略
改用.returnCacheDataElseLoad作为缓存策略,同时在离线状态下拦截网络请求,强制使用缓存。此方式需要额外处理离线状态的网络拦截逻辑。
示例代码(手动校验方式)
// 假设已获取到缓存响应response和对应数据data guard let httpResponse = response as? HTTPURLResponse, let dateStr = httpResponse.allHeaderFields["Date"] as? String, let date = DateFormatter.rfc1123DateFormatter.date(from: dateStr), let cacheControl = httpResponse.allHeaderFields["Cache-Control"] as? String else { // 无法校验,按失败处理 completion(.failure(CacheError.invalidResponse)) return } // 提取响应头中的max-age值 let maxAgeRegex = try! NSRegularExpression(pattern: "max-age=(\\d+)") let maxAgeMatch = maxAgeRegex.firstMatch(in: cacheControl, range: NSRange(cacheControl.startIndex..., in: cacheControl)) let maxAge = maxAgeMatch.flatMap { String(cacheControl[Range($0.range(at: 1), in: cacheControl)!]) }.flatMap(Int.init) ?? 0 // 读取请求头中的max-stale值(或直接使用预设的14400秒) let maxStale = urlRequest.allHTTPHeaderFields?["Cache-Control"]?.split(separator: "=").last.flatMap(Int.init) ?? 14400 // 计算允许的最大过期时间 let allowedExpirationTime = date.addingTimeInterval(TimeInterval(maxAge + maxStale)) if Date() > allowedExpirationTime { // 缓存已超出允许的过期时长,返回失败 completion(.failure(CacheError.staleCache)) } else { // 缓存有效,返回数据 completion(.success(data)) } // 辅助工具:RFC1123日期格式化器 extension DateFormatter { static let rfc1123DateFormatter: DateFormatter = { let formatter = DateFormatter() formatter.locale = Locale(identifier: "en_US_POSIX") formatter.dateFormat = "EEE, dd MMM yyyy HH:mm:ss zzz" formatter.timeZone = TimeZone(identifier: "GMT") return formatter }() } // 自定义缓存错误类型 enum CacheError: Error { case invalidResponse case staleCache }
内容的提问来源于stack exchange,提问作者7vikram7
相关产品推荐
相关产品推荐

