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.prototypeproto指向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


免責聲明!

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



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