打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
WebView 与JS 的交互

版权申明:作者 Vong 已将本文在微信公众平台的发表权「独家代理」给 iOS 开发(iOSDevTip)微信公共账号。本文所有打赏将归原作者所有。

本文主要分析一些 iOS 中 WebView 与 JavaScript 交互的一些框架。

UIWebView 调 JS 方法

通过调用如下方法:

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

比如获取网页 title,也可以动态注入 JS,先写一个 JS 函数

function showAlert() {  
    alert('show alert');  
}

然后保存为js 文件,最后读取这个文件并注入

NSString *filePath = [[NSBundle mainBundle] pathForResource:@"test" ofType:@"js"];  NSString *jsString = [[NSString alloc] initWithContentsOfFile:filePath];  
[webView stringByEvaluatingJavaScriptFromString:jsString];

JS 调原生方法

直接调用无法做到,可以间接实现。

方法1

JS 中要从现在的网页跳到另外一个网页的时候,就会去修改 window.location.href ,而在 @protocol UIWebViewDelegate 中有一个回调方法

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

可以监听到网页的跳转,所以可以在此做文章。
通过指定window.location.href = schemename://nativemethodname:args就可以去间接调用到原生函数。JS 一旦修改了window.location.hrefUIWebView就会收到相应回调,也就是上面说的方法,这样我们可以通过判断requesturl是否为自定义的 scheme来决定是否调用原生函数。

方法2

创建iframe,设置src,并插入到body节点

function execute(url) {  var iframe = document.createElement("IFRAME");
  iframe.setAttribute("src", url);  document.documentElement.appendChild(iframe);
  iframe.parentNode.removeChild(iframe);
  iframe = null;
}
execute("schemename://nativemethodname:args");

