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/

相关推荐

玩转JavaScript正则表达式

前端 fetch 通信

构建流式应用—RxJS详解



此文已由作者授权腾讯云技术社区发布,转载请注明文章出处

原文链接:https://www.qcloud.com/community/article/516026

获取更多腾讯海量技术实践干货,欢迎大家前往腾讯云技术社区

时间: 2024-10-25 21:00:56

JavaScriptCore全面解析 (下篇)的相关文章

Andfix热修复框架原理及源码解析-下篇

热补丁介绍及Andfix的使用 Andfix热修复框架原理及源码解析-上篇 Andfix热修复框架原理及源码解析-下篇 如果没有看过上篇的建议从上篇看起.先大概回忆下,上一篇分析了mPatchManager.init("1.0"),addPatch()方法.还有通过分析打补丁工具,了解补丁文件是怎么生成的.下面就来讲讲我们如何去读它.思绪回到Application的loadPatch()方法. 这个方法就是遍历mPatchs,就是上篇介绍的存储patch的一个集合.根据补丁名找到对应的

[WebKit内核] JavaScriptCore深度解析--基础篇(一)字节码生成及语法树的构建

看到HorkeyChen写的文章<[WebKit] JavaScriptCore解析--基础篇(三)从脚本代码到JIT编译的代码实现>,写的很好,深受启发.想补充一些Horkey没有写到的细节比如字节码是如何生成的等等,为此成文. JSC对JavaScript的处理,其实与Webkit对CSS的处理许多地方是类似的,它这么几个部分: (1)词法分析->出来词语(Token): (2)语法分析->出来抽象语法树(AST:Abstract Syntax Tree): (3)遍历抽象语法

深入springMVC源码------文件上传源码解析(下篇)

在上篇<深入springMVC------文件上传源码解析(上篇) >中,介绍了springmvc文件上传相关.那么本篇呢,将进一步介绍springmvc 上传文件的效率问题. 相信大部分人在处理文件上传逻辑的时候会直接获取输入流直接进行操作,伪代码类似这样: @RequestMapping(value = "/upload", method = RequestMethod.POST) public ResultView upload(@RequestParam("

Andfix热修复框架原理及源码解析-上篇

热补丁介绍及Andfix的使用 Andfix热修复框架原理及源码解析-上篇 Andfix热修复框架原理及源码解析-下篇 1.不知道如何使用的同学,建议看看我上一篇写的介绍热补丁和Andfix的使用,这样你才有一个大概的框架.通过使用Andfix,其实我们心中会有一个大概的轮廓,它的工作原理,大概就是,所谓的补丁文件,就是通过打包工具apkpatch比对新的apk和旧的apk之间的差异.然后让我们的旧包运行的时候,就加载它,把以前的一些信息替换掉.我们现在就抱着这个大方向去深入源码探个究竟!!首先

Andfix热修复框架原理及源代码解析-上篇

热补丁介绍及Andfix的使用 Andfix热修复框架原理及源代码解析-上篇 Andfix热修复框架原理及源代码解析-下篇 1.不知道怎样使用的同学,建议看看我上一篇写的介绍热补丁和Andfix的使用,这样你才有一个大概的框架.通过使用Andfix,事实上我们心中会有一个大概的轮廓,它的工作原理,大概就是.所谓的补丁文件.就是通过打包工具apkpatch比对新的apk和旧的apk之间的差异. 然后让我们的旧包执行的时候.就载入它,把曾经的一些信息替换掉. 我们如今就抱着这个慷慨向去深入源代码探个

一个只有99行代码的JS流程框架(二)

张镇圳,腾讯Web前端高级工程师,对内部系统前端建设有多年经验,喜欢钻研捣鼓各种前端组件和框架. 导语 前面写了一篇文章,叫<一个只有99行代码的JS流程框架>,虽然该框架基本已经能实现一个流程正常的逻辑流转,但是在分模块应用下还是缺少一定的能力,无法将一个页面中的不同模块很好的连接在一起,于是对之前的框架进行了升级,新增了子流程的概念. 子流程 什么是子流程?在这个升级后的框架里(当然代码已经不止99行了,不要在乎标题),每个步骤不但可以是一个function,还可以引用另一个流程,这个被引

解析 Qt 字库移植并能显示中文 (下篇)

原文http://mobile.51cto.com/symbian-272563.htm 本文介绍的是Qt 字库移植并能显示中文,需要的字体库文件,一般是多个.具体移植那一个,看你使用的字库是什么了,先来看内容. AD: 解析 Qt 字库移植并能显示中文 (下篇)是本节介绍的内容,接着上篇 解析 Qt 字库移植并能显示中文 (上篇)继续介绍,烂来看本节内容. 1.几种格式字库的简介 QT支持四种格式的字库(TTF,BDF,PFA/PFB,QPF)(见参考文献[3]),但在产品中,如果直接使用,T

[WebKit] JavaScriptCore解析--基础篇 (一)JSC与WebCore

先看一下官方的基本介绍,短短几句就塞满了关键字. SquirrelFish,正式名称是JavaScriptCore,包括register-based(基于寄存器的虚拟机), direct-threaded, high-level bytecode engine(字节码引擎).它使用基于内置copy propagation(复制性传播算法)的一次性编译器(one-pass compiler),能够延迟从语法树(Syntax Tree)上生成字节码(Bytecodes). 由此可见JavaScrip

【Spring源码分析】AOP源码解析(下篇)

AspectJAwareAdvisorAutoProxyCreator及为Bean生成代理时机分析 上篇文章说了,org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator这个类是Spring提供给开发者的AOP的核心类,就是AspectJAwareAdvisorAutoProxyCreator完成了[类/接口-->代理]的转换过程,首先我们看一下AspectJAwareAdvisorAutoProx