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));
}
在WXWebComponent的类方法里面就多了一个wx_export_method_64的方法。由于在同一个文件里面,WX_EXPORT_METHOD宏是不允许写在同一行的,所以转换出来的方法名字肯定不会相同。但是不同类里面行数就没有规定,行数是可能相同的,从而不同类里面可能就有相同的方法名。但是完全不会有什么影响,因为获取类方法的时候是通过class_copyMethodList,保证这个list里面都是唯一的名字即可。
这里还有一点需要说明,用class_copyMethodList会获取所有的类方法(+号方法),包括不通过WX_EXPORT_METHOD宏对外暴露的普通的+号方法。但是这里有一个判断条件,会避开这些不通过WX_EXPORT_METHOD宏对外暴露的普通的+号类方法。

如果不通过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"。这里需要注意的是:

由于是在子线程上注册组件,那么JSFramework如果没有加载完成,native去调用js的方法,必定调用失败。所以需要在JSFramework加载完成之前,把native调用JS的方法都缓存起来,一旦JSFramework加载完成,把缓存里面的方法都丢给JSFramework去加载。
- (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(调用)。

 

posted @ 2018-05-10 16:17  LeeGof  阅读(...)  评论(...编辑  收藏