YownYang's blog

iOS-Weex源码解析

上一篇介绍了Weex中文件夹的作用以及重要性,这一篇将正式开始Weex源码的解析,版本是0.10.0

入口

学习一个开源库,最好是知其思想,找其入口,学其核心。大致思想在第一篇,核心文件夹的分类在第二篇,这一篇就从其入口文件开始学习。

WeexDemo的入口是[WXSDKEngine initSDKEnvironment];。这个Method的主要代码,如下:

1
2
3
NSString *filePath = [[NSBundle bundleForClass:self] pathForResource:@"main" ofType:@"js"];
NSString *script = [NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:nil];
[WXSDKEngine initSDKEnvironment:script];

首先,读取一个叫做main.js的文件内容,然后将其内容作为[WXSDKEngine initSDKEnvironment:script]初始化的参数。
我们继续来看这个method的代码,如下:

1
2
3
4
5
6
if (!script || script.length <= 0) {
WX_MONITOR_FAIL(WXMTJSFramework, WX_ERR_JSFRAMEWORK_LOAD, @"framework loading is failure!");
return;
}
[self registerDefaults];
[[WXSDKManager bridgeMgr] executeJsFramework:script];

注册和执行

Register

首先,是对传递进来的js代码做判断,其次调用了一个registerDefaults的method,代码如下:

1
2
3
4
5
6
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self _registerDefaultComponents];
[self _registerDefaultModules];
[self _registerDefaultHandlers];
});
Register Componet

这个method使用了单例模式,对Weex定义的Component、Module、Handler进行注册,注册代码太长,我就不贴了。注册Component,代码如下:

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
+ (void)registerComponent:(NSString *)name withClass:(Class)clazz
{
[self registerComponent:name withClass:clazz withProperties: @{@"append":@"tree"}];
}
+ (void)registerComponent:(NSString *)name withClass:(Class)clazz withProperties:(NSDictionary *)properties
{
if (!name || !clazz) {
return;
}
WXAssert(name && clazz, @"Fail to register the component, please check if the parameters are correct !");
[WXComponentFactory registerComponent:name withClass:clazz withPros:properties];
NSMutableDictionary *dict = [WXComponentFactory componentMethodMapsWithName:name];
dict[@"type"] = name;
if (properties) {
NSMutableDictionary *props = [properties mutableCopy];
if ([dict[@"methods"] count]) {
[props addEntriesFromDictionary:dict];
}
[[WXSDKManager bridgeMgr] registerComponents:@[props]];
} else {
[[WXSDKManager bridgeMgr] registerComponents:@[dict]];
}
}

第一个method中比较让人迷惑的也就是Properties中的参数了,这个其实是vue的渲染模式。@"append":@"tree"代表是整个vue结点包括子结点生成完之后才会一次性渲染到屏幕,@"append":@"node"代表是先渲染自身然后再渲染子节点。第二个method中先对name和class进行判空,其次使用WXComponentFactory进行注册,在这之前先讲几个相关类的功能,免得迷糊。

  • WXInvocationConfig:抽象单例类,为什么用单例(懵逼脸),使用时需要子类继承
  • WXComponentConfig: 继承WXInvocationConfig类,存储每个Component的method、name、classname
  • WXComponentFactory:单例类,通过字典存储WXComponentConfig对象,通过每个WXComponentConfig对象操作每个Component的method、name、classname。

首先通过WXComponentFactory调用- (void)registerComponent:(NSString *)name withClass:(Class)clazz withPros:(NSDictionary *)pros方法注册。使用Assert判断,然后创建一个WXComponentConfig对象,先从字典中取,不论是否存在都重新初始化,并将其覆盖,调用[config registerMethods];将类中的method通过runtime存储在WXComponentConfig中,存取时加锁保证安全。其次调用- (NSMutableDictionary *)_componentMethodMapsWithName:(NSString *)name方法获取某个component所有的method,同样是加锁读取。根据有没有properties传递不同参数,如果类中有导出给weex用的方法,那么一定会传递method和name过去。最后调用JS方法registerComponents,我在vue.js的源码中是找到这个方法了的。对于具体调用JS的过程,会在ExecuteJs模块讲解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export function registerComponents (newComponents) {
if (Array.isArray(newComponents)) {
newComponents.forEach(component => {
if (!component) {
return
}
if (typeof component === 'string') {
components[component] = true
} else if (typeof component === 'object' && typeof component.type === 'string') {
components[component.type] = component
}
})
}
}
Register Module

