关于ios:探秘WKWebView

11次阅读

共计 16250 个字符,预计需要花费 41 分钟才能阅读完成。

概述

之前次要应用 UIWebView 进行页面的加载,然而 UIWebView 存在很多问题,在 2020 年曾经被苹果正式摈弃。所以本篇文章次要解说 WKWebViewWKWebViewiOS8开始反对,当初大多数 App 应该都不反对 iOS7 了。

UIWebView存在两个问题,一个是内存耗费比拟大,另一个是性能很差。WKWebView绝对于 UIWebView 来说,性能要比 UIWebView 性能要好太多,刷新率能达到 60FPS。内存占用也比UIWebView 要小。

WKWebView是一个多过程组件,NetworkUI Render都在独立的过程中实现。

因为 WKWebViewApp不在同一个过程,如果 WKWebView 过程解体并不会导致利用解体,仅仅是页面白屏等异样。页面的载入、渲染等耗费内存和性能的操作,都在 WKWebView 的过程中解决,解决后再将后果交给 App 过程用于显示,所以 App 过程的性能耗费会小很多。

网页加载流程

  1. 通过域名的形式申请服务器,申请前浏览器会做一个 DNS 解析,并将 IP 地址返回给浏览器。
  2. 浏览器应用 IP 地址申请服务器,并且开始握手过程。TCP是三次握手,如果应用 https 则还须要进行 TLS 的握手,握手后依据协定字段抉择是否放弃连贯。
  3. 握手实现后,浏览器向服务端发送申请,获取 html 文件。
  4. 服务器解析申请,并由 CDN 服务器返回对应的资源文件。
  5. 浏览器收到服务器返回的 html 文件,交由 html 解析器进行解析。
  6. 解析 html 由上到下进行解析 xml 标签,过程中如果遇到 css 或资源文件,都会进行异步加载,遇到 js 则会挂起以后 html 解析工作,申请 js 并返回后持续解析。因为 js 文件可能会对 DOM 树进行批改。
  7. 解析完 html,并执行完js 代码,造成最终的 DOM 树。通过 DOM 配合 css 文件找出每个节点的最终展现款式,并交由浏览器进行渲染展现
  8. 完结链接。

代理办法

WKWebViewUIWebView 的代理办法产生了一些扭转,WKWebView的流程更加细化了。例如之前 UI 完结申请后,会立即渲染到 webView 上。而 WKWebView 则会在渲染到屏幕之前,会回调一个代理办法,代理办法决定是否渲染到屏幕上。这样就能够对申请下来的数据做一次校验,避免数据被更改,或验证视图是否容许被显示到屏幕上。

除此之外,WKWebView绝对于 UIWebView 还多了一些定制化操作。

  1. 重定向的回调,能够在申请重定向时获取到这次操作。
  2. WKWebView 过程异样退出时,能够通过回调获取。
  3. 自定义解决证书。
  4. 更深层的 UI 定制操作,将 alertUI操作交给原生层面解决,而 UI 计划 UIAlertView 是间接 webView 显示的。

WKUIDelegate

WKWebView将很多 UI 的显示都交给原生层面去解决,例如弹窗或者输入框的显示。这样如果我的项目里有对立定义的弹窗,就能够间接调用自定义弹窗,而不是只能展现零碎弹窗。

WKWebView 中,零碎将弹窗的显示交由客户端来管制。客户端能够通过上面的回调办法获取到弹窗的显示信息,并由客户端来调起 UIAlertController 来展现。参数中有一个 completionHandler 的回调block,须要客户端肯定要调用,如果不调用则会产生解体。

- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler;

有时候 H5 会要求用户进行一些输出,例如用户名明码之类的。客户端能够通过上面的办法获取到输入框事件,并由客户端展现输入框,用户输出实现后将后果回调给 completionHandler 中。

- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(nullable NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * _Nullable result))completionHandler;

WKNavigationDelegate

对于加载流程相干的办法,都被形象到 WKNavigationDelegate 中,这里挑几个比拟罕用的办法讲一下。

上面的办法,通过 decisionHandler 回调中返回一个枚举类型的参数,示意是否容许页面加载。这里能够对域名进行判断,如果是站外域名,则能够提醒用户是否进行跳转。如果是跳转其余 App 或商店的 URL,则能够通过openURL 进行跳转,并将这次申请拦挡。包含 cookie 的解决也在此办法中实现,前面会具体讲到 cookie 的解决。

