WK 与 JS 的那些事 WKWebView使用

 

苹果在iOS 8中推出了 WKWebView,这是一个高性能的 web 框架,相较于 UIWebView 来讲,有巨大提高。本文将针对 WKWebView 进行简单介绍,而后介绍下如何和 JS 进行愉快的交互。还望各位大佬不吝赐教。git

本文分为两大部分github

  1. WKWebView 简单介绍
  2. JS 交互

1 WKWebView

就目前移动开发趋势来讲,不少 APP 都会嵌套一些 H5 的应用。H5 有一些 Native 没法比拟的优点,例如:更新快,不用发版,随时上线等等。然而在 iOS 中, UIWebView 是及其难用的。随着 iOS 8 的推出,Apple 重构了 UIWebView,因而 WKWebView 横空出世。web

1.1 WKWebView VS UIWebView

根据官方文档,咱们来简单对比一下 UIWebView 和 WKWebView,看看这两个到底有什么区别算法

  WKWebView UIWebView
内存占用 大 且有内存泄漏
加载速度
与 JS 交互 难 (可与 JSCore 配合)
帧率 60FPS 掉帧

从文档来看,两者区别仍是很明显的,但到底区别有多大的,咱们用数听说话。打开京东,网易,新浪这三个网站,从打开时间和占用内存上来比较一下,看谁能胜出。该测试在 2015款 MBP 上打开,使用 Xcode 9 GM 版,在 iPhone 8 Plus 上运行json

使用 WKWebView 和 UIWebView 打开 京东 网易 新浪 三个网站所耗费的时长

使用 WKWebView 和 UIWebView 打开 京东 网易 新浪 三个网站所耗费的时长数组

使用 WKWebView 和 UIWebView 打开 京东 网易 新浪 三个网站所耗费的内存

使用 WKWebView 和 UIWebView 打开 京东 网易 新浪 三个网站所耗费的内存安全

在内存测试中发现,UIWebView 占用内存很不稳定,在打开新浪的网站时,最高内存能飙升到 200m 后来慢慢回落到 160m 左右,但会上下波动。但 WKWebView 上就没有这个问题。经过上述对比,不难看出,WKWebVeiw 要优于 UIWebView。网络

1.2 如何使用 WKWebView

得益于苹果 API 的高度封装,咱们使用 WKWebView 及其简单app

- (WKWebView *)wkWebView {
    if (!_wkWebView) {
        
        _wkWebView = [[WKWebView alloc] initWithFrame:self.view.frame configuration:[WKWebViewConfiguration new]]; //1. 
        NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.jd.com"]]; //2.
        [_wkWebView loadRequest:request]; //3. 
    }
    return _wkWebView;
}
  1. 初始化一个 WKWebView,咱们须要传一个 WKWebViewConfiguration 对象,来对 WKWebView 进行配置。
  2. 构造一个请求。
  3. 加载这个请求。

只须要这三步,咱们就可使用一个高性能的 web 框架。是否是很赞!!!
关于 WKWebView 如何使用,这里就不作过多的详细介绍了,网上这种文章太多了,你们能够自行翻阅。接下来咱们说如何与 JS 交互。框架

2. JS 交互

WebVeiw 与 JS 交互是一个很古老的问题,如何与 JS 交互是一个 WebVeiw 必须具有的能力,在 UIWebView 时代,咱们能够经过拦截 URL 的方式来进行交互,也能够经过 WebViewJavascriptBridge 来进行交互,还能够配合 JSCore 来进行交互。可是在 WKWebView 时代,因为它是在一个单独的进程中运行,咱们没法获取到 JSContext,因此咱们没法使用 JSCore 这个强大的框架来进行交互,那咱们怎么办呢,且听我一一道来。

2.1 Native 调用 JS

还记的上边说的 WKWebViewConfiguration 么,在这个类里边,有一个属性

@property (nonatomic, strong) WKUserContentController *userContentController;

Native 和 H5 交互基本全靠这个对象, 在 WKWebVeiw 中,咱们使用咱们有两种方式来调用 JS,

  1. 使用 WKUserScript
  2. 直接调用 JS 字符串

2.1.1 使用 WKUserScript

要想使用 WKUserScript,首先,咱们要构造一个 WKUserScript 对象,构造方法及其简单,咱们使用下边代码来建立一个 WKUserScript 对象。

