众所周知,WKWebView是一个独立进程,关于开发者来说,WKWebView适当于一个黑盒。那么当开发者需求监控/接收WKWebView网络恳求时,咱们是否有办法可循呢?假如你也有这样的疑问,那请听我娓娓道来~

如何接管WKWebView的网络请求?

布景

用户免流
在网页中,客户端对免流用户无法做到对免流,这无疑是丧命的。倘若免流用户在网页观看一个视频,实际却是运用自身流量,那么势必会收到用户投诉! 轻则被喷,重则赔钱。

监控网络
咱们经常收到用户投诉网页无法翻开,却因为WKWebView的黑盒属性,让咱们的剖析无从下手。所以咱们有必要监控WKWebView的网络恳求!然后剖析问题的严重性以及原因。

计划比照

NSURLProtocol是一个非常奇特的类,他是URL Loding System的一部分,能够阻拦依据URL Loading System的网络恳求,就相似于一个网络中间人。虽然WKWebView不会走APPURL Loading System,但咱们仍然能够经过一些技巧来进行阻拦。这篇文章不展开剖析,传送门 – NSURLProtocol对WKWebView的处理

WKURLSchemeHandler他是由苹果提供给开发者阻拦自定义的URLScheme协议。传送门 – WKURLSchemeHandler介绍,经过一些技巧,咱们也能够经过WKURLSchemeHandler来进行阻拦。

稳定性

NSURLProtocol阻拦到Post恳求时,因为考虑到IPC通讯效率问题,WebKit开发者将将body进行了丢掉操作,所以会发生丢body的问题。
但咱们能够经过注入js脚本,Hook-XMLHttpRequest/Fetch恳求,然后经过window.webkit.messageHandlers将包装后的body传输到App端,并由APP端进行数据存储,以便NSURLProtocol阻拦时运用。虽然经过这样的办法会耗费必定内存,而且使得流程变得复杂,但他是一种可行计划。

WKURLSchemeHandler关于绝大多数Post恳求都是不会丢body的,但关于blob,表单等类型的恳求也会有丢body的状况,具体操作和NSURLProtocol相似,后续我会展开描绘。

在稳定性方面,两者适当。

安全性

NSURLProtocol侵入性略强,当NSURLProtocol翻开后,一切依据URL Loading System的网络恳求都将被阻拦,这无疑添加了许多危险。其间最具代表的就是会侵略第三方库中的WKWebView, 导致其Post恳求统统失利。

WKURLSchemeHandler经过WKWebViewConfiguration对WKWebView进行注入, 这种办法能够将阻拦的粒度操控在单个WkWebView之中,从危险把控的视点来衡量,这无疑是更好的计划。

在安全性上,WkWebView无疑是更优的挑选。

扩展性

NSURLProtocol只会在网络建议阶段进行阻拦,所以关于那些被浏览器缓存的资源,NSURLProtocol将对其无从下手,丧失了必定的资源管控能力。

WKURLSchemeHandler阻拦机遇靠前,会在浏览器读取缓存之前就进行阻拦。所以理论上WKURLSchemeHandler是能够对资源全权掌控,这就带来更多的可能和创想(后续我会进行这方面的共享)。

在扩展性上,我认为WKURLSchemeHandler更胜一筹。

兼容性

NSURLProtocol支撑一切版别。

WKURLSchemeHandler从iOS11.0开始支撑,可是依据线上数据来看,iOS11.0以下的用户占比极小,所以这个问题并不丧命。

在兼容性上,NSURLProtocol体现的愈加全面。

小结

我依据重要性以及现实状况对各项目标进行排序打分。稳定性总分10分,安全性总分10分,扩展性总分5分,兼容性总分5分。

NSURLProtocol WKURLSchemeHandler
稳定性 8分 8分
安全性 5分 8分
扩展性 2分 4分
兼容性 5分 4分
总分 20分 24分

依据上述剖析,终究判定 WKURLSchemeHandler将会是一个更优的挑选。

履行计划

关于这种不知道且具有危险的功用,往往是让人担忧的。因为一个不小心就会导致线上事端,或许碰到意料之外的溃散(依据经历来看,这是必定事情)。而且怎么承认这个不知道功用是否有用,以及承认后续推进方向,这也是让人头痛的问题。所以让计划的线上战略变得尤为重要!

