UIWebView 中JavaScript 与 Objective-C 通信

胡向阳
2023-12-01


iOS7 之前

Objective-C -> JavaScript

UIWebView对象有以下方法

 - (NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script

该方法能够执行一段JavaScript字符串, 并返回字符串类型的返回值. 例如:

UIWebView *webView = [[UIWebView alloc] init];

// result ==  @"3"
NSString *result = [webView stringByEvaluatingJavaScriptFromString:@"1+2"];

// 调用js 对象的方法
NSString *result2 = [webView stringByEvaluatingJavaScriptFromString:
                        @"window.objectApis.doSomething('hello')"];

缺点

以上方法有以下缺点:

  1. 返回值类型只能是字符串类型
    • Objective-C 需要对字符串结果进行反序列化
    • JavaScript 可能需要对结果进行序列化
  2. 调用JavaScript对象的方法时, 传入参数比较麻烦
    • Objective-C 需要对参数进行序列化
    • JavaScript 可能需要对字符串参数进行反序列化
// 调用js 对象的方法
NSString *result2 = [webView stringByEvaluatingJavaScriptFromString:
                        @"window.objectApis.doSomething('hello \" world')"];

JavaScript -> Objective-C

URL请求截获

UIWebView的浏览器的JavaScript中, 没有相关的接口可以调用Objective-C的相 关方法. 一般采用JavaScript 在浏览器环境中发出URL请求, Objective-C 截获请 求以获取相关请求的思路. 在Objective-C 中在实现UIWebViewDelegate 时截获请求:


- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request
                                                 navigationType:(UIWebViewNavigationType)navigationType
{

    NSString *url = request.URL;
    // if (url是自定义的JavaScript通信协议) {
        //
        // do something
        //
        // 返回 NO 以阻止 `URL` 的加载或者跳转
        // return NO;
    // }
}

Objective-C可以在webView:shouldStartLoadWithRequest:navigationType 方法中可以返回NO 以阻止URL的加载或者跳转.

JavaScript有各种不同的方式发出URL 请求:

  • location.href : 修改window.location.href 替换成一个合成的URL, 比如 async://method:args
  • location.hash : 修改 window.location.hash
  • <a> click : 创建一个<a> 元素, 赋值href 属性, 并调用其click() 方法
  • iframe.src : 创建一个iframe 元素, 赋值src 属性
  • XHR sync/async : 创建一个XMLHttpRequest 对象, open() 中设置相关信息及是否异步, 并调用send() 方法发出请求
var linkNode = document.createElement("a");
var pongUrl;
var xhr = new XMLHttpRequest();
var iframe = document.createElement("iframe");
iframe.style.display = "none";

function ping(mechanism, startTime) {
    pongUrl = "pong://" + startTime;
    switch (mechanism) {
        // location.href
        case Mechanism.LocationHref:
            location.href = pongUrl;
            break;
        // location.hash
        case Mechanism.LocationHash:
            location.hash = "#" + pongUrl;
            break;
        // <a> click
        case Mechanism.LinkClick:
            linkNode.href = pongUrl;
            linkNode.click();
            break;
        // iframe. src
        case Mechanism.FrameSrc:
            iframe.src = pongUrl;
            document.body.appendChild(iframe);
            document.body.removeChild(iframe);
            break;
        // XHR sync/async
        case Mechanism.XhrSync:
        case Mechanism.XhrAsync:
            xhr.open("GET", pongUrl, mechanism == Mechanism.XhrAsync);
            xhr.send();
            break;
    }
}

UIWebView 中, Objective-C 可以通过NSHTTPCookieManagerCookiesChangedNotification 事件以监听cookie的变化.

NSNotificationCenter *center = NSNotificationCenter.defaultCenter;
[defaultCenter addObserverForName:NSHTTPCookieManagerCookiesChangedNotification
                           object:nil
                            queue:nil
                       usingBlock:^(NSNotification *notification) {
                           NSHTTPCookieStorage *cookieStorage = notification.object;
                           // do something with cookieStorage
                       }];

JavaScript 修改 document.cookie 后, Objective-C 可以通过分析cookie以得到信息.

缺点

无论是URL请求截获方式还是监听Cookie的方式, 都有以下缺点:

  1. 整个过程是异步的, 不能同步
  2. JavaScript中不能直接获取Objective-C处理的返回值
    • 需要Objective-C 调用JavaScript层自己实现的api才能得到返回值
  3. 使用callback 比较麻烦
    • 需要在JavaScript 上自己实现

iOS 7+

iOS7 引入了JavaScriptCore, 是的JavaScript 和 Objective-C 可以互操作.

Objective-C 可以使用JSContext 的 evalueScript() 方法调用JavaScript 提供 的方法.

#import <JavaScriptCore/JavaScriptCore.h>

...

UIWebView *webView = [[UIWebView alloc] init];
JSContext *jsContext = [webView valueForPath: @"documentView.webView.mainFrame.javaScriptContext"];

// call javascript
[jsContext evalueScript: @"window.objectApis.doSomething()"];

将实现JSExport 协议的对象直接赋值给JSContext 对象的属性即可暴露方法给JavaScript.


// provide obj-c apis
WBNativeApis *nativeApis = [[WBNativeApis alloc] init];
jsContext[@"nativeApis"] = nativeApis;

// `WBNativeApis` Class

#import <Foundation/Foundation.h>
#import <JavaScriptCore/JavaScriptCore.h>

@protocol NativeApis <JSExport>

-(void) logMessage: (NSString *) message;
-(NSString *) version;
// 异步
-(void) asyncPrint: (NSString *) message;
// callback
-(void) asyncPrint: (NSString *) message callback: (JSValue *) callback;

@end

@interface WBNativeApis : NSObject <NativeApis>
@end

在浏览器环境中使用JavaScript 调用Objective-C 的api

window.nativeApis.logMessage('A message from javascript!');
window.asyncPrintCallback('Message from javascript!', function (data) {
    var div = document.createElement('div');
    div.innerText = "Send message to native ok and get data from native";
    document.body.appendChild(div);
});

JavaScriptCore 将各种类型数据在不同编程语言间做了转换, 可进行直接操作.

   Objective-C type  |   JavaScript type
 --------------------+---------------------
         nil         |     undefined
        NSNull       |        null
       NSString      |       string
       NSNumber      |   number, boolean
     NSDictionary    |   Object object
       NSArray       |    Array object
        NSDate       |     Date object
       NSBlock (1)   |   Function object (1)
          id (2)     |   Wrapper object (2)
        Class (3)    | Constructor object (3)

性能测试

在iPhone 5S (ios 7.1) 模拟器条件下测试各种通信方式一次通信花费的毫秒(ms)时间.

MethodAvgMinMax
location.href1.440.7013.59
location.hash1.000.666.19
<a> click1.400.6615.29
iframe.src1.471.055.41
XHR sync1.360.853.44
XHR async0.850.4614.96
document.cookie0.420.211.59
JavaScriptCore0.060.040.13

从表格中可以看出, JavaScriptCore 的通信方式性能最好.

兼容性

各种通信方式的兼容性如下( + 表示支持, X 表示不支持):

Method/DeviceiOS4iOS5iOS6iOS7iOS8
location.href+++++
location.hash+++++
<a> click+++++
iframe.src+++++
XHR sync+X+++
XHR async+X+++
document.cookie++++X
JavaScriptCoreXXX++

WKWebView (iOS 8 + )

iOS 8 引入WKWebViewWKWebView 不支持JavaScriptCore的方式但提供message handler的方式为JavaScript 与Objective-C 通信.

Objective-C 中使用WKWebView的以下方法调用JavaScript:

- (void)evaluateJavaScript:(NSString *)javaScriptString
         completionHandler:(void (^)(id, NSError *))completionHandler

如果JavaScript 代码出错, 可以在completionHandler 进行处理.

Objective-C 中注册 message handler:

// WKScriptMessageHandler protocol


- (void)userContentController:(WKUserContentController *)userContentController
    didReceiveScriptMessage:(WKScriptMessage *)message
{
    NSLog(@"Message: %@", message.body);
}

[userContentController addScriptMessageHandler:handler name:@"myName"];

JavaScript 将信息发给Objective-C:

// window.webkit.messageHandlers.<name>.postMessage();


function postMyMessage()
 {

    var message = { 'message' : 'Hello, World!', 'numbers' : [ 1, 2, 3 ] };

    window.webkit.messageHandlers.myName.postMessage(message);

}

参考资料

  1. http://blog.persistent.info/2013/10/a-faster-uiwebview-communication.html
  2. https://github.com/mihaip/web-experiments/pull/1
  3. http://www.bignerdranch.com/blog/javascriptcore-example/
  4. http://oscaraperez.com/blog_assets/JavaScript%20with%20iOS7.pdf
  5. http://blog.impathic.com/post/64171814244/true-javascript-uiwebview-integration-in-ios7
 类似资料: