跃迁引擎

空気を読んだ雨降らないでよ

iOS Research & Development


WKWebView缓存协议验证

前言

本次我们基于 URLRequest.CachePolicy 协议进行 WKWebView 缓存能力的验证,主要验证方向为网页页面及相应资源的内容缓存。

API 如下

1
2
3
4
5
6
7
8
9
10
11
public struct URLRequest : ReferenceConvertible, Equatable, Hashable, Sendable {
...

/// Creates and initializes a URLRequest with the given URL and cache policy.
/// - parameter: url The URL for the request.
/// - parameter: cachePolicy The cache policy for the request. Defaults to `.useProtocolCachePolicy`
/// - parameter: timeoutInterval The timeout interval for the request. See the commentary for the `timeoutInterval` for more information on timeout intervals. Defaults to 60.0
public init(url: URL, cachePolicy: URLRequest.CachePolicy = .useProtocolCachePolicy, timeoutInterval: TimeInterval = 60.0)

...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
extension NSURLRequest {
/**
@enum NSURLRequestCachePolicy

@discussion The NSURLRequestCachePolicy enum defines constants that
can be used to specify the type of interactions that take place with
the caching system when the URL loading system processes a request.
Specifically, these constants cover interactions that have to do
with whether already-existing cache data is returned to satisfy a
URL load request.

@constant NSURLRequestUseProtocolCachePolicy Specifies that the
caching logic defined in the protocol implementation, if any, is
used for a particular URL load request. This is the default policy
for URL load requests.

@constant NSURLRequestReloadIgnoringLocalCacheData Specifies that the
data for the URL load should be loaded from the origin source. No
existing local cache data, regardless of its freshness or validity,
should be used to satisfy a URL load request.

@constant NSURLRequestReloadIgnoringLocalAndRemoteCacheData Specifies that
not only should the local cache data be ignored, but that proxies and
other intermediates should be instructed to disregard their caches
so far as the protocol allows.

@constant NSURLRequestReloadIgnoringCacheData Older name for
NSURLRequestReloadIgnoringLocalCacheData.

@constant NSURLRequestReturnCacheDataElseLoad Specifies that the
existing cache data should be used to satisfy a URL load request,
regardless of its age or expiration date. However, if there is no
existing data in the cache corresponding to a URL load request,
the URL is loaded from the origin source.

@constant NSURLRequestReturnCacheDataDontLoad Specifies that the
existing cache data should be used to satisfy a URL load request,
regardless of its age or expiration date. However, if there is no
existing data in the cache corresponding to a URL load request, no
attempt is made to load the URL from the origin source, and the
load is considered to have failed. This constant specifies a
behavior that is similar to an "offline" mode.

@constant NSURLRequestReloadRevalidatingCacheData Specifies that
the existing cache data may be used provided the origin source
confirms its validity, otherwise the URL is loaded from the
origin source.
*/
public enum CachePolicy : UInt, @unchecked Sendable {


case useProtocolCachePolicy = 0


case reloadIgnoringLocalCacheData = 1

case reloadIgnoringLocalAndRemoteCacheData = 4

public static var reloadIgnoringCacheData: NSURLRequest.CachePolicy { get }


case returnCacheDataElseLoad = 2

case returnCacheDataDontLoad = 3


case reloadRevalidatingCacheData = 5
}
}

验证策略

我们采取的验证策略为

  1. 不设置请求头状态下
    1. 联网状态正常进入,加载网页,查看网页是否正常显示
    2. 杀死 APP 后,断开网络,加载网页,查看网页是否正常显示
  2. 设置请求头状态下
    1. 联网状态正常进入,加载网页,查看网页是否正常显示
    2. 杀死 APP 后,断开网络,加载网页,查看网页是否正常显示

验证代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import UIKit
import WebKit

class ViewController: UIViewController, WKNavigationDelegate {
private lazy var webView: WKWebView = {
let config = WKWebViewConfiguration()
let webview = WKWebView(frame: self.view.bounds, configuration: config)
webview.backgroundColor = .white
webview.navigationDelegate = self
return webview
}()

override func viewDidLoad() {
super.viewDidLoad()

self.view.addSubview(self.webView)
let toutiao = "https://m.toutiao.com"
let url = URL(string: toutiao)!
let request = URLRequest.init(url: url,
cachePolicy: .useProtocolCachePolicy,
timeoutInterval: 15.0)
self.webView.load(request)
}

func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
if let url = navigationAction.request.url {
print("Loading URL: \(url)")
}
decisionHandler(.allow)
}
}

验证过程

固定协议策略

