WKWebView使用及WKWebViewJavascriptBridge框架

7,016 阅读9分钟

一、WKWebView

1. 简介

WKWebView 是苹果在 WWDC2014 发布会上发布 iOS 8时,推出的 WebKit 的核心控件,是新型的 H5 容器,可以用来替代 UIWebVeiw 。它与 UIWebView 相比较,拥有更快的加载速度和性能,更低的内存占用;将 UIWebViewDelegate 和 UIWebView 重构成了14个类,3个协议,可以让开发者进行更加细致的配置。

2. WKWebView 与UIWebView 相比的优劣势

优势:

  • 内存占用更低 内存占用只有 UIWebView 的1/4 左右
  • 更多的支持 HTML5 的特性
  • 高达 60fps 的滚动刷新率以及内置手势
  • 与 Safari 相同的 JavaScript 引擎
  • 通过 KVO 监听 estimatedProgress,可以获取加载进度( UIWebView 需要调用私有API)
  • 通过 KVO 监听 title 属性,设置导航栏标题
  • 新增 WKWebViewConfiguration 类实现JS调用OC(UIWebView 使用JavaScript Core)
  • 新增 customUserAgent 属性,可用来自定义UserAgent

劣势:

WKWebView 会忽视默认的网络存储,如:NSURLCache、NSHTTPCookieStorage、NSCredentialStorage。WKWebView 有自己的进程,同样也有自己的存储空间用来存储 Cookie 和 Cache, 其他的网络类如 NSURLConnection 是无法访问到的,不支持缓存。 WKWebView 对 Cookie 的管理不会同步到 NSHTTPCookieStorage,需要手动注入 Cookie,实现Cookie共享

3. WKWebView 的基本使用


WKWebView框架概览

这里只说一些比较重要的使用,具体参考: 参考链接一:WebKit框架 浅析参考链接二:iOS进阶之WKWebView

