六、 JSExport
JSExport协议提供了一种声明式的方法去向JavaScript代码导出Objective-C的实例类及其实例方法,类方法和属性。
1. 在JavaScript中调用native代码
两种方式:
1,.Block
2.JSExport
Block的方式很简单,如下:
context[@"add"] = ^(NSInteger a,
NSInteger b) { return a+b; };
JSValue *resultValue = [context
evaluateScript:@"add(5, 6)"];
//另外一种调用JS函数的方法
resultValue = [context[@"add"]
callWithArguments:@[@(5), @(6)]];
NSLog(@"resultValue = %@", resultValue); |
Output:
JSExport的方式需要通过继承JSExport协议的方式来导出指定的方法和属性:
@class MyPoint;
@protocol MyPointExports
<JSExport>
@property double x;
@property double y;
- (NSString *)description;
- (instancetype)initWithX:(double
)x y:(double)y;
+ (MyPoint *)makePointWithX:
(double)x y:(double)y;
@end
@interface MyPoint : NSObject
<MyPointExports>
- (void)myPrivateMethod; // Not
in the MyPointExports protocol, so
not visible to JavaScript code.
+ (void)test;
@endltValue);
|
继承于JSExport协议的MyPointExports协议中的实例变量,实例方法和类方法都会被导出,而MyPoint类的-
(void)myPrivateMethod方法却不会被导出。
在OC代码中我们这样导出:
//导出对象 context[@"point"] = [[MyPoint alloc]
initWithX:6 y:8];
//导出类
context[@"MyPoint"] = [MyPoint class]; |
在JS代码中可以这样调用:
// Objective-C properties become
fields. point.x;
point.x = 10;
// Objective-C instance methods
become functions.
point.description();
// Objective-C initializers can be
called with constructor syntax.
var p = MyPoint(1, 2);
// Objective-C class methods become
functions on the constructor object.
var q = MyPoint.makePointWithXY(0, 0); |
2. 导出OC方法和属性给JS
1.默认情况下,一个Objective-C类的方法和属性是不会导出给JavaScript的。你必须选择指定的方法和属性来导出。对于一个class实现的每个协议,如果这个协议继承了JSExport协议,JavaScriptCore就将这个协议的方法和属性列表导出给JavaScript。
2.对于每一个导出的实例方法,JavaScriptCore都会在prototype中创建一个存取器属性。对于每一个导出的类方法,JavaScriptCore会在constructor对象中创建一个对应的JavaScript
function。
3.在Objective-C中通过@property声明的属性决定了JavaScript中的对应属性的特征:
4.Objective-C类中的属性,成员变量以及返回值都将根据JSValue指定的拷贝协议进行转换。
3. 函数名转换
转换成驼峰形式:
1.去掉所有的冒号
2.所有冒号后的第一个小写字母都会被转为大写
4. 自定义导出函数名
如果不喜欢默认的转换规则,也可以使用JSExportAs来自定义转换
5. 导出OC对象给JS
1.如何导出自定义的对象?
2.自定义对象有复杂的继承关系是如何导出的?
在讨论这个话题之前,我们首先需要对JavaScript中的对象与继承关系有所了解。
七、 JavaScript对象继承
如果你已经了解JavaScript的对象继承,可以跳过本节。
这里会快速介绍JavaScript对象继承的一些知识:
1. JavaScript的数据类型
最新的 ECMAScript 标准定义了 7 种数据类型:
6 种 原始类型:
1.Boolean
2.Null
3.Undefined
4.Number
5.String
6.Symbol (ECMAScript 6 新定义)和 Object
2. JavaScript原始值
除 Object 以外的所有类型都是不可变的(值本身无法被改变)。我们称这些类型的值为“原始值”。
1.布尔类型:两个值:true 和 false
2.Null 类型:只有一个值: null
3.Undefined 类型:一个没有被赋值的变量会有个默认值 undefined
4.数字类型
5.字符串类型:不同于类 C 语言,JavaScript 字符串是不可更改的。这意味着字符串一旦被创建,就不能被修改
6.符号类型
3. JavaScript对象
在 Javascript 里,对象可以被看作是一组属性的集合。这些属性还可以被增减。属性的值可以是任意类型,包括具有复杂数据结构的对象。
以下代码构造了一个point对象:
var point = { x : 99, y : 66, revers : function() { var tmp = this.x this.x = this.y this.y = tmp }, name : 'BiuBiuBiu', next : null }
point.revers(); |
4. JavaScript属性
ECMAScript定义的对象中有两种属性:数据属性和访问器属性。
1.数据属性
数据属性是键值对,并且每个数据属性拥有下列特性:
2.访问器属性
访问器属性有一个或两个访问器函数 (get 和 set) 来存取数值,并且有以下特性:
5. JavaScript属性设置与检测
1.设置一个对象的属性会只会修改或新增其自有属性,不会改变其继承的同名属性
2.调用一个对象的属性会依次检索本身及其继承的属性,直到检测到
var point = {x:99, y:66}; var childPoint = Object.create(point); console.log(childPoint.x) childPoint.x = 88 console.log(childPoint.x) |
Output:
在chrome的控制台中,我们分别打印设置x属性前后point对象的内部结构:
设置前
设置后
可见,设置一个对象的属性并不会修改其继承的属性,只会修改或增加其自有属性。
这里我们谈到了proto和继承属性,下面我们详细讲解。
八、 Prototype
JavaScript对于有基于类的语言经验的开发人员来说有点令人困惑 (如Java或C ++) ,因为它是动态的,并且本身不提供类实现。(在ES2015/ES6中引入了class关键字,但是只是语法糖,JavaScript
仍然是基于原型的)。
当谈到继承时,Javascript 只有一种结构:对象。每个对象都有一个内部链接到另一个对象,称为它的原型
prototype。该原型对象有自己的原型,等等,直到达到一个以null为原型的对象。根据定义,null没有原型,并且作为这个原型链
prototype chain中的最终链接。
任何一个对象都有一个proto属性,用来表示其继承了什么原型。
以下代码定一个具有继承关系的对象,point对象继承了一个具有x,y属性的原型对象。
var point = { name : null, __proto__ : { x:99, y:66, __proto:Object.prototype } }
Object.prototype.__proto__
== null \\true |
在Chrome的控制台中,我们打印对象结构:
可见继承关系,point继承的原型又继承了Object.prototype,而Object.prototype的proto指向null,因而它是继承关系的终点。
这里我们首先要知道prototype和proto是两种属性,前者只有function才有,后者所有的对象都有。后面会详细讲到。
1. JavaScript类?
Javascript 只有一种结构:对象。类的概念又从何而来?
在JavaScript中我们可以通过function来模拟类,例如我们定
义一个MyPoint的函数,并把他认作MyPoint类,就可以通过new来创建具有x,y属性的对象
function MyPoint(x, y) { this.x = x; this.y = y; }
var point = new MyPoint(99, 66); |
打印point对象结构:
这里出现一个constructor的概念
2. JavaScript constructor
每个JavaScript函数都自动拥有一个prototype的属性,这个prototype属性是一个对象,这个对象包含唯一一个不可枚举属性constructor。constructor属性值是一个函数对象
执行以下代码我们会发现对于任意函数F.prototype. constructor
== F
var F = function(){}; //一个函数对象F
var p = F.prototype; //F关联的原型对象
var c = p.constructor; //原型对象关联的
constructor函数
c == F // =>true: 对于任意函数F.prototype
.constructor == F |
这里即存在一个反向引用的关系:
3. new发生了什么?
当调用new MyPoint(99, 66)时,虚拟机生成了一个point对象,并调用了MyPoint的prototype的constructor对象对point进行初始化,并且自动将MyPoint.prototype作为新对象point的原型。
相当于下面的伪代码
var point ; point = MyPoint.prototype.constructor(99,66); point.__proto__ = MyPoint.prototype; |
4. _ proto __ 与prototype
简单地说:
1._proto__是所有对象的属性,表示对象自己继承了什么对象
2.prototype是Function的属性,决定了new出来的新对象的proto
如图详细解释了两者的区别
5. 打印JavaScript对象结构
1.在浏览器提供的JavaScript调试工具中,我们可以很方便地打印出JavaScript对象的内部结构
2.在Mac/iOS客户端JavaScriptCore中并没有这样的打印函数,这里我自定义了一个打印函数。鉴于对象的内部结构容易出现循环引用导致迭代打印陷入死循环,我们在这里简单地处理,对属性不进行迭代打印。为了描述对象的原型链,这里手动在对象末尾对其原型进行打印。
function __typeof__(objClass) { if ( objClass && objClass.
constructor ) { var strFun = objClass.
constructor.toString(); var className = strFun.substr
(0, strFun.indexOf('(')); className = className.replace
('function', ''); return className.replace
(/(^\s*)|(\s*$)/ig, ''); } return typeof(objClass); }
function dumpObj(obj, depth) {
if (depth == null || depth ==
undefined) {
depth = 1;
}
if (typeof obj != "function" &&
typeof obj != "object") {
return '('+__typeof__(obj)+')' +
obj.toString();
}
var tab = ' ';
var tabs = '';
for (var i = 0; i<depth-1; i++) {
tabs+=tab;
}
var output = '('+__typeof__
(obj)+') {\n';
var names = Object.getOwnProper
tyNames(obj);
for (index in names) {
var propertyName = names[index];
try {
var property = obj[propertyName];
output += (tabs+tab+propertyName + '
= ' + '('+__typeof__(property)+')'
+property.toString()+ '\n');
}catch(err) {
output += (tabs+tab+propertyName +
' = ' + '('+__typeof__(property)+')'
+ '\n');}}var prt = obj.__proto__;
if (typeof obj == "function") {
prt = obj.prototype;
}
if (prt!=null && prt!= undefined)
{
output += (tabs+tab+'proto = ' + dump
Obj(prt, depth+1) + '\n');
}else {
output += (tabs+tab+'proto = '+prt+' \n');
}
output+=(tabs+'}');
return output;
}
function printObj(obj) {
log(dumpObj(obj));
} |
6. log
我们为所有的context都添加一个log函数,方便我们在JS中向控制台输出日志
context[@"log"] = ^(NSString *log) { NSLog(@"%@", log); }; |
九、 导出OC对象给JS
现在我们继续回到Objective-C中,看下OC对象是如何导出的
1. 简单对象的导出
当你从一个未指定拷贝协议的Objective-C实例创建一个JavaScript对象时,JavaScriptCore会创建一个JavaScript的wrapper对象。对于具体类型,JavaScriptCore会自动拷贝值到合适的JavaScript类型。
以下代码定义了一个继承自NSObject的简单类
@interface DPoint : NSObject
@property (nonatomic, retain)
NSString *type;
@end |
导出对象
DPoint *dPoint = [[DPoint alloc] init]; dPoint.type = @"Hello Point!"; //导出对象 context[@"d_point"] = dPoint; [context evaluateScript:@"printObj(d_point)"]; |
然后我们打印JavaScript中的d_point对象结构如下:
//Output () { proto = () { constructor = (Object)[object
DPointConstructor] proto = (Object) { toString = (Function)function
toString() { [native code] } toLocaleString = (Function)function
toLocaleString() { [native code] } valueOf = (Function)function
valueOf() { [native code] } hasOwnProperty = (Function)function
hasOwnProperty() { [native code] } propertyIsEnumerable = (Function)
function propertyIsEnumerable() { [native code] } isPrototypeOf = (Function)function
isPrototypeOf() { [native code] } __defineGetter__ = (Function)function
__defineGetter__() { [native code] } __defineSetter__ = (Function)function
__defineSetter__() { [native code] } __lookupGetter__ = (Function)function
__lookupGetter__() { [native code] } __lookupSetter__ = (Function)function
__lookupSetter__() { [native code] } __proto__ = (object) constructor = (Function)function
Object() { [native code] } proto = null } } } |
可见,其type属性并没有被导出。
JS中的对象原型是就是Object.prototype。
2. 继承关系的导出
在JavaScript中,继承关系是通过原型链(prototype chain)来支持的。对于每一个导出的Objective-C类,JavaScriptCore会在context中创建一个prototype。对于NSObject类,其prototype对象就是JavaScript
context的Object.prototype。
对于所有其他的Objective-C类,JavaScriptCore会创建一个prototype属性指向其父类的原型属性的原型对象。如此,JavaScript中的wrapper对象的原型链就反映了Objective-C中类型的继承关系。
我们让DPoint继承子MyPoint
@interface DPoint : MyPoint
@property (nonatomic, retain) NSString *type;
@end |
在OC中,它的继承关系是这样的
在JS中,它的继承关系是这样的
打印对象结构来验证:
//导出类 context[@“DPoint"] = [DPoint class] ; [context evaluateScript:@“log(Dpoint
.prototype.constructor==DPoint)"]; [context evaluateScript:@"printObj(DPoint)"]; |
Output:
true (Function) { name = (String)DPoint prototype = (DPoint)[object
DPointPrototype] proto = (DPoint) { constructor = (Function)function
DPoint() { [native code] } proto = (MyPoint) {
constructor = (Function)function
MyPoint() { [native code] } description = (Function)function
() { [native code] } x = (Function) y = (Function) proto = (Object) { toString = (Function)function toString
() { [native code] } toLocaleString = (Function)function
toLocaleString() { [native code] } …… __proto__ = (object) constructor = (Function)function
Object() { [native code] } proto = null } } } } |
可见,DPoint自身的未导出的属性type没有在JS对象中反应出来,其继承的MyPoint的导出的属性和函数都在JS对象的原型中。
十、 内存管理
1. 循环引用
之前已经讲到, 每个JSValue对象都持有其JSContext对象的强引用,只要有任何一个与特定JSContext关联的JSValue被持有(retain),这个JSContext就会一直存活。如果我们将一个native对象导出给JavaScript,即将这个对象交由JavaScript的全局对象持有
,引用关系是这样的:
这时如果我们在native对象中强引用持有JSContext或者JSValue,便会造成循环引用:
因此在使用时要注意以下几点:
2. 避免直接使用外部context
1.避免在导出的block/native函数中直接使用JSContext
2.使用 [JSContext currentContext] 来获取当前context能够避免循环引用
//错误用法 context[@"block"] = ^() { NSLog(@"%@", context); };
//纠正用法
context[@"block"] = ^() {
NSLog(@"%@", [JSContext currentContext]);
}; |
3. 避免直接使用外部JSValue
避免在导出的block/native函数中直接使用JSValue
//错误用法 JSValue *value = [JSValue valueWith
Object:@"test“ inContext:context]; context[@"block"] = ^(){ NSLog(@"%@", value); };
//纠正用法
JSValue *value = [JSValue valueWith
Object:@"test“ inContext:context];
JSManagedValue *managedValue = [JSManagedValue
managedValueWithValue:value andOwner:self];
context[@"block"] = ^(){
NSLog(@"%@", [managedValue value]);
} |
这里我们使用了JSManagedValue来解决这个问题
十一、 JSManagedValue
1.一个JSManagedValue对象包含了一个JSValue对象,“有条件地持有(conditional
retain)”的特性使其可以自动管理内存。
2.最基本的用法就是用来在导入到JavaScript的native对象中存储JSValue。
3.不要在在一个导出到JavaScript的native对象中持有JSValue对象。因为每个JSValue对象都包含了一个JSContext对象,这种关系将会导致循环引用,因而可能造成内存泄漏。
1. 有条件地持有
1.所谓“有条件地持有(conditional retain)”,是指在以下两种情况任何一个满足的情况下保证其管理的JSValue被持有:可以通过JavaScript的对象图找到该JSValue
2.可以通过native对象图找到该JSManagedValue。使用addManagedReference:withOwner:方法可向虚拟机记录该关系反之,如果以上条件都不满足,JSManagedValue对象就会将其value置为nil并释放该JSValue。
3.JSManagedValue对其包含的JSValue的持有关系与ARC下的虚引用(weak
reference)类似。
2. 为什么不直接用虚引用?
通常我们使用weak来修饰block内需要使用的外部引用以避免循环引用,由于JSValue对应的JS对象内存由虚拟机进行管理并负责回收,这种方法不能准确地控制block内的引用JSValue的生命周期,可能在block内需要使用JSValue的时候,其已经被虚拟机回收。
API Reference
/* 可以直接使用JSManagedValue的
类方法直接生产一个带owner的对象 */ + managedValueWithValue:andOwner:
/* 也可以使用JSVirtualMachine
的实例方法来手动管理 */
addManagedReference:withOwner:
removeManagedReference:withOwner:
/* owner即JSValue在native代码中
依托的对象,虚拟机就是通过owner来
确认native中的对象图关系 */ |
十二、 异常处理
1.JSContext的exceptionHandler属性可用来接收JavaScript中抛出的异常
2.默认的exceptionHandler会将exception设置给context的exception属性
3.因此,默认的表现就是从JavaScript中抛给native的未处理的异常又被抛回到JavaScript中
,异常并未被捕获处理。
4.将context.exception设置为nil将会导致JavaScript认为异常已经被捕获处理。
@property (copy) void(^exceptionHandler)
(JSContext *context, JSValue *exception);
context.exceptionHandler = ^(JSContext
*context, JSValue *exception) {
NSLog(@"exception : %@", exception);
context.exception = exception;
}; |
|