// source 就是咱们要调用的 JS 函数或者咱们要执行的 JS 代码
// injectionTime 这个参数咱们须要指定一个时间,在何时把咱们在这段 JS 注入到 WebVeiw 中,它是一个枚举值,WKUserScriptInjectionTimeAtDocumentStart 或者 WKUserScriptInjectionTimeAtDocumentEnd
// MainFrameOnly 由于在 JS 中,一个页面可能有多个 frame,这个参数指定咱们的 JS 代码是否只在 mainFrame 中生效
- initWithSource:injectionTime:forMainFrameOnly:

至此,咱们已经构建了一个 WKUserScript,而后呢,咱们要作的就是要把它添加进来

- addUserScript:

至此使用 WKUserScript 调用 JS 完成。

2.1.2 直接调用 JS 字符串

在 WKWebView 中,咱们也能够直接执行 JS 字符串

- (void)evaluateJavaScript: completionHandler:

咱们经过调用这个方法来执行 JS 字符串,而后在 completionHandler 中拿到执行这段 JS 代码后的返回值。

至此,Native 调用 JS 完成。是否是简单到惧怕

2.2 JS 调用 Native

在 WK 这套框架下,JS 调用 Native 简直简单到丧心病狂。还记的上边那个 WKUserContentController,咱们也是要经过它来进行,而你所须要作的,只须要三步,须要三步,三步。

  1. 向 JS 注入一个字符串
[_webView.configuration.userContentController addScriptMessageHandler:self name:@"nativeMethod"];

咱们向 JS 注入了一个方法,叫作 nativeMethod

  1. JS 调用 Native
window.webkit.messageHandlers.nativeMethod.postMessage(value);

一句话调用,咱们就能够在 Native 中接收到 value

  1. 接收 JS 调用

上边咱们调用 addScriptMessageHandler:name 的时候,咱们要遵照 WKScriptMessageHandler 协议,而后实现这个协议。

- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
 NSString * name = message.name // 就是上边注入到 JS 的哪一个名字,在这里是 nativeMethod
 id param = message.body // 就是 JS 调用 Native 时,传过来的 value
 // TODO: do your stuff
}

完了,Native 调用 JS 就这么简单,是否是丧心病狂,简直简单到不能再简单了。

可是,你觉得这么就完了么,上边写的这些东西在网上随便一搜都有一大片,从新再写一遍,貌似意义不是很大啊,怎么也得来点稍微不同的东西吧。

2.3 JS 调用 Native 后的回调

举一个很常见的例子,假设咱们有这么一个需求,个人 JS 要调用 Native 发一个网络请求,Native 执行完了,把请求数据回传给 JS。
很简单的一个需求,来,想一想怎么执行。

2.3.1 postMessage 的坑

可能很快就想到了,postMessage 的时候,直接把这个方法传过去不就好了。一开始我也是这么作的。

const person = {
        firstName: "John",
        lastName: "Doe",
        age: 50,
        eyeColor: "blue",
    };
    document.getElementById("li1").onclick = function (nativeValue) {
        person.callBack = function () {
            console.log("native call");
        }
        window.webkit.messageHandlers.nativeMethod.postMessage(person);
    };

首先构造一个 person,而后咱们给 person 增长一个 callBack 属性,而后传进去,运行程序。打开 Safari 选择 开发->模拟器,打开调试界面,而后咱们点击查看控制台。


而后你会发现,报错了,为何呢,这一切都是由于 postMessag 这个方法。
打开 postMessage文档 ,你会发现,

message
将要发送到其余 window的数据。它将会被结构化克隆算法序列化。这意味着你能够不受什么限制的将数据对象安全的传送给目标窗口而无需本身序列化

这个 message 须要支持 结构化克隆算法 。很遗憾,这个算法目前不支持传递 FunctionError,它只支持一下几种类型

对象类型 注意
全部的原始类型 除了symbols
Boolean 对象
String 对象
Date  
RegExp lastIndex 字段不会被保留。
Blob  
File  
FileList  
ArrayBuffer  
ArrayBufferView 这基本上意味着全部的 类型化数组 ,好比 Int32Array 等等。
ImageData  
Array  
Object 仅包括普通对象 (好比对象字面量 )
Map  
Set  

说好的不受限制呢

15088520633631.jpg

15088520633631.jpg

2.3.2 function 转为 字符串

那既然它不支持传一个 Function ,那咱们就得另辟蹊径了,String 总支持吧,咱们把一个方法转为字符串,而后传到 Native,而后 Native 执行这个字符串。貌似可行的,咱们来试一下。

JS 代码

document.getElementById("li1").onclick = function () {

        person.callBack = function (nativeValue) {
            console.log("native call");
        }.toString();
        window.webkit.messageHandlers.nativeMethod.postMessage(person);
    };

OC 代码

- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
    
    if ([message.name isEqualToString:@"nativeMethod"]) {
        NSLog(@"body:%@, ", message.body);
        NSDictionary *dict = @{@"key1": @"value1",
                               @"key2": @"value2"
                               }; // 构造回传 js 数据
        id data = [NSJSONSerialization dataWithJSONObject:dict options:NSJSONWritingPrettyPrinted error:nil];
        NSString *jsonString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; // 转为 json 字符串
        [_webView evaluateJavaScript:[NSString stringWithFormat:@"(%@)(%@)", message.body[@"callBack"], jsonString] completionHandler:^(id _Nullable jsData, NSError * _Nullable error) {
            
        }];        
    }
}

果真不出咱们所料,咱们能够直接获得这个 Native 传递给 JS 的值。
可是,这个做用域会不会变化呢,咱们在来改一下 JS 代码

document.getElementById("li1").onclick = function () {

    var arg1 = 100;
    var arg2 = 200;
    person.callBack = function (nativeValue) {
        console.log(nativeValue);
        console.log(arg1 + arg2);
    }.toString();
    window.webkit.messageHandlers.nativeMethod.postMessage(person);
};

你们猜能不能打印出来 300,咱们来试一下。

完蛋,找不到 arg1。。。。

怎么回事呢?

咱们把一个 function 转换成 字符串以后,传给 Native,Native 在执行的时候,他的做用域已经变了,变成了 window,这个时候,window 下是没有 arg1 和 arg2 的,因此咱们找不到。

若是咱们这么作的话,确实是能够实现上述的需求的,可是,这样做用域就改变了,全部的变量都要定义为全局变量,函数要改成全局函数,以遍可以在回调中获取正确的变量。

这确实是一个可行的方法,但有没有更好的方法呢?H5 原本写的好好的,匿名函数写的 6 的飞起,干吗都要改为全局变量,全局函数,要是这么写,我都很差意思给 H5 提需求让人家改。

我就想,能不能像 UIWebView 同样使用 JSCore,可是使用 JSCore 的话,咱们要获取 JSContext,而 WKWebView 是运行在一个单独的进程中,咱们是不可能进行应用间的通讯的(目前我没发现,若是有的话,还请多多指教)。我就想,要不去扒一扒 WebKit 的源码,看看会有什么发现。

2.3.3 改下源码 ?

而后我就找啊找,终于找到了关键的方法

virtual void didPostMessage(WebKit::WebPageProxy& page, WebKit::WebFrameProxy& frame, const WebKit::SecurityOriginData& securityOriginData, WebCore::SerializedScriptValue& serializedScriptValue)
{
   @autoreleasepool {
       RetainPtr<WKFrameInfo> frameInfo = wrapper(API::FrameInfo::create(frame, securityOriginData.securityOrigin()));
    
       ASSERT(isUIThread());
       static JSContext* context = [[JSContext alloc] init]; //1. 建立一个 JSContext
    
       JSValueRef valueRef = serializedScriptValue.deserialize([context JSGlobalContextRef], 0);
       JSValue *value = [JSValue valueWithJSValueRef:valueRef inContext:context];
       id body = value.toObject; // 把 JS 的类型转为 OC 类型
    
       auto message = adoptNS([[WKScriptMessage alloc] _initWithBody:body webView:fromWebPageProxy(page) frameInfo:frameInfo.get() name:m_name.get()]); // 构造 message
  
       [m_handler userContentController:m_controller.get() didReceiveScriptMessage:message.get()]; // 调用代理对象,传递 message
   }
}

看到这里,我想,能不能把这个 JSContext 漏出来,这样的话,说不定还能想 UIWebView 和 JSCore 同样。可是转念一想,WKWebView 从 iOS 8 就出现了,如今到 iOS 11 了,难道都没想过如何解决回调这个问题么?难道苹果那帮开发都没发现么?怎么办,这不科学啊。

2.3.4 我有一个同窗

其实,咱们一开始就想错了。一直在想,如何把这个方法传过来,其实纵使能把一个 function 传过来,咱们也没有办法去执行,由于咱们能执行的只有一个字符串,而这个字符串执行后做用域确定是会变的。因此,归根到底,这是 H5 的工做,咱们作不了,想要支持回调,让 H5 本身去研究。我敢保证,你若是这么去给 H5 说,他追出去三条街,也要把砍你。