注册Module,代码如下:

1
2
3
4
WXAssert(name && clazz, @"Fail to register the module, please check if the parameters are correct !");
NSString *moduleName = [WXModuleFactory registerModule:name withClass:clazz];
NSDictionary *dict = [WXModuleFactory moduleMethodMapsWithName:moduleName];
[[WXSDKManager bridgeMgr] registerModules:dict];

先介绍几个类:

  • WXModuleConfig:继承WXInvocationConfig类,存储每个Component的method、name
  • WXModuleFactory:单例类,通过字典操作WXModuleConfig对象

查阅下这两个类的代码会发现跟Component结构类似,跟注册Component流程也一样,感觉没必要废话一遍了,最后调用JS方法registerModules

Register Handler

注册Handler,代码如下:

1
2
3
WXAssert(handler && protocol, @"Fail to register the handler, please check if the parameters are correct !");
[WXHandlerFactory registerHandler:handler withProtocol:protocol];

照例介绍几个类:

  • WXHandlerFactory:单例类,通过字典存储协议对象,将协议的字符串作为key存储

注册Handler,这个不需要传给weex,因为就是我们Native端进行调用。所以只需要使用WXHandlerFactory操作就行了。

ExecuteJs

最后调用[[WXSDKManager bridgeMgr] executeJsFramework:script];执行js代码。这一块的知识点是Weex与js的交互,所以需要了解下面几个类,并且对OC中的JavaScriptCore框架有所了解。

首先介绍几个类:

  • WXSDKInstance:普通类,这个类是一个类似于Controller的类,具有非常多的功能,目前不需要了解具体功能
  • WXSDKManager:单例类,通过一个字典存储所有WXSDKInstance实例,key是一个唯一值;一个WXBridgeManager实例
  • WXBridgeManager:单例类,注册,渲染功能都通过调用WXBridgeContext对象去跟JS交互
  • WXBridgeContext:功能其实不多,render,regist component,regist module,executeJs。就是处理了需要调用js的逻辑。
  • WXJSCoreBridge: 这个类才是真正的处理JS调用的类。它实现了WXBridgeProtocol协议,对JavaScriptCore进行了封装,使WXBridgeContext调用

现在可以从那句代码开始讲了,[WXSDKManager bridgeMgr]这个对象是一个单例,他在JS线程调用executeJsFramework,代码如下:

1
2
3
4
5
if (!script) return;
__weak typeof(self) weakSelf = self;
WXPerformBlockOnBridgeThread(^(){
[weakSelf.bridgeCtx executeJsFramework:script];
});

先是判空,其次weakSelf防止循环引用,然后在一个叫做"com.taobao.weex.bridge"的线程调用executeJsFramework方法,代码如下:

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
29
30
31
32
33
WXAssertBridgeThread();
WXAssertParam(script);
WX_MONITOR_PERF_START(WXPTFrameworkExecute);
[self.jsBridge executeJSFramework:script];
WX_MONITOR_PERF_END(WXPTFrameworkExecute);
if ([self.jsBridge exception]) {
NSString *message = [NSString stringWithFormat:@"JSFramework executes error: %@", [self.jsBridge exception]];
WX_MONITOR_FAIL(WXMTJSFramework, WX_ERR_JSFRAMEWORK_EXECUTE, message);
} else {
WX_MONITOR_SUCCESS(WXMTJSFramework);
//the JSFramework has been load successfully.
self.frameworkLoadFinished = YES;
[self executeAllJsService];
JSValue *frameworkVersion = [self.jsBridge callJSMethod:@"getJSFMVersion" args:nil];
if (frameworkVersion && [frameworkVersion isString]) {
[WXAppConfiguration setJSFrameworkVersion:[frameworkVersion toString]];
}
//execute methods which has been stored in methodQueue temporarily.
for (NSDictionary *method in _methodQueue) {
[self callJSMethod:method[@"method"] args:method[@"args"]];
}
[_methodQueue removeAllObjects];
WX_MONITOR_PERF_END(WXPTInitalize);
};