固定使用一个缓存协议模式

  • useProtocolCachePolicy 默认方式
    • 测试结果:第一次联网进入,网页打开正常,杀死APP,断网重新进入,网页无法打开
    • 设置请求头状态下: 第一次联网进入,网页打开正常,杀死APP,断网重新进入,网页正常打开
    • 结论:
      • 优点:默认策略,无需特别关注配置,网页内容更新不会收到缓存影响,如果本地有缓存可用,会根据缓存控制头来加载缓存内容,会默认进行页面内容缓存。
      • 缺点:默认情况下断网后无法加载任何内容,需要根据 HTTP 协议中的缓存控制头(例如 Cache-Control 和 Expires)来决定是否使用缓存中的数据。
  • reloadIgnoringLocalCacheData 忽略本地缓存
    • 测试结果:测试结果:第一次联网进入,网页打开正常,杀死APP,断网重新进入,网页无法打开
    • 设置请求头状态下: 第一次联网进入,网页打开正常,杀死APP,断网重新进入,网页无法打开
      • 结论:
        • 优点:联网状态下每次都更新最新的内容,忽略本地缓存中的数据,即不使用任何本地存储的缓存副本,当使用此策略时,即使本地缓存中有数据,也会强制从网络重新加载资源。
        • 缺点:完全忽略了本地缓存,断网后无法加载任何内容
  • reloadIgnoringLocalAndRemoteCacheData 忽略任何缓存
    • 测试结果:测试结果:第一次联网进入,网页打开正常,杀死APP,断网重新进入,网页无法打开
    • 设置请求头状态下: 第一次联网进入,网页打开正常,杀死APP,断网重新进入,网页无法打开
      • 结论:
        • 优点:永远展示的是最新的数据,不会受到任何缓存干扰。
        • 缺点:完全依赖网络, 忽略本地缓存和远程缓存中的数据,还进一步忽略了远程缓存(例如 CDN 缓存)中的数据。
  • returnCacheDataElseLoad 如果缓存中有可用的数据,则使用缓存中的数据;如果没有缓存或者缓存已过期,则从网络重新加载数据
    • 测试结果:第一次联网进入,网页打开正常,杀死APP,断网重新进入,网页打开正常
    • 设置请求头状态下: 第一次联网进入,网页打开正常,杀死APP,断网重新进入,网页正常打开,缓存过期后无法打开
      • 结论:
        • 优点:断网后可以正常打开网页,打开的内容是上次最后一次联网记录的内容,如果缓存中有可用的数据,则使用缓存中的数据;如果没有缓存或者缓存已过期,则从网络重新加载数据。
        • 缺点:如果一旦缓存成功且不过期,即使联网也不会更新内容,即每次打开的页面都是相同的内容(只有没缓存内容或缓存过期时,才会从网络进行请求)
  • returnCacheDataDontLoad 离线模式,如果缓存中有可用的数据,则使用缓存中的数据;如果没有缓存或者缓存已过期,则不从网络重新加载数据。
    • 测试结果:第一次联网进入,网页打开正常,杀死APP,断网重新进入,网页打开正常
    • 设置请求头状态下: 第一次联网进入,网页打开正常,杀死APP,断网重新进入,网页正常打开,缓存过期后也能打开
      • 结论:
        • 优点:不依赖网络,纯离线模式,即使没有网,只要有缓存就可以展示内容。
        • 缺点:完全无法更新内容,每次打开内容都相同。
  • reloadRevalidatingCacheData 强制从网络重新加载数据,但在加载前会验证缓存中的数据是否仍然有效。
    • 测试结果:第一次联网进入,网页打开正常,杀死APP,断网重新进入,网页无法打开
    • 设置请求头状态下: 第一次联网进入,网页打开正常,杀死APP,断网重新进入,网页正常打开,缓存过期无法打开
      • 结论:
        • 优点:在缓存过期后能够立即更新到新的内容,同时能够节省流量,在缓存有效期内不进行网络请求
        • 缺点:依赖联网,需要请求头中配置内容,否则断网后无法打开,也不会进行页面缓存

动态协议策略

根据网络状态动态切换协议策略

  • 测试根据网络环境切换协议 useProtocolCachePolicy -> returnCacheDataDontLoad
    • 测试结果:有网时正常打开,无网络时正常打开,但只有缓存的资源可以加载展示
  • 测试根据网络环境切换协议 useProtocolCachePolicy -> returnCacheDataElseLoad
    • 测试结果:有网时正常打开,无网络时正常打开,但只有缓存的资源可以加载展示
  • 测试根据网络环境切换协议 useProtocolCachePolicy -> reloadRevalidatingCacheData
    • 测试结果:有网时正常打开,无网络时无法打开

结论

  • 当前 YCOneBrowserViewController 中使用的缓存协议为 useProtocolCachePolicy,断网后无法加载任何内容,但可以根据 HTTP 协议中的缓存控制头(例如 Cache-Control 和 Expires)来决定是否使用缓存中的数据。
  • returnCacheDataElseLoadreturnCacheDataDontLoadreloadRevalidatingCacheDatauseProtocolCachePolicy四种协议均可以在断网后使用页面缓存,但对网络的依赖程度和页面更新的影响不同。
  • 固定协议策略模式下,按断网时缓存生效的可靠性排名:returnCacheDataDontLoad > returnCacheDataElseLoad = reloadRevalidatingCacheData = useProtocolCachePolicy
  • 动态协议策略下,按断网时缓存生效的可靠性排名:returnCacheDataDontLoad > returnCacheDataElseLoad = reloadRevalidatingCacheData = useProtocolCachePolicy
  • 适合目前使用的策略为根据动态协议策略,在联网状态下采用 useProtocolCachePolicy 协议,该协议会在加载完毕后缓存本页面的内容,在断网状态下切换为 returnCacheDataDontLoad 保证页面可以加载出来(除非从来没有缓存成功过)。

Actions

  • Web页面分级
    • 强更新级
    • 强静态级
  • 切换策略
    • 保证缓存
      • 全局策略使用固定策略 reloadRevalidatingCacheData
      • 配置内容使用 默认useProtocolCachePolicy,断网returnCacheDataDontLoad
    • 根据网络状态
      • 有网 useProtocolCachePolicy,如果触发白屏检测切换到 returnCacheDataDontLoad 继续加载,添加逻辑进行二次白屏检测
      • 断网 returnCacheDataDontLoad,重新联网切换回 useProtocolCachePolicy
        • 检测协议缓存是否存在?
  • 开关
    • 全局配置:默认useProtocolCachePolicy,断网returnCacheDataDontLoad
    • 局部配置:根据 url 配置策略
1
2
3
4
5
6
7
8
9
10
11
{
"url": "yangcong345.com",
"config": {
"default": "useProtocolCachePolicy",
"loseNet": "returnCacheDataDontLoad"
}

"baidu.com/xxx/xxx",
"douyin.com/xxx/xxx",
"yangcong345.com/xxx/xxxx"
}

YCOneBrowserViewController 缓存逻辑

  1. 初始化浏览器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @implementation YCOneBrowserViewController

    - (void)viewDidLoad {
    [super viewDidLoad];
    ...
    [self initBrowser];
    ...
    }


    @end
  2. 加载 requesetURL

    1
    2
    3
    4
    5
    6
    7
    8
    - (void)initBrowser {

    ...
    if (![self.configuration disableStartLoadRequest]) {
    [self loadRequest];
    }
    ...
    }
  3. 开始加载 url

    1
    2
    3
    4
    5
    - (void)loadRequest {

    [self loadRequestWithData:nil baseURL:nil];
    ...
    }
  4. 进入加载逻辑

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    /*!
    * 手动加载请求,默认自动执行加载, 不需要手动调用
    * @note configuration.disableStartLoadRequest 为 YES 时,手动执行该方法调用
    */
    - (void)loadRequestWithData:(NSString *)requestData baseURL:(NSURL *)baseURL {

    if (requestData) { // 用HTML数据直接加载
    ...
    }else if (self.URL.isFileURL) {
    ...
    } else {
    [[YCHttpCacheFactory returnWebCacheControl] headForHtmlWithRequest:[NSURLRequest requestWithURL:self.URL]
    resultBlock:^(NSURLRequest *resultReqeust) {
    ...
    }];
    }
    }
  5. 进入缓存位

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    @implementation YCWebCacheHeaderControl

    // 根据HEAD请求,判断静态资源是否更新
    - (void)headForHtmlWithRequest:(NSURLRequest *)request resultBlock:(void(^)(NSURLRequest *resultReqeust))resultBlock {
    // 若是本地url,则不进行缓存校验
    if (request.URL.isFileURL
    || ![self isInWhiteList:request.URL.absoluteString]) {

    // 移除已有缓存
    [self clearHttpCacheForRequest:request];

    NSMutableURLRequest *mutableRequest = request.mutableCopy;
    [mutableRequest setCachePolicy:NSURLRequestReloadIgnoringLocalCacheData];
    if (resultBlock) {
    resultBlock(mutableRequest.copy);
    }

    return;
    }

    NSMutableURLRequest *mutableRequest = request.mutableCopy;

    mutableRequest.HTTPMethod = @"HEAD";
    [mutableRequest setCachePolicy:NSURLRequestReloadIgnoringLocalCacheData];
    mutableRequest.timeoutInterval = 5.f;

    // 获取请求的本地header
    NSDictionary *cachedHeaders = [self.diskCache getHeaderByRequestUrl:mutableRequest.URL.absoluteString];

    // 设置 headers
    if (cachedHeaders) {
    NSString *etag = [cachedHeaders objectForKey:@"Etag"];
    if (etag) {
    [mutableRequest setValue:etag forHTTPHeaderField:@"If-None-Match"];
    }
    NSString *lastModified = [cachedHeaders objectForKey:@"Last-Modified"];
    if (lastModified) {
    [mutableRequest setValue:lastModified forHTTPHeaderField:@"If-Modified-Since"];
    }
    }

    [[[NSURLSession sharedSession] dataTaskWithRequest:mutableRequest
    completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {

    NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;

    // 判断响应的状态码 未改变或者离线
    if (httpResponse.statusCode == 304
    || httpResponse.statusCode == 0) {
    // 读取本地缓存
    [mutableRequest setCachePolicy:NSURLRequestReturnCacheDataElseLoad];
    } else {
    // 存header
    if (httpResponse.allHeaderFields) {
    [self.diskCache saveHeader:httpResponse.allHeaderFields ByRequestUrl:request.URL.absoluteString];
    }

    // 加载远程
    [mutableRequest setCachePolicy:NSURLRequestReloadIgnoringLocalCacheData];
    }

    dispatch_async(dispatch_get_main_queue(), ^{
    // 加载资源
    mutableRequest.HTTPMethod = @"GET";
    if (resultBlock) {
    resultBlock(mutableRequest.copy);
    }
    });
    }] resume];
    }

    @end

