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