除此之外,很多页面显示前的逻辑解决,也在此办法中实现。但须要留神的是,办法中不要做过多的耗时解决,会影响页面加载速度。

- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler;

开始加载页面,并申请服务器。

- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(null_unspecified WKNavigation *)navigation;

当页面加载失败的时候,会回调此办法,包含 timeout 等谬误。在这个页面能够展现谬误页面,清空进度条,重置网络指示器等操作。须要留神的是,调用 goBack 时也会执行此办法,能够通过 error 的状态判断是否 NSURLErrorCancelled 来过滤掉。

- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation withError:(NSError *)error;

页面加载及渲染实现,会调用此办法,调用此办法时 H5dom曾经解析并渲染实现,展现在屏幕上。所以在此办法中能够进行一些加载实现的操作,例如移除进度条,重置网络指示器等。

- (void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation;

WKUserContentController

回调

WKWebView将和 js 的交互都由 WKUserContentController 类来解决,前面统称为userContent

如果须要接管并解决 js 的调用,通过调用 addScriptMessageHandler:name: 办法,并传入一个实现了 WKScriptMessageHandler 协定的对象,即可接管 js 的回调,因为 userContent 会强援用传入的对象,所以应该是新创建一个对象,而不是 self。注册对象时,前面的name 就是 js 调用的函数名。

WKUserContentController *userContent = [[WKUserContentController alloc] init];
[userContent addScriptMessageHandler:[[WKWeakScriptMessageDelegate alloc] initWithDelegate:self] name:@"clientCallback"];

dealloc 中应该通过上面的办法,移除对指定 name 的解决。

[userContent removeScriptMessageHandlerForName:@"clientCallback"];

H5通过上面的代码即可对客户端发动调用,调用是通过 postMessage 函数传一个 json 串过去,须要加上转移字符。客户端接管到调用后,依据回调办法传入的 WKScriptMessage 对象,获取到 body 字典,解析传入的参数即可。

window.webkit.messageHandlers.clientCallback.postMessage("{\"funName\":\"getMobileCode\",\"value\":\"srggshqisslfkj\"}");

调用

原生调用 H5 的办法也是一样,创立一个 WKUserScript 对象,并将 js 代码当做参数传入。除了调用 js 代码,也能够通过此办法注入代码扭转页面dom,然而这样代码量较大,不倡议这么做。

WKUserScript *wkcookieScript = [[WKUserScript alloc] initWithSource:self.javaScriptString
                                                          injectionTime:WKUserScriptInjectionTimeAtDocumentStart
                                                       forMainFrameOnly:NO];
[webView.configuration.userContentController addUserScript:wkcookieScript];

WKUserScript vs evaluateJavaScript

WKWebView对于执行 js 代码提供了两种形式,通过 userContent 增加一个 WKUserScript 对象的形式,以及通过 webViewevaluateJavaScript:completionHandler:形式,注入 js 代码。

NSString *removeChildNode = @"""var header = document.getElementsByTagName:('header')[0];"
"header.parentNote.removeChild(header);"
[self.webView evaluateJavaScript:removeChildNode completionHandler:nil];

首先要阐明的是,这两种形式都能够注入 js 代码,然而其外部的实现形式我没有深入研究,WebKit内核是开源的,有趣味的同学能够看看。然而这两种形式还是有一些性能上的区别的,能够依据具体业务场景去抉择对应的API

先说说 evaluateJavaScript:completionHandler: 的形式,这种形式个别是在页面展现实现后执行的操作,用来调用 js 的函数并获取返回值十分不便。当然也能够用来注入一段 js 代码,但须要本人管制注入机会。

WKUserScript则能够管制注入机会,能够针对 document 是否加载完抉择注入 js。以及被注入的js 是在以后页面无效,还是包含其子页面也无效。绝对于 evaluateJavaScript: 办法,此办法不能取得 js 执行后的返回值,所以两个办法在性能上还是有区别的。

容器设计

设计思路

我的项目中个别不会间接应用 WKWebView,而是通过对其进行一层包装,成为一个WKWebViewController 交给业务层应用。设计 webViewVC 时应该遵循简略灵便的思维去设计,本身只提供展现性能,不波及任何业务逻辑。对外提供展现导航栏、设置题目、进度条等性能,都能够通过 WKWebViewConfiguration 赋值并在 WKWebViewController 实例化的时候传入。

对调用方提供 js 交互、webView生命周期、加载谬误等回调,外接通过对应的回调进行解决。这些回调都是可选的,不实现对 webView 加载也没有影响。上面是实例代码,也能够把不同类型的回调拆分定义不同的代理。

@protocol WKWebViewControllerDelegate <NSObject>
@optional
- (void)webViewDidStartLoad:(WKWebViewController *)webViewVC;
- (void)webViewDidFinishLoad:(WKWebViewController *)webViewVC;
- (void)webView:(WKWebViewController *)webViewVC didFailLoadWithError:(NSError *)error;
- (void)webview:(WKWebViewController *)webViewVC closeWeb:(NSString *)info;
- (void)webview:(WKWebViewController *)webViewVC login:(NSDictionary *)info;
- (void)webview:(WKWebViewController *)webViewVC jsCallbackParams:(NSDictionary *)params;
@end

此外,WKWebViewController还应该负责解决公共参数,并且能够基于公共参数进行扩大。这里咱们定义了一个办法,能够指定根底参数的地位,是通过 URL 拼接、headerjs注入等形式增加,这个枚举是多选的,也就是能够在多个地位进行注入。除了根底参数,还能够额定增加自定义参数,也会增加到指定的地位。

- (void)injectionParamsType:(SVParamsType)type additionalParams:(NSDictionary *)additionalParams;

复用池

WKWebView第一次初始化的时候,会先启动 webKit 内核,并且有一些初始化操作,这个操作是十分耗费性能的。所以,复用池设计的第一步,是在 App 启动的时候,初始化一个全局的WKWebView

并且,创立两个池子,创立 visiblePool 寄存正在应用的,创立 reusablePool 寄存闲暇状态的。并且,在页面退出时,从 visiblePool 放入 reusablePool 的同时,应该将页面进行回收,革除页面上的数据。

当须要初始化一个 webView 容器时,从 reusablePool 中取出一个容器,并且放入到 visiblePool 中。通过复用池的实现,能够缩小从初始化一个 webView 容器,到页面展现进去的工夫。

WKProcessPool

WKWebView 中定义了 processPool 属性,能够指定对应的过程池对象。每个 webView 都有本人的内容过程,如果不指定则默认是一个新的内容过程。内容过程中包含一些本地cookie、资源之类的,如果不在一个内容过程中,则不能共享这些数据。

能够创立一个公共的 WKProcessPool,是一个单例对象。所有webView 创立的时候,都应用同一个内容过程,即可实现资源共享。

UserAgent

User-Agent是在 http 协定中的一个申请头字段,用来告知服务器一些信息的,User-Agent中蕴含了很多字段,例如零碎版本、浏览器内核版本、网络环境等。这个字段能够间接用零碎提供的,也能够在原有 User-Agent 的根底上增加其余字段。

例如上面是从零碎的 webView 中获取到的User-Agent

Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_2 like Mac OS X) AppleWebKit/603.2.4 (KHTML, like Gecko) Mobile/14F89

iOS9 之后提供了 customUserAgent 属性,间接为 WKWebView 设置 User-Agent,而iOS9 之前须要通过 js 写入的形式对 H5 注入User-Agent

通信协议

一个设计的比拟好的 WebView 容器,应该具备很好的互相通信性能,并且灵便具备扩展性。H5和客户端的通信次要有以下几种场景。

  • js调用客户端,以及 js 调用客户端后获取客户端的 callback 回调及参数。
  • 客户端调用 js,以及调用js 后的 callback 回调及参数。
  • 客户端被动告诉H5,客户端的一些生命周期变动。例如进入锁屏和进入前台等零碎生命周期。

js 调用客户端为例,有两个纬度的调用。能够通过 URLRouter 的形式间接调用某个模块,这种调用形式遵循客户端的 URL 定义即可调起,并且反对传参。还能够通过 userContentController 的形式,进行页面级的调用,例如敞开 webView、调起登录性能等,也就是通过js 调用客户端的某个性能,这种形式须要客户端提供对应的解决代码。

二者之间互相调用,尽量避免高频调用,而且个别也不会有高频调用的需要。但如果产生雷同性能高频调用,则须要设置一个 actionID 来辨别不同的调用,以保障产生回调时能够失常被辨别。

callback的回调办法也能够通过参数传递过去,这种形式灵活性比拟强,如果固定写死会有版本限度,较早版本的客户端可能并不反对这个回调。

解决回调

webView的回调除了根底的调用,例如 refresh 刷新以后页面、close敞开以后页面等,间接由对应的性能类来解决调用,其余的工夫应该交给外界解决。

这里的设计方案并不是一个事件对应一个回调办法,而后外界遵循代理并实现多个代理办法的形式来实现。而是将每次回调事件都封装成一个对象,间接将这个对象回调给外界解决,这样灵活性更强一些,而且外界获取的信息也更多。事件模型的定义能够参考上面的。

@interface WKWebViewCallbackModel : NSObject
@property(nonatomic, strong) WKWebViewController *webViewVC;
@property(nonatomic, strong) WKCallType *type;
@property(nonatomic, copy) NSDictionary *parameters;
@property(nonatomic, copy) NSString *callbackID;
@property(nonatomic, copy) NSString *callbackFunction;
@end

长久化

目前 H5 页面的长久化计划,次要是 WebKit 自带的 localStorageCookie,然而 Cookie 并不是用来做长久化操作的,所以也不应该给 H5 用来做长久化。如果想更稳固的进行长久化,能够思考提供一个 js bridgeCRUD接口,让 H5 能够用来存储和查问数据。

长久化计划就采取和客户端统一的计划,给 H5 独自建一张数据表即可。

缓存机制

缓存规定

前端浏览器包含 WKWebView 在内,为了保障疾速关上页面,缩小用户流量耗费,都会对资源进行缓存。这个缓存规定在 WKWebView 中也能够指定,如果咱们为了保障每次的资源文件都是最新的,也能够抉择不应用缓存,但咱们个别不这么做。

  • NSURLRequestUseProtocolCachePolicy = 0,默认缓存策略,和 Safari 内核的缓存体现一样。
  • NSURLRequestReloadIgnoringLocalCacheData = 1, 疏忽本地缓存,间接从服务器获取数据。
  • NSURLRequestReturnCacheDataElseLoad = 2, 本地有缓存则应用缓存,否则加载服务端数据。这种策略不会验证缓存是否过期。
  • NSURLRequestReturnCacheDataDontLoad = 3, 只从本地获取,并且不判断有效性和是否扭转,本地没有不会申请服务器数据,申请会失败。
  • NSURLRequestReloadIgnoringLocalAndRemoteCacheData = 4, 疏忽本地以及路由过程中的缓存,从服务器获取最新数据。
  • NSURLRequestReloadRevalidatingCacheData = 5, 从服务端验证缓存是否可用,本地不可用则申请服务端数据。
  • NSURLRequestReloadIgnoringCacheData = NSURLRequestReloadIgnoringLocalCacheData,

依据苹果默认的缓存策略,会进行三步查看。

  1. 缓存是否存在。
  2. 验证缓存是否过期。
  3. 缓存是否产生扭转。

缓存文件

iOS9苹果提供了缓存治理类 WKWebsiteDataStore,通过此类能够对磁盘上,指定类型的缓存文件进行查问和删除。因为当初很多App 都从 iOS9 开始反对,所以十分举荐此 API 来治理本地缓存,以及 cookie。本地的文件缓存类型定义为以下几种,罕用的次要是cookiediskCachememoryCache 这些。

  • WKWebsiteDataTypeFetchCache,磁盘中的缓存,依据源码能够看出,类型是DOMCache
  • WKWebsiteDataTypeDiskCache,本地磁盘缓存,和 fetchCache 的实现不同,是所有的缓存数据
  • WKWebsiteDataTypeMemoryCache,本地内存缓存
  • WKWebsiteDataTypeOfflineWebApplicationCache,离线 web 应用程序缓存
  • WKWebsiteDataTypeCookiescookie缓存
  • WKWebsiteDataTypeSessionStoragehtml会话存储
  • WKWebsiteDataTypeLocalStoragehtml本地数据缓存
  • WKWebsiteDataTypeWebSQLDatabasesWebSQL数据库数据
  • WKWebsiteDataTypeIndexedDBDatabases,数据库索引
  • WKWebsiteDataTypeServiceWorkerRegistrations,服务器注册数据

通过上面的办法能够获取本地所有的缓存文件类型,返回的汇合字符串,就是下面定义的类型。

+ (NSSet<NSString *> *)allWebsiteDataTypes;

能够指定删除某个时间段内,指定类型的数据,删除后会回调block

- (void)removeDataOfTypes:(NSSet<NSString *> *)dataTypes modifiedSince:(NSDate *)date completionHandler:(void (^)(void))completionHandler;

零碎还提供了定制化更强的办法,通过 fetchDataRecordsOfTypes: 办法获取指定类型的所有 WKWebsiteDataRecord 对象,此对象蕴含域名和类型两个参数。能够依据域名和类型进行判断,随后调用 removeDataOfTypes: 办法传入须要删除的对象,对指定域名下的数据进行删除。

// 获取
- (void)fetchDataRecordsOfTypes:(NSSet<NSString *> *)dataTypes completionHandler:(void (^)(NSArray<WKWebsiteDataRecord *> *))completionHandler;
// 删除
- (void)removeDataOfTypes:(NSSet<NSString *> *)dataTypes forDataRecords:(NSArray<WKWebsiteDataRecord *> *)dataRecords completionHandler:(void (^)(void))completionHandler;

http 缓存策略

客户端和 H5 在打交道的时候,常常会呈现页面缓存的问题,H5的开发同学就常常说“你清一下缓存试试”,实际上产生这个问题的起因,在于 H5 的缓存管理策略有问题。这里就讲一下 H5 的缓存管理策略。

H5的缓存治理其实就是利用 http 协定的字段进行治理的,比拟罕用的是 Cache-ControlLast-Modified搭配应用的形式。

  • Cache-Control:文件缓存无效时长,例如申请文件后服务器响应头返回 Cache-Control:max-age=600,则示意文件无效时长600 秒。所以此文件在无效时长内,都不会收回网络申请,直到过期为止。
  • Last-Modified:申请文件后服务器响应头中返回的,示意文件的最新更新工夫。如果 Cache-Control 过期后,则会申请服务器并将这个工夫放在申请头的 If-Modified-Since 字段中,服务器收到申请后会进行工夫比照,如果工夫没有产生扭转则返回304,否则返回新的文件和响应头字段,并返回200

Cache-Controlhttp1.1 进去的,示意文件的绝对无效时长,在此之前还有 Expires 字段,示意文件的相对无效时长,例如Expires: Thu, 10 Nov 2015 08:45:11 GMT,二者都能够用。

Last-Modified也有相似的字段 Etag,区别在于Last-Modified 是以工夫做比照,Etag是以文件的哈希值做比照。当文件无效时长过期后,申请服务器会在申请头的 If-None-Match 字段带上 Etag 的值,并交由服务器比照。

Cookie 解决

家喻户晓,http协定中是反对 cookie 设置的,服务器能够通过 Set-Cookie: 字段对浏览器设置 cookie,并且还能够指定过期工夫、域名等。这些在Chrome 这些浏览器中比拟实用,然而如果在客户端内进行显示,就须要客户端传一些参数过来,能够让 H5 获取到登录等状态。

苹果尽管提供了一些 Cookie 治理的 API,但在WKWebView 的应用上还是有很多坑的,最初我会给出一个比拟通用的计划。

WKWebView Cookie 设计

之前应用 UIWebView 的时候,和传统的 cookie 治理类 NSHTTPCookieStorage 读取的是一块区域,或者说 UIWebViewcookie也是由此类治理的。然而 WKWebViewcookie设计不太一样,和 Appcookie并没有存储在同一块内存区域,所以二者须要离开做解决。

WKWebViewcookieNSHTTPCookieStorage之间也有同步操作,然而这个同步有显著的延时,而且规定不容易推敲。所以为了代码的稳定性,还是本人解决 cookie 比拟适合。

WKapp 是两个过程,cookie也是两份,然而 WKcookieapp 的沙盒里。有一个定时同步,然而并没有一个特定规定,所以最好不要依赖同步。WKcookie 变动只有两个机会,一个是 js 执行代码 setCookie,另一个是response 返回cookie

WKWebsiteDataStore

Cookie的治理始终都是 WKWebView 的一个弊病,对于 Cookie 的解决很不不便。在 iOS9 中能够通过 WKWebsiteDataStoreCookie进行治理,然而用起来并不直观,须要进行 dataType 进行筛选并删除。而且 WKWebsiteDataStore 本身性能并不具备增加性能,所以对 cookie 的解决也只有删除,不能增加cookie

if (@available(iOS 9.0, *)) {NSSet *cookieTypeSet = [NSSet setWithObject:WKWebsiteDataTypeCookies];
    [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:cookieTypeSet modifiedSince:[NSDate dateWithTimeIntervalSince1970:0] completionHandler:^{}];
}

WKHTTPCookieStore

iOS11 中苹果在 WKWebsiteDataStore 的根底上,为其减少了 WKHTTPCookieStore 类专门进行 cookie 的解决,并且反对减少、删除、查问三种操作,还能够注册一个 observercookie的变动进行监听,当 cookie 发生变化后通过回调的办法告诉监听者。

WKWebsiteDataStore能够获取 H5 页面通过 document.cookie 的形式写入的 cookie,以及服务器通过Set-Cookie 的形式写入的 cookie,所以还是很举荐应用这个类来治理cookie 的,惋惜只反对iOS11

上面是给 WKWebView 增加 cookie 的一段代码。

NSMutableDictionary *params = [NSMutableDictionary dictionary];
[params setObject:@"password" forKey:NSHTTPCookieName];
[params setObject:@"e10adc3949ba5" forKey:NSHTTPCookieValue];
[params setObject:@"www.google.com" forKey:NSHTTPCookieDomain];
[params setObject:@"/" forKey:NSHTTPCookiePath];
[params setValue:[NSDate dateWithTimeIntervalSinceNow:60*60*72] forKey:NSHTTPCookieExpires];
NSHTTPCookie *cookie = [NSHTTPCookie cookieWithProperties:params];
[self.cookieWebview.configuration.websiteDataStore.httpCookieStore setCookie:cookie completionHandler:nil];

我公司计划

解决 Cookie 最好的形式是通过 WKHTTPCookieStore 来解决,但其只反对 iOS11 及以上设施,所以这种计划目前还不能作为咱们的抉择。其次是 WKWebsiteDataStore,但其只能作为一个删除cookie 的应用,并不不能用来治理cookie

我公司的计划是,通过 iOS8 推出的 WKUserContentController 来治理 webViewcookie,通过 NSHTTPCookieStorage 来管理网络申请的 cookie,例如H5 收回的申请。通过 NSURLSessionNSURLConnection 收回的申请,都会默认带上 NSHTTPCookieStorage 中的 cookieH5 外部的申请也会被零碎交给 NSURLSession 解决。

在代码实现层面,监听 didFinishLaunching 告诉,在程序启动时从服务端申请用户相干信息,当然从本地取也能够,都是一样的。数据是 keyvalue 的模式下发,依照 key=value 的模式拼接,并通过 document.cookie 组装成设置 cookiejs代码,所有代码拼接为一个以分号宰割的字符串,前面给 webViewcookie时就通过这个字符串执行。

对于网络申请的 cookie,通过NSHTTPCookieStorage 间接将 cookie 种到根域名下的,能够对根域名下所有子域名失效,这里的解决比较简单。

SVREQUEST.type(SVRequestTypePost).parameters(params).success(^(NSDictionary *cookieDict) {self.cookieData = [cookieDict as:[NSDictionary class]];
    [self addCookieWithDict:cookieDict forHost:@".google.com"];
    [self addCookieWithDict:cookieDict forHost:@".google.cn"];
    [self addCookieWithDict:cookieDict forHost:@".google.jp"];
    
    NSMutableString *scriptString = [NSMutableString string];
    for (NSString *key in self.cookieData.allKeys) {NSString *cookieString = [NSString stringWithFormat:@"%@=%@", key, cookieDict[key]];
        [scriptString appendString:[NSString stringWithFormat:@"document.cookie ='%@;expires=Fri, 31 Dec 9999 23:59:59 GMT;';", cookieString]];
    }
    self.webviewCookie = scriptString;
}).startRequest();

- (void)addCookieWithDict:(NSDictionary *)dict forHost:(NSString *)host {[dict enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, NSString * _Nonnull value, BOOL * _Nonnull stop) {NSMutableDictionary *properties = [NSMutableDictionary dictionary];
        [properties setObject:key forKey:NSHTTPCookieName];
        [properties setObject:value forKey:NSHTTPCookieValue];
        [properties setObject:host forKey:NSHTTPCookieDomain];
        [properties setObject:@"/" forKey:NSHTTPCookiePath];
        [properties setValue:[NSDate dateWithTimeIntervalSinceNow:60*60*72] forKey:NSHTTPCookieExpires];
        NSHTTPCookie *cookie = [NSHTTPCookie cookieWithProperties:properties];
        [[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookie:cookie];
    }];
}

webViewcookie是通过 WKUserContentController 写入 js 的形式实现的,也就是下面拼接的 js 字符串。然而这个类有一个问题就是不能长久化 cookie,也就是cookieuserContentController的申明周期,如果退出 Appcookie就会隐没,下次进入 App 还须要种一次,这是个大问题。

所以我司的解决形式是在 decidePolicyForNavigationAction: 回调办法中退出上面这段代码,代码中会判断此域名是否种过 cookie,如果没有则种cookie。对于cookie 的解决,我新建了一个 cookieWebview 专门解决 cookie 的问题,当执行 addUserScript 后,通过 loadHTMLString:baseURL: 加载一个空的本地 html,并将域名设置为以后将要显示页面的域名,从而使方才种的cookie 对以后 processPool 内所有的 webView 失效。

这种计划种 cookie 是同步执行的,而且对 webView 的影响很小,通过我的测试,均匀增加一次 cookie 只须要耗费 28ms 的工夫。从用户的角度来看是无感知的,并不会有页面的卡顿或从新刷新。

- (void)setCookieWithUrl:(NSURL *)url {NSString *host = [url host];
    if ([self.cookieURLs containsObject:host]) {return;}
    [self.cookieURLs addObject:host];
    
    WKUserScript *wkcookieScript = [[WKUserScript alloc] initWithSource:self.webviewCookie
                                                          injectionTime:WKUserScriptInjectionTimeAtDocumentStart
                                                       forMainFrameOnly:NO];
    [self.cookieWebview.configuration.userContentController addUserScript:wkcookieScript];
    
    NSString *baseWebUrl = [NSString stringWithFormat:@"%@://%@", url.scheme, url.host];
    [self.cookieWebview loadHTMLString:@"" baseURL:[NSURL URLWithString:baseWebUrl]];
}

删除 cookie 的解决则绝对比较简单,NSHTTPCookieStorage通过 cookies 属性遍历到本人须要删除的 NSHTTPCookie,调用办法将其删除即可。webView 的删除办法更是简略粗犷,间接调用 removeAllUserScripts 删除所有 WKUserScript 即可。

- (void)removeWKWebviewCookie {
    self.webviewCookie = nil;
    [self.cookieWebview.configuration.userContentController removeAllUserScripts];
    
    NSMutableArray<NSHTTPCookie *> *cookies = [NSMutableArray array];
    [[NSHTTPCookieStorage sharedHTTPCookieStorage].cookies enumerateObjectsUsingBlock:^(NSHTTPCookie * _Nonnull cookie, NSUInteger idx, BOOL * _Nonnull stop) {if ([self.cookieData.allKeys containsObject:cookie.name]) {[cookies addObjectOrNil:cookie];
        }
    }];
    
    [cookies enumerateObjectsUsingBlock:^(NSHTTPCookie * _Nonnull cookie, NSUInteger idx, BOOL * _Nonnull stop) {[[NSHTTPCookieStorage sharedHTTPCookieStorage] deleteCookie:cookie];
    }];
}

白屏问题

如果 WKWebView 加载内存占用过多的页面,会导致 WebContent Process 过程解体,进而页面呈现白屏,也有可能是零碎其余过程占用内存过多导致的白屏。对于低内存导致的白屏问题,有以下两种计划能够解决。

iOS9 中苹果推出了上面的 API,当WebContent 过程产生异样退出时,会回调此 API。能够在这个API 中进行对应的解决,例如展现一个异样页面。

- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView;

如果从其余 App 回来导致白屏问题,能够在视图将要显示的时候,判断 webView.title 是否为空。如果为空则展现异样页面。

正文完
 0