JavaScriptCore全面解析 (下篇)


歡迎大家關注騰訊雲技術社區-博客園官方主頁,我們將持續在博客園為大家推薦技術精品文章哦~

殷源,專注移動客戶端開發,微軟Imagine Cup中國區特等獎獲得者,現就職於騰訊。

JavaScriptCore全面解析 (上篇)

六、 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
獲取更多騰訊海量技術實踐干貨,歡迎大家前往騰訊雲技術社區

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM