干货:探秘WKWebView
共 17123字,需浏览 35分钟
·
2021-11-01 15:13
本文字数:6540字
预计阅读时间:18分钟
概述
之前主要使用UIWebView
进行页面的加载,但是UIWebView
存在很多问题,在2020年已经被苹果正式抛弃。所以本篇文章主要讲解WKWebView
,WKWebView
从iOS8
开始支持,现在大多数App
应该都不支持iOS7
了。
UIWebView
存在两个问题,一个是内存消耗比较大,另一个是性能很差。WKWebView
相对于UIWebView
来说,性能要比UIWebView
性能要好太多,刷新率能达到60FPS
。内存占用也比UIWebView
要小。
WKWebView
是一个多进程组件,Network
、UI Render
都在独立的进程中完成。
由于WKWebView
和App
不在同一个进程,如果WKWebView
进程崩溃并不会导致应用崩溃,仅仅是页面白屏等异常。页面的载入、渲染等消耗内存和性能的操作,都在WKWebView
的进程中处理,处理后再将结果交给App
进程用于显示,所以App
进程的性能消耗会小很多。
网页加载流程
通过域名的方式请求服务器,请求前浏览器会做一个 DNS
解析,并将IP
地址返回给浏览器。浏览器使用 IP
地址请求服务器,并且开始握手过程。TCP
是三次握手,如果使用https
则还需要进行TLS
的握手,握手后根据协议字段选择是否保持连接。握手完成后,浏览器向服务端发送请求,获取 html
文件。服务器解析请求,并由 CDN
服务器返回对应的资源文件。浏览器收到服务器返回的 html
文件,交由html
解析器进行解析。解析 html
由上到下进行解析xml
标签,过程中如果遇到css
或资源文件,都会进行异步加载,遇到js
则会挂起当前html
解析任务,请求js
并返回后继续解析。因为js
文件可能会对DOM
树进行修改。解析完 html
,并执行完js
代码,形成最终的DOM
树。通过DOM
配合css
文件找出每个节点的最终展示样式,并交由浏览器进行渲染展示结束链接。
代理方法
WKWebView
和UIWebView
的代理方法发生了一些改变,WKWebView
的流程更加细化了。例如之前UI
结束请求后,会立刻渲染到webView
上。而WKWebView
则会在渲染到屏幕之前,会回调一个代理方法,代理方法决定是否渲染到屏幕上。这样就可以对请求下来的数据做一次校验,防止数据被更改,或验证视图是否允许被显示到屏幕上。
除此之外,WKWebView
相对于UIWebView
还多了一些定制化操作。
重定向的回调,可以在请求重定向时获取到这次操作。 当 WKWebView
进程异常退出时,可以通过回调获取。自定义处理证书。 更深层的 UI
定制操作,将alert
等UI
操作交给原生层面处理,而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;
页面加载及渲染完成,会调用此方法,调用此方法时H5
的dom
已经解析并渲染完成,展示在屏幕上。所以在此方法中可以进行一些加载完成的操作,例如移除进度条,重置网络指示器等。
- (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
对象的方式,以及通过webView
的evaluateJavaScript: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
拼接、header
、js
注入等方式添加,这个枚举是多选的,也就是可以在多个位置进行注入。除了基础参数,还可以额外添加自定义参数,也会添加到指定的位置。
- (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
自带的localStorage
和Cookie
,但是Cookie
并不是用来做持久化操作的,所以也不应该给H5
用来做持久化。如果想更稳定的进行持久化,可以考虑提供一个js bridge
的CRUD
接口,让H5
可以用来存储和查询数据。
持久化方案就采取和客户端一致的方案,给H5
单独建一张数据表即可。
缓存机制
缓存规则
前端浏览器包括WKWebView
在内,为了保证快速打开页面,减少用户流量消耗,都会对资源进行缓存。这个缓存规则在WKWebView
中也可以指定,如果我们为了保证每次的资源文件都是最新的,也可以选择不使用缓存,但我们一般不这么做。
NSURLRequestUseProtocolCachePolicy = 0
,默认缓存策略,和Safari
内核的缓存表现一样。NSURLRequestReloadIgnoringLocalCacheData = 1,
忽略本地缓存,直接从服务器获取数据。NSURLRequestReturnCacheDataElseLoad = 2
, 本地有缓存则使用缓存,否则加载服务端数据。这种策略不会验证缓存是否过期。NSURLRequestReturnCacheDataDontLoad = 3
, 只从本地获取,并且不判断有效性和是否改变,本地没有不会请求服务器数据,请求会失败。NSURLRequestReloadIgnoringLocalAndRemoteCacheData = 4
, 忽略本地以及路由过程中的缓存,从服务器获取最新数据。NSURLRequestReloadRevalidatingCacheData = 5
, 从服务端验证缓存是否可用,本地不可用则请求服务端数据。NSURLRequestReloadIgnoringCacheData = NSURLRequestReloadIgnoringLocalCacheData
,
根据苹果默认的缓存策略,会进行三步检查。
缓存是否存在。 验证缓存是否过期。 缓存是否发生改变。
缓存文件
iOS9
苹果提供了缓存管理类WKWebsiteDataStore
,通过此类可以对磁盘上,指定类型的缓存文件进行查询和删除。因为现在很多App
都从iOS9
开始支持,所以非常推荐此API
来管理本地缓存,以及cookie
。本地的文件缓存类型定义为以下几种,常用的主要是cookie
、diskCache
、memoryCache
这些。
WKWebsiteDataTypeFetchCache
,磁盘中的缓存,根据源码可以看出,类型是DOMCache
WKWebsiteDataTypeDiskCache
,本地磁盘缓存,和fetchCache
的实现不同,是所有的缓存数据WKWebsiteDataTypeMemoryCache
,本地内存缓存WKWebsiteDataTypeOfflineWebApplicationCache
,离线web
应用程序缓存WKWebsiteDataTypeCookies
,cookie
缓存WKWebsiteDataTypeSessionStorage
,html
会话存储WKWebsiteDataTypeLocalStorage
,html
本地数据缓存WKWebsiteDataTypeWebSQLDatabases
,WebSQL
数据库数据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-Control
和Last-Modified
搭配使用的方式。
Cache-Control
:文件缓存有效时长,例如请求文件后服务器响应头返回Cache-Control:max-age=600
,则表示文件有效时长600
秒。所以此文件在有效时长内,都不会发出网络请求,直到过期为止。Last-Modified
:请求文件后服务器响应头中返回的,表示文件的最新更新时间。如果Cache-Control
过期后,则会请求服务器并将这个时间放在请求头的If-Modified-Since
字段中,服务器收到请求后会进行时间对比,如果时间没有发生改变则返回304
,否则返回新的文件和响应头字段,并返回200
。
Cache-Control
是http1.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
读取的是一块区域,或者说UIWebView
的cookie
也是由此类管理的。但是WKWebView
的cookie
设计不太一样,和App
的cookie
并没有存储在同一块内存区域,所以二者需要分开做处理。
WKWebView
的cookie
和NSHTTPCookieStorage
之间也有同步操作,但是这个同步有明显的延时,而且规则不容易琢磨。所以为了代码的稳定性,还是自己处理cookie
比较合适。
WK
和app
是两个进程,cookie
也是两份,但是WK
的cookie
在app
的沙盒里。有一个定时同步,但是并没有一个特定规则,所以最好不要依赖同步。WK
的cookie
变化只有两个时机,一个是js
执行代码setCookie
,另一个是response
返回cookie
。
WKWebsiteDataStore
Cookie
的管理一直都是WKWebView
的一个弊端,对于Cookie
的处理很不方便。在iOS9
中可以通过WKWebsiteDataStore
对Cookie
进行管理,但是用起来并不直观,需要进行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
的处理,并且支持增加、删除、查询三种操作,还可以注册一个observer
对cookie
的变化进行监听,当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
来管理webView
的cookie
,通过NSHTTPCookieStorage
来管理网络请求的cookie
,例如H5
发出的请求。通过NSURLSession
、NSURLConnection
发出的请求,都会默认带上NSHTTPCookieStorage
中的cookie
,H5
内部的请求也会被系统交给NSURLSession
处理。
在代码实现层面,监听didFinishLaunching
通知,在程序启动时从服务端请求用户相关信息,当然从本地取也可以,都是一样的。数据是key
、value
的形式下发,按照key=value
的形式拼接,并通过document.cookie
组装成设置cookie
的js
代码,所有代码拼接为一个以分号分割的字符串,后面给webView
种cookie
时就通过这个字符串执行。
对于网络请求的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];
}];
}
对webView
种cookie
是通过WKUserContentController
写入js
的方式实现的,也就是上面拼接的js
字符串。但是这个类有一个问题就是不能持久化cookie
,也就是cookie
随userContentController
的声明周期,如果退出App
则cookie
就会消失,下次进入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
是否为空。如果为空则展示异常页面。