首先断言当前线程是否是"com.taobao.weex.bridge"线程,其次断言js代码,使用WXBridgeProtocol协议对象执行js代码,接着判断js执行是否有异常,有异常输出,无异常,标记读取结束,执行所有的jsService,获取JSFMVersion,执行methodQueue中所有的method,清除信息,结束。

JavaScriptCore

JavaScriptCore简介

上面只是理清了逻辑,如果对JavaScriptCore不了解的人可能看源码时有些懵逼,下面我讲解一些JavaScriptCore的基本概念以及用法。

  • JSVirtualMachine:为JavaScript提供运行资源
  • JSContext:为JavaScript提供运行环境
  • JSValue:可以将JavaScript变量转换为OC变量,也可以将OC变量转换为JavaScript变量

JavaScriptCore示例

这些是Weex使用的JavaScriptCore框架一部分功能,其实还有别的。先来一段代码好了:

简单使用
1
2
3
4
5
JSContext *context = [[JSContext alloc] init];
JSValue *value = [context evaluateScript:@"var sum = 2 + 3; sum"];
NSLog(@"%@", value); 输出5
context[@"sum"] = @"40";
NSLog(@"%@", context[@"sum"]); 输出40

首先初始化一个JSContext对象,可以使用JSVirtualMachine对象初始化,也可以直接初始化,直接初始化系统仍会在内部给你初始化一个JSVirtualMachine对象,所以这个js运行的资源,不可或缺。

第二句代码的意思是先使用context对象运行js代码,定义一个叫做sum的变量,并赋值2+3,然后将sum赋值给value。

第三句代码输出value的值是5,第四句代码给sum赋值40,第五句输出sum值为40。这主要是因为JSContext是js的环境,而且在js中,所有全局变量和方法都是一个全局变量的属性。所以在第四句,你可以直接从context中取出sum这个变量,并赋值。

异常处理
1
2
3
4
_jsContext.exceptionHandler = ^(JSContext *context, JSValue *exception) {
context.exception = exception;
NSString *message = [NSString stringWithFormat:@"[%@:%@:%@] %@\n%@", exception[@"sourceURL"], exception[@"line"], exception[@"column"], exception, [exception[@"stack"] toObject]];
};

另一个要注意的点就是这里,JavaScriptCore会在exceptionHandler中抛出异常,为了我们能在这个时候做点什么,所以我们赋值给他一个blcok。并且如果你要在block中使用context对象,要么将其作为参数传递进block,要么使用[JSContext currentContext]获取当前的context。如果直接引用外部的context会造成循环饮用。

invokeMethod
1
2
3
4
JSContext *context = [[JSContext alloc] init];
[context evaluateScript:@"function add(a, b) { return a + b; }"];
JSValue *sum = [[context globalObject] invokeMethod:@"add" withArguments:@[@(3), @(4)]];
NSLog(@"%@", sum); 输出7

初始化一个context,将一个名为add的method加入到context中,使用js全局变量调用add方法,并传入参数3,4,输出结果为7。

总结

上面讲了Weex注册的基本逻辑和JavaScriptCore框架的一些基本使用,我想应该对理解Weex框架的运作原理有帮助的。其实剩下的源码还有很多,比如向js端发送消息等,但其实质逃不过上面的流程,所以我认为也没有往下写的必要了。最后,为大家整理下类的调用顺序。