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

作者 : 开心源码 本文共9136个字,预计阅读时间需要23分钟 发布时间: 2022-05-12 共131人阅读

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

  • iOS-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与之相似,后面详细探讨组件的导出和渲染过程。

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » Weex-iOS源码阅读(一)初始化和函数调使用

发表回复