Weex-iOS源码阅读(一)初始化和函数调用

weex是基于JavaScriptCore实现的,看代码之前有必要先了解下JavaScriptCore,相关内容移到:


先说说我理解的跨平台技术,其实我们做的移动端产品基本都是跨平台的,Android、iOS基于相同的协议数据实现出一样功能的用户产品。这个协议定义的越通用,热更新能力就越强,比如可以用一个json表示一个页面的所有元素,这个json格式定义的越丰富,它的动态性就越好,但同时数据结构就会越复杂,所以通常我们都会取一个折中的方案,避免过度设计。

假如我们定义一套相对完善的数据格式并维护更新来满足大部分业务需求,也算得上一个跨平台的雏形了。但这样缺点也很明显,一是数据越来越复杂,维护成本高;再者没有统一的标准,很难推广和学习。

而JavaScript就是一个现成的标准,js端有成熟的框架(React.js、vue.js),原生iOS、Android上也有很好的支持(JavaScriptCore、google V8)。所以weex所造的轮子就是在原生端实现virtual dom的解析和渲染,提供可扩展的功能和组件库,使得同一份js代码能在三端运行:

(weex的js框架代码是内置到sdk中的,在初始化的时候会加载框架的jsBundle,业务代码的jsBundle就不包含框架代码,这样可以减少了每个bundle体积。)


下面从iOS端sdk源码理解下weex的实现原理:(代码版本v0.18.0)
官方文档 - 集成 Weex 到已有应用

这篇官方文档介绍了weex的使用方法,主要工作就两个:1.初始化weex环境 2.渲染weexInstance。
1、初始化weex环境一般放在app启动时进行:

+ (void)initSDKEnvironment:(NSString *)script
{
    // ...
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self registerDefaults];    
        [[WXSDKManager bridgeMgr] executeJsFramework:script];
    });
    // ...
}

这个函数主要做了两件事:

  • [self registerDefaults] 注册一些Components(基础组件)、Modules(原生方法api)、Handlers(需要自己实现的协议)
+ (void)registerDefaults
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self _registerDefaultComponents];
        [self _registerDefaultModules];
        [self _registerDefaultHandlers];
    });
}

注册这些是为了能让js端来调用,这部分在下面会详细讨论。

  • 加载框架js代码,就是内置在sdk里面的native-bundle-main.js:
+ (void)initSDKEnvironment
{
    
    NSString *filePath = [[NSBundle bundleForClass:self] pathForResource:@"native-bundle-main" ofType:@"js"];
    NSString *script = [NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:nil];
    [WXSDKEngine initSDKEnvironment:script];
    // ...
}

这里是webpack压缩过后的文件,原始的js代码可以在js工程的node_modules/weex-vue-render/dist/index.js(不同版本可能不一样)。

2.渲染weexInstance
渲染weexInstance就是我们具体使用的方法了,从官方文档给的例子来看:

- (void)viewDidLoad
{
    // ...
    _instance = [[WXSDKInstance alloc] init];
    _instance.viewController = self;
    _instance.frame = self.view.frame;

    __weak typeof(self) weakSelf = self;
    _instance.onCreate = ^(UIView *view) {
        [weakSelf.weexView removeFromSuperview];
        weakSelf.weexView = view;
        [weakSelf.view addSubview:weakSelf.weexView];
    };

    _instance.onFailed = ^(NSError *error) {
        //process failure
    };

    _instance.renderFinish = ^ (UIView *view) {
        //process renderFinish
    };
    NSURL *url = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"js"];
    [_instance renderWithURL:url options:@{@"bundleUrl":[self.url absoluteString]} data:nil];
}

主要工作就是最后两行,即加载业务js代码进行渲染。weexInstance提供了一些功能:比如设置frame大小、设置viewController(实现导航跳转),以及渲染阶段的几个回调函数 ^ onCreate、^ renderFinish等。

其核心逻辑应该在renderWithURL中,跟代码可以看到,首先会请求url获取jsBundleString,然后解析bundleString渲染界面,在周期各节点执行回调,大概如下图:

左上部分是框架与原生端的交互部分,右下是框架与js端的交互部分。

这篇就先探讨一下weex是如何实现js和native之间的函数调用的。


之前提到在sdk初始化时需要注册组件和模块供js端使用,为什么注册之后js端就可以调用了呢,注册的过程都做了些什么?

前一篇学习javaScriptCore时知道我们可以将oc的block注入到js环境中,实现js调用Native。weex的实现也类似,只不过不能用一个函数就往全局对象上加一个函数。拿module来说,我们在module中通过WX_EXPORT_METHOD就可以将一个oc方法导出供js端调用。这个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

所以当使用WX_EXPORT_METHOD导出一个方法时,实际上就是声明了一个类方法:

WX_EXPORT_METHOD(@selector(openUrl:))
//相当于定义了如下类方法 :(32是所在行数,所以两个WX_EXPORT_METHOD不能写在同一行)
+ (NSString *)wx_export_method_32 {
    return NSStringFromSelector(@selector(openUrl:));
}

另一个宏WX_EXPORT_METHOD_SYNC与它类似,只不过前缀是wx_export_method_sync_。

定义了这样的类方法有什么用呢,就要看下注册的时候做的工作,在sdk初始化的时候会注册一些基础模块,我们自己写的桥也需要在合适的时机注册进去,注册一个模块的代码如下:

+ (void)registerModule:(NSString *)name withClass:(Class)clazz
{
    WXAssert(name && clazz, @"Fail to register the module, please check if the parameters are correct !");
    if (!clazz || !name) {
        return;
    }
    NSString *moduleName = [WXModuleFactory registerModule:name withClass:clazz];
    NSDictionary *dict = [WXModuleFactory moduleMethodMapsWithName:moduleName];
    
    [[WXSDKManager bridgeMgr] registerModules:dict];
}

这里涉及一个WXModuleFactory类,负责创建module的相关工作。这里分别生成native和js两份方法表:

  • native方法配置表
// WXModuleFactory.m
- (NSString *)_registerModule:(NSString *)name withClass:(Class)clazz
{
    WXAssert(name && clazz, @"Fail to register the module, please check if the parameters are correct !");
    
    [_moduleLock lock];
    //allow to register module with the same name;
    WXModuleConfig *config = [[WXModuleConfig alloc] init];
    config.name = name;
    config.clazz = NSStringFromClass(clazz);
    [config registerMethods];
    [_moduleMap setValue:config forKey:name];
    [_moduleLock unlock];
    
    return name;
}

在WXModuleFactory中存了一个moduleMap,当注册一个module时,实际上就是创建了一个WXModuleConfig对象并保存在moduleMap中,WXModuleConfig里保存了之前通过WX_EXPORT_METHOD宏导出的所有方法:

- (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++) {
            NSString *selStr = [NSString stringWithCString:sel_getName(method_getName(methodList[i])) encoding:NSUTF8StringEncoding];
            BOOL isSyncMethod = NO;
            // 只取WX_EXPORT_METHOD和WX_EXPORT_METHOD_SYNC导出的方法
            if ([selStr hasPrefix:@"wx_export_method_sync_"]) {
                isSyncMethod = YES;
            } else if ([selStr hasPrefix:@"wx_export_method_"]) {
                isSyncMethod = NO;
            } else {
                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);
    }
    
}

这里通过class_copyMethodList获取module所有类方法,取到前缀是wx_export_method_sync_和wx_export_method_的方法分别保存在_syncMethods和_asyncMethods两个字典中。所有注册的module就形成了一份native的“方法表”。

  • js方法表
    注册完成后通过moduleMethodMapsWithName方法获取一份模块的所有方法名:
// WXModuleFactory.m
- (NSMutableDictionary *)_moduleMethodMapsWithName:(NSString *)name
{
    NSMutableDictionary *dict = [NSMutableDictionary dictionary];
    NSMutableArray *methods = [self _defaultModuleMethod];
    
    [_moduleLock lock];
    [dict setValue:methods forKey:name];
    
    WXModuleConfig *config = _moduleMap[name];
    void (^mBlock)(id, id, BOOL *) = ^(id mKey, id mObj, BOOL * mStop) {
        [methods addObject:mKey];
    };
    [config.syncMethods enumerateKeysAndObjectsUsingBlock:mBlock];
    [config.asyncMethods enumerateKeysAndObjectsUsingBlock:mBlock];
    [_moduleLock unlock];
    
    return dict;
}

