WeexSDK之注册Components
先来看一下注册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 = 0; // 获取类的方法列表 Method *methodList = class_copyMethodList(object_getClass(currentClass), &methodCount); for (unsigned int i = 0; 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 <= 0) { 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(调用)。