前言
相对而言,这篇文章最贴近日常的iOS开发了,应该是我们最熟悉,同时又有点陌生的一篇了,这里陌生的原因是会加入一些对源码的分析。
在JavaScriptCore.h文件中可以看到:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
JavaScriptCore一共向我们暴露了这几个主要的头文件,先看一张全局关系图
接下来逐个阐述,阅读的时候,可能需要回翻,因为有些就好像鸡生蛋、蛋生鸡的哲学问题,无法分清楚先后…
JSVirtualMachine
一个JSVirtualMachine的实例就是一个完整独立的JavaScript的执行环境,为JavaScript的执行提供底层资源。
这个类主要用来做两件事情:
- 实现JavaScript的并发执行
- JavaScript和Objective-C桥接对象的内存管理
每一个JSContext对象都归属于一个JSVirtualMachine虚拟机。每个虚拟机可以包含多个不同的上下文,并允许在这些不同的上下文之间传值(JSValue对象)。
然而,每个虚拟机都是完整且独立的,有其独立的堆空间和垃圾回收器,GC无法处理别的虚拟机堆中的对象,因此不能把一个虚拟机中创建的值传给另一个虚拟机。
接下来来窥探一下源码,先看下JSVirtualMachine
的头文件1
2
3
4
5@interface JSVirtualMachine : NSObject
- (instancetype)init;
- (void)addManagedReference:(id)object withOwner:(id)owner;
- (void)removeManagedReference:(id)object withOwner:(id)owner;
@end
接下来看下JSVirtualMachine
的mm文件1
2
3
4
5
6@implementation JSVirtualMachine {
JSContextGroupRef m_group;// JSContext集合
NSMapTable *m_contextCache;
NSMapTable *m_externalObjectGraph;
NSMapTable *m_externalRememberedSet;
}
1 | - (instancetype)init |
倒数第二行代码的映射,实际上也是放到一个全局的NSMapTable表中,JSContextGroupRef为key,JSVirtualMachine为value,这样给定一个JSContextGroupRef
,就可以比较方便地定位到其所属的JSVirtualMachine
。1
2
3
4
5
6
7// globalWrapperCache = [[NSMapTable alloc] initWithKeyOptions:keyOptions valueOptions:valueOptions capacity:0];
+ (void)addWrapper:(JSVirtualMachine *)wrapper forJSContextGroupRef:(JSContextGroupRef)group
{
std::lock_guard<StaticLock> lock(wrapperCacheMutex);
NSMapInsert(globalWrapperCache, group, wrapper);
}
addManagedReference
实际上,我们更想关注的是它的两个方法,因为这两个方法涉及到了JS对象和OC对象的内存管理,为了看的更为清晰一点,先看下该方法如何被调用的。1
2
3
4
5
6
7
8
9- (void)testDemo
{
JSContext *context = [[JSContext alloc] init];
JSValue *message = [JSValue valueWithObject:@"hello" inContext:context];
JSCollection *collection = [[JSCollection alloc] init];
JSValue *jsCollection = [JSValue valueWithObject:collection inContext:context];
JSManagedValue *weakCollection = [JSManagedValue managedValueWithValue:jsCollection andOwner:rootObject];
[context.virtualMachine addManagedReference:weakCollection withOwner:message];
}
可以看到addManagedReference
的第一个参数是一个JSManagedValue
对象,第二个参数是一个JSValue
。
1 | - (void)addManagedReference:(id)object withOwner:(id)owner |
didAddOwner
可以看到,方法的一开始逻辑didAddOwner
,就是为JSValue增加引用计数,并存放到m_owners
中,m_owners
是JSManagedValue
的一个属性:NSMapTable *m_owners;
。1
2
3
4
5- (void)didAddOwner:(id)owner
{
size_t count = reinterpret_cast<size_t>(NSMapGet(m_owners, owner));
NSMapInsert(m_owners, owner, reinterpret_cast<void*>(count + 1));
}
getInternalObjcObject
接下来的getInternalObjcObject
的代码如下,用一个字来解释:unwrap1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18static id getInternalObjcObject(id object)
{
if ([object isKindOfClass:[JSManagedValue class]]) {
// static_cast编译时类型检查,必须人工保证
JSValue* value = [static_cast<JSManagedValue *>(object) value];
id temp = tryUnwrapObjcObject([value.context JSGlobalContextRef], [value JSValueRef]);
if (temp)
return temp;
return object;
}
if ([object isKindOfClass:[JSValue class]]) {
JSValue *value = static_cast<JSValue *>(object);
object = tryUnwrapObjcObject([value.context JSGlobalContextRef], [value JSValueRef]);
}
return object;
}
removeManagedReference
1 | - (void)removeManagedReference:(id)object withOwner:(id)owner |
didRemoveOwner
didRemoveOwner
是didAddOwner
的逆过程,减少引用计数,如果减少之后为0,就将owner从m_owners
中移除1
2
3
4
5
6
7
8
9
10
11
12
13- (void)didRemoveOwner:(id)owner
{
size_t count = reinterpret_cast<size_t>(NSMapGet(m_owners, owner));
if (!count) return;
if (count == 1) {
NSMapRemove(m_owners, owner);
return;
}
NSMapInsert(m_owners, owner, reinterpret_cast<void*>(count - 1));
}
是不是已经感觉凌乱了?我们先看一下图:
实际上到这里,我们也没讲明白,JSVirturlMachine和JSManagedValue是怎么做到有条件强持有的,并且没有循环引用。
以下是我的一点推理:
我们知道Objective-C的内存管理是ARC,而JavaScript的是GC,我们这里已经研究到JavaScriptCore的源码层面,GC的实现也必然在这里。不管怎样,一个JavaScript的对象要想真正的释放,对应到JavaScriptCore源码层级,必然对应到某一个C++对象的析构函数的调用,否则这个GC就无法实现。有了这个前提,上面的代码就派上用处了:当我们使用JSManagedValue
对象JMV去包裹了一个JSValue
对象JV的时候(JMV对象并没有强持有JV对象,而是弱引用),只要JMV对象的生命周期还在,我们就能在m_externalObjectGraph
寻找到至少一个JV对象对应的JMV对象,那么就可以在GC回收内存的时候告诉它,不要释放JV对象,尽管该JV对象的引用计数已经可能为0,可能已经成为孤岛。这样,只要JSValue
的内存块没有被释放掉,它就可以一直被我们访问。如果到了某一个时刻,我们在m_externalObjectGraph
找不到任何JV对应的JMV对象,GC就可以把JV的内存收回了。并且在这整个过程中,没有产生循环引用。
实际上这部分内容在scanExternalObjectGraph
方法中可以看到一些端倪,感兴趣的童鞋可以看一下。
JSContext
一个JSContext对象代表一个JavaScript执行环境。在native代码中,使用JSContext去执行JS代码,访问JS中定义或者计算的值,并使JavaScript可以访问native的对象、方法、函数,它就好像是OC和JS语言之间的一座桥梁。
1 | SValue *value = [context evaluateScript:@"var a = 1+2*3;"]; |
JSContext初始化
1 | - (instancetype)initWithVirtualMachine:(JSVirtualMachine *)virtualMachine |
全局对象
肤浅地看下全局对象(因为这个方法深究下去,非常复杂),简而言之,一个JSContext对应着一个全局对象1
2
3
4- (JSValue *)globalObject
{
return [JSValue valueWithJSValueRef:JSContextGetGlobalObject(m_context) inContext:self];
}
1 | JSObjectRef JSContextGetGlobalObject(JSContextRef ctx) |
执行脚本
1 | - (JSValue *)evaluateScript:(NSString *)script withSourceURL:(NSURL *)sourceURL |
可以看到核心逻辑在JSEvaluateScript
中1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24JSValueRef JSEvaluateScript(JSContextRef ctx, JSStringRef script, JSObjectRef thisObject, JSStringRef sourceURL, int startingLineNumber, JSValueRef* exception)
{
// 再次碰到了ExecState,当前脚本执行状态
ExecState* exec = toJS(ctx);
JSLockHolder locker(exec);
JSObject* jsThisObject = toJS(thisObject);
startingLineNumber = std::max(1, startingLineNumber);
// evaluate sets "this" to the global object if it is NULL
JSGlobalObject* globalObject = exec->vmEntryGlobalObject();
// 加载源码
SourceCode source = makeSource(script->string(), sourceURL ? sourceURL->string() : String(), TextPosition(OrdinalNumber::fromOneBasedInt(startingLineNumber), OrdinalNumber::first()));
// 执行JS代码,这里是我们要分析的核心入口
JSValue returnValue = profiledEvaluate(globalObject->globalExec(), ProfilingReason::API, source, jsThisObject, evaluationException);
// 转换为Ref形式
if (returnValue) return toRef(exec, returnValue);
return toRef(exec, jsUndefined());
}
JSValue
一个JSValue实例就是一个JavaScript值的引用。使用JSValue类在JavaScript和native代码之间转换一些基本类型的数据(比如数值和字符串)。你也可以使用这个类去创建包装了自定义类的native对象的JavaScript对象,或者创建由native方法或者block实现的JavaScript函数。
每个JSValue实例都来源于一个代表JavaScript执行环境JSContext的对象,这个执行环境就包含了这个JSValue对应的值。每个JSValue对象都持有其JSContext对象的强引用,只要有任何一个与特定JSContext关联的JSValue被持有(retain),这个JSContext就会一直存活。通过调用JSValue的实例方法返回的其他的JSValue对象都属于与最始的JSValue相同的JSContext。
每个JSValue都通过其JSContext间接关联了一个特定的代表执行资源基础的JSVirtualMachine对象。你只能将一个JSValue对象传给由相同虚拟机管理(host)的JSValue或者JSContext的实例方法。如果尝试把一个虚拟机的JSValue传给另一个虚拟机,将会触发一个Objective-C异常
初始化
1 | - (JSValue *)initWithValue:(JSValueRef)value inContext:(JSContext *)context |
倒数第二句相当于GC做一个强引用,该方法一直追溯下去,发现是一个ProtectCountSet集合,它在添加元素的时候,会增加元素的引用计数,导致元素不会被释放1
2
3
4
5
6
7ProtectCountSet m_protectedValues;
void Heap::protect(JSValue k)
{
if (!k.isCell()) return;
m_protectedValues.add(k.asCell());
}
类型转换
OC对象和JS对象或者值的转换对应图如下:1
2
3
4
5
6
7
8
9
10
11
12 Objective-C type | JavaScript type
--------------------+---------------------
nil | undefined
NSNull | null
NSString | string
NSNumber | number, boolean
NSDictionary | Object object
NSArray | Array object
NSDate | Date object
NSBlock (1) | Function object (1)
id (2) | Wrapper object (2)
Class (3) | Constructor object (3)
OC->JS
1 | // 将OC对象转换为JSValue |
1 | JSValueRef objectToValue(JSContext *context, id object) |
1 | + (JSValue *)valueWithJSValueRef:(JSValueRef)value inContext:(JSContext *)context |
JS->OC
1 | - (id)toObject |
1 | static id containerValueToObject(JSGlobalContextRef context, JSContainerConvertor::Task task) |
辅助类JSContainerConvertor
1 | class JSContainerConvertor { |
JSManagedValue
Objective-C 用的是ARC,不能自动解决循环引用问题,需要程序员手动处理,而JavaScript 用的是GC,所有的引用都是强引用,但是垃圾回收器会解决循环引用问题,JavaScriptCore 也一样,一般来说,大多数时候不需要我们去手动管理内存,但是有些情况需要注意:
- 不要在在一个导出到JavaScript的native对象中持有JSValue对象。因为每个JSValue对象都包含了一个JSContext对象,这种关系将会导致循环引用,因而可能造成内存泄漏。
1 | JSValue *value = [JSValue valueWithObject:@"test" inContext:context]; |
通常我们使用weak来修饰block内需要使用的外部引用以避免循环引用,由于JSValue对应的JS对象内存由虚拟机进行管理并负责回收,这种方法不能准确地控制block内的引用JSValue的生命周期,可能在block内需要使用JSValue的时候,其已经被虚拟机回收。
因为JSValue的引用计数为0,所以早早就被释放了,不能达到我们的预期。
Apple引入了有条件的强引用:conditional retain
,而对应的类就叫JSManagedValue
一个JSManagedValue对象包含了一个JSValue对象,“有条件地持有(conditional retain)”的特性使其可以自动管理内存。
最基本的用法就是用来在导入到JavaScript的native对象中存储JSValue。1
2
3
4
5JSValue *value = [JSValue valueWithObject:@"test" inContext:context];
JSManagedValue *managedValue = [JSManagedValue managedValueWithValue:value andOwner:self];
context[@"block"] = ^(){
NSLog(@"%@", [managedValue value]);
};
- 所谓“有条件地持有(conditional retain)”,是指在以下两种情况任何一个满足的情况下保证其管理的JSValue被持有:可以通过JavaScript的对象图找到该JSValue。可以通过native对象图找到该JSManagedValue。使用addManagedReference:withOwner:方法可向虚拟机记录该关系反之,如果以上条件都不满足,JSManagedValue对象就会将其value置为nil并释放该JSValue。
初始化
1 | @implementation JSManagedValue { |
1 | - (instancetype)initWithValue:(JSValue *)value |
1 | - (void)dealloc |
这部分的内容可以和JSVirtualMachine
结合看,可能有助于理解,因为这二者的联系比较紧密
JSExport
JSExport协议提供了一种声明式的方法去向JavaScript代码导出Objective-C的实例类及其实例方法,类方法和属性。
使用示例
1 | @class MySize; |
在js代码中可以这样调用:1
2
3
4
5
6
7
8
9
10
11
12
13// Objective-C initializers can be called with constructor syntax.
var size = MySize(1, 2);
// Objective-C properties become fields.
size.w;
size.h = 10;
// Objective-C instance methods become functions.
size.description();
// Objective-C class methods become functions on the constructor object.
var q = MySize.makeSizeWithWH(0, 0);
JSExport实现原理
对于一个class实现的每个协议,如果这个协议继承了JSExport协议,JavaScriptCore就将这个协议的方法和属性列表导出给JavaScript。
- 对于每一个导出的实例方法,JavaScriptCore都会在prototype中创建一个对应的方法;
- 对于么一个导出的实例属性,JavaScriptCore都会在prototype中创建一个对应一个存取器属性;
- 对于每一个导出的类方法,JavaScriptCore会在constructor对象中创建一个对应的JavaScript function.
说起来,JavaScript一直是让人很费解的一门语言,它是动态的,本身没有类的概念,尽管ES6引入了关键字class,但是这种语法糖仍然改变不了JavaScript是基于原型的这一事实,掌握原型和原型链的本质是JavaScript进阶的非常重要一环。
引用一章非常经典的原型链和构造函数的关系图
但是这里我们不打算细究JavaScript的原型链,感兴趣的同学可以参考这里:
js基础篇——原型与原型链的详细理解
javascript的原型与原型链
我们来看下JavaScriptCore
的JSExport
实现原理,在分析之前,得做一些铺垫,从JSWrapperMap
说起,而后者正是作为JSContext
的一个成员而存在的:1
JSWrapperMap *m_wrapperMap;
JSWrapperMap
JSWrapperMap,顾名思义,这是一个包装容器,它的初始化方法中有一个参数JSContext,看来他本身也是离不开JSContext而存在的。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18@interface JSWrapperMap : NSObject
- (id)initWithContext:(JSContext *)context;
- (JSValue *)jsWrapperForObject:(id)object;
- (JSValue *)objcWrapperForJSValueRef:(JSValueRef)value;
@end
@implementation JSWrapperMap {
JSContext *m_context;
// Class -> JSObjCClassInfo
NSMutableDictionary *m_classMap;
// OC对象 -> JSObject
std::unique_ptr<JSC::WeakGCMap<id, JSC::JSObject>> m_cachedJSWrappers;
//
NSMapTable *m_cachedObjCWrappers;
}
对于一个OC对象,其jsWrapper的生成如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25- (JSValue *)jsWrapperForObject:(id)object
{
// 先从m_cachedJSWrappers容器中查找
JSC::JSObject* jsWrapper = m_cachedJSWrappers->get(object);
// 如果找到,封装为JSValue返回
if (jsWrapper)
return [JSValue valueWithJSValueRef:toRef(jsWrapper) inContext:m_context];
if (class_isMetaClass(object_getClass(object)))
// 如果是object的类是元类,即object本身是一个Class,构建原型对象
jsWrapper = [[self classInfoForClass:(Class)object] constructor];
else {
// 如果object是对象,先生成JSObjCClassInfo类信息,然后包装object对象
JSObjCClassInfo* classInfo = [self classInfoForClass:[object class]];
jsWrapper = [classInfo wrapperForObject:object];
}
// 设置key-value
m_cachedJSWrappers->set(object, jsWrapper);
// 将生成的jsWrapper,封装为JSValue返回
return [JSValue valueWithJSValueRef:toRef(jsWrapper) inContext:m_context];
}
classInfoForClass
下来看下怎么通过一个Class生成JSObjCClassInfo信息1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17- (JSObjCClassInfo*)classInfoForClass:(Class)cls
{
if (!cls)
return nil;
// 如果已经有缓存好的JSObjCClassInfo信息,直接返回
if (JSObjCClassInfo* classInfo = (JSObjCClassInfo*)m_classMap[cls])
return classInfo;
// 跳过一些以下划线开头的内部中间类,直接获取其父类信息
if ('_' == *class_getName(cls))
return m_classMap[cls] = [self classInfoForClass:class_getSuperclass(cls)];
// 通过cls类信息生成JSObjCClassInfo信息,并存入m_classMap
return m_classMap[cls] = [[[JSObjCClassInfo alloc] initWithContext:m_context forClass:cls] autorelease];
}
JSObjCClassInfo
1 | @interface JSObjCClassInfo : NSObject { |
重点来了,JavaScript中很重要的两个概念:constructor和prototype
1 | - (JSC::JSObject*)constructor |
这两个方法都不约而同的调到了allocateConstructorAndPrototype
:
allocateConstructorAndPrototype
1 | - (ConstructorPrototypePair)allocateConstructorAndPrototype |
更新
这里看到了getJSExportProtocol
,我们的神经是不是应该敏感一下?1
2
3
4
5Protocol *getJSExportProtocol()
{
static Protocol *protocol = objc_getProtocol("JSExport");
return protocol;
}
实际上它仅仅返回JSExport
这个Protocol
,关键的是接下来的遍历:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29inline void forEachProtocolImplementingProtocol(Class cls, Protocol *target, void (^callback)(Protocol *))
{
Vector<Protocol *> worklist;
HashSet<void*> visited;
// Initially fill the worklist with the Class's protocols.
unsigned protocolsCount;
Protocol ** protocols = class_copyProtocolList(cls, &protocolsCount);
worklist.append(protocols, protocolsCount);
free(protocols);
while (!worklist.isEmpty()) {
Protocol *protocol = worklist.last();
worklist.removeLast();
// Are we encountering this Protocol for the first time?
if (!visited.add(protocol).isNewEntry)
continue;
// If it implements the protocol, make the callback.
if (protocolImplementsProtocol(protocol, target))
callback(protocol);
// Add incorporated protocols to the worklist.
protocols = protocol_copyProtocolList(protocol, &protocolsCount);
worklist.append(protocols, protocolsCount);
free(protocols);
}
}
对于每个声明在JSExport里的属性和方法,classInfo会在prototype和constructor里面存入对应的property和method。之后我们就可以通过具体的methodName和PropertyName生成的setter和getter方法,来获取实际的SEL。最后就可以让JSExport中的方法和属性得到正确的访问。所以简单点讲,JSExport就是负责把这些方法打个标,以methodName为key,SEL为value,存入一个map(prototype和constructor本质上就是一个Map)中去,之后就可以通过methodName拿到对应的SEL进行调用。
生成prototype
1 | // Make an object that is in all ways a completely vanilla JavaScript object, |
生成contructor
1 | static JSC::JSObject* allocateConstructorForCustomClass(JSContext *context, const char* className, Class cls) |
1 | static JSC::JSObject *constructorWithCustomBrand(JSContext *context, NSString *brand, Class cls) |
总结
本篇是对JavaScriptCore的API的分析总结,由于源码过于庞大,为了避免掉进泥潭不能自拔,很多地方都是浅尝辄止,后续篇章将会逐步深入进行分析。
但是总的来说,JavaScriptCore隐藏了很多实现细节,API的接口也非常简洁