(1)一个重要的属性 WKWebViewConfiguration:

  • 网页的渲染与展示,通过 WKWebViewConfiguration 可以进行配置。
  • WKUserContentController、WKUserScript 、- (void)addUserScript:(WKUserScript *)userScript 网页中手动加入一段JS代码(也可以通过这种方式向 webView 注入 Cookie )
  • WKWebViewConfiguration 通过使用 WKUserContentController 、WKScriptMessageHandler 协议,实现 JavaScript调用 OC (以下所有 JavaScript 都用 JS 代替)。步骤如下:
    • 注册 JS 方法 :
      • [userContentController addScriptMessageHandler:self  name:@"sayhello"] ;
        实现 WKScriptMessageHandler 协议中的代理方法,根据message的name属性进行判断执行相应的方法

      • - (void)userContentController: (WKUserContentController *) userContentController didReceiveScriptMessage:(WKScriptMessage *)message

    • dealloc中移除 注册的方法:
      • [userContentController removeScriptMessageHandlerForName:@"sayhello"];
        (2)三个重要的代理:

  • WKNavigationDelegate
    • 追踪加载过程
      • 开始加载
        • - (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(null_unspecified WKNavigation *)navigation
      • 开始返回
        • - (void)webView:(WKWebView *)webView didCommitNavigation:(null_unspecified WKNavigation *)navigation
      • 加载完成
        • - (void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation
      • 加载失败
        • - (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error
  • 进行页面跳转
    • 接受重定向跳转请求后调用
      • - (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(null_unspecified WKNavigation*)navigation
    • 收到请求后,决定是否跳转
      • - (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler
    • 发请求之前,决定是否跳转
      • - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler
  • WKUIDelegate
    • 创建一个新的 webview
      • 创建一个新的 WKWebview
        • - (nullable WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration*)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures*)windowFeatures;
    • 提示框 ,针对于 web 界面的三种提示框(警告框、确认框、输入框)分别对应三种代理方法。
      • 界面弹出警示框 :
        • - (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler
      • 界面弹出确认框 :
        • - (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL result))completionHandler
      • 界面弹出输入框 :
        • - (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(nullable NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * __nullableresult))completionHandler
      • 注意:h5调用 js.alert() 时,UIWebView 会直接弹出提示框,WKWebview 则需要实现这些代理方法才能正常弹出提示框(不知道这个的,都以为 提示框这块是个坑)
  • WKScriptMessageHandler 协议
    • JS 和 Native 交互这个协议中包含一个必须实现的方法,这个方法是 Native 与 Web 端交互的关键,它可以直接将接收到的 JS 脚本转为 OC 或Swift 对象。
    • - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message;

(3)WKWebView 处理与JS 交互问题:

  • OC 调用 JS :
    • 直接调用
      • - (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^ _Nullable)(_Nullable id result, NSError * _Nullable error))completionHandler; 
        这个方法通过block异步返回结果。解决了 UIWebView 使用的 stringByEvaluatingJavaScriptFromString 这个方法返回值只能是字符串,无法捕获错误的问题。

  • JS调用OC:
    • 方式一:WKWebViewConfiguration 通过使用 WKUserContentController、WKScriptMessageHandler协议 在协议方法中执行OC方法。
    • 方式二:H5调用js.alert()时,会调起WKWebView 的WKUIDelegate 协议下的代理方法显示提示框。
    • 方式三:使用第三方框架WKWebViewJavascriptBridge 完成JS与Native之间的交互。

4.WKWebView 的Cookie相关

为什么要把 Cookie 单独拎出来说呢?在使用 UIWebView 的时候, 我们并不存在使用 Cookie 的困扰, 我们只需要写一个方法, 往 HTTPCookieStorage里面注入一个我们用户的 HTTPCookie 就可以了。同一个应用,不同 UIWebView 之间的 Cookie 是自动同步的。并且可以被其他网络类访问比如NSURLConnection、AFNetworking。
由于WKWebView会忽视默认的网络存储,如: NSURLCache、 NSHTTPCookieStorage、NSCredentialStorage。无法实现,不会主动把 Cookie 存储的NSHTTPCookieStorage,网络类无法访问 WKWebView 自己管理的 Cookie,也无法从 NSHTTPCookieStorage 中取到 WKWebView 请求时用到 Cookie。所以在WKWebView需要发起网路请求时,需要我们手动去注入 Cookie。大概思路:将cookie信息存入 NSHTTPCookieStorage,然后在适当的时机从NSHTTPCookieStorage 获取 Cookie 注入 WKWebView 。下面就带着这样几个问题具体说一下:用户端现在的注入 Cookie 方案是什么?解决了什么样的问题?既然不能共享在 NSHTTPCookieStorage 为什么还需要使用 NSHTTPCookieStorage 存取 Cookie?
  • 用户端现在的注入 Cookie 方案是什么?解决了什么样的问题?
    • 方案一:在 WKWebView 发起网络请求向请求头中注入 Cookie ,解决了首次请求 Cookie 获取不到的问题
    • 方案二:在 WKWebView 的WKNavigationDelegate 代理方法 decidePolicyForNavigationAction 中 拦截网络请求向请求头中注入 Cookie 注入cookie ,解决了打开新的页面 Cookie 带不过去的问题
    • 方案三:在 WKWebView 的 WKNavigationDelegate 代理方法 decidePolicyForNavigationAction 中 拦截网络请求注入 Cookie,解决了打开新的页面 Cookie 内容发生改变,返回上级页面 Cookie 未更新的问题
    • 小结:经测试方案三页可以同时解决方案二能解决的问题,因此用户端现在的 Cookie 注入方案是同时了方案一与方案三
    • 参考链接:参考链接一:iOS中UIWebView与WKWebView、JavaScript与OC交互、Cookie管理看我就够、 参考链接二:【腾讯Bugly干货分享】WKWebView 那些坑
  • 既然不能共享在NSHTTPCookieStorage为什么还需要使用NSHTTPCookieStorage存取cookie?
    • 苹果提供了NSHTTPCookieStorage、NSHTTPCookie这两个类是用来存储Cookie。
    • cookie信息不仅限于WKWebView的网络请求使用,某些native页面的网络请求也需要cookie信息,并且网络类NSURLConnection、AFNetworking 等是通过访问 NSHTTPCookieStorage 获取 Cookie 信息。
    • 如果 Cookie 信息,使用其他方式存储,我们不仅要管理 WKWebView 的 Cookie ,还要管理非 WKWebView 的网络请求能否获取到 Cookie。

二. WKWebViewJavascriptBridge 的使用

大概原理:WebViewJavascriptBridge 的实现过程就是在 OC 环境和 JS 环境各自保存一个相互调用的信息(可以理解为方法名和方法的实现)。每一个调用之间都有 Id 和 CallBackId 来找到两个环境对应的处理。

1.WebViewJavascriptBridge框架重要的类:

  • WebViewJavascriptBridge_JS: JS 环境的 bridge 初始化和处理,里面负责接收 OC 发给 JS 的消息,并且把 JS 环境的消息发送给OC。
  • WKWebViewJavascriptBridge:主要负责 OC 环境的消息处理,并且把 OC 环境的消息发送给 JS 环境。
  • WebViewJavascriptBridgeBase:主要实现了 OC 环境的 bridge 初始化和以及与 JS 交互的消息的封装等重要逻辑处理。

2.OC环境的消息处理流程:

(1) WKWebViewJavascriptBridge 打开日志方便调试

(2) 初始化bridge,大概做了几件事:

  • 初始化 bridge 对象
  • 把 webView 作为成员变量,成为 navigationDelegate
  • 初始化 WebViewJavascriptBridgeBase 对象,成为 delegate 用于处理交互的传递等逻辑

(3) OC环境注册方法,供 JS 调用

  • 其实是向 WebViewJavascriptBridgeBase 注册,WebViewJavascriptBridgeBase 对象将这个方法 以 “方法名:方法的实现”的键值对的形式存入messagehandlers,在收到JS端发送的消息时执行 navigationDelegate 方法时从 messageHander 获取方法进行执行。

(4) 初始化Web环境(这个不需要我们做)

(5) OC 向 JS 发消息 调用 JS 注册的方法

(6) 当 JS 向 WebView 发送消息的时候回调用这个代理方法,拦截到的URL判断URL类型做出相应的处理。到这就是整个 OC 和 JS 交互在 OC 环境下的基本流程啦,具体参见:参考链接一:GitHub WebViewJavascriptBridge框架下载链接参考链接二:WebViewJavascriptBridge原理解析参考链接三:Objective-C 和 JavaScript 交互那些事

  • 代理方法:
    - (void)webView:(WKWebView *)webView   decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction  decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler
  • 拦截到的URL类型相关判别方法
方法
作用

-(BOOL)isCorrectProcotocolScheme:(NSURL*)url

判断是否是WKWebViewJavascriptBridge接收或者是发送的消息URL

-(BOOL)isBridgeLoadedURL:(NSURL*)url

是否是 初始化JavaScript环境的bridge消息

-(BOOL)isQueueMessageURL:(NSURL*)url

是否是WKWebViewJavascriptBridge发出的消息

-(void)logUnkownMessage:(NSURL*)url

未知消息类型
  • 处理逻辑:是否是 WKWebViewJavascriptBridge 接收或者是发送的消息URL [_base isCorrectProcotocolScheme:url]
    • 是 :首先是否是初始化 Web 环境 bridge 的初始化加载消息----> https://__bridge_loaded__ [_base isBridgeLoadedURL:url]
      • 是: 初始化 JS环境的 bridge。
      • 否:再判断是否是WKWebViewJavascriptBridge发出的消息 [_base isQueueMessageURL:url]
        • 是 调用[self WKFlushMessageQueue];方法。然后通过调用WebViewJavascriptBridge._fetchQueue()来获取 JS 给 OC的回调信息。拿到回调消息后,调用[_base flushMessageQueue:result] 方法从 OC 发送给 OC 。
        • 否 调用 [_base logUnkownMessage:url] 打印未知的消息。
    • 否 :不是JS交互请求,按照正常的网络请求处理。

3.WKWebViewJavascriptBridge 的坑:

由于源码中对_webViewDelegate进行了强引用关键代码如下图,所以使用 WKWebViewJavascriptBridge 必然会导致循环引用。



原来的解决方案:


这个解决方案是 WKWebViewJavaScriptBridge循环引用最简单处理办法 。但是这么处理会导致一些问题,在网路请求未拿到数据后进入其他页面,待数据加载到后回到之前页面,代理方法失效。解决方法 通过控制器的 isBeingDismiss 这个方法判断控制器来源,当控制器将要销毁时,打破循环引用。如下图:


关于 WKWebView 缓存相关的总结可以通过下一篇博客 WKWebView缓存总结 了解。

三、参考链接汇总