weex是基于JavaScriptCore實(shí)現(xiàn)的,看代碼之前有必要先了解下JavaScriptCore,相關(guān)內(nèi)容移到:
先說(shuō)說(shuō)我理解的跨平臺(tái)技術(shù)具钥,其實(shí)我們做的移動(dòng)端產(chǎn)品基本都是跨平臺(tái)的森渐,Android俱两、iOS基于相同的協(xié)議數(shù)據(jù)實(shí)現(xiàn)出一樣功能的用戶產(chǎn)品。這個(gè)協(xié)議定義的越通用廷没,熱更新能力就越強(qiáng),比如可以用一個(gè)json表示一個(gè)頁(yè)面的所有元素垂寥,這個(gè)json格式定義的越豐富颠黎,它的動(dòng)態(tài)性就越好,但同時(shí)數(shù)據(jù)結(jié)構(gòu)就會(huì)越復(fù)雜滞项,所以通常我們都會(huì)取一個(gè)折中的方案狭归,避免過(guò)度設(shè)計(jì)。
假如我們定義一套相對(duì)完善的數(shù)據(jù)格式并維護(hù)更新來(lái)滿足大部分業(yè)務(wù)需求文判,也算得上一個(gè)跨平臺(tái)的雛形了过椎。但這樣缺點(diǎn)也很明顯,一是數(shù)據(jù)越來(lái)越復(fù)雜戏仓,維護(hù)成本高疚宇;再者沒(méi)有統(tǒng)一的標(biāo)準(zhǔn),很難推廣和學(xué)習(xí)赏殃。
而JavaScript就是一個(gè)現(xiàn)成的標(biāo)準(zhǔn)敷待,js端有成熟的框架(React.js、vue.js)仁热,原生iOS榜揖、Android上也有很好的支持(JavaScriptCore、google V8)。所以weex所造的輪子就是在原生端實(shí)現(xiàn)virtual dom的解析和渲染举哟,提供可擴(kuò)展的功能和組件庫(kù)思劳,使得同一份js代碼能在三端運(yùn)行:(weex的js框架代碼是內(nèi)置到sdk中的,在初始化的時(shí)候會(huì)加載框架的jsBundle炎滞,業(yè)務(wù)代碼的jsBundle就不包含框架代碼敢艰,這樣可以減少了每個(gè)bundle體積。)
下面從iOS端sdk源碼理解下weex的實(shí)現(xiàn)原理:(代碼版本v0.18.0)
官方文檔 - 集成 Weex 到已有應(yīng)用
這篇官方文檔介紹了weex的使用方法册赛,主要工作就兩個(gè):1.初始化weex環(huán)境 2.渲染weexInstance钠导。
1、初始化weex環(huán)境一般放在app啟動(dòng)時(shí)進(jìn)行:
+ (void)initSDKEnvironment:(NSString *)script
{
// ...
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self registerDefaults];
[[WXSDKManager bridgeMgr] executeJsFramework:script];
});
// ...
}
這個(gè)函數(shù)主要做了兩件事:
- [self registerDefaults] 注冊(cè)一些Components(基礎(chǔ)組件)森瘪、Modules(原生方法api)牡属、Handlers(需要自己實(shí)現(xiàn)的協(xié)議)
+ (void)registerDefaults
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self _registerDefaultComponents];
[self _registerDefaultModules];
[self _registerDefaultHandlers];
});
}
注冊(cè)這些是為了能讓js端來(lái)調(diào)用,這部分在下面會(huì)詳細(xì)討論扼睬。
- 加載框架js代碼逮栅,就是內(nèi)置在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壓縮過(guò)后的文件,原始的js代碼可以在js工程的node_modules/weex-vue-render/dist/index.js(不同版本可能不一樣)窗宇。
2.渲染weexInstance
渲染weexInstance就是我們具體使用的方法了措伐,從官方文檔給的例子來(lái)看:
- (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];
}
主要工作就是最后兩行,即加載業(yè)務(wù)js代碼進(jìn)行渲染军俊。weexInstance提供了一些功能:比如設(shè)置frame大小侥加、設(shè)置viewController(實(shí)現(xiàn)導(dǎo)航跳轉(zhuǎn)),以及渲染階段的幾個(gè)回調(diào)函數(shù) ^ onCreate粪躬、^ renderFinish等担败。
其核心邏輯應(yīng)該在renderWithURL中,跟代碼可以看到镰官,首先會(huì)請(qǐng)求url獲取jsBundleString提前,然后解析bundleString渲染界面,在周期各節(jié)點(diǎn)執(zhí)行回調(diào)泳唠,大概如下圖:左上部分是框架與原生端的交互部分狈网,右下是框架與js端的交互部分。
這篇就先探討一下weex是如何實(shí)現(xiàn)js和native之間的函數(shù)調(diào)用的警检。
之前提到在sdk初始化時(shí)需要注冊(cè)組件和模塊供js端使用孙援,為什么注冊(cè)之后js端就可以調(diào)用了呢,注冊(cè)的過(guò)程都做了些什么扇雕?
前一篇學(xué)習(xí)javaScriptCore時(shí)知道我們可以將oc的block注入到j(luò)s環(huán)境中拓售,實(shí)現(xiàn)js調(diào)用Native。weex的實(shí)現(xiàn)也類似镶奉,只不過(guò)不能用一個(gè)函數(shù)就往全局對(duì)象上加一個(gè)函數(shù)础淤。拿module來(lái)說(shuō)崭放,我們?cè)趍odule中通過(guò)WX_EXPORT_METHOD就可以將一個(gè)oc方法導(dǎo)出供js端調(diào)用。這個(gè)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
所以當(dāng)使用WX_EXPORT_METHOD導(dǎo)出一個(gè)方法時(shí)鸽凶,實(shí)際上就是聲明了一個(gè)類方法:
WX_EXPORT_METHOD(@selector(openUrl:))
//相當(dāng)于定義了如下類方法 :(32是所在行數(shù)币砂,所以兩個(gè)WX_EXPORT_METHOD不能寫在同一行)
+ (NSString *)wx_export_method_32 {
return NSStringFromSelector(@selector(openUrl:));
}
另一個(gè)宏WX_EXPORT_METHOD_SYNC與它類似,只不過(guò)前綴是wx_export_method_sync_玻侥。
定義了這樣的類方法有什么用呢决摧,就要看下注冊(cè)的時(shí)候做的工作,在sdk初始化的時(shí)候會(huì)注冊(cè)一些基礎(chǔ)模塊凑兰,我們自己寫的橋也需要在合適的時(shí)機(jī)注冊(cè)進(jìn)去掌桩,注冊(cè)一個(gè)模塊的代碼如下:
+ (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];
}
這里涉及一個(gè)WXModuleFactory類姑食,負(fù)責(zé)創(chuàng)建module的相關(guān)工作波岛。這里分別生成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中存了一個(gè)moduleMap音半,當(dāng)注冊(cè)一個(gè)module時(shí)则拷,實(shí)際上就是創(chuàng)建了一個(gè)WXModuleConfig對(duì)象并保存在moduleMap中,WXModuleConfig里保存了之前通過(guò)WX_EXPORT_METHOD宏導(dǎo)出的所有方法:
- (void)registerMethods
{
Class currentClass = NSClassFromString(_clazz);
if (!currentClass) {
WXLogWarning(@"The module class [%@] doesn't exit曹鸠!", _clazz);
return;
}
// 按繼承關(guān)系遍歷
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導(dǎo)出的方法
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);
}
}
這里通過(guò)class_copyMethodList獲取module所有類方法,取到前綴是wx_export_method_sync_和wx_export_method_的方法分別保存在_syncMethods和_asyncMethods兩個(gè)字典中彻桃。所有注冊(cè)的module就形成了一份native的“方法表”宣旱。
- js方法表
注冊(cè)完成后通過(guò)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;
}
得到類似如下格式的一個(gè)字典:
{
storage : [
"length",
"getItem",
"setItem",
"setItemPersistent",
"getAllKeys",
"removeItem"
]
}
表示在“storage”這個(gè)module中提供了這些函數(shù)可以調(diào)用。
將這個(gè)信息告訴js端:
// WXBridgeContext.m
- (void)registerModules:(NSDictionary *)modules
{
WXAssertBridgeThread();
if(!modules) return;
[self callJSMethod:@"registerModules" args:@[modules]];
}
而js端的調(diào)用統(tǒng)一交給一個(gè)全局函數(shù)callNativeModule來(lái)處理叛薯,就是上一篇javaScriptCore注入block到j(luò)s中的方式:
// 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端通過(guò)callNativeModule 傳過(guò)來(lái)instanceId、moduleName笙纤、args參數(shù)列表耗溜,native通過(guò)之前存好的方法配置表找到相應(yīng)的selector,用NSInvocation傳入?yún)?shù)數(shù)組執(zhí)行方法省容。
整理了一張關(guān)系圖:(實(shí)線持有關(guān)系 虛線調(diào)用關(guān)系)總的來(lái)說(shuō)抖拴,將一個(gè)原生方法暴露給js調(diào)用需要:
- 通過(guò)WX_EXPORT_METHOD或WX_EXPORT_METHOD_SYNC宏將方法selector導(dǎo)出(實(shí)際上是定義了帶weex前綴的類方法返回實(shí)際的selector)
- 注冊(cè)module時(shí)遍歷module所有類方法,找出帶weex前綴的類方法將它們存在WXModuleConfig中腥椒,將所有注冊(cè)的module的方法表保存在WXModuleFactory的moduleMap中
- 將所有的module和對(duì)應(yīng)的所有方法名傳入WXBridgeContext阿宅,通過(guò)jsContext調(diào)用js端的registerModules方法進(jìn)行注冊(cè)
- 初次使用bridge時(shí)會(huì)向jsContext注入callNativeModule函數(shù),js端通過(guò)callNativeModule傳遞需要調(diào)用的函數(shù)名和參數(shù)列表笼蛛,native端在moduleMap中找到對(duì)應(yīng)module的對(duì)應(yīng)selector洒放,通過(guò)NSInvocation傳入?yún)?shù)執(zhí)行調(diào)用。
以上以module為例學(xué)習(xí)了weex導(dǎo)出原生方法和js端調(diào)用的過(guò)程滨砍。component和handler與之類似往湿,后面詳細(xì)討論組件的導(dǎo)出和渲染過(guò)程妖异。