得到类似如下格式的一个字典:

{
    storage : [
        "length",
        "getItem",
        "setItem",
        "setItemPersistent",
        "getAllKeys",
        "removeItem"
    ]
}

表示在“storage”这个module中提供了这些函数可以调用。
将这个信息告诉js端:

// WXBridgeContext.m
- (void)registerModules:(NSDictionary *)modules
{
    WXAssertBridgeThread();
    
    if(!modules) return;
    
    [self callJSMethod:@"registerModules" args:@[modules]];
}

而js端的调用统一交给一个全局函数callNativeModule来处理,就是上一篇javaScriptCore注入block到js中的方式:

// WXBridgeContext.m
- (void)registerGlobalFunctions
{
    __weak typeof(self) weakSelf = self;
    // ...

    [_jsBridge registerCallNativeModule:^NSInvocation*(NSString *instanceId, NSString *moduleName, NSString *methodName, NSArray *arguments, NSDictionary *options) {
        
        WXSDKInstance *instance = [WXSDKManager instanceForID:instanceId];
        
        if (!instance) {
            WXLogInfo(@"instance not found for callNativeModule:%@.%@, maybe already destroyed", moduleName, methodName);
            return nil;
        }
        
        WXModuleMethod *method = [[WXModuleMethod alloc] initWithModuleName:moduleName methodName:methodName arguments:arguments options:options instance:instance];
        if(![moduleName isEqualToString:@"dom"] && instance.needPrerender){
            [WXPrerenderManager storePrerenderModuleTasks:method forUrl:instance.scriptURL.absoluteString];
            return nil;
        }
        return [method invoke];
    }];

    // ...
}
// WXJSCoreBridge.m
- (void)registerCallNativeModule:(WXJSCallNativeModule)callNativeModuleBlock
{
    _jsContext[@"callNativeModule"] = ^JSValue *(JSValue *instanceId, JSValue *moduleName, JSValue *methodName, JSValue *args, JSValue *options) {
        NSString *instanceIdString = [instanceId toString];
        NSString *moduleNameString = [moduleName toString];
        NSString *methodNameString = [methodName toString];
        NSArray *argsArray = [args toArray];
        NSDictionary *optionsDic = [options toDictionary];
        
        WXLogDebug(@"callNativeModule...%@,%@,%@,%@", instanceIdString, moduleNameString, methodNameString, argsArray);
        
        NSInvocation *invocation = callNativeModuleBlock(instanceIdString, moduleNameString, methodNameString, argsArray, optionsDic);
        JSValue *returnValue = [JSValue wx_valueWithReturnValueFromInvocation:invocation inContext:[JSContext currentContext]];
        [WXTracingManager startTracingWithInstanceId:instanceIdString ref:nil className:nil name:moduleNameString phase:WXTracingInstant functionName:methodNameString options:nil];
        return returnValue;
    };
}

js端通过callNativeModule 传过来instanceId、moduleName、args参数列表,native通过之前存好的方法配置表找到相应的selector,用NSInvocation传入参数数组执行方法。

整理了一张关系图:(实线持有关系 虚线调用关系)

总的来说,将一个原生方法暴露给js调用需要:

  • 通过WX_EXPORT_METHOD或WX_EXPORT_METHOD_SYNC宏将方法selector导出(实际上是定义了带weex前缀的类方法返回实际的selector)
  • 注册module时遍历module所有类方法,找出带weex前缀的类方法将它们存在WXModuleConfig中,将所有注册的module的方法表保存在WXModuleFactory的moduleMap中
  • 将所有的module和对应的所有方法名传入WXBridgeContext,通过jsContext调用js端的registerModules方法进行注册
  • 初次使用bridge时会向jsContext注入callNativeModule函数,js端通过callNativeModule传递需要调用的函数名和参数列表,native端在moduleMap中找到对应module的对应selector,通过NSInvocation传入参数执行调用。

以上以module为例学习了weex导出原生方法和js端调用的过程。component和handler与之类似,后面详细讨论组件的导出和渲染过程。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容