0
点赞
收藏
分享

微信扫一扫

JavaScriptCore框架在iOS7中的对象交互和管理教程


上一篇文章中已经简单入门了iOS7中新加的JavaScriptCore框架的基本用法,十分的简单方便而且高效,不过也仅限于数值型、布尔型、字符串、数组等这些基础类型。本文将扩展到更复杂的类型,介绍一下该强大的框架是如何让Objective-C对象和JavaScript对象进行直接互通的。

​log​​方法和​​eventHandler​

JSContext *context = [[JSContext alloc] init];
context.exceptionHandler = ^(JSContext *con, JSValue *exception) {
NSLog(@"%@", exception);
con.exception = exception;
};


context[@"log"] = ^() {
NSArray *args = [JSContext currentArguments];
for (id obj in args) {
NSLog(@"%@",obj);
}
};

键值对编程—Dictionary

class​,后者基于​prototype​。但所有的对象其实可以视为一组键值对的集合,所以JavaScript中的对象可以返回到Objective-C中当做​​NSDictionary​​类型进行访问。 JSValue * obj = [context evaluateScript: @ "var jsObj = {number: 7, name: 'Ider'}; jsObj"];


NSLog (@ "% @,% @", obj [@ "name"], obj [@ 'number']);


NSDictionary * dic = [obj toDictionary];


NSLog (@ "% @,% @", dic [@ "name"], dic [@ 'number']);


/ / Output:


/ / Ider, 7


/ / Ider, 7


同样的,NSDicionary和NSMutableDictionary传入到JSContext之后也可以直接当对象来调用:




NSDictionary * dic = @ @ {"name": @ "Ider", @ "#": @ (21)};


context [@ "dic"] = dic;


[Context evaluateScript: @ "log (dic.name, dic ['#'])"];


/ / Output:


/ / Ider


/ / 21


语言穿梭机—JSExport协议

​prototype​​继承完全用JSON来定义对象,但是Objective-C编程里可不能脱离类和继承了写代码。所以JavaScriptCore就提供了​​JSExport​​作为两种语言的互通协议。​​JSExport​​中没有约定任何的方法,连可选的(​​​@optional​​​)都没有,但是所有继承了该协议(​​@protocol​​)的协议(注意不是Objective-C的类(@interface))中定义的方法,都可以在​​JSContext​​中被使用。语言表述起来有点绕,还是用例子来说明会更明确一点。 @protocol PersonProtocol <JSExport>




@property (nonatomic, retain) NSDictionary *urls;


- (NSString *)fullName;




@end




@interface Person :NSObject <PersonProtocol>




@property (nonatomic, copy) NSString *firstName;


@property (nonatomic, copy) NSString *lastName;




@end;




@implementation Person




@synthesize firstName, lastName, urls;




- (NSString *)fullName {


return [NSString stringWithFormat:@"%@ %@", self.firstName, self.lastName];


}




@end


​PersonProtocol​​,并让它继承了神秘的​​JSExport​​协议,在新定义的协议中约定​​urls​​属性和​​fullName​​方法。之后又定义了​​Person​​类,除了让它实现​​PersonProtocol​​外,还定义了firstName和lastName属性。而fullName方法返回的则是两部分名字的结合。​​Person​​对象,然后传入到​​JSContext​​中并尝试使用JavaScript来访问和修改该对象。 / / Initialize target person


Person * person = [ [ Person alloc ] init ];


context [@ "p" ] = person ;


person.firstName = @ " Idera ";


person.lastName = @ " Zheng ";


person.urls @ = @ { "site " : @ " http://www.iderzheng.com "} ;


 


/ / Ok to get FullName


[ evaluateScript context : @ " log ( p.fullName () ); " ];


/ / Can not access firstname


[ evaluateScript context : @ " log ( p.firstName ) " ];


/ / Ok to access object as dictionary


[ evaluateScript context : @ " log ( ' site: ' p.urls.site , ' blog: ' p.urls.blog ) " ];


/ / Ok to change urls property


[ evaluateScript context : @ " = { p.urls blog: ' http://blog.iderzheng.com ' }" ];


[ evaluateScript context : @ " log ( ' ------- ------- AFTER CHANGE URLS ' ) " ];


[ evaluateScript context : @ " log ( ' site: ' p.urls.site , ' blog: ' p.urls.blog ) " ];


 


