先来看一下注册Components的源码:
+ (void)_registerDefaultComponents
{
[self registerComponent:@"container" withClass:NSClassFromString(@"WXDivComponent") withProperties:nil];
[self registerComponent:@"div" withClass:NSClassFromString(@"WXComponent") withProperties:nil];
[self registerComponent:@"text" withClass:NSClassFromString(@"WXTextComponent") withProperties:nil];
[self registerComponent:@"image" withClass:NSClassFromString(@"WXImageComponent") withProperties:nil];
[self registerComponent:@"scroller" withClass:NSClassFromString(@"WXScrollerComponent") withProperties:nil];
[self registerComponent:@"list" withClass:NSClassFromString(@"WXListComponent") withProperties:nil];
[self registerComponent:@"recycler" withClass:NSClassFromString(@"WXRecyclerComponent") withProperties:nil];
[self registerComponent:@"waterfall" withClass:NSClassFromString(@"WXRecyclerComponent") withProperties:nil]; [self registerComponent:@"header" withClass:NSClassFromString(@"WXHeaderComponent")];
[self registerComponent:@"cell" withClass:NSClassFromString(@"WXCellComponent")];
[self registerComponent:@"embed" withClass:NSClassFromString(@"WXEmbedComponent")];
[self registerComponent:@"a" withClass:NSClassFromString(@"WXAComponent")]; [self registerComponent:@"select" withClass:NSClassFromString(@"WXSelectComponent")];
[self registerComponent:@"switch" withClass:NSClassFromString(@"WXSwitchComponent")];
[self registerComponent:@"input" withClass:NSClassFromString(@"WXTextInputComponent")];
[self registerComponent:@"video" withClass:NSClassFromString(@"WXVideoComponent")];
[self registerComponent:@"indicator" withClass:NSClassFromString(@"WXIndicatorComponent")];
[self registerComponent:@"slider" withClass:NSClassFromString(@"WXCycleSliderComponent")];
[self registerComponent:@"cycleslider" withClass:NSClassFromString(@"WXCycleSliderComponent")];
[self registerComponent:@"web" withClass:NSClassFromString(@"WXWebComponent")];
[self registerComponent:@"loading" withClass:NSClassFromString(@"WXLoadingComponent")];
[self registerComponent:@"loading-indicator" withClass:NSClassFromString(@"WXLoadingIndicator")];
[self registerComponent:@"refresh" withClass:NSClassFromString(@"WXRefreshComponent")];
[self registerComponent:@"textarea" withClass:NSClassFromString(@"WXTextAreaComponent")];
[self registerComponent:@"canvas" withClass:NSClassFromString(@"WXCanvasComponent")];
[self registerComponent:@"slider-neighbor" withClass:NSClassFromString(@"WXSliderNeighborComponent")]; [self registerComponent:@"recycle-list" withClass:NSClassFromString(@"WXRecycleListComponent")];
[self registerComponent:@"cell-slot" withClass:NSClassFromString(@"WXCellSlotComponent") withProperties: @{@"append":@"tree", @"isTemplate":@YES}]; }
从源码可以看到,WeexSDK会默认注册这28个组件。这里以WXWebComponent组件注册为例,来分析组件注册的过程。
【说明】:上面标红可以看到,有两个注册组件的方法,区别在于最后一个入参是否传@{@"append":@"tree"}。如果被标记成了@"tree",那么在syncQueue堆积了很多任务的时候,会被强制执行一次layout。在上面的28个组件中,只有前8种没有被标记成@"tree",剩下的20种都有可能强制执行一次layout。
+ (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 !"); // 1.WXComponentFactory注册组件的方法
[WXComponentFactory registerComponent:name withClass:clazz withPros:properties];
// 2.遍历出所有异步的方法
NSMutableDictionary *dict = [WXComponentFactory componentMethodMapsWithName:name];
dict[@"type"] = name;
// 3.把组件注册到WXBridgeManager中
if (properties) {
NSMutableDictionary *props = [properties mutableCopy];
if ([dict[@"methods"] count]) {
[props addEntriesFromDictionary:dict];
}
[[WXSDKManager bridgeMgr] registerComponents:@[props]];
} else {
[[WXSDKManager bridgeMgr] registerComponents:@[dict]];
}
}
注册组件全部都是通过WXComponentFactory完成的,WXComponentFactory是一个单例。
@interface WXComponentFactory : NSObject
{
NSMutableDictionary *_componentConfigs;
NSLock *_configLock;
} @end
在WXComponentFactory中,_componentConfigs会存储所有的组件配置,注册的过程也是生成_componentConfigs的过程。
- (void)registerComponent:(NSString *)name withClass:(Class)clazz withPros:(NSDictionary *)pros
{
WXAssert(name && clazz, @"name or clazz must not be nil for registering component."); WXComponentConfig *config = nil;
[_configLock lock];
config = [_componentConfigs objectForKey:name]; // 如果组件已经注册过,会提示重复注册,并且覆盖原先的注册行为
if(config){
WXLogInfo(@"Overrider component name:%@ class:%@, to name:%@ class:%@",
config.name, config.class, name, clazz);
} config = [[WXComponentConfig alloc] initWithName:name class:NSStringFromClass(clazz) pros:pros];
[_componentConfigs setValue:config forKey:name];
// 注册类方法
[config registerMethods]; [_configLock unlock];
}
在WXComponentFactory的_componentConfigs字典中会按照组件的名字作为key,WXComponentConfig作为value存储各个组件的配置。
@interface WXComponentConfig : WXInvocationConfig @property (nonatomic, strong) NSDictionary *properties; @end @interface WXInvocationConfig : NSObject @property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSString *clazz;
/**
* The methods map
**/
@property (nonatomic, strong) NSMutableDictionary *asyncMethods;
@property (nonatomic, strong) NSMutableDictionary *syncMethods; @end
WXComponentConfig继承自WXInvocationConfig,在WXInvocationConfig中存储了组件名name、类名clazz、类里面的同步方法字典syncMethods和异步方法字典asyncMethods。
组件注册比较关键的一点是注册类方法。
- (void)registerMethods
{
Class currentClass = NSClassFromString(_clazz); if (!currentClass) {
WXLogWarning(@"The module class [%@] doesn't exit!", _clazz);
return;
} while (currentClass != [NSObject class]) {
unsigned int methodCount = ;
// 获取类的方法列表
Method *methodList = class_copyMethodList(object_getClass(currentClass), &methodCount);
for (unsigned int i = ; i < methodCount; i++) {
// 获取SEL的字符串名称
NSString *selStr = [NSString stringWithCString:sel_getName(method_getName(methodList[i])) encoding:NSUTF8StringEncoding];
BOOL isSyncMethod = NO;
// 如果是SEL名字带sync,就是同步方法
if ([selStr hasPrefix:@"wx_export_method_sync_"]) {
isSyncMethod = YES;
}
// 如果是SEL名字不带sync,就是异步方法
else if ([selStr hasPrefix:@"wx_export_method_"]) {
isSyncMethod = NO;
} else {
// 如果名字里面不带wx_export_method_前缀的方法,那么都不算是暴露出来的方法,直接continue,进行下一轮的筛选
continue;
} NSString *name = nil, *method = nil;
SEL selector = NSSelectorFromString(selStr);
if ([currentClass respondsToSelector:selector]) {
method = ((NSString* (*)(id, SEL))[currentClass methodForSelector:selector])(currentClass, selector);
} if (method.length <= ) {
WXLogWarning(@"The module class [%@] doesn't has any method!", _clazz);
continue;
}
// 去掉方法名里面带的:号
NSRange range = [method rangeOfString:@":"];
if (range.location != NSNotFound) {
name = [method substringToIndex:range.location];
} else {
name = method;
}
// 最终字典里面会按照异步方法和同步方法保存到最终的方法字典里
NSMutableDictionary *methods = isSyncMethod ? _syncMethods : _asyncMethods;
[methods setObject:method forKey:name];
} free(methodList);
currentClass = class_getSuperclass(currentClass);
} }
上面的代码理解起来比较容易,找到对应的类方法,判断名字里面是否带有“sync”来判断方法是同步还是异步。重点需要解析的是组件的方法是如何转换成类方法暴露出去的。
Weex是通过WX_EXPORT_METHOD宏做到对外暴露类方法的。
#define WX_EXPORT_METHOD(method) WX_EXPORT_METHOD_INTERNAL(method,wx_export_method_) #define WX_EXPORT_METHOD_INTERNAL(method, token) \
+ (NSString *)WX_CONCAT_WRAPPER(token, __LINE__) { \
return NSStringFromSelector(method); \
} #define WX_CONCAT_WRAPPER(a, b) WX_CONCAT(a, b) #define WX_CONCAT(a, b) a ## b
从宏定义可以看到,完全展开之后就是这个样子:
#define WX_EXPORT_METHOD(method) + (NSString *)wx_export_method_ __LINE__ { \
return NSStringFromSelector(method); \
}
举个例子,在WXWebComponent的64行有如下代码:
WX_EXPORT_METHOD(@selector(goBack))
那么这个宏在预编译的时候就会被展开成下面这个样子:
+ (NSString *)wx_export_method_64 {
return NSStringFromSelector(@selector(goBack));
}
如果不通过WX_EXPORT_METHOD宏来申明对外暴露的普通的+号类方法,那么名字里面就不会带wx_export_method_的前缀的方法,那么都不算是暴露出来的方法,上面筛选的代码里面会直接continue,进行下一轮的筛选,所以不必担心那些普通的+号类方法会进来干扰。
这样通过上面的筛选之后,字典里面就会存储如下信息:
methods = {
goBack = goBack;
goForward = goForward;
reload = reload;
}
这就完成了组件注册的第一步,完成了注册配置WXComponentConfig。
组件注册的第二步,是遍历所有的异步方法。
- (NSMutableDictionary *)_componentMethodMapsWithName:(NSString *)name
{
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
NSMutableArray *methods = [NSMutableArray array]; [_configLock lock];
[dict setValue:methods forKey:@"methods"]; WXComponentConfig *config = _componentConfigs[name];
void (^mBlock)(id, id, BOOL *) = ^(id mKey, id mObj, BOOL * mStop) {
[methods addObject:mKey];
};
[config.asyncMethods enumerateKeysAndObjectsUsingBlock:mBlock];
[_configLock unlock]; return dict;
}
上面的方法也是在WXComponentFactory中,该方法遍历出异步方法,并放入字典中,返回异步方法的字典。
以WXWebComponent,这里会返回如下的异步方法字典:
{
methods = (
goForward,
goBack,
reload
);
}
【注意】:大部分 Component 并没有wx_export
前缀的 method,所以这里拿到的方法,很多Component都为空。
注册组件的最后一步:在JSFrame中注册组件。
if (properties) {
NSMutableDictionary *props = [properties mutableCopy];
if ([dict[@"methods"] count]) {
[props addEntriesFromDictionary:dict];
}
[[WXSDKManager bridgeMgr] registerComponents:@[props]];
} else {
[[WXSDKManager bridgeMgr] registerComponents:@[dict]];
}
我们先来看一下WXSDKManager和bridgeMgr对应的WXBridgeManager:
@interface WXSDKManager () @property (nonatomic, strong) WXBridgeManager *bridgeMgr; @property (nonatomic, strong) WXThreadSafeMutableDictionary *instanceDict; @end @interface WXBridgeManager () @property (nonatomic, weak, readonly) WXSDKInstance *topInstance;
@property (nonatomic, strong) WXBridgeContext *bridgeCtx;
@property (nonatomic, assign) BOOL stopRunning;
@property (nonatomic, strong) NSMutableArray *instanceIdStack; @end
WXBridgeManager中会弱引用WXSDKInstance实例,是为了能调用WXSDKInstance的一些属性和方法。WXBridgeManager里面最重要的一个属性就是WXBridgeContext。
@interface WXBridgeContext : NSObject @property (nonatomic, weak, readonly) WXSDKInstance *topInstance;
@property (nonatomic, strong) id<WXBridgeProtocol> jsBridge;
@property (nonatomic, strong) id<WXBridgeProtocol> devToolSocketBridge;
@property (nonatomic, assign) BOOL debugJS;
//store the methods which will be executed from native to js
@property (nonatomic, strong) NSMutableDictionary *sendQueue; // 存储native要即将调用js的一些方法
//the instance stack
@property (nonatomic, strong) WXThreadSafeMutableArray *insStack; // 实例堆栈
//identify if the JSFramework has been loaded
@property (nonatomic) BOOL frameworkLoadFinished; // 标识JSFramework是否已经加载完成
//store some methods temporarily before JSFramework is loaded
@property (nonatomic, strong) NSMutableArray *methodQueue; // 在JSFramework加载完成之前,临时存储一些方法
// store service
@property (nonatomic, strong) NSMutableArray *jsServiceQueue; // 存储js模板的service @end
在WXBridgeContext中强持有了一个jsBridge,这个属性就是用来和js进行交互的Bridge。
现在我们回到注册的最后一步:
[[WXSDKManager bridgeMgr] registerComponents:@[dict]];
WXBridgeManager调用registerComponents方法:
- (void)registerComponents:(NSArray *)components
{
if (!components) return; __weak typeof(self) weakSelf = self;
WXPerformBlockOnBridgeThread(^(){
[weakSelf.bridgeCtx registerComponents:components];
});
}
从上面可以看到,最终是WXBridgeManager里面的WXBridgeContext 调用registerComponents,进行组件的注册。该注册过程是在一个特殊的线程中进行的:
void WXPerformBlockOnBridgeThread(void (^block)(void))
{
[WXBridgeManager _performBlockOnBridgeThread:block];
} + (void)_performBlockOnBridgeThread:(void (^)(void))block
{
if ([NSThread currentThread] == [self jsThread]) {
block();
} else {
[self performSelector:@selector(_performBlockOnBridgeThread:)
onThread:[self jsThread]
withObject:[block copy]
waitUntilDone:NO];
}
}
所有的组件注册都在jsThread这个子线程中执行,这个jsThread也是一个单例,全局唯一。
+ (NSThread *)jsThread
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{ WXBridgeThread = [[NSThread alloc] initWithTarget:[[self class]sharedManager] selector:@selector(_runLoopThread) object:nil];
[WXBridgeThread setName:WX_BRIDGE_THREAD_NAME];
if(WX_SYS_VERSION_GREATER_THAN_OR_EQUAL_TO(@"8.0")) {
[WXBridgeThread setQualityOfService:[[NSThread mainThread] qualityOfService]];
} else {
[WXBridgeThread setThreadPriority:[[NSThread mainThread] threadPriority]];
} [WXBridgeThread start];
}); return WXBridgeThread;
}
jsThread会把@selector(_runLoopThread)作为selector:
- (void)_runLoopThread
{
[[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode]; while (!_stopRunning) {
@autoreleasepool {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
}
}
在这里用[NSMachPort port]的方式开启了一个runloop,之后再也无法获取到这个port了,而且这个runloop不是CFRunloop,所以用官方文档上的那3个方法已经不能停止这个runloop了,只能自己通过while的方式来停止。
回到注册:
- (void)registerComponents:(NSArray *)components
{
WXAssertBridgeThread(); if(!components) return; [self callJSMethod:@"registerComponents" args:@[components]];
}
从上面可以看到,注册实际上就是调用js的方法"registerComponents"。这里需要注意的是:
- (void)callJSMethod:(NSString *)method args:(NSArray *)args
{
if (self.frameworkLoadFinished) {
[self.jsBridge callJSMethod:method args:args];
} else {
[_methodQueue addObject:@{@"method":method, @"args":args}];
}
}
所以在WXBridgeContext中需要一个NSMutableArray,用来缓存在JSFramework加载完成之前,调用JS的方法。这里是保存在_methodQueue里面。如果JSFramework加载完成,那么就会调用callJSMethod:args:方法。
- (JSValue *)callJSMethod:(NSString *)method args:(NSArray *)args
{
WXLogDebug(@"Calling JS... method:%@, args:%@", method, args);
return [[_jsContext globalObject] invokeMethod:method withArguments:args];
}
以WXWebComponent为例,注册组件的method就是@“registerComponents”,args参数就是:
(
{
append = tree;
methods = (
goForward,
goBack,
reload
);
type = web;
}
)
至此,组件注册的全部过程就结束了。 图示如下:
附录:Vue 标签是如何加载以及渲染到视图上的?
从刚才的注册过程中发现,最后一步是通过_jsBridge
调用callJSMethod
这个方法来注册的,而且从WXBridgeContext
中可以看到,这个_jsBridge
就是WXJSCoreBridge
的实例。WXJSCoreBridge
可以认为是 Weex 与 Vue 进行通信的最底层的部分。在调用callJSMethod
方法之前,_jsBridge
向 JavaScriptCore 中注册了很多全局 function,因为jsBridge
是懒加载的,所以这些操作只会执行一次,具体可以看精简后的源码:
- (void)registerGlobalFunctions
{
[_jsBridge registerCallNative:^NSInteger(NSString *instance, NSArray *tasks, NSString *callback) {
//...
}];
[_jsBridge registerCallAddElement:^NSInteger(NSString *instanceId, NSString *parentRef, NSDictionary *elementData, NSInteger index) {
//...
}]; [_jsBridge registerCallCreateBody:^NSInteger(NSString *instanceId, NSDictionary *bodyData) {
//...
}]; [_jsBridge registerCallRemoveElement:^NSInteger(NSString *instanceId, NSString *ref) {
//...
}]; [_jsBridge registerCallMoveElement:^NSInteger(NSString *instanceId,NSString *ref,NSString *parentRef,NSInteger index) {
//...
}]; [_jsBridge registerCallUpdateAttrs:^NSInteger(NSString *instanceId,NSString *ref,NSDictionary *attrsData) {
//...
}]; [_jsBridge registerCallUpdateStyle:^NSInteger(NSString *instanceId,NSString *ref,NSDictionary *stylesData) {
//...
}]; [_jsBridge registerCallAddEvent:^NSInteger(NSString *instanceId,NSString *ref,NSString *event) {
//...
}]; [_jsBridge registerCallRemoveEvent:^NSInteger(NSString *instanceId,NSString *ref,NSString *event) {
//...
}]; [_jsBridge registerCallCreateFinish:^NSInteger(NSString *instanceId) {
//...
}]; [_jsBridge registerCallNativeModule:^NSInvocation*(NSString *instanceId, NSString *moduleName, NSString *methodName, NSArray *arguments, NSDictionary *options) {
//...
}]; [_jsBridge registerCallNativeComponent:^void(NSString *instanceId, NSString *componentRef, NSString *methodName, NSArray *args, NSDictionary *options) {
//...
}];
}
从这些方法名看,大多数都是一些与 Dom 更新相关的方法,我们在WXJSCoreBridge
中更细致的看一下是怎么实现的:
- (void)registerCallAddElement:(WXJSCallAddElement)callAddElement
{
id callAddElementBlock = ^(JSValue *instanceId, JSValue *ref, JSValue *element, JSValue *index, JSValue *ifCallback) { NSString *instanceIdString = [instanceId toString];
NSDictionary *componentData = [element toDictionary];
NSString *parentRef = [ref toString];
NSInteger insertIndex = [[index toNumber] integerValue];
[WXTracingManager startTracingWithInstanceId:instanceIdString ref:componentData[@"ref"] className:nil name:WXTJSCall phase:WXTracingBegin functionName:@"addElement" options:@{@"threadName":WXTJSBridgeThread,@"componentData":componentData}];
WXLogDebug(@"callAddElement...%@, %@, %@, %ld", instanceIdString, parentRef, componentData, (long)insertIndex); return [JSValue valueWithInt32:(int32_t)callAddElement(instanceIdString, parentRef, componentData, insertIndex) inContext:[JSContext currentContext]];
}; _jsContext[@"callAddElement"] = callAddElementBlock;
}
这是一个更新 Dom 添加 UIView 的方法,这里需要把 Native 的方法暴露给 JS 调用。但是有一个问题:
OC 的方法参数格式和 JS 的不一样,不能直接提供给 JS 调用。
所以这里用了两个 Block 嵌套的方式,在 JS 中调用方法时会先 invoke 里层的 callAddElementBlock,这层 Block 将 JS 传进来的参数转换成 OC 的参数格式,再执行 callAddElement 并返回一个 JSValue 给 JS,callAddElement Block中是在WXComponentManager
中完成的关于 Component 的一些操作。
至此,简单来说就是:Weex 的页面渲染是通过先向 JSCore 注入方法,Vue 加载完成就可以调用这些方法并传入相应的参数完成 Component 的渲染和视图的更新。
要注意,每一个 WXSDKInstance
对应一个 Vue 页面,Vue 加载之前就会创建对应的 WXSDKInstance,所有的 Component 都继承自WXComponent
,它们的初始化方法都是:
- (instancetype)initWithRef:(NSString *)ref
type:(NSString*)type
styles:(nullable NSDictionary *)styles
attributes:(nullable NSDictionary *)attributes
events:(nullable NSArray *)events
weexInstance:(WXSDKInstance *)weexInstance;
这个方法会在 JS 调用callCreateBody
时被 invoke(调用)。