React Native 组件初始化与通信分析(一)

该系列文章希望从非 UI 模块以及 UI 模块的初始化与通信来剖析 React Native 一些实现原理。这篇文章是该系列的第一篇,主要分析了非 UI 模块从 APP 启动、Native 端的模块定义到 JavaScript 端的模块生成过程(基于 React Native @0.29.0 iOS 实现)。

时序图总览

先来看下整体的时序图,后面会依次介绍。

image
image

[Native] iOS main 函数执行

首先,iOS APP 启动后先执行的是 main 函数,此处不多说。

[Native] 注册 Native Module(在各个 Native Module 的定义中)

在 Native 端实现模块时都需要调用 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]; // 方法列表(数组)
    if (asyncMethods) {
      [config addObject:asyncMethods]; // Promise 方法列表(数组)
    }
  }
  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] 根据上述模块配置信息生成 JS 模块

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

  1. BatchedBridge 是 MessageQueue 的一个实例
  2. 第一次访问 BatchedBridge.RemoteModules 时 MessageQueue 会利用 global.__fbBatchedBridgeConfig 模块配置信息来生成 JS 模块

[JS] JS 模块的生成

首先,JS 模块的结构

{
    name: "模块名称",
    {
        方法名:方法实现(区分是否 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;
}

第三步 根据方法列表、属性对象生成 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 的逻辑后续写一篇JS代码执行后的分析来剖析
// 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 生成

经过前面的步骤,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 模块(区别是 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]];
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容

  • ​ React Native(以下简称RN)的目标是用基于react的JavaScript写代码,在iOS/A...
    Iceguest阅读 3,451评论 0 9
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,569评论 25 707
  • 如何批量操作 css 如何获取 DOM 计算后的样式 使用getComputedStyle获取元素计算后的样式 实...
    _Dot912阅读 536评论 1 3
  • 因为生病往返医院打针加上最近工作变动,前几天又忙着收拾行李,有很多之前定下的日常学习计划没有按时的完成。这几天晚上...
    冷冷无常阅读 377评论 1 2
  • 总有一些心事是无法关住的,任凭冰雪覆盖得再久,仍有绿色的芽儿破土而出,将一切染为春色,总有一些思念是无法抑止的,宛...
    影儿影儿阅读 398评论 0 3