上述的这一串schemename://nativemethodname:args由客户端和前端约定好即可。剩余的事就是截获这个request,然后解析得到相应的参数,传入要调用的原生函数即可。同时在回调方法中要return NO
大致代码如下:

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{    NSURL *requestUrl = request.URL;    if ([requestUrl.scheme isEqualToString:@"schemename"]) {            NSArray *components = [requestUrl.absoluteString componentsSeparatedByString:@":"];            NSString *resultJSONString = [components[2] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
            [self customMethod:resultJsonString];            return NO;
        }   return YES;
}

WebViewJavaScriptBridge


实现原理

大致原理与上面说的一致。只不过WebViewJavaScriptBridge进行了更完善的封装,使得 JSNative之间的通信变得更为简便。

一开始注入WebViewJavaScriptBridge.js,该文件中的JS方法主要做了以下几件事

  • 创建了一个用于发送消息的iFrame(通过创建一个隐藏的ifrmae,并设置它的URL 来发出一个请求,从而触发UIWebView的shouldStartLoadWithRequest回调协议)

  • 创建了一个核心对象WebViewJavaScriptBridge,并给它定义了几个方法,这些方法大部分是公开的API方法

  • 创建了一个事件:WebViewJavaScriptBridgeReady,并dispatch。##

native将方法名、参数、回调的id放到一个对象中传给jsjs根据方法名字调用相应方法,之后将返回数据和responseId拼装,最后通过src 重定向到UIWebviewdelegatenative得到数据后根据responseId调用事先装入_responseCallbacksblock,动态读取调用,从而完成交互。

流程(Native端)

Public Interface
+ (instancetype)bridgeForWebView:(WVJB_WEBVIEW_TYPE*)webView handler:(WVJBHandler)handler;
+ (instancetype)bridgeForWebView:(WVJB_WEBVIEW_TYPE*)webView webViewDelegate:(WVJB_WEBVIEW_DELEGATE_TYPE*)webViewDelegate handler:(WVJBHandler)handler;
+ (instancetype)bridgeForWebView:(WVJB_WEBVIEW_TYPE*)webView webViewDelegate:(WVJB_WEBVIEW_DELEGATE_TYPE*)webViewDelegate handler:(WVJBHandler)handler resourceBundle:(NSBundle*)bundle;
+ (void)enableLogging;
- (void)send:(id)message;
- (void)send:(id)message responseCallback:(WVJBResponseCallback)responseCallback;
- (void)registerHandler:(NSString*)handlerName handler:(WVJBHandler)handler;
- (void)callHandler:(NSString*)handlerName;
- (void)callHandler:(NSString*)handlerName data:(id)data;
- (void)callHandler:(NSString*)handlerName data:(id)data responseCallback:(WVJBResponseCallback)responseCallback;
初始化一个 bridge

初始化的工作主要如下:

  • 设置默认的消息处理 block ———— messageHandler

  • 初始化用来保存消息处理 block 的字典 ———— messageHandlers

  • 初始化消息队列数组 ———— startupMessageQueue

  • 初始化响应回调 ———— responseCallbacks

  • 以及初始化全局唯一标识 ———— uniqueId

当在外部调用

- (void)registerHandler:(NSString *)handlerName handler:(WVJBHandler)handler;

方法时,会将 handler 保存到上面初始化好的 messageHandlers 当中,key为上述方法中的 handlerName,value 为上述方法的 handler。

- (void)sendData:(id)data responseCallback:(WVJBResponseCallback)responseCallback handlerName:(NSString*)handlerName;

发送消息时,会将消息加入到消息队列数组,加到数组当中的object 为字典型,字典有三个 key,分别为 data,callbackId, handlerName,分别对应上述方法的三个参数。入队时,如果当前消息队列存在,则将该消息入队,否则立即分发该消息。

网页加载过程
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;

加载请求或 html 时,在 shouldStartLoadWithRequest 回调中会先判断请求是否带自定义协议。如果带有自定义协议,会调用注入的 js 中 WebViewJavaScriptBridge 的_fetchQueue 方法来获取当前消息,然后分发该消息。主要是在分发消息这一块,拿到消息 json 然后序列化,如果这个消息是队列(数组)才进行处理,消息队列中也是一系列字典型对象,这些对象可能有这么几个key:responseId,responseData,callbackId,handlerName,data。
然后对消息队列做一个遍历大致逻辑如下:

  • 如果 responseId 对应的 value 存在,那么就到 responseCallbacks 字典中去寻找对应的 WVJBResponseCallback 型回调 block,然后执行,block 参数为 responseData 对应的 value。然后把这个 block 从 responseCallbacks 字典中移除。

  • 如果 responseId 对应的 value 不存在,再看 callbackId 对应的 value 是否存在,存在则设置回调 responseCallback,这个 responseCallback 主要是创建一个消息,然后是消息入队,这个消息字典为 @{ @"responseId":callbackId, @"responseData":responseData };反之 responseCallback 中什么也不执行;最后判断 handlerName 对应的 value 是否存在,存在则取 messageHandlers 中对应的回调 handler,不存在就是用默认的 handler,最后执行这个 WVJBHandler 型的 handler,参数为 data 对应的 value 以及 responseCallback。

文字有点多,参考下下面这个流程图:


- (void)webViewDidFinishLoad:(UIWebView *)webView;

加载结束的回调中判断 js 是否初始化了 WebViewJavaScriptBridge实例,如果不存在,就注入本地的 js。然后检测到 bridge 调用自定义 scheme 后会分发整个消息队列即 startupMessageQueue,遍历消息队列然后取出每一个消息对象(NSDictionary) 然后将其序列化成 JSON,在主线程中调用 JS 的 WebViewJavaScriptBridge._handleMessageFromObjC 方法,参数就是序列化后的 JSON 数据。分发完成后,将 startupMessageQueue 队列置为 nil。

至此 OC 端的整个流程完毕。

JS 端流程

JS 端流程和 OC 端流程大致是一样的

Public Interface
function init(messageHandler)function send(data, responseCallback)function registerHandler(handlerName, handler)function callHandler(handlerName, data, responseCallback)function _fetchQueue()function _handleMessageFromObjC(messageJSON)
初始化

类似 OC 中的初始化

  • 注入一个默认的 messageHandler

  • 初始化一个消息接收队列,然后调用内部方法 _dispatchMessageFromObjC 来分发消息,同时将消息接收队列置空

发送消息

调用内部 function _doSend(message, responseCallback) 函数,该函数通过判断 responseCallback 回调是否存在,存在则将这个回调存入到 responseCallbacks 字典中,其 key 是全局唯一的,同时将这个 key 存入到 message 这个字典参数中,其 key 和 value 一致,及 message[‘key’] = key。接着把这个 message 参数入队即加到 sendMessageQueue 数组中。然后重定向 frame 的 src,这样 OC 端就可以在代理回调方法中去拦截这个 src 对应的 request。

消息处理机制

如果消息接受队列存在则将消息 JSON 入队即添加到 receiveMessageQueue 数组中。反之,调用内部消息分发方法

function _dispatchMessageFromObjC(messageJSON)

分发机制类似 OC 端的那张流程图,在此不做详述。

注册及调用 handler
  • 注册:在 messageHandlers 这个字典添加对应的 key 和 value。key 为 name,value 为 handler

  • 调用:类似发送消息,message 参数为 { handlerName:handlerName, data:data }

获取消息队列(供 OC 端调用)

这里会将消息发送数组进行 JSON 转化,转换后清空消息队列,然后返回给 OC 端。

内部注入的 JS

上面说的所有都在内部注入的 JS(WebViewJavaScriptBridge.js.txt) 中完成,该 JS 做的事情在上述的实现原理中也有提到,这里不再展开。

外部 html 或 js 需要处理的事

可参考下例的写法:

document.addEventListener('WebViewJavascriptBridgeReady', function(event) {    var bridge = event.bridge    // Start using the bridge}, false)

可将上述代码封装到一个 JS 函数中,然后在函数中进行其它一系列操作,如init,send 等具体参见 Demo。

JavaScriptCore(iOS7 & OS X 10.9 later)

主要的类:

  • JSVirtualMachine:非常轻量,可初始化多个 VM 来支持 JS 中的多线程

  • JSContext:给 JS 提供运行上下文环境以及一系列值操作(通过下标来获取,类似 NSdictionary,即context[@”objectKey”]),一个 VM 中可有多个 context

  • JSValue:数据桥梁

  • JSManagedValue:用于解决 retain cycle

OC 调用 JS

JSContext 可调用 evaluateScript: 方法来执行某个脚本如下:

[context evaluateScript:@”var square = function(x) {return x*x;}”]; 
JSValue *squareFunction = context[@”square”]; NSLog(@”%@”, squareFunction);  // function (x) {return x*x;}JSValue *aSquared = [squareFunction callWithArguments:@[context[@”a”]]]; NSLog(@”a^2: %@”, aSquared); //a^2: 25JSValue *nineSquared = [squareFunction callWithArguments:@[@9]]; 
NSLog(@”9^2: %@”, nineSquared); //81

JS 调用 OC

两种方式:

  • Block

  • JSExport 协议

Block
context[@"factorial"] = ^(int x) {        int factorial = 1;        for (; x > 1; x--) {
            factorial *= x;
        }        return factorial;
    };
[context evaluateScript:@"var fiveFactorial = factorial(5);"];
JSValue *fiveFactorial = context[@"fiveFactorial"];// 5! = 120NSLog(@"5! = %@", fiveFactorial);

值得注意的是:

  • 不要在 block 中持有 JSValue,而是应该将JSValue 作为参数来传递

  • 不要在 block 中持有 JSContext,可通过 [JSContext currentContext]来获取当前 context

JSContext *context = [[JSContext alloc] init];
context[@"callback"] = ^{    //错误示例 
     JSValue *object = [JSValue valueWithNewObjectInContext:context];     //正确的姿势
     JSValue *object = [JSValue valueWithNewObjectInContext:
        [JSContext currentContext]];
     object[@"x"] = 2;
     object[@"y"] = 3;     return object;
};
JSExport 协议

如果没有这个协议,OC 端的修改会同步到 JS 端,但是 JS 端的修改对 JS 和 OC 均无影响。见下例

//TestModel.m- (NSString *)description
{    NSString *str = [@"TestModel With testString:" stringByAppendingString:self.testString];    return [str stringByAppendingString:[NSString stringWithFormat:@" and numberStr:%@",self.numberStr]];
}// viewDidLoadTestModel *model = [[TestModel alloc] init];
model.testString = @"test string";
model.numberStr = @"123";
JSContext *context = [[JSContext alloc] initWithVirtualMachine:[[JSVirtualMachine alloc] init]];
context[@"model"] = model;
JSValue *modelValue = context[@"model"];// model: TestModel With testString:test string and numberStr:123NSLog(@"model: %@",model);// model JSValue: TestModel With testString:test string and numberStr:123NSLog(@"model JSValue: %@",modelValue);
model.numberStr = @"456";// model: TestModel With testString:test string and numberStr:456NSLog(@"model: %@",model);// model JSValue: TestModel With testString:test string and numberStr:456NSLog(@"model JSValue: %@",modelValue);
[context evaluateScript:@"model.testString = \"anotoher test\";model.numberStr = \"789\""];// model: TestModel With testString:test string and numberStr:456NSLog(@"model: %@",model);// model JSValue: TestModel With testString:test string and numberStr:456NSLog(@"model JSValue: %@",modelValue);

如果想要上述 JS 修改起作用,则需要实现 JSExport 协议。
通过实现该协议来暴露自定义类给 JS,这样 JS 会为这个类创建一个 wrapper object,这样看起来就像 OC 和 JS 在互相传值一样。这样,一个对象可以在 JS 和 OC 间共享,任何一端的更改都将同步到另外一端。需要注意的是,JS 只能修改暴露在协议中的属性或调用协议中的方法。

// .h@protocol TestModelDelegate <JSExport>@property (nonatomic, copy) NSString *testString;
- (void)modelTest;@end@interface TestModel : NSObject <TestModelDelegate>@property (nonatomic, copy) NSString *testString;@property (nonatomic, copy) NSString *numberStr;@end// .m- (NSString *)description
{    NSString *str = [@"TestModel With testString:" stringByAppendingString:self.testString];    return [str stringByAppendingString:[NSString stringWithFormat:@" and numberStr:%@",self.numberStr]];
}

- (void)modelTest
{    NSLog(@"modelTest!!!");
}

- (void)test
{    NSLog(@"Test!!!");
}// viewDidLoad[context evaluateScript:@"model.testString = \"anotoher test\";model.numberStr = \"567\""];// model: TestModel With testString:anotoher test and numberStr:123NSLog(@"model: %@",model);// model JSValue: TestModel With testString:anotoher test and numberStr:123NSLog(@"model JSValue: %@",modelValue);// modelTest!!![context evaluateScript:@"model.modelTest()"];
JSValue *unknowValue = [context evaluateScript:@"model.test()"];// unknowValue :undefinedNSLog(@"unknowValue :%@",unknowValue);

上例中的 numberStr 之所以还是保持为 123 是因为,这个属性不在协议中,JS 对其修改不起作用,同样如果 JS 中调用 model 不在协议中的方法,也不起作用,如果用 JSValue 去接收这个值,其值为 undefined。
没有任何响应,如果用一个 JSValue 去接收上面代码的值,得到的是 undifine

对象对应关系
Objective-C typeJavaScript type
nilundefined
NSNullnull
NSStringstring
NSNumbernumber, boolean
NSDictionaryObject object
NSArrayArray object
NSDateDate object
NSBlockFunction object
idWrapper object
ClassConstructor object

UIWebView 与 JavaScriptCore 的交互

UIWebview 也有一个 JSContext 实例,但是没有暴露在 API 中,但是我们可以通过 KVC 或者在 NSObject 分类去拿到这个实例,然后来进行自定义的一些操作。关于 NSObject 分类实现可以参考这里。但是这两种方法都有可能被拒。

其实WebView 与 JS 的交互和上述的 TestModel 与 JS 交互区别不大。只不过上例都是自己创建的 context,而在webView 中则是我们通过 KVC 来拿到这个 context 而不是自己创建。来看一个例子:

<!DOCTYPE html><html>
    <head>
        <script>
            function test()
            {
                objcObject.testDemo();
                alert(objcObject);
            }        </script>
    </head>
    <body>
        <h1>JavaScriptCore Demo</h1>
        <button type="button" onclick="test()">测试</button>
    </body></html>

上述 html 简单创建了一个 button,然后绑定一个事件。
接下来看看 ViewController 里面做了什么。

// .h@protocol TestJSDelegate <JSExport>- (void)testDemo;@end@interface WebViewController : UIViewController <TestJSDelegate,UIWebViewDelegate>@end// .m- (void)viewDidLoad
{
    [super viewDidLoad];    NSURL *path = [[NSBundle mainBundle] URLForResource:@"test" withExtension:@"html"];    NSString *html = [NSString stringWithContentsOfURL:path encoding:NSUTF8StringEncoding error:nil];
    [self.webView loadHTMLString:html baseURL:nil];
}
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
    JSContext *context = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
    context[@"objcObject"] = self;
}

- (void)testDemo
{    NSLog(@"test!!!");
}

运行效果:



如果协议方法中有多个参数该怎么调用呢?举个例子

// ObjC 中的某个协议方法- (void)testWithName:(NSString *)name age:(NSNumber *)age
{    NSLog(@"name:%@,age:%@",name,age);
}
// 在上述 html 的 js 中添加一行代码
objcObject.testWithNameAge("Tracy",20);

那么在按钮点击后,协议方法将会被执行,然后打印出 name:Tracy,age:20

内存管理

OC 中是用的是 ARC,JavaScriptCore 中用的是垃圾回收机制(garbage collection),JavaScriptCore 中所有引用都为强引用。在大部分情况下,JavaScriptCore 能做到在这两种内存管理机制之间无缝切换,但是在以下两种情况下需要特别注意:

  • 在 OC 对象中存储 JavaScript 值

  • 在 OC 对象中添加 JavaScript 域

如下例就会造成循环引用:

// ClickHandler 构造器,button 为 OC 对象,callback 是按钮点击事件回调function ClickHandler(button, callback) {     this.button = button;     this.button.onClickHandler = this;     this.handleEvent = callback;
};
@implementation MyButton- (void)setOnClickHandler:(JSValue *)handler
{
     _onClickHandler = handler; // Retain cycle}@end

上例中 ClickHandler 对 button 进行了强引用,而 MyButton 中又对 _onClickHandler 这个 JSValue 进行了强引用,最终导致循环引用,如下图所示:


如果将 _onClickHandler 设置为 weak,那么我们将收不到点击事件回调。

举个栗子,在某个方法中有一个临时的 OC 对象,然后通过 JSContext 被 JS 中的变量引用,但是该 OC 方法调用结束后,这个临时对象将被释放,因此 JS 会造成错误访问。

同样的,如果用 JSContext 创建了对象,然后在 OC 中用 JSValue 去接收,即使把 JSValue 变量在 OC 中被 retain,但可能因为 JS 中因为变量没有了引用而被释放内存,那么对应的JSValue也没有用了。

所以苹果引入了另一个类来解决这种循环引用的问题。

@implementation MyButton- (void)setOnClickHandler:(JSValue *)handler
{    //正确的姿势
     _onClickHandler = [JSManagedValue managedValueWithValue:handler];
     [_context.virtualMachine addManagedReference:_onClickHandler
                                        withOwner:self]
} 
@end

addManagedReference做的事情主要如下:它创建了一个 garbage collected reference,这种引用既不是强引用也不是弱引用。


JSManagedValue 本身是一个对 JavaScript Value 的弱引用,而 JSValue 是强引用。addManagedReference 将 JSManagedValue 转换为 garbage collected reference。如果 JS 在垃圾回收过程中能够找到 managed reference 的所有者,那么这个引用将不会被释放,否则将被释放。JSManagedValue 需要调用其addManagedReference:withOwner: 方法把它添加到JSVirtualMachine 中,确保使用过程中 JSValue 不会被释放。

多线程

如前面所说,每一个 JSVirtualMachine 都可以有多个 JScontext,在每个进程中又可以有多个 JSVirtualMachine。JSValue 可以在同一个 JSVirtualMachine 中的不同 JSContext 之间传递,但是不能跨 JSVirtualMachine 来传递。因为每个 JSVirtualMachine 都有自己的内存堆以及垃圾回收器,如果 JSValue 跨 JSVirtualMachine 传递,那么垃圾回收器将不知如何处理来自不同内存堆的 JSvalue。

  • JavaScriptCore 的 API 是线程安全的

  • 同步锁粒度:JSVirtualMachine,即我们可以在 JSVirtualMachine 不同线程中调用 JS,但是如果有线程正在执行 JS,那么其它线程将不能执行 JS 操作。所以要想进行并发操作,那么需要为每个操作创建一个单独的 JSVirtualMachine 来实现并发。

WKWebView (iOS8 and later)

新特性

  • 在性能、稳定性、功能方面有很大提升(最直观的体现就是加载网页是占用的内存);

  • 允许JavaScript的Nitro库加载并使用(UIWebView中限制);

  • 支持了更多的HTML5特性以及 native 和 web 的高效交互;

  • 高达60fps的滚动刷新率以及内置手势;

  • 将UIWebViewDelegate与UIWebView重构成了14类与3个协议

WebKit 为非线程安全的,所以要确保该 framework 的所有方法在主线程上调用。

更多内容请参考Nshipster:http://nshipster.cn/wkwebkit/

总结

总得来说两种方式都可以实现二者的交互,JavaScriptBridge 相对而言复杂一些,但是安全且不需要做版本适配,APP 上架不会被拒,但是 JavaScriptCore 更加简洁,不需要写繁琐的代码,但是有被拒的风险,同时这个框架是在 iOS7 之后才有,所以如果要适配 iOS6的话还是选择 JavaScriptBridge。

https://github.com/wang9262/WebViewJSDemo


本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
JSCore的基本使用
JSContext監控UIWebView上JS事件,並執行JS方法,實現js與ios方法互調
Category: iOS插件化 | chentoo's blog
JSPatch总结
iOS与JavaScript交互总结
ios_WKWebView与JS交互实战技巧
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服