线上战略

灰度开关

我在功用的总入口处配置了灰度开关,且经过后台操控灰度值。经过这个灰度开关能够到达三个意图:

  1. 保证安全:呈现严重bug时,将灰度值设置为0即可全关网络阻拦计划,走原生计划。
  2. 保证稳定:逐步掩盖,避免呈现大面积bug呈现。
  3. 数据比照:比照履行计划与原生计划数据,让数据更有说服力。 ps: 代码上怎么做灰度开关?代码中取[1-100]随机数A,后台下发灰度值G(规模[0-100]),若A<=G则命中灰度,履行阻拦计划,反之亦然。举个例子:让灰度值为20%,那么将后台下发灰度值G设置为20即可。

构建白名单

我经过后台下发网页白名单,当WKWebView加载一个HTML时,会与白名单进行匹配。当HTML与白名单匹配成功&&命中灰度值时,则履行阻拦计划。构建白名单的必要性在于:

  1. 降低侵入:将端外网页扫除在外,避免不可控危险。
  2. 隔离危险:当某一些网页因阻拦计划呈现异常时,能够将其扫除白名单,保证其他网页还能正常运行。

数据计算

一个好的数据计算不只能让阻拦计划更具有说服力,更能从数据上剖析出潜在问题,并为后期迭代优化指明方向。为了到达上述意图,我构建了如下目标:

  1. 阻拦计划掩盖率:用于判别阻拦计划线上影响规模。
  2. 网页翻开成功率:用于判别阻拦计划是否发生负面影响。
  3. 阻拦恳求成功率:用于证明阻拦计划的可靠性
  4. 阻拦恳求失利事情:挑选和剖析失利场景,排名Top的失利事情都是后续优化的集火目标。
  5. 数据比照:在数据2中添加阻拦计划与原生计划进行比照,添加数据的可信度。

遇到的问题

阻拦http/https时发生溃散

场景: 按照文档运用指引,咱们需求进行注册,代码如下:

WKWebViewConfiguration *wkconfig = [[WKWebViewConfiguration alloc] init];
WWKProxyWKURLSchemeHandler* schemeHandler = [[WWKProxyWKURLSchemeHandler alloc] init]; 
[wkconfig setURLSchemeHandler:schemeHandler forURLScheme:@"https"];
[wkconfig setURLSchemeHandler:schemeHandler forURLScheme:@"http"];

可是当咱们实在运行时,会收到如下报错:

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: ''http' is a URL scheme that WKWebView handles natively'
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: ''https' is a URL scheme that WKWebView handles natively'

剖析: 能够看到这是一个NSException异常,而且WebKit又是开源的,所以直接在源码中查找这段异常信息,就能快速定位到对应的代码逻辑。

如何接管WKWebView的网络请求?
然后深入到+[WKWebView handlesURLScheme:]里面,研讨其底层逻辑,其实就是最底层有个类型判别只要是WS、WSS、File、FTP、HTTP、HTTPS,都会报错。这篇文章就不做过多赘述了,感兴趣的朋友能够去研讨下源码。

计划:经过上述剖析,能够得到一个解决计划,那就是Hook +[WKWebView handlesURLScheme:],并将其重写绕过特别判别逻辑。代码如下:

+ (void)load
{
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    Method originalMethod = class_getClassMethod([WKWebView class], @selector(handlesURLScheme:));
    Method swizzledMethod = class_getClassMethod([self class], @selector(qm_handlesURLScheme:));
    method_exchangeImplementations(originalMethod, swizzledMethod);
  });
}
+ (BOOL)qm_handlesURLScheme:(NSString *)urlScheme
{
  if ([urlScheme isEqualToString:@"https"] || [urlScheme isEqualToString:@"http"])
  {
    return NO;
  }
  else
  {
    return [QMManager qm_handlesURLScheme:urlScheme];
  }
}

线程切换

场景: 退出网页时,有必定概率闪退。在正在播映视频的网页,更简单复现此bug。

剖析WKURLSchemeHandler会在WKWebView进程中进行处理,然后抛出WKURLSchemeTask给到客户端。那么当网页退出时,WKURLSchemeTask会被中止,此刻假如客户端网络恳求返回,而且运用WKURLSchemeTask进行操作,就会呈现The task has already been stopped的溃散。究其原因是因为线程切换,导致WKURLSchemeTask状态无法及时同步。

