Weex-iOS源碼閱讀(一)初始化和函數(shù)調(diào)用

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ò)程妖异。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市领追,隨后出現(xiàn)的幾起案子他膳,更是在濱河造成了極大的恐慌,老刑警劉巖绒窑,帶你破解...
    沈念sama閱讀 216,402評(píng)論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件棕孙,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡些膨,警方通過(guò)查閱死者的電腦和手機(jī)蟀俊,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)傀蓉,“玉大人欧漱,你說(shuō)我怎么就攤上這事≡崃牵” “怎么了误甚?”我有些...
    開封第一講書人閱讀 162,483評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)谱净。 經(jīng)常有香客問(wèn)我窑邦,道長(zhǎng),這世上最難降的妖魔是什么壕探? 我笑而不...
    開封第一講書人閱讀 58,165評(píng)論 1 292
  • 正文 為了忘掉前任冈钦,我火速辦了婚禮,結(jié)果婚禮上李请,老公的妹妹穿的比我還像新娘瞧筛。我一直安慰自己,他們只是感情好导盅,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,176評(píng)論 6 388
  • 文/花漫 我一把揭開白布较幌。 她就那樣靜靜地躺著,像睡著了一般白翻。 火紅的嫁衣襯著肌膚如雪乍炉。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,146評(píng)論 1 297
  • 那天滤馍,我揣著相機(jī)與錄音岛琼,去河邊找鬼。 笑死巢株,一個(gè)胖子當(dāng)著我的面吹牛槐瑞,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播阁苞,決...
    沈念sama閱讀 40,032評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼随珠,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼灭袁!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起窗看,我...
    開封第一講書人閱讀 38,896評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤茸歧,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后显沈,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體软瞎,經(jīng)...
    沈念sama閱讀 45,311評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,536評(píng)論 2 332
  • 正文 我和宋清朗相戀三年拉讯,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了涤浇。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,696評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡魔慷,死狀恐怖只锭,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情院尔,我是刑警寧澤蜻展,帶...
    沈念sama閱讀 35,413評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站邀摆,受9級(jí)特大地震影響纵顾,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜栋盹,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,008評(píng)論 3 325
  • 文/蒙蒙 一施逾、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧例获,春花似錦汉额、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至件余,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間遭居,已是汗流浹背啼器。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留俱萍,地道東北人端壳。 一個(gè)月前我還...
    沈念sama閱讀 47,698評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像枪蘑,于是被迫代替她去往敵國(guó)和親损谦。 傳聞我的和親對(duì)象是個(gè)殘疾皇子岖免,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,592評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容