咱们要先帮 H5 解决这个问题,咱们才能去推进 H5 解决这个问题。

然而,我有一个同窗,一个作 H5 的同窗,@励志成为网红的网黄,在我苦苦思索不能解决的时候,我给他说了个人问题。而后咱们就这个问题和见解进行了深刻的探讨和交流。在达成了某些不可描述的交易以后,咱们终于找到了一种解决办法。

他说,能够用 BroadcastChannel 来解决这个问题。

BroadcastChannel API 容许同一原始域和用户代理下的全部窗口,iFrames等进行交互。也就是说,若是用户打开了同一个网站的的两个标签窗口,若是网站内容发生了变化,那么两个窗口会同时获得更新通知。

而后进行了一波研究以后,发现 API 不支持。有兴趣的能够研究这个 API

而后,咱们继续进行交易,好在,此次交易,取得了重大成功。
有一天,他在看 Vue 的源码时,发现了这么一个类 MessageChannel ,看起来能够解决这个问题。

官方文档上这么说

Channel Messaging API的MessageChannel接口容许咱们建立一个新的消息通道,并经过它的两个MessagePort属性发送数据

它有两个端口,port1 和 port2,这两个端口能够互相发消息,能够互相监听,这样的话,咱们是否是能够另辟蹊径来解决这个问题呢,咱们来看下代码。

JS 代码

document.getElementById("li1").onclick = function () {
    const  arg1 = 100;
    const  arg2 = 200;
    _postMessage(person, 'nativeMethod').then((val) => {
      // 6.
      console.log(val);
      console.log(arg1 + arg2);
    })
};
    
function _postMessage(val, name){
   var channel = new MessageChannel(); // 建立一个 MessageChannel
   window.nativeCallBack = function(nativeValue) {
     // 3. 
     channel.port1.postMessage(nativeValue) 
   };
   // 1.
   window.webkit.messageHandlers[name].postMessage(val); 
   return new Promise((resolve, reject) => {
     channel.port2.onmessage = function(e){ 
         // 4
         var data = e.data;
         // 5.
         resolve(data); 
         channel = null;
         window.nativeCallBack = null;
     }
   })
}

咱们封装了一个 _postMessage 方法,在这个方法中咱们,返回了一个 Promise 对象,其实 JS 调用 Native 是一个异步操做,JS 调用客户端,等待客户端执行完毕,执行完毕后,告诉 JS,JS 在执行接下来的操做。

OC 代码

- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
    
    if ([message.name isEqualToString:@"nativeMethod"]) {
       NSLog(@"body:%@, ", message.body);
       NSDictionary *dict = @{
           @"key1": @"value1",
           @"key2": @"value2"
       }; // 构造回传 js 数据
       id data = [NSJSONSerialization dataWithJSONObject:dict options:NSJSONWritingPrettyPrinted error:nil];
       NSString *jsonString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; // 转为 json 字符串        
    
       // 2
       [_webView evaluateJavaScript:[NSString stringWithFormat:@"%@(%@)", @"nativeCallBack", jsonString] completionHandler:^(id _Nullable jsData, NSError * _Nullable error) {
           
       }];
       
    }
}

在 OC 代码中,咱们构造一个 JSON ,而后执行 JS nativeCallBack(jsonString) ,把构造的 JSON 传给 JS。

注意上边代码的注释,咱们来一步一步看,发生了什么。

  1. 把值传给 Native。
  2. Native 接受到以后,调用 JS 的 nativeCallBack 方法。
  3. 接收到 Native 调用以后,channel 的 port1 把 Native 的值转出去。
  4. channel 的 port2 接收到 port1 发送的值以后,在 prot2 的 onmessage 方法中接收。
  5. 执行 Promise 的 then,并把 data 传过去。
  6. then 接收到调用,执行里边的代码。

那到底能不能执行呢,咱们运行一下试试

哈哈哈,果真和咱们预料的同样,我只想说一句,

总结

上边啰嗦了这么多,其实很简单,利用 MessageChannel 端口转发功能来解决做用域改变的问题,JS 不用传递方法给 Native,Native 直接调用一个统一的全局方法就行。交互简单方便。

做者:XcodeMen 连接:http://www.jianshu.com/p/c9ceb6a824e2 來源:简书 著做权归做者全部。商业转载请联系做者得到受权,非商业转载请注明出处。

相关文章
相关标签/搜索