美文网首页
探秘WKWebView

探秘WKWebView

作者: 刘小壮 | 来源:发表于2021-11-01 10:15 被阅读0次

    概述

    之前主要使用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是否为空。如果为空则展示异常页面。

    相关文章

      网友评论

          本文标题:探秘WKWebView

          本文链接:https://www.haomeiwen.com/subject/kglzaltx.html