zoukankan      html  css  js  c++  java
  • 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(调用)。

  • 相关阅读:
    es6 常用方法
    vue HTTP 请求(vue-resource)
    vue 常用语法糖
    js中slice,SubString和SubStr的区别
    浅谈JavaScript中forEach与each
    vue 新版本 webpack 代理 跨域设置
    js 动态添加class封装(es6语法)
    jsonp promise 封装
    location.origin兼容IE
    给zTree的treeNode添加class
  • 原文地址:https://www.cnblogs.com/LeeGof/p/9016863.html
Copyright © 2011-2022 走看看