收錄待用,修改轉載已取得 "騰訊雲" 授權 作者 | 殷源 編輯 | 迷鹿 殷源,專註移動客戶端開發,微軟Imagine Cup中國區特等獎獲得者,現就職於騰訊。 接 "JavaScriptCore全面解析 (上篇)" 六、 JSExport JSExport協議提供了一種聲明式的方法去向 "Jav ...
收錄待用,修改轉載已取得騰訊雲授權
作者 | 殷源
編輯 | 迷鹿
殷源,專註移動客戶端開發,微軟Imagine Cup中國區特等獎獲得者,現就職於騰訊。
六、 JSExport
JSExport協議提供了一種聲明式的方法去向JavaScript代碼導出Objective-C的實例類及其實例方法,類方法和屬性。
1. 在JavaScript中調用native代碼
兩種方式:
Block
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:
11
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
預設情況下,一個Objective-C類的方法和屬性是不會導出給JavaScript的。你必須選擇指定的方法和屬性來導出。對於一個class實現的每個協議,如果這個協議繼承了JSExport協議,JavaScriptCore就將這個協議的方法和屬性列表導出給JavaScript。
對於每一個導出的實例方法,JavaScriptCore都會在prototype中創建一個存取器屬性。對於每一個導出的類方法,JavaScriptCore會在constructor對象中創建一個對應的JavaScript function。
在Objective-C中通過@property聲明的屬性決定了JavaScript中的對應屬性的特征:
- Objective-C類中的屬性,成員變數以及返回值都將根據JSValue指定的拷貝協議進行轉換。
3. 函數名轉換
轉換成駝峰形式:
去掉所有的冒號
所有冒號後的第一個小寫字母都會被轉為大寫
4. 自定義導出函數名
如果不喜歡預設的轉換規則,也可以使用JSExportAs來自定義轉換
5. 導出OC對象給JS
如何導出自定義的對象?
自定義對象有複雜的繼承關係是如何導出的?
在討論這個話題之前,我們首先需要對JavaScript中的對象與繼承關係有所瞭解。
七、 JavaScript對象繼承
如果你已經瞭解JavaScript的對象繼承,可以跳過本節。
這裡會快速介紹JavaScript對象繼承的一些知識:
1. JavaScript的數據類型
最新的 ECMAScript 標准定義了 7 種數據類型:
6 種 原始類型:
Boolean
Null
Undefined
Number
String
Symbol (ECMAScript 6 新定義)和 Object
2. JavaScript原始值
除 Object 以外的所有類型都是不可變的(值本身無法被改變)。我們稱這些類型的值為“原始值”。
布爾類型:兩個值:true 和 false
Null 類型:只有一個值: null
Undefined 類型:一個沒有被賦值的變數會有個預設值 undefined
數字類型
字元串類型:不同於類 C 語言,JavaScript 字元串是不可更改的。這意味著字元串一旦被創建,就不能被修改
符號類型
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定義的對象中有兩種屬性:數據屬性和訪問器屬性。
- 數據屬性
數據屬性是鍵值對,並且每個數據屬性擁有下列特性:
- 訪問器屬性
訪問器屬性有一個或兩個訪問器函數 (get 和 set) 來存取數值,並且有以下特性:
5. JavaScript屬性設置與檢測
設置一個對象的屬性會只會修改或新增其自有屬性,不會改變其繼承的同名屬性
調用一個對象的屬性會依次檢索本身及其繼承的屬性,直到檢測到
var point = {x:99, y:66};
var childPoint = Object.create(point);
console.log(childPoint.x)
childPoint.x = 88
console.log(childPoint.x)
Output:
99
88
在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
簡單地說:
_proto__是所有對象的屬性,表示對象自己繼承了什麼對象
prototype是Function的屬性,決定了new出來的新對象的proto
如圖詳細解釋了兩者的區別
!
5. 列印JavaScript對象結構
在瀏覽器提供的JavaScript調試工具中,我們可以很方便地列印出JavaScript對象的內部結構
在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.getOwnPropertyNames(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 = ' + dumpObj(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
避免在導出的block/native函數中直接使用JSContext
使用 [JSContext currentContext] 來獲取當前context能夠避免迴圈引用
//錯誤用法
context[@"block"] = ^() {
NSLog(@"%@", context);
};
//糾正用法
context[@"block"] = ^() {
NSLog(@"%@", [JSContext currentContext]);
};
3. 避免直接使用外部JSValue
- 避免在導出的block/native函數中直接使用JSValue
//錯誤用法
JSValue *value = [JSValue valueWithObject:@"test“ inContext:context];
context[@"block"] = ^(){
NSLog(@"%@", value);
};
//糾正用法
JSValue *value = [JSValue valueWithObject:@"test“ inContext:context];
JSManagedValue *managedValue = [JSManagedValue managedValueWithValue:value andOwner:self];
context[@"block"] = ^(){
NSLog(@"%@", [managedValue value]);
};
這裡我們使用了JSManagedValue來解決這個問題
十一、 JSManagedValue
一個JSManagedValue對象包含了一個JSValue對象,“有條件地持有(conditional retain)”的特性使其可以自動管理記憶體。
最基本的用法就是用來在導入到JavaScript的native對象中存儲JSValue。
不要在在一個導出到JavaScript的native對象中持有JSValue對象。因為每個JSValue對象都包含了一個JSContext對象,這種關係將會導致迴圈引用,因而可能造成記憶體泄漏。
1. 有條件地持有
所謂“有條件地持有(conditional retain)”,是指在以下兩種情況任何一個滿足的情況下保證其管理的JSValue被持有:可以通過JavaScript的對象圖找到該JSValue
可以通過native對象圖找到該JSManagedValue。使用addManagedReference:withOwner:方法可向虛擬機記錄該關係反之,如果以上條件都不滿足,JSManagedValue對象就會將其value置為nil並釋放該JSValue。
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中的對象圖關係 */
十二、 異常處理
JSContext的exceptionHandler屬性可用來接收JavaScript中拋出的異常
預設的exceptionHandler會將exception設置給context的exception屬性
因此,預設的表現就是從JavaScript中拋給native的未處理的異常又被拋回到JavaScript中,異常並未被捕獲處理。
將context.exception設置為nil將會導致JavaScript認為異常已經被捕獲處理。
@property (copy) void(^exceptionHandler)(JSContext *context, JSValue *exception);
context.exceptionHandler = ^(JSContext *context, JSValue *exception) {
NSLog(@"exception : %@", exception);
context.exception = exception;
};
參考:
https://trac.webkit.org/wiki/JavaScriptCore
https://trac.webkit.org/browser/trunk/Source/JavaScriptCore
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Inheritance_and_the_prototype_chain
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/prototype
https://developer.apple.com/reference/javascriptcore
http://blog.iderzheng.com/introduction-to-ios7-javascriptcore-framework/
http://blog.iderzheng.com/ios7-objects-management-in-javascriptcore-framework/
原文鏈接:https://www.qcloud.com/community/article/516026