其他

为何缓存失效时间和发起请求的当前时间一致

Q: 解释这段response,为何缓存失效时间和发起请求的当前时间一致?

1
2
3
4
5
6
7
8
9
当前时间: 2024-08-28 06:13:17 +0000 
SK---x-cache-webcdn: BYPASS from blzone02
SK---idc: shjd
SK---Content-Type: text/html; charset=utf-8
SK---Cache-Control: no-cache
SK---gear: 1
SK---Content-Encoding: gzip
SK---Date: Wed, 28 Aug 2024 06:13:17 GMT
SK---Expires: Wed, 28 Aug 2024 06:13:16 GMT

A: 根据提供的响应头部信息,我们可以详细解释为什么缓存失效时间(Expires)和发起请求的当前时间几乎一致。

详细解释

  1. SK—Cache-Control: no-cache

含义: 指示客户端不要缓存响应内容。

作用: 确保客户端每次请求都会从服务器获取最新数据,不使用缓存版本。

  1. SK—Expires: Wed, 28 Aug 2024 06:13:16 GMT

含义: 表示响应内容的有效期截止时间为 Wed, 28 Aug 2024 06:13:16 GMT。

作用: 告诉客户端在指定的时间之前,可以缓存该响应内容;超过该时间后,需要重新请求。

  1. SK—Date: Wed, 28 Aug 2024 06:13:17 GMT

含义: 表示响应的生成时间为 Wed, 28 Aug 2024 06:13:17 GMT。

作用: 帮助客户端和服务器同步时间,用于缓存验证和其他时间相关的操作。

为什么缓存失效时间和发起请求的当前时间一致?

  1. Cache-Control: no-cache:

该头部明确指示客户端不要缓存响应内容。这意味着每次请求都应该从服务器获取最新数据。

  1. Expires 和 Date 时间几乎一致:

Expires 时间为 Wed, 28 Aug 2024 06:13:16 GMT。

Date 时间为 Wed, 28 Aug 2024 06:13:17 GMT。

这两个时间几乎一致,表明服务器希望响应内容在生成后立即失效。

具体原因

  1. 即时失效:

由于 Expires 时间几乎与 Date 时间一致,这意味着响应内容在生成后立即失效。

这种设置通常用于需要频繁更新的内容,确保客户端每次请求都能获取到最新数据。

  1. 结合 Cache-Control: no-cache:

即使 Expires 时间已经过去,Cache-Control: no-cache 也会强制客户端每次都从服务器获取最新数据。

总结

  1. Cache-Control: no-cache 指示客户端不要缓存响应内容。
  2. Expires 时间几乎与 Date 时间一致 表明响应内容在生成后立即失效。
  3. 这种设置通常用于需要频繁更新的内容,确保客户端每次请求都能获取到最新数据。

returnCacheDataDontLoad 为何在缓存过期后仍然可以展示?

Q: 在得到上述response时我使用的缓存策略是useProtocolCachePolicy,如果webview遵循Cache-Control: no-cache 指示客户端不要缓存响应内容,为何我切换为returnCacheDataDontLoad在断网后仍然能展示之前加载过的内容?

A: 在使用 WKWebView 时,缓存策略的选择会影响页面加载的行为。具体来说,WKWebView 提供了几种不同的缓存策略,其中包括 useProtocolCachePolicy 和 returnCacheDataDontLoad。

缓存策略详解

  1. useProtocolCachePolicy:

使用协议级别的缓存策略。

遵循 HTTP 缓存控制头(如 Cache-Control 和 Expires)。

如果缓存控制头指示不缓存,则不会使用缓存数据。

  1. returnCacheDataDontLoad:

优先返回缓存数据,即使缓存已过期也不再发起网络请求。

如果没有缓存数据,则显示空白页面。

为什么 returnCacheDataDontLoad 在断网后仍能展示之前加载过的内容

