面试题

JavaScriptCore 详解

2020-06-29  本文已影响0人  mtry

写在前面

WebViewJavascriptBridgeReactNativeJSPatch 这些 JavaScriptObjective-C 交互框架都有 JavaScriptCore 的影子,所以有必要好好了解一下 JavaScriptCore

JSCore 简介

JSCore 是 JS 引擎,通常会被叫做虚拟机,专门设计来解释和执行 JS 代码。在 WebKit 中的结构如下。

JavaScriptCore_img01.png

另外 Google 的 Chromium(Chorme的开源项目)也是使用 WebKit 。WebKit 起源于 KDE 的开源项目 Konqueror 的分支,由苹果公司用于 Safari 浏览器。其一条分支发展成为 Chorme 的内核,2013 年 Google 在此基础上开发了新的 Blink 内核。

Snip20200609_2.png

其他 JS 解析引擎

JavaScriptCore_img03.png

JSCore 提供给 OC 的接口

JSCore 提供给 OC 的接口如下

OC 接口类 作用
JSVirtualMachine 为 JS 的运行提供了底层资源,虚拟机是线程安全的
JSContext 为 JS 提供运行环境,所有的 JS 在这个上下文中执行,这里可以用来管理对象,添加方法
JSValue 是 JS 和 OC 之间交互的桥梁,负责两端对象的互相转换
JSManagedValue 可以处理内存管理中的一些特殊情形,它能帮助引用技术和垃圾回收这两种内存管理机制之间进行正确的转换
JSExport 实现 JSExport 协议可以开放 OC 类和它们的实例方法,类方法,以及属性给 JS 调用
C 接口类 作用
JSBase 定义了 JavaScriptCore 接口文件
JSContextRef 主要提供 JS 执行所需所有资源和环境
JSObjectRef 是一个 JavaScript 对象,主要提供了两部分API,一部分是创建 JS 对象,还有一部分是给创建的 JS 对象添加对应的 Callback。
JSValueRef 一个 JS 值,提供用 OC 的基础数据类型来创建 JS 的值,或者将 JS 的值转变为 OC 的基础数据类型
JSStringRef JavaScript 对象中字符串对象
JSStringRefCF CFString 与 JavaScript String 相互转化

我们在主要使用的是 OC 接口类,接下来依次分析 OC 接口类

JSVirtualMachine

一个 JSVirtualMachine 的实例就是一个完整独立的 JS 的执行环境,为 JS 的执行提供底层资源。

这个类主要用来做两件事情:

  1. 实现并发的 JavaScript 执行
  2. JavaScript 和 Objective-C 桥接对象的内存管理

提供的接口也非常简单

NS_CLASS_AVAILABLE(10_9, 7_0)
@interface JSVirtualMachine : NSObject

(instancetype)init;

// 进行内存管理
- (void)addManagedReference:(id)object withOwner:(id)owner;
- (void)removeManagedReference:(id)object withOwner:(id)owner;

@end

每一个 JSVirtualMachine 可以包含多个 JS 上下文(JSContext 对象)。同一个虚拟机下不同的上下文之间可以相互传值(JSValue对象)。

然而,每个虚拟机都是完整且独立的,有其独立的堆空间和垃圾回收器(Garbage Collector ),GC 无法处理别的虚拟机堆中的对象,因此你不能把一个虚拟机中创建的值传给另一个虚拟机。

JSContext

一个 JSContext 表示了一次 JS 的执行环境。我们可以通过创建一个 JSContext 去调用 JS 脚本,访问一些 JS 定义的值和函数,同时也提供了让 JS 访问 Native 对象,方法的接口。

一个 JSContext 对象对应了一个全局对象。例如 Web 浏览器中的 JSContext ,其全局对象就是 Window 对象。在其他环境中,全局对象也承担了类似的角色,用来区分不同的 JavaScript Context 的作用域。

同样我们看一下接口,也是非常精简的

JS_EXPORT API_AVAILABLE(macos(10.9), ios(7.0))
@interface JSContext : NSObject

// 初始化,可以指定一个虚拟机,如果没有指定底层默认创建一个
- (instancetype)init;
- (instancetype)initWithVirtualMachine:(JSVirtualMachine *)virtualMachine;

// 执行 JS 脚本,返回值是 JS 中最后生成的一个值,sourceURL 认作其源码 URL,用作标记
- (JSValue *)evaluateScript:(NSString *)script;
- (JSValue *)evaluateScript:(NSString *)script
              withSourceURL:(NSURL *)sourceURL API_AVAILABLE(macos(10.10), ios(8.0));

// 获取当前执行的 JavaScript 代码的 context
+ (JSContext *)currentContext;

// 获取当前执行的 JavaScript function
+ (JSValue *)currentCallee API_AVAILABLE(macos(10.10), ios(8.0));

// 获取当前执行的 JavaScript 代码的 this
+ (JSValue *)currentThis;

// 获取当前 context 回调函数的参数
+ (NSArray *)currentArguments;

// 获取当前 context 的全局对象
@property (readonly, strong) JSValue *globalObject;

// 用于 JavaScript 执行异常
@property (strong) JSValue *exception;
@property (copy) void(^exceptionHandler)(JSContext *context, JSValue *exception);

// 获取当前虚拟机
@property (readonly, strong) JSVirtualMachine *virtualMachine;

// 标记当前 context 
@property (copy) NSString *name API_AVAILABLE(macos(10.10), ios(8.0));

@end

JS 访问 OC

{
    JSContext *context = [[JSContext alloc] init];
    context[@"add"] = ^(NSInteger a, NSInteger b) {
        NSLog(@"a + b = %ld", a + b);
    };
    [context evaluateScript:@"add(1, 2)"];
}

OC 访问 JS

{
    JSContext *context = [[JSContext alloc] init];
    [context evaluateScript:@"function add(a, b) {return a + b}"];
    JSValue *addFun = context[@"add"];
    JSValue *resValue = [addFun callWithArguments:@[@1, @2]];
    NSLog(@"a + b = %@", resValue);
}

JSValue

JSValue 实例是一个指向 JS 值的引用指针。我们可以使用 JSValue 类,在 OC 和 JS 的基础数据类型之间相互转换。你也可以使用这个类去创建包装了自定义类的 Native 对象的 JS 对象,或者是那些由 Native 方法或者 Block 实现的 JS 函数。

在 JSCore 中,JSValue 自动做了 OC 和 JS 的类型转换

Objective-C type JavaScript type
nil undefined
NSNull null
NSString string
NSNumber number, boolean
NSDictionary Object object
NSArray Array object
NSDate Date object
NSBlock Function object
id Wrapper object
Class Constructor object

我们继续看一下接口,非常简洁

NS_CLASS_AVAILABLE(10_9, 7_0)
@interface JSValue : NSObject

@property (readonly, strong) JSContext *context;

+ (JSValue *)valueWithObject:(id)value inContext:(JSContext *)context;
+ (JSValue *)valueWithBool:(BOOL)value inContext:(JSContext *)context;
+ (JSValue *)valueWithDouble:(double)value inContext:(JSContext *)context;
+ (JSValue *)valueWithInt32:(int32_t)value inContext:(JSContext *)context;
+ (JSValue *)valueWithUInt32:(uint32_t)value inContext:(JSContext *)context;
+ (JSValue *)valueWithNewObjectInContext:(JSContext *)context;
+ (JSValue *)valueWithNewArrayInContext:(JSContext *)context;
+ (JSValue *)valueWithNewRegularExpressionFromPattern:(NSString *)pattern flags:(NSString *)flags inContext:(JSContext *)context;
+ (JSValue *)valueWithNewErrorFromMessage:(NSString *)message inContext:(JSContext *)context;
+ (JSValue *)valueWithNewPromiseInContext:(JSContext *)context fromExecutor:(void (^)(JSValue *resolve, JSValue *reject))callback API_AVAILABLE(macos(10.15), ios(13.0));
+ (JSValue *)valueWithNewPromiseResolvedWithResult:(id)result inContext:(JSContext *)context API_AVAILABLE(macos(10.15), ios(13.0));
+ (JSValue *)valueWithNewPromiseRejectedWithReason:(id)reason inContext:(JSContext *)context API_AVAILABLE(macos(10.15), ios(13.0));
+ (JSValue *)valueWithNewSymbolFromDescription:(NSString *)description inContext:(JSContext *)context API_AVAILABLE(macos(10.15), ios(13.0));
+ (JSValue *)valueWithNullInContext:(JSContext *)context;
+ (JSValue *)valueWithUndefinedInContext:(JSContext *)context;

- (id)toObject;
- (id)toObjectOfClass:(Class)expectedClass;
- (BOOL)toBool;
- (double)toDouble;
- (int32_t)toInt32;
- (uint32_t)toUInt32;
- (NSNumber *)toNumber;
- (NSString *)toString;
- (NSDate *)toDate;
- (NSArray *)toArray;
- (NSDictionary *)toDictionary;

@property (readonly) BOOL isUndefined;
@property (readonly) BOOL isNull;
@property (readonly) BOOL isBoolean;
@property (readonly) BOOL isNumber;
@property (readonly) BOOL isString;
@property (readonly) BOOL isObject;
@property (readonly) BOOL isArray API_AVAILABLE(macos(10.11), ios(9.0));
@property (readonly) BOOL isDate API_AVAILABLE(macos(10.11), ios(9.0));
@property (readonly) BOOL isSymbol API_AVAILABLE(macos(10.15), ios(13.0));

- (BOOL)isEqualToObject:(id)value;
- (BOOL)isEqualWithTypeCoercionToObject:(id)value;
- (BOOL)isInstanceOf:(id)value;

// 当前 JSValue 为一个函数的时候,可以通过这个方法调用
- (JSValue *)callWithArguments:(NSArray *)arguments;
// 调用 JS 中的构造函数,arguments 数组内容必须是 JSValue 对象,以供 JS 能顺利转化
- (JSValue *)constructWithArguments:(NSArray *)arguments;
// 当前 JSValue 对象为 JS 中的全局对象名称,method 为全局对象的方法名称,arguments 为参数
- (JSValue *)invokeMethod:(NSString *)method withArguments:(NSArray *)arguments;

@end

OC 的 Block 转换

OC 层面的 Block 是可以自动转换为 JS 层面的函数,JS 可以直接访问;但是 JS 的函数 OC 确不能直接访问,而要通过 callWithArguments: 方法来调用。

OC 的 id 类型转换

OC 的 id 类型传给 JS,只是一个指针,是没法访问其属性和方法的,但是 JS 回传到 OC 的时候 OC 还是可以正常访问的。如果需要在 JS 中,访问 OC 对象的属性和方法可以通过 JSExport 协议来实现,下面会详细介绍。

JSExport

实现 JSExport 协议可以开放 OC 类和它们的实例方法,类方法,以及属性给 JS 调用

我们先看一个常规例子

@protocol OC2JSObjectExport <JSExport>

@property (nonatomic, strong) NSString *name;

- (void)callName;

+ (void)helloAtOC;

@end

@interface OC2JSObject : NSObject<OC2JSObjectExport>

@end

@implementation OC2JSObject

@synthesize name = _name;

- (void)callName
{
    NSLog(@"callName:%@", self.name);
}

+ (void)helloAtOC
{
    NSLog(@"helloAtOC");
}

@end

// 调用如下
{
    OC2JSObject *ocObj = [OC2JSObject new];
    ocObj.name = @"bob";
    
    JSContext *context = [[JSContext alloc] init];
    context[@"log"] = ^(NSString *msg){
        NSLog(@"%@", msg);
    };
    context[@"ocObj"] = ocObj;
    context[@"OC2JSObject"] = OC2JSObject.class;
    
    // 访问属性
    [context evaluateScript:@"log(ocObj.name)"];
    // 访问实例方法
    [context evaluateScript:@"ocObj.callName()"];
    // 访问类方法
    [context evaluateScript:@"OC2JSObject.helloAtOC()"];
}

如果 OC 方法有多个参数的时候

@protocol OC2JSObjectExport <JSExport>

+ (void)callVal1:(NSString *)val1 val2:(NSString *)val2;

@end

@interface OC2JSObject : NSObject<OC2JSObjectExport>

@end

@implementation OC2JSObject

+ (void)callVal1:(NSString *)val1 val2:(NSString *)val2
{
    NSLog(@"val1:%@ val2:%@", val1, val2);
}

@end

// 调用如下
{
    JSContext *context = [[JSContext alloc] init];
    context[@"OC2JSObject"] = OC2JSObject.class;
    [context evaluateScript:@"OC2JSObject.callVal1Val2('a', 'b')"];
}

多个参数的时候,转换规则成驼峰形式

如果不喜欢默认的转换规则,也可以使用 JSExportAs 来自定义转换,比如

@protocol OC2JSObjectExport <JSExport>

JSExportAs(callVal, + (void)callVal1:(NSString *)val1 val2:(NSString *)val2);

@end

@interface OC2JSObject : NSObject<OC2JSObjectExport>

@end

@implementation OC2JSObject

+ (void)callVal1:(NSString *)val1 val2:(NSString *)val2
{
    NSLog(@"val1:%@ val2:%@", val1, val2);
}

@end

// 调用如下
{
    JSContext *context = [[JSContext alloc] init];
    context[@"OC2JSObject"] = OC2JSObject.class;
    [context evaluateScript:@"OC2JSObject.callVal('a', 'b')"];
}

小结

JSCore 多线程

我们先看看一段 OC 的多线程代码

{
    __block NSInteger cnt = 0;
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    for (NSInteger i = 0; i < 1000; i++)
    {
        dispatch_group_async(group, queue, ^{
            cnt = cnt + 1;
        });
    }
    dispatch_group_notify(group, queue, ^{
        NSLog(@"cnt:%ld", cnt);
    });
}

因为访问 cnt 是线程不安全的,所以最后 cnt 的值,不一定是 1000

我们再看一段 JS 的代码

{
    JSVirtualMachine *vm = [[JSVirtualMachine alloc] init];
    JSContext *context = [[JSContext alloc] initWithVirtualMachine:vm];
    context[@"log"] = ^(NSString *msg) {
        NSLog(@"log:%@", msg);
    };
    
    [context evaluateScript:@"var cnt = 0"];
    [context evaluateScript:@"function addCnt(){cnt = cnt + 1}"];
    
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    for (NSInteger i = 0; i < 1000; i++)
    {
        dispatch_group_async(group, queue, ^{
            [context evaluateScript:@"addCnt()"];
        });
    }
    dispatch_group_notify(group, queue, ^{
        [context evaluateScript:@"log(cnt)"];
    });
}

这段代码发现,cnt 都是 1000,说明线程安全。

JavaScriptCore 提供的 API 是线程安全的。

你可以在不同的线程中,创建 JSValue,用 JSContext 执行 JS 语句,但是当一个线程正在执行 JS 语句时,其他线程想要使用这个正在执行 JS 语句的 JSContext 所属的 JSVirtualMachine 就必须得等待,等待前前一个线程执行完,才能使用这个 JSVirtualMachine。

JSCore 内存管理

目前 OC 使用的是 ARC,不能自动解决循环引用的问题,需要我们程序员手动去解除循环,但是 JS 使用的是 GC(垃圾回收机制),所有的引用都是强引用,同时垃圾回收器可以帮我们解决循环引用的问题,JSCore 也是一样的,一般来说,大多数情况下不需要我们去手动的管理内存。

注意1:OC 或 JS 对象,只要有一端存在强引用,对象就不会释放

比如:

@interface TCURLRequest : NSURLRequest

@end

@implementation TCURLRequest

- (void)dealloc
{
    NSLog(@"dealloc");
}

@end

{
    JSContext *context = [[JSContext alloc] init];
    {
        TCURLRequest *request = [TCURLRequest requestWithURL:[NSURL URLWithString:@"https://www.google.com"]];
        JSValue *value = [JSValue valueWithObject:request inContext:context];
        [context evaluateScript:@"var ocValue"];
        [context evaluateScript:@"function setValue(value){ocValue = value}"];
        JSValue *setValueFun = context[@"setValue"];
        [setValueFun callWithArguments:@[value]];
        // 在 OC 中,request 这之后就要释放了
    }
    JSValue *ocValue = context[@"ocValue"];
    TCURLRequest *request = ocValue.toObject;
    NSLog(@"%@", request);
    // 在这之后才释放的
}

注意2:循环引用,由于 JSValueJSContextJSVirtualMachine 都是强引用

比如,下面的代码就会出现循环引用

{
    JSContext *context = [[JSContext alloc] init];
    JSValue *value = [JSValue valueWithInt32:10086 inContext:context];
    context[@"log"] = ^{
        NSLog(@"%@", value);
    };
    [context evaluateScript:@"log()"];
}

我们可以使用 JSManagedValue 来解决,JSManagedValueJSValue 采用的是弱引用

{
    JSContext *context = [[JSContext alloc] init];
    JSValue *value = [JSValue valueWithInt32:10086 inContext:context];
    JSManagedValue *mValue = [JSManagedValue managedValueWithValue:value];
    context[@"log"] = ^{
        NSLog(@"%@", mValue);
    };
    [context evaluateScript:@"log()"];
}

当然,这里我们也可以使用 weak 来处理。有一种情况是 OC 引用了 JS 的属性,然后 JS 中也引用了 OC 的属性,这个时候由于 JS 中没有弱引用之说,所以必须要用 JSManagedValue。

比如:

@protocol QSExport <JSExport>

@property (nonatomic, strong) JSValue *jsValue;

@end

@interface QSObject : NSObject <QSExport>

@end

@implementation QSObject

@synthesize jsValue = _jsValue;

- (void)dealloc 
{
    NSLog(@"dealloc:%@", NSStringFromClass(self.class));
}

@end

@interface QSContext : JSContext

@end

@implementation QSContext

- (void)dealloc
{
    NSLog(@"dealloc:%@", NSStringFromClass(self.class));
}

@end

// 调用
{
    NSString *script = @"var arr = [1,2,3];\
                         function setObj(obj) {\
                            this.obj = obj;\
                            obj.jsValue = arr;\
                        }";
    QSContext *context = [[QSContext alloc] init];
    [context evaluateScript:script];
    
    QSObject *obj = [[QSObject alloc] init];
    [context[@"setObj"] callWithArguments:@[obj]];
}

QSObject 中的 jsValue 引用了 JS 中的 arr,而且 QSObject 对象同时也被 JS 引用。当然我们可以让 QSObject 中的 jsValue 为 weak 指针,但是由于 JS 最新在 OC 中没有强引用对象,所以 weak 指针是行不通的,

比如:

@protocol QSExport <JSExport>

@property (nonatomic, weak) JSValue *jsValue;

@end

// 调用
{
    NSString *script = @"var arr = [1,2,3];\
                         function setObj(obj) {\
                            this.obj = obj;\
                            obj.jsValue = arr;\
                        }";
    QSContext *context = [[QSContext alloc] init];
    [context evaluateScript:script];
    
    QSObject *obj = [[QSObject alloc] init];
    [context[@"setObj"] callWithArguments:@[obj]];
    NSLog(@"%@", obj.jsValue); // 这里是没有值的
}

在这种情况下,我们只能使用 JSManagedValue

@protocol QSExport <JSExport>

@property (nonatomic, strong) JSValue *jsValue;

@end

@interface QSObject : NSObject <QSExport>

@property (nonatomic, strong) JSManagedValue *jsManagedValue;

@end

@implementation QSObject

@synthesize jsValue = _jsValue;

- (void)setJsValue:(JSValue *)jsValue
{
    _jsManagedValue = [JSManagedValue managedValueWithValue:jsValue];
}

- (JSValue *)jsValue
{
    return self.jsManagedValue.value;
}

- (void)dealloc
{
    NSLog(@"dealloc:%@", NSStringFromClass(self.class));
}

@end

@interface QSContext : JSContext

@end

@implementation QSContext

- (void)dealloc
{
    NSLog(@"dealloc:%@", NSStringFromClass(self.class));
}

@end

// 调用
{
    NSString *script = @"var arr = [1,2,3];\
                         function setObj(obj) {\
                            this.obj = obj;\
                            obj.jsValue = arr;\
                        }";
    QSContext *context = [[QSContext alloc] init];
    [context evaluateScript:script];
    
    QSObject *obj = [[QSObject alloc] init];
    [context[@"setObj"] callWithArguments:@[obj]];
    NSLog(@"%@", obj.jsValue); 
}

这样就能正常调用和释放了。

参考资料

上一篇下一篇

猜你喜欢

热点阅读