歡迎大家關注騰訊雲技術社區-博客園官方主頁,我們將持續在博客園為大家推薦技術精品文章哦~
殷源,專注移動客戶端開發,微軟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/
相關推薦
玩轉JavaScript正則表達式
前端 fetch 通信
構建流式應用—RxJS詳解
此文已由作者授權騰訊雲技術社區發布,轉載請注明文章出處
原文鏈接:https://www.qcloud.com/community/article/516026
獲取更多騰訊海量技術實踐干貨,歡迎大家前往騰訊雲技術社區