当你使用 returnCacheDataDontLoad 缓存策略时,即使缓存控制头指示不缓存(如 Cache-Control: no-cache),WKWebView 仍然会尝试从缓存中加载数据。这是因为:

  1. 缓存策略优先级:
    1. returnCacheDataDontLoad 策略优先返回缓存数据,无论缓存控制头如何指示。
    2. 这意味着即使 Cache-Control: no-cache,WKWebView 也会尝试从缓存中加载数据。
  2. 缓存存储机制:
    1. WKWebView 有一个本地缓存存储机制,即使缓存控制头指示不缓存,实际的响应数据仍然可能被缓存。
    2. 这种缓存存储机制是为了提高性能和用户体验,即使在某些情况下缓存控制头指示不缓存,实际数据仍然会被缓存。

总结

  1. useProtocolCachePolicy:
    1. 遵循 HTTP 缓存控制头(如 Cache-Control 和 Expires)。
    2. 如果缓存控制头指示不缓存,则不会使用缓存数据。
  2. returnCacheDataDontLoad:
    1. 优先返回缓存数据,即使缓存已过期也不再发起网络请求。
    2. 即使 Cache-Control: no-cache,WKWebView 仍然会尝试从缓存中加载数据。

因此,在断网情况下,使用 returnCacheDataDontLoad 策略时,WKWebView 会优先返回缓存数据,即使缓存控制头指示不缓存。

这个缓存存储机制的具体机制是什么,比如缓存内容的大小限制,对缓存的清理时机等等

A: WKWebView 的缓存机制涉及多个方面,包括缓存内容的大小限制、缓存的清理时机等。下面详细解释这些机制。

缓存存储机制

  1. 缓存内容的大小限制

    WKWebView 的缓存机制主要依赖于 Safari 的缓存系统。具体来说,WKWebView 使用的是 WebKit 引擎的缓存机制,而 WebKit 的缓存机制又基于 Safari 的缓存策略。

    1. 默认缓存大小:

      1. Safari 和 WebKit 默认的缓存大小为 50MB。这意味着总的缓存大小不会超过 50MB。
      2. 这个大小限制是为了避免缓存占用过多的存储空间。
    2. 自定义缓存大小:

      1. 可以通过修改 WebKit 的缓存配置来调整缓存大小。

      2. 例如,可以通过 WKWebsiteDataStore 来设置缓存大小。

        1
        2
        3
        4
        5
        6
        7
        8
        import WebKit

        func configureWebCacheSize() {
        let websiteDataStore = WKWebsiteDataStore.default()
        let cacheConfig = WKStorageName.default().appendingPathComponent("WebCache")

        websiteDataStore.setCapacityOfBytes(for: cacheConfig, to: 100 * 1024 * 1024) // 设置为 100MB
        }
  2. 缓存的清理时机

WKWebView 的缓存清理机制主要包括以下几种情况:

  1. 缓存过期:
    1. 当缓存内容的 Expires 时间到达时,缓存内容将被视为过期。
    2. Expires 头部指定了缓存内容的有效期。
  2. 缓存大小限制:
    1. 当缓存总大小超过设定的大小限制时,WKWebView 会自动清理旧的缓存内容。
    2. 清理逻辑通常是按照最近最少使用(LRU)原则进行。
  3. 手动清理缓存:
    1. 可以通过调用 WKWebsiteDataStore 的方法手动清理缓存。
    2. 例如,可以定期清理缓存以释放存储空间。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import WebKit

func clearWebCache() {
let websiteDataStore = WKWebsiteDataStore.default()
websiteDataStore.fetchDataRecords(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes()) { records in
let types = records.map { $0.type }
websiteDataStore.removeData(ofTypes: types, modifiedSince: Date.distantPast) { error in
if let error = error {
print("Error clearing cache: \(error.localizedDescription)")
} else {
print("Cache cleared successfully.")
}
}
}
}

示例代码

下面是一个完整的示例代码,展示了如何配置缓存大小和手动清理缓存:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
import UIKit
import WebKit

class ViewController: UIViewController, WKNavigationDelegate {

private var webView: WKWebView!

override func viewDidLoad() {
super.viewDidLoad()

// 初始化 WKWebView
webView = WKWebView()
webView.navigationDelegate = self
view.addSubview(webView)

// 添加约束
webView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
webView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
webView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
webView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
webView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor)
])

// 配置缓存大小
configureWebCacheSize()

// 加载 URL
if let url = URL(string: "https://example.com") {
let request = URLRequest(url: url)
webView.load(request)
}
}

// MARK: - WKNavigationDelegate

func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse) async -> WKNavigationResponsePolicy {
if let httpResponse = navigationResponse.response as? HTTPURLResponse {
// 打印状态码
print("HTTP Status Code: \(httpResponse.statusCode)")

// 获取并打印头部信息
if let allHeaderFields = httpResponse.allHeaderFields {
for (key, value) in allHeaderFields {
if let keyString = key as? String, let valueString = value as? String {
print("\(keyString): \(valueString)")
}
}
}
}

return .allow
}