计划:需求做如下两种维护操作

  1. WKURLSchemeTask添加相关目标(QMStopURLSchemeTask),当接收到回调- (void)webView:(nonnull WKWebView *)webView stopURLSchemeTask:(nonnull id<WKURLSchemeTask>)urlSchemeTask时,将QMStopURLSchemeTask设置为YES。而每次运用WKURLSchemeTask时,都必先判别QMStopURLSchemeTask状态,若为YES则中止后续流程,反之亦然。
  2. 建立WKWebView与网络使命的映射关系,当WKWebView-dealloc时,将其对应的网络使命都中止。

网络重定向

场景WKURLSchemeHandler没有敞开重定向API。

剖析WKURLSchemeHandler是苹果给到开发者阻拦自定义的URLScheme协议,本质上就不会去关心网络相关内容。所以没有暴露重定向API也是情理之中了。

计划:所以直接去源码里面看看吧~ 很走运的是,私有API中是有重定向的办法的

如何接管WKWebView的网络请求?
但需求说明的两点是:

  1. 这里运用的是私有API,所以必定要做混淆,避免被拒审。
  2. 内部实现是直接修正WKURLSchemeTaskrequest信息,实现重定向的。 毕竟私有API不是完美解法,若咱们有更好计划,欢迎留言/私聊评论~

xhr upload的crash

场景:功用上线后呈现了一个较多的溃散,仓库如下。

#0 0x00007fff4b8d6a3e in WebCore::blobRegistry() ()
#1 0x00007fff4b9052ad in WebCore::createHTTPBodyCFReadStream(WebCore::FormData&) () 
#2 0x00007fff4b905d18 in WebCore::setHTTPBody(_CFURLRequest*, WebCore::FormData*) ()
#3 0x00007fff4a851e0c in WebCore::ResourceRequest::doUpdatePlatformHTTPBody() () 
#4 0x00007fff4b8fe47d in WebCore::ResourceRequestBase::updatePlatformRequest(WebCore::HTTPBodyUpdatePolicy) const () 
#5 0x00007fff4a8506de in WebCore::ResourceRequest::nsURLRequest(WebCore::HTTPBodyUpdatePolicy) const () 
#6 0x00007fff2d1950ea in WebKit::WebURLSchemeTask::nsRequest() const () 
#7 0x0000000105f082f4 in WebViewProtocol.webView(_:start:) at /Users/t_honda/iOSProjects/smartdb-mobile-ios/smartdb/DocumentDetail/webView/WebViewProtocol.swift:30 
#8 0x0000000105f09989 in @objc WebViewProtocol.webView(_:start:) () 
#9 0x00007fff2d13b41f in WebKit::WebURLSchemeHandlerCocoa::platformStartTask(WebKit::WebPageProxy&, WebKit::WebURLSchemeTask&) () 
#10 0x00007fff2d1940ca in WebKit::WebURLSchemeHandler::startTask(WebKit::WebPageProxy&, WebKit::WebProcessProxy&, unsigned long long, WebCore::ResourceRequest&&, WTF::CompletionHandler

剖析:经过日志定位到犯错网页,然后复现其操作。发现其在运用xhr upload时,发生了溃散。当时比较怀疑是体系的问题,经过一系列曲折,终究在WebKit的issue中找到关键点:Bug205685 -XMLHTTPRequest POSTs blob data to a custom WKURLSchemeHandler protocol crash。这里描绘的溃散原因是:当WKWebView创立一个httpBody中的data时,会调用WebCore::blobRegistry办法,但返回的是一个空指针,然后导致了溃散。
另外在stackoverflow中也找到对应的解决计划,其经过私有api+[WebView _setLoadResourcesSerially:]来解决这个bug:iOS WKWebView – WKURLSchemeHandler crash on posting body (EXC_BAD_ACCESS)

计划:经过剖析得出如下解决计划:

  1. 经过crash搜集体系,概括溃散的体系版别。
  2. 依据线上bug体系搜集的iOS版别信息,对指定iOS版别履行私有api,避免溃散。

cookies同步问题

