OC与JS数据交互

OC与JS 交互在APP设计中越来越重要,不管是现在比较流行的Hybrid 还是 JSPatch,都会涉及到OC与JS的数据交互问题,下面总结一下主要的交互实现方式。

现阶段主要是会使用的技术是:

  • 1、使用UIWebViewDelegate
  • 2、第三方桥接库 WebViewJavascriptBridge (本质是使用UIWebViewDelegate)
  • 3、JavaScriptCore

本文主要介绍JavaScriptCore的主要用法,WebViewJavascriptBridge的用法见其他博客

OC调用JS

stringByEvaluatingJavaScriptFromString :

只限于webView内使用,使用方法类似evaluateScript
在webView的delegate中,添加代码

1
2
[webView stringByEvaluatingJavaScriptFromString:@"alert('test js OC')"]; //1
height = [[webView stringByEvaluatingJavaScriptFromString:@"document.body.clientHeight"] floatValue];//2

1处:可以发现在加载webView中弹框

2处: 可以获取webView设置的高度,在Hybrid中可以将webView和native进行混排

JavaScriptCore

iOS 7 引入了 JavaScriptCore 库,它把 WebKit 的 JavaScript 引擎用 Objective-C 封装,提供了简单,快速以及安全的方式接入世界上最流行的语言。

最主要的意义是执行JS代码,脱离了webView的依赖。

JavaScriptCore 主要包含的功能或者组件,可以看它的头文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#ifndef JavaScriptCore_h
#define JavaScriptCore_h
#include <JavaScriptCore/JavaScript.h>
#include <JavaScriptCore/JSStringRefCF.h>
#if defined(__OBJC__) && JSC_OBJC_API_ENABLED
#import "JSContext.h"
#import "JSValue.h"
#import "JSManagedValue.h"
#import "JSVirtualMachine.h"
#import "JSExport.h"
#endif
#endif /* JavaScriptCore_h */
JSContext (详细见.h声明)

  • JSContext (详细见.h声明)

    @discussion A JSContext is a JavaScript execution environment. All
    JavaScript execution takes place within a context, and all JavaScript values
    are tied to a context.

    JS执行的环境,同时也通过JSVirtualMachine管理着所有对象的生命周期,每个JSValue都和JSContext相关联并且强引用context。

  • JSValue

    @discussion A JSValue is a reference to a JavaScript value. Every JSValue
    originates from a JSContext and holds a strong reference to it.
    When a JSValue instance method creates a new JSValue, the new value
    originates from the same JSContext.

    All JSValues values also originate from a JSVirtualMachine
    (available indirectly via the context property). It is an error to pass a
    JSValue to a method or property of a JSValue or JSContext originating from a
    different JSVirtualMachine. Doing so will raise an Objective-C exception.

    JS对象在JSVirtualMachine中的一个强引用,其实就是Hybird对象。我们对JS的操作都是通过它。并且每个JSValue都是强引用一个context。同时,OC和JS对象之间的转换也是通过它。

{image} 待补充

  • SManagedValue

    @discussion JSManagedValue represents a “conditionally retained” JSValue.
    “Conditionally retained” means that as long as the JSManagedValue’s
    JSValue is reachable through the JavaScript object graph,
    or through the Objective-C object graph reported to the JSVirtualMachine using
    addManagedReference:withOwner:, the corresponding JSValue will
    be retained. However, if neither graph reaches the JSManagedValue, the
    corresponding JSValue will be released and set to nil.

    The primary use for a JSManagedValue is to store a JSValue in an Objective-C
    or Swift object that is exported to JavaScript. It is incorrect to store a JSValue
    in an object that is exported to JavaScript, since doing so creates a retain cycle.

    JS和OC对象的内存管理辅助对象。由于JS内存管理是垃圾回收,并且JS中的对象都是强引用,而OC是引用计数。如果双方相互引用,势必会造成循环引用,而导致内存泄露。我们可以用JSManagedValue保存JSValue来避免。

  • JSVirtualMachine
    >
    @discussion An instance of JSVirtualMachine represents a single JavaScript “object space”
    or set of execution resources. Thread safety is supported by locking the
    virtual machine, with concurrent JavaScript execution supported by allocating
    separate instances of JSVirtualMachine.
    >
    JS运行的虚拟机,有独立的堆空间和垃圾回收机制。
    >
    线程安全

  • JSExport

    @protocol
    @abstract JSExport provides a declarative way to export Objective-C objects and
    classes – including properties, instance methods, class methods, and
    initializers – to JavaScript. 更多用法详见API,还有例子。

    一个协议,如果JS对象想直接调用OC对象里面的方法和属性,那么这个OC对象只要实现这个JSExport协议就可以了。

调用方法

创建一个JSContext对象,然后将JS代码加载到context里面,最后取到这个函数对象,调用callWithArguments这个方法进行参数传值。(JS里面函数也是对象)

对 JSContext 和 JSValue 实例使用下标的方式我们可以很容易地访问我们之前创建的 context 的任何值。JSContext 需要一个字符串下标,而 JSValue 允许使用字符串或整数标来得到里面的对象和数组:

JSValue 包装了一个 JavaScript 函数,我们可以从 Objective-C / Swift 代码中使用 Foundation 类型作为参数来直接调用该函数。再次,JavaScriptCore 很轻松的处理了这个桥接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
JSContext *context = [[JSContext alloc] init];
JSValue *numValue = [context evaluateScript:@"var num = 5 + 5"];
NSLog(@"%@", [numValue toString]);//undefined
[context evaluateScript:@"var names = ['Grace', 'Ada', 'Margaret']"];
JSValue *names = context[@"names"];
NSLog(@"%@", [names[0] toString]);//Grace
[context evaluateScript:@"var triple = function(value) { return value * 3 }"];
JSValue *tripleNum = [context evaluateScript:@"triple(num)"];
NSLog(@"%@", [tripleNum toNumber]);// 30
JSValue *tripleFunction = context[@"triple"];
JSValue *result = [tripleFunction callWithArguments:@[@5] ];
NSLog(@"Five tripled: %d", [result toInt32]); //30
  • 错误处理

JSContext 还有另外一个有用的招数:通过设置上下文的 exceptionHandler 属性,你可以观察和记录语法,类型以及运行时错误。 exceptionHandler 是一个接收一个 JSContext 引用和异常本身的回调处理,可以根据信息查看上下文以及错误类型:

1
2
3
4
5
6
7
context.exceptionHandler = ^(JSContext *context, JSValue *exception) {
NSLog(@"JS Error: %@ , %@", context, exception);
};
//如修改上面代码,都会调用到
JSValue *tripleNum = [context evaluateScript:@"triple1(num)"];
JSValue *tripleFunction = context[@"triple1"];
[context evaluateScript:@"function multiply(value1, value2) { return value1 * value2 "];

需要注意的是,这个赋值必须在函数调用JS函数之间,否则无法调用到,建议直接跟JSContext定义在一起

JS调用OC

WebViewJavascriptBridge

主要是注册handler,进行消息的回调,与回调block一致。

JavaScript Core

通过context,可以block和JSExport protocol 进行JS调用OC

Block

通过context,将block,转化成JS的function,通过执行这个function,调用OC里的block, 因为在JS中调用,需要注意一些桥接。

1
2
3
4
5
6
self.context = [[JSContext alloc] init];
self.context[@"add"] = ^(NSInteger a, NSInteger b) {
NSLog(@"---%@", @(a + b));
};
[self.context evaluateScript:@"add(2,3)"];
//等同js代码直接调用add(2,3)

由于 block 可以保有变量引用,而且 JSContext 也强引用它所有的变量,为了避免强引用循环需要特别小心。避免保有你的 JSContext 或一个 block 里的任何 JSValue。相反,使用 [JSContext currentContext] 得到当前上下文,并把你需要的任何值用参数传递。

JSExport

自定义一个协议遵守JSExport协议,然后想要导出这些方法的类再遵守这个协议,就可以在JS中调用OC的方法
通过JSContext将类导出,即设置JS对象中与OC的一种关联

1
2
3
4
5
6
7
OC中
context[@"SomeClass"] = [SomeClass class];
or
context[@"SomeClass"] = self;
JS中
SomeClass.method -> self.method

  • JSExportAs

    Objective-C 的方法 createWithFirstName:lastName: 变成了在JavaScript中的 createWithFirstNameLastName()
    OC中如果参数很多,那么写起来就很不方便,为了解决参数的问题,JSExportAs
    JSExportAs(createWithFirstName,

1
2
(instancetype) createWithFirstName:(NSString )firstName lastName:(NSString )lastName
);

内存管理

OC使用的ARC,JS使用的是垃圾回收机制,正常情况下,OC和JS对象之间内存管理都无需我们去关心。不过需要注意以下几点:

  • 不要在block里面直接使用context,或者使用外部的JSValue对象,用参数进行传递

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    //错误代码:
    self.context[@"block"] = ^(){
    JSValue *value = [JSValue valueWithObject:@"aaa" inContext:self.context];
    };
    //一个比较隐蔽的
    JSValue *value = [JSValue valueWithObject:@"ssss" inContext:self.context];
    self.context[@"log"] = ^(){
    NSLog(@"%@",value);
    };
    //block捕获并持有外部的value,value强持有context和它管理的JS对象的。
    //正确的做法,str对象是JS那边传递过来。
    self.context[@"log"] = ^(NSString *str){
    NSLog(@"%@",str);
    };
  • OC对象不要用属性直接保存JSValue对象,通过内存管理辅助对象JSManagedValue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[self.context evaluateScript:
@"function callback (){};
function setObj(obj) {
this.obj = obj;
obj.jsValue=callback;
}"];
//JS对象保留了传进来的obj,最后,JS将自己的回调callback赋值给了obj,方便obj下次回调给JS;由于JS那边保存了obj,而且obj这边也保留了JS的回调。这样就形成了循环引用。
//重写setter方法
- (void)setJsValue:(JSValue *)jsValue
{
_managedValue = [JSManagedValue managedValueWithValue:jsValue];
[[[JSContext currentContext] virtualMachine] addManagedReference:_managedValue
withOwner:self];
}
  • 不要在不同的 JSVirtualMachine 之间进行传递JS对象。

一个 JSVirtualMachine可以运行多个context,由于都是在同一个堆内存和同一个垃圾回收下,所以相互之间传值是没问题的。但是如果在不同的 JSVirtualMachine传值,垃圾回收就不知道他们之间的关系了,可能会引起异常。

线程

JavaScriptCore 线程是安全的,每个context运行的时候通过lock关联的JSVirtualMachine。如果要进行并发操作,可以创建多个JSVirtualMachine实例进行操作。

获得webView的context

1
self.context = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];

这个方法是苹果私有属性,

这边要注意的是每个页面加载完都是一个新的context,但是都是同一个JSVirtualMachine。如果JS调用OC方法进行操作UI的时候,请注意线程是不是主线程。

参考

http://nshipster.cn/javascriptcore/

http://www.jianshu.com/p/a329cd4a67ee