React Native 組件初始化與通信分析(一)

該系列文章希望從非 UI 模塊以及 UI 模塊的初始化與通信來剖析 React Native 一些實現(xiàn)原理。這篇文章是該系列的第一篇凿宾,主要分析了非 UI 模塊從 APP 啟動矾屯、Native 端的模塊定義到 JavaScript 端的模塊生成過程(基于 React Native @0.29.0 iOS 實現(xiàn))。

時序圖總覽

先來看下整體的時序圖初厚,后面會依次介紹件蚕。

image
image

[Native] iOS main 函數(shù)執(zhí)行

首先,iOS APP 啟動后先執(zhí)行的是 main 函數(shù)产禾,此處不多說排作。

[Native] 注冊 Native Module(在各個 Native Module 的定義中)

在 Native 端實現(xiàn)模塊時都需要調用 RCT_EXPORT_MODULE 來聲明該模塊需要暴露給 JavaScript。除此之外還有一批宏定義用來聲明其他的信息:

  • RCT_EXPORT_MODULE:聲明模塊
  • RCT_EXPORT_METHOD:聲明方法
  • RCT_EXPORT_VIEW_PROPERTY:聲明屬性

通過這種方式完成模塊定義后亚情,就可以非常方便的獲取到所有需要對外暴露的模塊以及模塊需要對外暴露的方法與屬性妄痪。

以下是核心的宏定義:

#define RCT_EXPORT_MODULE(js_name) \
RCT_EXTERN void RCTRegisterModule(Class); \
+ (NSString *)moduleName { return @#js_name; } \
+ (void)load { RCTRegisterModule(self); }

#define RCT_EXPORT_VIEW_PROPERTY(name, type) \
+ (NSArray<NSString *> *)propConfig_##name { return @[@#type]; }

...

[Native] 收集各個模塊的 config

通過上一步完成所有模塊的定義后,RCTBatchedBridge 中會把所有模塊信息集中收集起來楞件,并且按照固定的格式重新組織衫生,最后會把這些模塊信息序列化后注入到 JavaScript 中。

核心代碼如下:

// RCTBatchedBridge.m
- (NSString *)moduleConfig
{
  NSMutableArray<NSArray *> *config = [NSMutableArray new];
  for (RCTModuleData *moduleData in _moduleDataByID) {
    if (self.executorClass == [RCTJSCExecutor class]) {
      [config addObject:@[moduleData.name]];
    } else {
      [config addObject:RCTNullIfNil(moduleData.config)];
    }
  }

  return RCTJSONStringify(@{
    @"remoteModuleConfig": config,
  }, NULL);
}

@RCTModuleData
- (NSArray *)config
{
  [self gatherConstants];
  __block NSDictionary<NSString *, id> *constants = _constantsToExport;
  _constantsToExport = nil; // Not needed anymore

  if (constants.count == 0 && self.methods.count == 0) {
    return (id)kCFNull; // Nothing to export
  }

  RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, [NSString stringWithFormat:@"[RCTModuleData config] %@", _moduleClass], nil);

  NSMutableArray<NSString *> *methods = self.methods.count ? [NSMutableArray new] : nil;
  NSMutableArray<NSNumber *> *asyncMethods = nil;
  for (id<RCTBridgeMethod> method in self.methods) {
    if (method.functionType == RCTFunctionTypePromise) {
      if (!asyncMethods) {
        asyncMethods = [NSMutableArray new];
      }
      [asyncMethods addObject:@(methods.count)];
    }
    [methods addObject:method.JSMethodName];
  }

  NSMutableArray *config = [NSMutableArray new];
  [config addObject:self.name];
  if (constants.count) {
    [config addObject:constants];
  }
  if (methods) {
    [config addObject:methods];
    if (asyncMethods) {
      [config addObject:asyncMethods];
    }
  }
  RCT_PROFILE_END_EVENT(RCTProfileTagAlways, [NSString stringWithFormat:@"[RCTModuleData config] %@", _moduleClass], nil);
  return config;
}

[Native] 初始化 Bridge(在 ViewController 中)

self.rctRootView = 
    [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
                                moduleName:@"TiebaNext"
                         initialProperties:nil
                             launchOptions:nil];

[Native] 將模塊信息注入到 JSCExecutor 中的全局變量

// RCTBatchedBridge.m
- (void)injectJSONConfiguration:(NSString *)configJSON
                     onComplete:(void (^)(NSError *))onComplete
{
  if (!_valid) {
    return;
  }

  [_javaScriptExecutor injectJSONText:configJSON
                  asGlobalObjectNamed:@"__fbBatchedBridgeConfig"
                             callback:onComplete];
}

__fbBatchedBridgeConfig 的結構如下

{
    "remoteModuleConfig": [
        [
            "模塊名稱",
            {
                屬性對象(鍵值對土浸,值類型不限)
            },
            [
                方法列表
            ],
            [
                Promise 方法 index(方法列表中哪些方法是異步的)
            ]
        ],

  1. 每個模塊的 index 就是這個模塊的 moduleID
  2. 每個方法的 index 就是這個方法的 methodID
  3. 最后輸出到 remoteModuleConfig 中的模塊并不是所有的 Module罪针,有的會被過濾掉,所以輸出里有的是 null

以上結構的生成在 RCTModuleData config 方法中:

// RCTModuleData.m
- (NSArray *)config
{
  //......
  NSMutableArray *config = [NSMutableArray new];
  [config addObject:self.name]; // 模塊名稱(字符串)
  if (constants.count) {
    [config addObject:constants]; // 屬性對象(對象)
  }
  if (methods) {
    [config addObject:methods]; // 方法列表(數(shù)組)
    if (asyncMethods) {
      [config addObject:asyncMethods]; // Promise 方法列表(數(shù)組)
    }
  }
  RCT_PROFILE_END_EVENT(RCTProfileTagAlways, [NSString stringWithFormat:@"[RCTModuleData config] %@", _moduleClass], nil);
  return config;
}

舉個例子

{
    "remoteModuleConfig": [
        null,
        [
            "RCTAccessibilityManager",
            [
                "setAccessibilityContentSizeMultipliers",
                "getMultiplier",
                "getCurrentVoiceOverState"
            ]
        ],
        [
            "RCTViewManager",
            {
                "forceTouchAvailable": false
            }
        ],
        ...
        [
            "RCTClipboard",
            [
                "setString",
                "getString"
            ],
            [
                1
            ]
        ],
        ...
        [
            "RCTPushNotificationManager",
            {
                "initialNotification": null
            },
            [
                "setApplicationIconBadgeNumber",
                "getApplicationIconBadgeNumber",
                "requestPermissions",
                "abandonPermissions",
                "checkPermissions",
                "presentLocalNotification",
                "scheduleLocalNotification",
                "cancelAllLocalNotifications",
                "cancelLocalNotifications",
                "getInitialNotification",
                "getScheduledLocalNotifications",
                "addListener",
                "removeListeners"
            ],
            [
                2,
                9
            ]
        ]

[JS] 根據(jù)上述模塊配置信息生成 JS 模塊

//BatchedBridge.js
const BatchedBridge = new MessageQueue(
  () => global.__fbBatchedBridgeConfig
);

  1. BatchedBridge 是 MessageQueue 的一個實例
  2. 第一次訪問 BatchedBridge.RemoteModules 時 MessageQueue 會利用 global.__fbBatchedBridgeConfig 模塊配置信息來生成 JS 模塊

[JS] JS 模塊的生成

首先栅迄,JS 模塊的結構

{
    name: "模塊名稱",
    {
        方法名:方法實現(xiàn)(區(qū)分是否 Promise(Sync)) 類型
        屬性名:
        moduleID: 模塊Index
    }
}

具體包括幾塊:

  • 模塊名
  • 模塊屬性
  • moduleID
  • 模塊方法

第一步 遍歷所有 remoteModules站故,即 global.__fbBatchedBridgeConfig 列表:

// MessageQueue.js
_genModules(remoteModules) {
  const modules = {};

  remoteModules.forEach((config, moduleID) => {
    const info = this._genModule(config, moduleID);
    if (info) {
      modules[info.name] = info.module;
    }
  });

  return modules;
}

第二步 解析出每個 Module 的信息皆怕,包括模塊名毅舆、屬性對象西篓、方法列表、異步(Promise)方法列表憋活、syncHooks(這個和 native 結構對應不上岂津,有可能棄用了)

// MessageQueue.js
let moduleName, constants, methods, asyncMethods, syncHooks;
if (moduleHasConstants(config)) {
  [moduleName, constants, methods, asyncMethods, syncHooks] = config;
} else {
  [moduleName, methods, asyncMethods, syncHooks] = config;
}

第三步 根據(jù)方法列表、屬性對象生成 module 對象

// MessageQueue.js
// 初始化新 module
const module = {};
methods && methods.forEach((methodName, methodID) => {
  const isAsync = asyncMethods && arrayContains(asyncMethods, methodID);
  const isSyncHook = syncHooks && arrayContains(syncHooks, methodID);
  invariant(!isAsync || !isSyncHook, 'Cannot have a method that is both async and a sync hook');
  // 方法類型悦即,Async 方法調用會返回 Promise 對象
  const methodType = isAsync ? MethodTypes.remoteAsync :
      isSyncHook ? MethodTypes.syncHook :
      MethodTypes.remote;
  // 生成方法
  module[methodName] = this._genMethod(moduleID, methodID, methodType);
});
// 將屬性淺拷貝到 module 上
Object.assign(module, constants);

關于方法的生成:

  1. 實際上都是將方法的實際操作代理給了 __nativeCall(moduleID, methodID, args, onFail, onSucc)
  2. 對于 async 方法會用 Promise 對象包裝一下吮成,然后 onFail 的時候 reject,以及 onSucc 的時候 resolve(但是看代碼里onFail 和 onSucc 的位置不一致辜梳,是否有問題需要進一步求證)
  3. 關于 __nativeCall 的邏輯后續(xù)寫一篇JS代碼執(zhí)行后的分析來剖析
// MessageQueue.js
_genMethod(module, method, type) {
    let fn = null;
    const self = this;
    if (type === MethodTypes.remoteAsync) {
      fn = function(...args) {
        return new Promise((resolve, reject) => {
          self.__nativeCall(
            module,
            method,
            args,
            (data) => {
              resolve(data);
            },
            (errorData) => {
              var error = createErrorFromErrorData(errorData);
              reject(error);
            });
        });
      };
    } else if (type === MethodTypes.syncHook) {
      return function(...args) {
        return global.nativeCallSyncHook(module, method, args);
      };
    } else {
      fn = function(...args) {
        const lastArg = args.length > 0 ? args[args.length - 1] : null;
        const secondLastArg = args.length > 1 ? args[args.length - 2] : null;
        const hasSuccCB = typeof lastArg === 'function';
        const hasErrorCB = typeof secondLastArg === 'function';
        hasErrorCB && invariant(
          hasSuccCB,
          'Cannot have a non-function arg after a function arg.'
        );
        const numCBs = hasSuccCB + hasErrorCB;
        const onSucc = hasSuccCB ? lastArg : null;
        const onFail = hasErrorCB ? secondLastArg : null;
        args = args.slice(0, args.length - numCBs);
        return self.__nativeCall(module, method, args, onFail, onSucc);
      };
    }
    fn.type = type;
    return fn;
  }

[JS] NativeModules 生成

經(jīng)過前面的步驟粱甫,native 注入到 JSCExecutor 中的 global.__fbBatchedBridgeConfig 在 BatchedBridge.RemoteModules 屬性首次被訪問的時候被解析成完整的 module,下一步則是將這些 module 放入 NativeModules 對象并供外部訪問作瞄。
另外茶宵,這里有個一個小處理,native 注入的模塊通常以 RCT 或者 RK 為前綴宗挥,此處會將前綴干掉(以下代碼未包含)乌庶。

//NativeModules.js
const BatchedBridge = require('BatchedBridge');
const RemoteModules = BatchedBridge.RemoteModules;
//代碼省略...
/**
 * 這里也是一個 lazy getters
 * Define lazy getters for each module.
 * These will return the module if already loaded, or load it if not.
 */
const NativeModules = {};
Object.keys(RemoteModules).forEach((moduleName) => {
  Object.defineProperty(NativeModules, moduleName, {
    configurable: true,
    enumerable: true,
    get: () => {
      let module = RemoteModules[moduleName];
      //代碼省略...
      Object.defineProperty(NativeModules, moduleName, {
        configurable: true,
        enumerable: true,
        value: module,
      });
      return module;
    },
  });
});

[JS] UIManager 的再加工

native 生成 __fbBatchedBridgeConfig 配置信息的時候,view 模塊和非 view 模塊(區(qū)別是 view 模塊都是由 RCTViewManager 來管理)是分別對待的契耿,所有的 view 模塊信息都放在了 "RCTUIManager" 的屬性對象中瞒大,參照 __fbBatchedBridgeConfig的結構大概是下面這樣的。

通過 NativeModules 的生成過程完成了 remoteModuleConfig 中外層模塊的處理(包括 NativeModules.UIManager 也在這一過程中生成了初始版本)搪桂,那 UIManager 的再加工過程則是解析和處理 RCTUIManager 內各種 view 的過程透敌。

{
    "remoteModuleConfig": [
        [
            // 模塊名稱
            "RCTUIManager",
            // 屬性對象
            {
                // 此處省略
                "RCTRawText": {
                    "Manager": "RCTRawTextManager",
                    "NativeProps": {
                        "text": "NSString"
                    }
                },
                "RCTSwitch": {
                    "Manager": "RCTSwitchManager",
                    "NativeProps": {
                        "thumbTintColor": "UIColor",
                        "tintColor": "UIColor",
                        "value": "BOOL",
                        "onChange": "BOOL",
                        "onTintColor": "UIColor",
                        "disabled": "BOOL"
                    }
                }
                // 此處省略
            },
            // 方法列表
            [
                "removeSubviewsFromContainerWithID",
                "removeRootView",
                "replaceExistingNonRootView",
                "setChildren",
                "manageChildren",
                "createView",
                "updateView",
                "focus",
                "blur",
                "findSubviewIn",
                "dispatchViewManagerCommand",
                "measure",
                "measureInWindow",
                "measureLayout",
                "measureLayoutRelativeToParent",
                "measureViewsInRect",
                "takeSnapshot",
                "setJSResponder",
                "clearJSResponder",
                "configureNextLayoutAnimation"
            ],
            // Promise 方法 index(方法列表中哪些方法是異步的)
            [
                16
            ]
        ],

UIManager 的再加工最主要包括兩個方面,為 UIManager 上的每個 view 添加 Constants 和 Commands 屬性锅棕。

  • 1 生成 Constants 屬性

    • 獲取每個 view 的 Manager 名稱拙泽,例如 RCTSwitchManager
    • 在 NativeModules 上查找 RCTSwitchManager 對象
    • 如果存在則將 RCTSwitchManager 對象中所有非 function 屬性放入 Constants
// UIManager.js
const viewConfig = UIManager[viewName];
if (viewConfig.Manager) {
let constants;
/* $FlowFixMe - nice try. Flow doesn't like getters */
Object.defineProperty(viewConfig, 'Constants', {
  configurable: true,
  enumerable: true,
  get: () => {
    if (constants) {
      return constants;
    }
    constants = {};
    const viewManager = NativeModules[normalizePrefix(viewConfig.Manager)];
    viewManager && Object.keys(viewManager).forEach(key => {
      const value = viewManager[key];
      if (typeof value !== 'function') {
        constants[key] = value;
      }
    });
    return constants;
  },
});   
  • 2 生成 Commands 屬性

    • 獲取每個 view 的 Manager 名稱,例如 RCTSwitchManager
    • 在 NativeModules 上查找 RCTSwitchManager 對象
    • 如果存在則將 RCTSwitchManager 對象中所有 function 屬性放入 Commands裸燎,value 為屬性的索引值
// UIManager.js
let commands;
/* $FlowFixMe - nice try. Flow doesn't like getters */
Object.defineProperty(viewConfig, 'Commands', {
  configurable: true,
  enumerable: true,
  get: () => {
    if (commands) {
      return commands;
    }
    commands = {};
    const viewManager = NativeModules[normalizePrefix(viewConfig.Manager)];
    let index = 0;
    viewManager && Object.keys(viewManager).forEach(key => {
      const value = viewManager[key];
      if (typeof value === 'function') {
        commands[key] = index++;
      }
    });
    return commands;
  },
});

啟動 JavaScript App

- (void)runApplication:(RCTBridge *)bridge
{
  NSString *moduleName = _moduleName ?: @"";
  NSDictionary *appParameters = @{
    @"rootTag": _contentView.reactTag,
    @"initialProps": _appProperties ?: @{},
  };

  [bridge enqueueJSCall:@"AppRegistry.runApplication"
                   args:@[moduleName, appParameters]];
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末顾瞻,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子德绿,更是在濱河造成了極大的恐慌荷荤,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件移稳,死亡現(xiàn)場離奇詭異蕴纳,居然都是意外死亡,警方通過查閱死者的電腦和手機个粱,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進店門古毛,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事稻薇∩┒常” “怎么了?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵塞椎,是天一觀的道長桨仿。 經(jīng)常有香客問我,道長案狠,這世上最難降的妖魔是什么服傍? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮骂铁,結果婚禮上吹零,老公的妹妹穿的比我還像新娘。我一直安慰自己拉庵,他們只是感情好瘪校,可當我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著名段,像睡著了一般阱扬。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上伸辟,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天麻惶,我揣著相機與錄音,去河邊找鬼信夫。 笑死窃蹋,一個胖子當著我的面吹牛,可吹牛的內容都是我干的静稻。 我是一名探鬼主播警没,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼振湾!你這毒婦竟也來了杀迹?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤押搪,失蹤者是張志新(化名)和其女友劉穎树酪,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體大州,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡续语,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了厦画。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片疮茄。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出力试,到底是詐尸還是另有隱情焚虱,我是刑警寧澤,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布懂版,位于F島的核電站,受9級特大地震影響躏率,放射性物質發(fā)生泄漏躯畴。R本人自食惡果不足惜薇芝,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望夯到。 院中可真熱鬧,春花似錦耍贾、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽晃听。三九已至百侧,卻和暖如春能扒,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背初斑。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留见秤,地道東北人频蛔。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓晦溪,卻偏偏與公主長得像,于是被迫代替她去往敵國和親挣跋。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,762評論 2 345

推薦閱讀更多精彩內容

  • ? React Native(以下簡稱RN)的目標是用基于react的JavaScript寫代碼,在iOS/A...
    Iceguest閱讀 3,545評論 0 10
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,501評論 25 707
  • 如何批量操作 css 如何獲取 DOM 計算后的樣式 使用getComputedStyle獲取元素計算后的樣式 實...
    _Dot912閱讀 552評論 1 3
  • 因為生病往返醫(yī)院打針加上最近工作變動舟肉,前幾天又忙著收拾行李修噪,有很多之前定下的日常學習計劃沒有按時的完成。這幾天晚上...
    冷冷無常閱讀 393評論 1 2
  • 總有一些心事是無法關住的路媚,任憑冰雪覆蓋得再久黄琼,仍有綠色的芽兒破土而出,將一切染為春色整慎,總有一些思念是無法抑止的脏款,宛...
    影兒影兒閱讀 430評論 0 3