场景WKWebViewcookies是由WKHTTPCookieStore进行办理,客户端的cookies是由NSHTTPCookieStoreage进行办理。履行网络阻拦计划后,绝大多数场景都得到了更好的体会,如:网页拉起客户端登录场景,因为网络都由客户端进行处理,所以免去了惯例的NSHTTPCookieStoreageWKHTTPCookieStore同步操作。
可是在一些特别状况下,如:某些网页需求获取cookies用作特别逻辑判别,而网页只能从WKHTTPCookieStore获取到cookie,那么就会存在WKHTTPCookieStore还未更新,导致获取cookies已过期的问题。

剖析:我经过对NSHTTPCookieStoreageWKHTTPCookieStore的单元测试,得出以下定论:

  1. WKWebView中的WKHTTPCookieStore是共用的。
  2. 体系会自动同步NSHTTPCookieStoreageWKHTTPCookieStore
    2.1 从WKHTTPCookieStore同步到NSHTTPCookieStoragecookie操作,都是安全的,及时的,因为NSHTTPCookieStoragecookie操作在主线程操作。
    2.2 从NSHTTPCookieStorage同步到WKHTTPCookieStorecookie操作,是不安全的,不及时的,因为WKHTTPCookieStorecookie操作都为异步线程且机遇也是无法操控的。
  3. H5运用document.cookie会修正WKHTTPCookieStorecookie,而且还能够及时同步到NSHTTPCookieStore
    更多概况点这里: 关于WKHTTPCookieStore与NSHTTPCookieStorage同步问题
    注意:自测时请运用真机,模拟器是不会同步

计划:考虑到此处为小事情,为了缩小影响规模,我挑选放弃对NSHTTPCookieStorage进行监听。转而为网页定制JSAPI,由网页来自动同步。
核心流程为
H5调用定制JSAPI
-> 传入当前需求同步的URL
-> 客户端接受到URL后,依据URL在NSHTTPCookieStorage中查找对应的cookies,并回传给H5
-> H5获取到cookies,并将cookies存储到webStorage中。
至此完成cookies同步。

Blob类型丢body问题

场景:初度发现是H5裁剪图片后,并运用Blob类型上传裁剪图片,此刻WKURLSchemeHandler阻拦到的request恳求中的body为空。

剖析:Blob是一个目标类型,且是在WKWebView进程中发生。当客户端经过WKURLSchemeHandler阻拦时,因为此刻Blob仅仅一个目标,且这个目标的地址是在WKWebView的进程之中,而咱们无法在客户端进程去读取WKWebView进程中的内存信息,然后导致无法经过Blob目标获取实在的data数据。然后就会呈现丢body的状况。这类状况都归于流式上传,所以像表单也会有丢body的问题。

计划:既然客户端进程无法运用Blob目标,那么就只能在WKWebView进程中操作Blob目标再传递给客户端进程。具体流程如下:

如何接管WKWebView的网络请求?

总结

本文从布景开始讲述,从用户免流以及监控网络证明了接收WKWebView网络恳求的必要性。
之后从稳定性、安全性、扩展性、兼容性,四个视点比照WKURLSchemeHanlderNSURLProtocol,终究挑选WKURLSchemeHanlder为技能计划。
在实在履行计划时,我首要共享了线上战略,主要囊括为三类:灰度开关、白名单、数据计算。让咱们的计划在履行时具有稳定、安全且可剖析的状态。之后又例举了实在遇到的问题与解决计划,咱们能够从中参阅,解决实际问题。
在我看来,此问题最大的难点是其不知道性,因为竞品和网络文章中没有一个稳定的技能计划,而且网页的影响面极大,导致我无法给出准确的预期以及声明一切危险。相信咱们在遇到相似的问题时,都会有迷茫,纠结,乃至畏惧的心情。我经过此次问题,总结了如下几点,希望咱们之后遇到相似问题时,能够助你一臂之力:

  1. 计划预研要全面。从多个视点进行剖析。
  2. 从源码中找答案。(假如条件答应的状况下)
  3. 安全永远是第一优先级。请在上线后做好安全开关,以备不时之需。
  4. 小步快跑。请做好灰度战略
  5. 构建合理的数据计算。 这会在证明、剖析问题以及后续迭代起到决定性效果。
  6. 胆大心细才能有所成长。不要怂,就是干。
  7. 实践是验证真理的唯一标准