• WKWebview使用二三事


    三、拦截请求

    1、支持NSURLProtocol 拦截
    • 离线包方案关键之一:需要拦截请求,并返回本地资源;使用UIWebview时候,因为能通过NSURLProtocol可以拦截UIWebView的网络请求,问题不大。
    • WKWebview使用离线包方案,遇到最大问题:在WKWebView上无法直接利用NSURLProtocol拦截请求;这是因为WKWebview在独立的进程(App进程之外)中执行网络请求,请求数据不经过App进程。
    • WKWebview上的解决办法:使用私有API解决,iOS 11 之前使用[WKBrowsingContextController registerSchemeForCustomProtocol:schema]来注册http/https,iOS之后可以通过 hook +(BOOL)handlesURLScheme方式注册http/https;
    • 但是一旦注册http(s) scheme了,网络请求将从Network Process发送到App Process,然后被NSURLProtocol 拦截网络请求。Network Process请求encode成一个Message,然后通过 IPC 发送给 App Process。出于性能的原因,encode的时候HTTPBody和HTTPBodyStream这两个字段被丢弃掉了
    2、WKURLSchemeHandler使用
    • iOS 11 中,Apple 为 WebKit framework 增加了WKURLSchemeHandler协议,用于加载自定义 URL Scheme。当WebKit遇到无法识别的 URL时,会调用WKURLSchemeHandler协议。该协议包括以下两个必须实现的方法

      @protocol WKURLSchemeHandler <NSObject>
      
      //开始加载特定资源时调用
      - (void)webView:(WKWebView *)webView startURLSchemeTask:(id <WKURLSchemeTask>)urlSchemeTask;
      
      //停止载特定资源时调用
      - (void)webView:(WKWebView *)webView stopURLSchemeTask:(id <WKURLSchemeTask>)urlSchemeTask;
      
      @end
       
      复制代码
    • 使用WKURLSchemeHandler协议处理完任务后,调用WKURLSchemeTask协议内方法加载资源。WKURLSchemeTask协议属性和方法如下:

      @protocol WKURLSchemeTask <NSObject>
      
      @property (nonatomic, readonly, copy) NSURLRequest *request;
      
      //设置当前任务的response。每个 task 至少调用一次该方法。如果尝试在任务终止或完成后调用该方法,则会抛出异常。
      - (void)didReceiveResponse:(NSURLResponse *)response;
      
      //设置接收到的数据。当接收到任务最后的 response 后,使用该方法发送数据。每次调用该方法时,新数据会拼接到先前收到的数据中。如果尝试在发送 response 前,或任务完成、终止后调用该方法,则会引发异常。
      - (void)didReceiveData:(NSData *)data;
      
      //将任务标记为成功完成。如果尝试在发送 response 前,或将已完成、终止的任务标记为完成,则会引发异常。
      - (void)didFinish;
      
      //将任务标记为失败。如果尝试将已完成、失败,终止的任务标记为失败,则会引发异常。
      - (void)didFailWithError:(NSError *)error;
      复制代码
    3、离线资源更新能力
    • 很多项目中,为了优化Webview加载H5效果,使用了离线包方案,不仅需要拦截请求的能力,还需要更新离线资源的能力;比较有意思的是:ReactNative、Weex和小程序等跨端方案也需要依赖离线资源更新能力。
    • 基于此,很多公司打造了离线资源打包、diff计算、动态下发,全量更新和增量更新等能力。以帮助更好地支持端上离线资源更新能力。

    四、WKWebview中OC和JS通信

    1、Message Handler机制
    • iOS 2引入UIWebview,iOS7引入JavaScriptCore框架,它提供了 JS 代码与原生代码交互的能力;至此iOS7之后,UIWebview可以通过KVC方式获取JSContext; 但是iOS 8引入的WKWebview由于独立在App进程之外,不能获得JSContext,不能通过JSContext实现 JS 代码与原生代码通信。(iOS13开始,不再支持UIWebview)

    • WKWebview提供了新的 JS 代码 和 原生代码通信的方式,这就是Message Handler这种机制,当JS执行 window.webkit.messageHandlers.<name>.postMessage(<messageBody>)时,OC端被添加的ScriptMessageHandler就会执行实现的WKScriptMessageHandler协议中的方法

      @protocol WKScriptMessageHandler <NSObject>
      
      @required
      - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message;
      
      @end
      复制代码
    2、传统通信方式--URL拦截方式
    • 基于URL拦截方式,实现JS和Native的交互,iOS非常经典的实现有: WebViewJavascriptBridge, 在很多业务中,选择URL拦截方式也是个不错的选择;

    五、WebView性能优化总结

    1、加载性能优化思路
    • 节省Webview初始化时间:提前初始化WebView,or 复用Webview对象
    • 预先加载资源离线包方案 or link prefetching方案
    • 节约资源请求时间: DNS缓存、静态资源存放在CDN上
    • H5页面优化:CSS、JavaScript、HTML优化等
    2、禁止WKWebview中长按弹出UIMenuController
    • 在WebView中,长按文字会使得WebView默认开始选择文字;长按链接会弹出提示是否在新页面打开。

    • 解决方法:可以通过给body增加CSS来禁止这些默认规则。

      // 禁止选择CSS
      NSString *css = @"body{-webkit-user-select:none;-webkit-user-drag:none;}";
      
      // CSS选中样式取消
      NSMutableString *javascript = [NSMutableString string];
      [javascript appendString:@"var style = document.createElement('style');"];
      [javascript appendString:@"style.type = 'text/css';"];
      [javascript appendFormat:@"var cssContent = document.createTextNode('%@');", css];
      [javascript appendString:@"style.appendChild(cssContent);"];
      [javascript appendString:@"document.body.appendChild(style);"];
      
      // javascript注入
      WKUserScript *noneSelectScript = [[WKUserScript alloc] initWithSource:javascript injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES];
      WKUserContentController *userContentController = [[WKUserContentController alloc] init];
      [userContentController addUserScript:noneSelectScript];
      WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
      configuration.userContentController = userContentController;
      
      // WKWebView 初始化
      WKWebView *webView = [[WKWebView alloc] initWithFrame:frame configuration:configuration];
      //...
      复制代码
    3、点击延迟优化
    • 点击延迟的原因:早期苹果为了判断移动端上的双击缩放事件而加的,在touchendclick事件之间加300-350ms的延迟,来判断用户到底是点击还是双击

    • 优化方案1:使用fastclick库,其原理是:在检测到touchend事件时,通过DOM自定义事件立即触发模拟一个click事件,并把浏览器在300ms之后的click事件阻止掉;

    • 优化方案2:禁用缩放, WKWebView上能解决延迟问题【不太适用于UIWebView,但是在WKWebview上非常推荐】

      <meta name="viewport" content="user-scalable=no" />
      复制代码
    4、禁止Webview放大和缩小
    • 方案1:实现UIScrollViewDelegateviewForZoomingInScrollView:的办法

      - (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView{
          return nil;
      }
      复制代码
    • 方案2:HTML中的mata标签加入user-scalable = no,若是原生js交互的话可采用注入js的方法更改meta。

      NSString *injectionJSString = @"var metaScript = document.createElement('meta');"
          "metaScript.name = 'viewport';"
          "metaScript.content="width=device-width, initial-scale=1.0,maximum-scale=1.0, minimum-scale=1.0, user-scalable=no";"
          "document.head.appendChild(metaScript);";
      WKUserScript *userScript = [[WKUserScript alloc] initWithSource:injectionJSString injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES];
      WKUserContentController *userContentController = [[WKUserContentController alloc] init];
      [wkuController addUserScript:userScript];
          
      WKWebViewConfiguration *webViewConfig = [[WKWebViewConfiguration alloc] init];
      webViewConfig.userContentController = userContentController;
      self.webView = [[WKWebView alloc] initWithFrame:webviewFrame configuration:webViewConfig];
      复制代码
    5、自动弹出键盘
    • H5页面 focus 获得焦点状态下弹出键盘,UIWebView 中keyboardDisplayRequiresUserAction 设置为 NO 就可以;(默认为YES,必须用户点击才可以弹出键盘)

    • WKWebview没有此属性,需要通过hook私有API实现,代码如下:

      - (void)allowDisplayingKeyboardWithoutUserAction {
          Class class = NSClassFromString(@"WKContentView");
              char * methodSignature = "_startAssistingNode:userIsInteracting:blurPreviousNode:changingActivityState:userObject:";
              if (@available(iOS 11.3, *)) {
                  methodSignature = "_elementDidFocus:userIsInteracting:blurPreviousNode:activityStateChanges:userObject:";
              } else if (@available(iOS 12.2, *)) {
                  methodSignature = "_elementDidFocus:userIsInteracting:blurPreviousNode:changingActivityState:userObject:";
              }
              if (@available(iOS 11.3, *)) {
                  SEL selector = sel_getUid(methodSignature);
                  Method method = class_getInstanceMethod(class, selector);
                  IMP original = method_getImplementation(method);
                  IMP override = imp_implementationWithBlock(^void(id me, void* arg0, BOOL arg1, BOOL arg2, BOOL arg3, id arg4) {
                      ((void (*)(id, SEL, void*, BOOL, BOOL, BOOL, id))original)(me, selector, arg0, TRUE, arg2, arg3, arg4);
                  });
                  method_setImplementation(method, override);
              } else {
                  SEL selector = sel_getUid("_startAssistingNode:userIsInteracting:blurPreviousNode:userObject:");
                  Method method = class_getInstanceMethod(class, selector);
                  IMP original = method_getImplementation(method);
                  IMP override = imp_implementationWithBlock(^void(id me, void* arg0, BOOL arg1, BOOL arg2, id arg3) {
                      ((void (*)(id, SEL, void*, BOOL, BOOL, id))original)(me, selector, arg0, TRUE, arg2, arg3);
                  });
                  method_setImplementation(method, override);
              }
      }
      复制代码
    6、WKWebview白屏优化
    • WKWebView是一个多进程组件,Network Loading以及UI Rendering在其它进程中执行,当WKWebView总体的内存占用比较大时,WebContent Process会crash,从而出现白屏现象。

    • 解决办法1:KVO监听URL, 当URL为nil,重新reload

    • 解决办法2:在进程被终止回调中,重新reload

      // 此方法适用iOS9.0以上 
      - (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView NS_AVAILABLE(10_11, 9_0){
      		//reload
      }
      复制代码

    六、Webview安全

    1、不打开WKWebview跨域开关

    • UIWebView 是允许跨域的,而 WKWebView(默认)不允许;但是WKWebView可以利用KVO的方式修改私有属性实现跨域;

    • **个人建议:不要打开WKWebView跨域开关,因为打开后存在风险;**攻击者可以利用App文件下载机制将恶意文件写入沙盒内并诱导用户打开;当用户打开恶意文件后,恶意代码可可通过ajaxfile://域发起请求,从而远程获取App沙盒内所有的本地数据。

    • 2018年,国家信息安全漏洞库(CNNVD)将WKWebView跨域漏洞定义为高危漏洞,为了更安全的WKWebview建议不打开;如果项目要送检国家软件评测中心的话,千万千万不能有如下打开跨域开关代码;

      WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
      [config.preferences setValue:@YES forKey:@"allowFileAccessFromFileURLs"];
      if (@available(iOS 10.0, *)) {
      	 [config setValue:@YES forKey:@"allowUniversalAccessFromFileURLs"];
      }
      复制代码
    • 更多请参考: CNNVD 关于iOS平台WebView组件跨域漏洞情况的通报

    2、https解决WebView运营商劫持问题

    • 因为WebView加载的页面代码是从Server获取的,这些代码将会很容易被中间环节所窃取或者修改,其中最主要的问题是运营商劫持问题。主要表现为:页面被注入广告、页面被重定向等。
    • 目前比较主流解决办法:使用https可以防止页面被劫持或者注入。
    • https避不开中间人攻击,两种对抗中间人工具的方案:
      • 对request 和 response 的敏感内容进行加密;建议AES加密,对AES的秘钥管理建议:x小时后使用新的密钥 or 对密钥进行进一步处理(加密),增加攻击者的破解成本。
      • 客户端处理证书校验: 将服务器提供的SSL/TLS证书内置到APP客户端中,当客户端发起请求时,通过比对内置的证书和服务器端证书的内容,以确定这个连接的合法性。
    • 某些H5业务中,采用了 对request 和 response 的敏感内容进行加密 这种保护方式;大部分H5业务不需要关注这些,用https就够了。

    3、App内WebView打开第三方App能力和收

    • :本质上,WKWebView限制H5页面打开三方 APP 的能力,但是我们可以绕开这个限制,打开第三方App。

      - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler{
      		// 判断URL 中的 Scheme 或 host,然后通过 [[UIApplication sharedApplication] openURL:] 方法打开。
      }
      复制代码
    • :泛滥H5页面打开App能力比如必然不是不好的,可以设置白名单机制,只允许打开友商的App。

    七、其他

    1、WKWebview的IP直连方案

    • 在阿里云上看到WKWebview中"IP直连"方案说明,具体可看:WebView业务场景“IP直连”方案说明
    • 让WKWebview支持IP直连,遇到的挑战应该挺多的吧,毕竟拦截WKWebview的请求,将域名换成IP后,需要接管基于IP的网络请求的发送、数据接收、页面重定向、页面解码、Cookie、缓存等逻辑;
    • 要hold住这个方案,需要具备较强的网络 + OS Framework的代码级掌控能力,难度系数非常高,收益多大,不敢确定;

    2、WKWebview支持WebP展示

    • WebP是Google开发的一种高效的图片编码格式,Android的Webview是天然支持的,如果想要在WKWebview上也支持,办法也是有的;
    • 解决方案是:
      • 拦截Webp图片资源请求
      • 通过NSURLSession下载图片数据
      • 将WebP解码成相应的格式(可以是JPG,也可使是PNG)
    • 至此,WKWebview变相支持了WebP展示;如果想进一步优化,可以将转换后的图片缓存在内存 or 磁盘,当下次还要使用这个Webp图片的时候,直接从缓存中获取并返回;
    • 使用WebP图片格式可以省流量,至于是否要在WKWebview中支持WebP,结合业务实际情况看吧;

    3、WKWebview常见加载网络错误

    当之无愧Top3

    • NSURLErrorTimedOut错误(-1001) :连接超时遇到此类问题可以重试;
    • NSURLErrorNetworkConnectionLost错误( -1005) :The network connection was lost, 原因可能是:当前这个https请求准备复用TCP连接的时,实际上连接已经被服务器断开了,遇到此类问题可以重试;
    • NSURLErrorCannotFindHost错误(-1003):原因可能是:DNS劫持 or 运营商LDNS故障引起的问题。

    4、获取Webview所在的UIViewController

    //给WKWebview增加个分类,利用 响应链原理 获取(宿主)ViewController
    - (UIViewController *)hostViewController
    {
      	UIResponder *responder = [self nextResponder];
      	if (!responder) {
          	return nil
        }
      	while(responder && ![responder isKindOfClass:[UIViewController class]]) {
        	responder = [responder nextResponder];
      	}
      	return (UIViewController *)responder;
    }
    复制代码

     


    作者:南华Coder
    链接:https://juejin.cn/post/6844904089294209038
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    ------------------越是喧嚣的世界,越需要宁静的思考------------------ 合抱之木,生于毫末;九层之台,起于垒土;千里之行,始于足下。 积土成山,风雨兴焉;积水成渊,蛟龙生焉;积善成德,而神明自得,圣心备焉。故不积跬步,无以至千里;不积小流,无以成江海。骐骥一跃,不能十步;驽马十驾,功在不舍。锲而舍之,朽木不折;锲而不舍,金石可镂。蚓无爪牙之利,筋骨之强,上食埃土,下饮黄泉,用心一也。蟹六跪而二螯,非蛇鳝之穴无可寄托者,用心躁也。
  • 相关阅读:
    vscode 全局安装和配置 stylelint 像 webstorm 等 ide 一样来检查项目
    vscode 和 atom 全局安装和配置 eslint 像 webstorm 等 ide 一样使用 standard标准 来检查项目
    visual studio code (vscode)像 sublime text 的 ctrl+d 一样多光标选中
    intellij idea pycharm phpstorm webstorm 使用 FiraCode 作为编程字体,更新后字符乱码问题解决
    cmder 使用 linux bash 管道符 | grep 来筛选文件
    intellij idea pycharm phpstorm webstorm 这些编辑器像 sublime text 一样使用 ctrl d 来操作光标多选。
    前端开发中js变量定义及命名的规范建议
    论JavaWeb前后端分离放弃jsp
    ORACLE client 11g r2 客户端开发环境配置
    Javascript中document.execCommand()的用法
  • 原文地址:https://www.cnblogs.com/feng9exe/p/14606735.html
Copyright © 2020-2023  润新知