// 可选:实现其他 WKNavigationDelegate 方法
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
// 页面加载完成时的处理
print("Page loaded.")
}

// 配置缓存大小
func configureWebCacheSize() {
let websiteDataStore = WKWebsiteDataStore.default()
let cacheConfig = WKStorageName.default().appendingPathComponent("WebCache")

websiteDataStore.setCapacityOfBytes(for: cacheConfig, to: 100 * 1024 * 1024) // 设置为 100MB
}

// 清理缓存
func clearWebCache() {
let websiteDataStore = WKWebsiteDataStore.default()
websiteDataStore.fetchDataRecords(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes()) { records in
let types = records.map { $0.type }
websiteDataStore.removeData(ofTypes: types, modifiedSince: Date.distantPast) { error in
if let error = error {
print("Error clearing cache: \(error.localizedDescription)")
} else {
print("Cache cleared successfully.")
}
}
}
}
}

总结

  1. 缓存内容的大小限制:
    1. 默认缓存大小为 50MB。
    2. 可以通过 WKWebsiteDataStore 调整缓存大小。
  2. 缓存的清理时机:
    1. 缓存过期:根据 Expires 头部。
    2. 缓存大小限制:当缓存总大小超过设定的大小限制时,自动清理旧的缓存内容。
    3. 手动清理缓存:通过调用 WKWebsiteDataStore 的方法进行清理。

通过这些机制,WKWebView 能够有效地管理和维护缓存数据,确保在需要时能够及时加载最新的内容,同时避免占用过多的存储空间。

当缓存内容的 Expires 时间到达时,缓存内容将被视为过期,就会被清理,那为什么我在过期时间以后使用returnCacheDataDontLoad策略依然可以展示已缓存的网页内容呢?

A: 在使用 WKWebView 时,尽管 Expires 或 Cache-Control 指令可以指示缓存何时过期,但是 WKWebView 的行为受到其缓存策略的影响。特别是当你使用 WKWebViewConfiguration.requestCachePolicy = .returnCacheDataDontLoad 时,即使缓存已过期,WKWebView 仍然会尝试从缓存中加载数据。

缓存策略 .returnCacheDataDontLoad 的行为

  1. .returnCacheDataDontLoad:
    1. 这个策略指示 WKWebView 在加载资源时优先从缓存中读取数据。
    2. 如果缓存数据可用,那么 WKWebView 将直接使用缓存数据而不发起新的网络请求。
    3. 如果没有缓存数据或者缓存数据不可用(例如,缓存已被清理),则 WKWebView 可能会显示空白页面或其它默认内容。

为什么过期后仍然能展示缓存内容

  1. 缓存过期不等于立即清理:
    1. Expires 或 Cache-Control 指令标记缓存数据何时被认为是“过期”的,但这并不意味着缓存数据会立即被删除。
    2. 实际上,缓存数据可能会继续保留在存储中,直到缓存管理器根据其自身的策略决定清理它为止。
  2. 缓存管理器的清理策略:
    1. WKWebView 的缓存管理器通常会根据 LRU(Least Recently Used,最近最少使用)原则来决定哪些缓存数据应该被保留,哪些应该被清理。
    2. 如果缓存空间足够,并且没有达到最大缓存大小限制,那么即使缓存数据过期,也可能暂时不会被清理掉。
  3. 缓存策略优先级:
    1. 当使用 .returnCacheDataDontLoad 策略时,WKWebView 会优先尝试使用任何可用的缓存数据。
    2. 这意味着即使缓存数据已经过期,只要它仍然存在于缓存中,WKWebView 就会使用它。

总结

  1. 缓存过期不等于立即清理:
    1. Expires 或 Cache-Control 指令标记缓存数据何时被认为是“过期”的,但这并不意味着缓存数据会立即被删除。
  2. 缓存策略优先级:
    1. 当使用 .returnCacheDataDontLoad 策略时,WKWebView 会优先尝试使用任何可用的缓存数据,即使这些数据已经过期。

因此,在使用 .returnCacheDataDontLoad 策略时,即使缓存数据已经过期,只要它仍然存在于缓存中,WKWebView 就会使用它。这意味着即使在断网情况下,用户仍然可以看到之前加载过的页面内容。

这里所说的大小限制是多少

Q: “如果缓存空间足够,并且没有达到最大缓存大小限制,那么即使缓存数据过期,也可能暂时不会被清理掉” 这里所说的大小限制是多少

A: 关于 WKWebView 的缓存大小限制,具体来说,默认情况下,WKWebView 使用的缓存大小限制为 50MB。这是 Safari 和 WebKit 默认的缓存大小限制

缓存大小限制

  1. 默认缓存大小:
    1. WKWebView 默认的缓存大小为 50MB。
    2. 这个大小限制是为了防止缓存占用过多的存储空间。
  2. 自定义缓存大小:
    1. 可以通过 WKWebsiteDataStore 来调整缓存大小。
    2. 你可以根据需要设置一个更大的或更小的缓存大小。