/ / Affect on Objective- C side as well


NSLog (@ " % @ " , person.urls );


 


/ / Output :


/ / IDER Zheng


/ / Undefined


/ / Undefined


/ / Site :


/ / Http://www.iderzheng.com


/ / Blog:


/ / Undefined


/ / ------- ------- AFTER CHANGE URLS


/ / Site :


/ / Undefined


/ / Blog:


/ / Http://blog.iderzheng.com


/ / {


/ / Blog = " http://blog.iderzheng.com ";


/ / }


​firstName​​和​​lastName​​的时候给出的结果是​​undefined​​,因为它们跟JavaScript没有​​JSExport​​的联系。但这并不影响从​​fullName()​​中正确得到两个属性的值。和之前说过的一样,对于​​NSDictionary​​类型的​​urls​​,可以在​​JSContext​​中当做对象使用,而且还可以正确地给​​urls​​赋予新的值,并反映到实际的Objective-C的​​Person​​对象上。​​JSExport​​不仅可以正确反映属性到JavaScript中,而且对属性的特性也会保证其正确,比如一个属性在协议中被声明成​readonly​,那么在JavaScript中也就只能读取属性值而不能赋予新的值。​​doFooWithBar(foo, bar);​​ @protocol MultiArgs <JSExport>


- (void)doFoo:(id)foo withBar:(id)bar;


@end


如果希望方法在JavaScript中有一个比较短的名字,就需要用的JSExport.h中提供的宏:​​JSExportAs(PropertyName, Selector)​​。

@protocol LongArgs <JSExport>
 
JSExportAs(testArgumentTypes,
           - (NSString *)testArgumentTypesWithInt:(int)i double:(double)d 
                    boolean:(BOOL)b string:(NSString *)s number:(NSNumber *)n 
                    array:(NSArray *)a dictionary:(NSDictionary *)o
           );
 
@end


​testArgumentTypes(i, d, b, s, n, a, dic);​​来调用就可以了。

虽然JavaScriptCore框架还没有官方编程指南,但是在JSExport.h文件中对神秘协议的表述还是比较详细的,其中有一条是这样描述的:

By default no methods or properties of the Objective-C class will be exposed to JavaScript, however methods and properties may explicitly be exported. For each protocol that a class conforms to, if the protocol incorporates the protocol JSExport, then the protocol will be interpreted as a list of methods and properties to be exported to JavaScript.


这里面有个incorporate一词值得推敲,经过验证只有直接继承了​​JSExport​​的自定义协议(​​@protocol​​)才能在​​JSContext​​中访问到。也就是说比如有其它的协议继承了上边的​​PersonProtocol​​,其中的定义的方法并不会被引入到​​JSContext​​中。从源码中也能看出JavaScriptCore框架会通过​​​class_copyProtocolList​​​方法找到类所遵循的协议,然后再对每个协议通过​​​protocol_copyProtocolList​​​检查它是否遵循JSExport协议进而将方法反映到JavaScript之中。

对已定义类扩展协议— class_addProtocol

​JSExport​​的协议来实现与JavaScript的交互。对于已经定义好的系统类或者从外部引入的库类,她们都不会预先定义协议提供与JavaScript的交互的。好在​​​Objective-C是可以在运行时实行对类性质的修改的​​​。​​UITextField​​添加了协议,让其能在JavaScript中可以直接访问​​text​​属性。该接口如下: @protocol JSUITextFieldExport <JSExport>


 


@property(nonatomic,copy) NSString *text;


 


@end


之后在通过​​class_addProtocol​​为其添加上该协议:

- (void)viewDidLoad {
    [super viewDidLoad];
 
    textField.text = @"7";
    class_addProtocol([UITextField class], @protocol(JSUITextFieldExport));
}

为一个​​UIButton​​添加如下的事件,其方法只要是将​​textField​​传入到​​JSContext​​中然后读取其​​text​​值,自增1后重新赋值:

- (IBAction)pressed:(id)sender {
    JSContext *context = [[JSContext alloc] init];
 
    context[@"textField"] = textField;
 
    NSString *script = @"var num = parseInt(textField.text, 10);"
    "++num;"
    "textField.text = num;";
    [context evaluateScript:script];
}

当运行点击UIButton时就会看到​​UITextField​​的值在不断增加,也证明了对于已定义的类,也可以在运行时添加神奇的​​JSExport​​协议让它们可以在Objective-C和JavaScript直接实现友好互通。

JavaScriptCore框架在iOS7中的对象交互和管理教程_自定义

 

JavaScriptCore框架在iOS7中的对象交互和管理教程_自定义_02


Garbage Collection

虽然Objetive-C和JavaScript都是面向对象的语言,而且它们都可以让程序员专心于业务逻辑,不用担心内存回收的问题。但是两者的内存回首机制全是不同的,Objective-C是基于引用计数,之后Xcode编译器又支持了​​自动引用计数(ARC, Automatic Reference Counting)​​​;JavaScript则如同Java/C#那样用的是​​垃圾回收机制(GC, Garbage Collection)​​。当两种不同的内存回收机制在同一个程序中被使用时就难免会产生冲突。

​JSContext​​放在JavaScript中的变量中被使用。因为JavaScript中的变量有引用所以不会被释放回收,但是Objective-C上的对象可能在方法调用结束后,引用计数变0而被回收内存,因此JavaScript层面也会造成错误访问。​​JSContext​​创建了对象或者数组,返回​​JSValue​​到Objective-C,即使把​​JSValue​​变量​​retain​​下,但可能因为JavaScript中因为变量没有了引用而被释放内存,那么对应的​​JSValue​​也没有用了。​​JSManagedValue​​类型帮助开发人员更好地管理对象内存。 @interface JSManagedValue : NSObject


 


// Convenience method for creating JSManagedValues from JSValues.


+ (JSManagedValue *)managedValueWithValue:(JSValue *)value;


 


// Create a JSManagedValue.


- (id)initWithValue:(JSValue *)value;


 


// Get the JSValue to which this JSManagedValue refers. If the JavaScript value has been collected,


// this method returns nil.


- (JSValue *)value;


 


@end


在《​​​iOS7新JavaScriptCore框架入门介绍​​​》有提到​​JSVirtualMachine​​为整个JavaScriptCore的执行提供资源,所以当将一个​​JSValue​​转成​​JSManagedValue​​后,就可以添加到​​JSVirtualMachine​​中,这样在运行期间就可以保证在Objective-C和JavaScript两侧都可以正确访问对象而不会造成不必要的麻烦。

@interface JSVirtualMachine : NSObject
 
// Create a new JSVirtualMachine.
- (id)init;
 
// addManagedReference:withOwner and removeManagedReference:withOwner allow 
// clients of JSVirtualMachine to make the JavaScript runtime aware of 
// arbitrary external Objective-C object graphs. The runtime can then use 
// this information to retain any JavaScript values that are referenced 
// from somewhere in said object graph.
// 
// For correct behavior clients must make their external object graphs 
// reachable from within the JavaScript runtime. If an Objective-C object is 
// reachable from within the JavaScript runtime, all managed references 
// transitively reachable from it as recorded with 
// addManagedReference:withOwner: will be scanned by the garbage collector.
// 
- (void)addManagedReference:(id)object withOwner:(id)owner;
- (void)removeManagedReference:(id)object withOwner:(id)owner;
 
@end

了解更多更多—Source Code

对于iOS7提供JavaScriptCore已经介绍的差不多了,之前也提到这其实是一个开源的框架,所以如果想要在低版本的iOS上使用,也可以很容易地自行添加源码进行编译和使用。

阅读源码也可以更加了解JavaScriptCore是怎么实现的,在开发时候也可以注意到更多的细节避免错误的发生,想要阅读框架的源码可以在这里(​​源码1​​​,​​源码2​​​,​​源码3​​)。

文章中的代码和例子都比较简单,如果想了解更多JavaScriptCore的使用方法,在​​这里​​有详细的测试案例可以提供一些线索。不过经验证并不是所有的测试案例在iOS7中都会通过,这大概是测试案例所用的JavaScriptCore是为chromium实现的而iOS7是webkit吧。

References:

  1. ​​Steamclock Software – Apple’s new Objective-C to Javascript Bridge​​
  2. ​​JavaScriptCore and iOS 7 » Big Nerd Ranch BlogBig Nerd Ranch Blog​​
  3. ​​API in trunk/Source/JavaScriptCore – WebKit​​
  4. ​​Objective-C Runtime Reference​​
  5. ​​Automatic Reference Counting vs. Garbage Collection – The Oxygene Language Wiki​​


举报

相关推荐

0 条评论