如何查看和调整缓存大小

  1. 查看当前缓存大小

可以通过 WKWebsiteDataStore 来查询当前的缓存大小:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import WebKit

func getCurrentCacheSize(completion: @escaping (UInt64) -> Void) {
let websiteDataStore = WKWebsiteDataStore.default()
websiteDataStore.fetchDataRecords(ofTypes: [WKWebsiteDataTypeDiskCache]) { records in
let totalSize = records.reduce(0) { $0 + $1.estimatedDataSize }
completion(UInt64(totalSize))
}
}

// 使用示例
getCurrentCacheSize { size in
print("Current cache size: \(size) bytes")
}
  1. 调整缓存大小

可以通过 WKWebsiteDataStore 来调整缓存大小:

1
2
3
4
5
6
7
8
9
10
import WebKit

func setCacheSize(to sizeInBytes: UInt64) {
let websiteDataStore = WKWebsiteDataStore.default()
let cacheConfig = WKStorageName.default().appendingPathComponent("WebCache")
websiteDataStore.setCapacityOfBytes(for: cacheConfig, to: sizeInBytes)
}

// 使用示例
setCacheSize(to: 100 * 1024 * 1024) // 设置为 100MB

缓存清理机制

  1. 缓存过期:
    1. 当缓存数据的 Expires 时间到达时,缓存数据将被视为过期。
    2. 过期的数据可能会被清理,但不是立即清理。
  2. 缓存大小限制:
    1. 当缓存总大小超过设定的大小限制时,WKWebView 会自动清理旧的缓存内容。
    2. 清理逻辑通常是按照最近最少使用(LRU)原则进行。
  3. 手动清理缓存:
    1. 可以通过调用 WKWebsiteDataStore 的方法手动清理缓存。
    2. 例如,可以定期清理缓存以释放存储空间。

示例代码

以下是一个完整的示例代码,展示了如何配置缓存大小和手动清理缓存:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
import UIKit
import WebKit

class ViewController: UIViewController, WKNavigationDelegate {

private var webView: WKWebView!

override func viewDidLoad() {
super.viewDidLoad()

// 初始化 WKWebView
webView = WKWebView()
webView.navigationDelegate = self
view.addSubview(webView)

// 添加约束
webView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
webView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
webView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
webView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
webView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor)
])

// 配置缓存大小
configureWebCacheSize()

// 加载 URL
if let url = URL(string: "https://example.com") {
let request = URLRequest(url: url)
webView.load(request)
}
}

// MARK: - WKNavigationDelegate

func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse) async -> WKNavigationResponsePolicy {
if let httpResponse = navigationResponse.response as? HTTPURLResponse {
// 打印状态码
print("HTTP Status Code: \(httpResponse.statusCode)")

// 获取并打印头部信息
if let allHeaderFields = httpResponse.allHeaderFields {
for (key, value) in allHeaderFields {
if let keyString = key as? String, let valueString = value as? String {
print("\(keyString): \(valueString)")
}
}
}
}

return .allow
}

// 可选:实现其他 WKNavigationDelegate 方法
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
// 页面加载完成时的处理
print("Page loaded.")
}

// 配置缓存大小
func configureWebCacheSize() {
let websiteDataStore = WKWebsiteDataStore.default()
let cacheConfig = WKStorageName.default().appendingPathComponent("WebCache")

websiteDataStore.setCapacityOfBytes(for: cacheConfig, to: 100 * 1024 * 1024) // 设置为 100MB
}

// 清理缓存
func clearWebCache() {
let websiteDataStore = WKWebsiteDataStore.default()
websiteDataStore.fetchDataRecords(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes()) { records in
let types = records.map { $0.type }
websiteDataStore.removeData(ofTypes: types, modifiedSince: Date.distantPast) { error in
if let error = error {
print("Error clearing cache: \(error.localizedDescription)")
} else {
print("Cache cleared successfully.")
}
}
}
}
}

总结

  1. 默认缓存大小:
    1. WKWebView 默认的缓存大小为 50MB。
  2. 自定义缓存大小:
    1. 可以通过 WKWebsiteDataStore 来调整缓存大小。
  3. 缓存清理机制:
    1. 缓存过期:根据 Expires 头部。
    2. 缓存大小限制:当缓存总大小超过设定的大小限制时,自动清理旧的缓存内容。
    3. 手动清理缓存:通过调用 WKWebsiteDataStore 的方法进行清理。

通过这些机制,WKWebView 能够有效地管理和维护缓存数据,确保在需要时能够及时加载最新的内容,同时避免占用过多的存储空间。

如果我可以自己调整缓存大小,那这个大小是否存在一个规定的上限值?

A: 关于 WKWebView 的缓存大小,虽然你可以自定义缓存大小,但确实存在一些实际的上限值。这些上限值取决于设备的存储空间以及系统的限制。以下是一些关键点:

缓存大小的上限

  1. 系统限制:
    1. iOS 和 macOS 系统对缓存大小有一定的限制。
    2. 一般来说,缓存大小不应超过设备的可用存储空间。
  2. 实际限制:
    1. 通常情况下,WKWebView 的缓存大小建议不超过几百 MB。
    2. 具体的上限值取决于设备的存储空间和系统配置。

实际操作中的限制

  1. iOS 设备:
    1. 对于 iOS 设备,通常建议缓存大小不要超过几百 MB,例如 100MB 到 500MB。
    2. 实际上限取决于设备的存储空间。
  2. macOS 设备:
    1. 对于 macOS 设备,缓存大小可以稍微大一些,但也建议不要超过几百 MB。

实际测试和调整

  1. 测试不同大小:
    1. 你可以通过实际测试来确定最适合应用的缓存大小。
    2. 测试不同的缓存大小,观察应用的表现和设备的性能。
  2. 动态调整:
    1. 根据设备的实际存储空间动态调整缓存大小。
    2. 例如,可以检查设备的剩余存储空间,并根据剩余空间动态设置缓存大小。

示例代码

以下是一个示例代码,展示了如何动态调整缓存大小:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
import UIKit
import WebKit

class ViewController: UIViewController, WKNavigationDelegate {

private var webView: WKWebView!

override func viewDidLoad() {
super.viewDidLoad()

// 初始化 WKWebView
webView = WKWebView()
webView.navigationDelegate = self
view.addSubview(webView)

// 添加约束
webView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
webView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
webView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
webView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
webView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor)
])

// 动态调整缓存大小
adjustCacheSizeBasedOnDeviceStorage()

// 加载 URL
if let url = URL(string: "https://example.com") {
let request = URLRequest(url: url)
webView.load(request)
}
}

// MARK: - WKNavigationDelegate

func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse) async -> WKNavigationResponsePolicy {
if let httpResponse = navigationResponse.response as? HTTPURLResponse {
// 打印状态码
print("HTTP Status Code: \(httpResponse.statusCode)")

// 获取并打印头部信息
if let allHeaderFields = httpResponse.allHeaderFields {
for (key, value) in allHeaderFields {
if let keyString = key as? String, let valueString = value as? String {
print("\(keyString): \(valueString)")
}
}
}
}

return .allow
}

// 可选:实现其他 WKNavigationDelegate 方法
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
// 页面加载完成时的处理
print("Page loaded.")
}

// 动态调整缓存大小
func adjustCacheSizeBasedOnDeviceStorage() {
guard let storageManager = FileManager.default.storageManager() else {
print("Failed to get storage manager.")
return
}

let freeSpace = storageManager.freeDiskSpace()
let cacheSize = min(freeSpace / 10, 500 * 1024 * 1024) // 不超过总剩余空间的10%,最多500MB

let websiteDataStore = WKWebsiteDataStore.default()
let cacheConfig = WKStorageName.default().appendingPathComponent("WebCache")
websiteDataStore.setCapacityOfBytes(for: cacheConfig, to: cacheSize)
}
}

extension ViewController {
func getCurrentCacheSize(completion: @escaping (UInt64) -> Void) {
let websiteDataStore = WKWebsiteDataStore.default()
websiteDataStore.fetchDataRecords(ofTypes: [WKWebsiteDataTypeDiskCache]) { records in
let totalSize = records.reduce(0) { $0 + $0.estimatedDataSize }
completion(UInt64(totalSize))
}
}
}

总结

  1. 系统限制:
    1. iOS 和 macOS 系统对缓存大小有一定的限制。
    2. 通常建议缓存大小不要超过几百 MB。
  2. 实际限制:
    1. 具体的上限值取决于设备的存储空间和系统配置。
    2. 一般建议不要超过设备剩余存储空间的一定比例,例如 10%。
  3. 动态调整:
    1. 可以根据设备的实际存储空间动态调整缓存大小,以确保应用的性能和用户体验。

通过这些措施,你可以更好地管理 WKWebView 的缓存大小,确保应用在不同设备上的表现和性能。

最近的文章

Xcode lldb控制台打印 po 命令失效问题解决记录

问题背景iOS开发中,使用Xcode 开发时,有时候原本运行好好的项目,突然调试时,发现 po 命令无法正常显示变量的值,无论是清空编译目录,还是重装Xcode,都无法解决问题。 问题原因自从 Xcode 13.3 到 Xcode 14 测试版之前,当尝试使用静态框架/库与 LLDB …

, , , 开始阅读
更早的文章

Rust - 移动端跨平台开发实践

1. 整体方案 Rust的移动端跨平台开发方案,具体如下 最底层SDK的业务逻辑由Rust实现 使用ffi工具,编写binding code Android、iOS:中间层使用uniffi编写binding代码。使用uniffi-bindgen将binding代码生成kotlin、Swift代 …

, 开始阅读
comments powered by Disqus