理解React-Native(0.46) 中native和js通信原理(iOS)


使用App.png

本文结构

  • 目前App的几种常见的开发模式
  • 关于React-Native的一点小看法
  • React-Native相关的初始化过程源码探索
  • OC 和 JS 互调部分

首先看一下, 目前App的几种常见的开发模式

Native App

  1. 直接依托于操作系统,交互性最强,功能最为强大,性能最好, 体验最好
  2. 开发成本高,无法跨平台,不同平台Android和iOS上都要各自独立开发
  3. 更新缓慢,特别是发布应用商店后,需要等待审核周期
  4. 维护成本高

Hybrid App

混合开发,也就是半原生半Web的开发模式,有跨平台效果,最终发布的是独立的原生APP

优点:

  1. 开发成本较低,可以跨平台,调试方便(chrome...)
  2. 维护成本低,功能可复用(基本上是维护一套前端代码)
  3. 更新较为自由
  4. 部分性能要求的页面可用原生实现

缺点:

  1. 基于webKit, 受限于webKit, 相比原生,性能仍然有较大损耗
  2. 动画体验差, 事件反馈不好, 不适用于交互性较强的app

React-Native App

优点:

  1. 基于JavaScriptCore, 所有的页面都是native, 体验接近原生, 解决了Hybrid的性能问题
  2. 拥有Hybrid和webApp的动态更新优势
  3. 使用React.JS理念开发原生App, 可以实现大多的代码多端复用

缺点:

  1. React Native 只能调用原生接口,但是不能对原生做扩展,要做扩展只能写 Native
  2. 学习曲线比较陡峭, 指导性文档少, 官方文档也不齐全, 开发中遇到的问题很难解决
  3. React Native 的首次加载很慢
  4. 只能部分跨平台,并不是像Hybrid中只需要编写一次前端代码, 即可多端运行,而是不同平台代码有所区别

关于React-Native的一点小看法

随着Apple官方禁用热更新之后, 看上去动态化是越来越火了, hybrid 和 React-Native.
hybrid方案大多是基于webView来实现的动态化和即时更新功能,由Native通过JSBridge等方法提供统一的API,然后用Html5+JS来写实际的逻辑,调用API,这种模式下,由于Android,iOS的API一般有一致性,而且最终的页面也是在webview中显示, 所以能实现跨平台统一开发, 但是很多的功能体现受限于webView的方面. 比如动画的流畅性, 事件响应的及时反馈.
而Facebook开源的React-Native在保留hybrid的优势上,似乎解决了webView带来的交互体验问题, 因为React-Native的项目并非基于webView来实现的, App运行的时候全部都是原生的系统UI, 响应系统, 甚至动画(新版部分), 所以它可以带来原生App的体验. 同时React-Native的官方介绍是: Build native mobile apps using JavaScript and React. 使用JavaScript来开发原生的应用, 也就意味着, 它是同样具备有hybrid动态更新的优势的, 只要能够动态更新对应的js代码. 使用React来开发原生应用, 意味着可以使用React.JS的一整套的开发理念来开发原生App, 向写前端一样来开发原生App吧!

React-Native提出了Learn once, write anywhere, 看上去有点像Java的write once, run anywhere. 虽然React-Native并没有提出像Java一样的run anywhere的概念, 但是实际上, 在真实开发项目的时候(Android, iOS), 两个平台的JS代码的复用性是非常高的. 但是因为, React-Native的项目运行起来真实使用的是原生的系统UI, 响应系统等, 这些东西Android, iOS使用都是有差异的, 所以这些代码不能run anywhere, 就需要在各个平台使用原生的开发方式来实现.

暂且不提React-Native自身的缺陷(官方的长列表可以用不能用来形容), 另一个更现实的问题是, 使用React-Native开发的任务应给在那些人. 前端? Native? 虽然说的是使用JS和React来开发原生App, 但是这几乎是不可能的, 因为React-Native官方提供的Component和功能有限, 很多功能都是需要Native端自己来实现的, 所以前端开发人员大多也应该负责React部分了, Native部分的学习成本就有点太高了. 如果单独是Native端的人员的任务, 那么同时也是需要Android和iOS的开发人员都要学习React.JS相关的技能了, 学习的成本看上去比前端人员会少一些. 实际上, 看上去, 对于一个大一些的项目, 至少还是需要有Android和iOS的开发人员同时来参与比较现实.

demo.png

上面的截图就是一个很简单的React-Native的iOS项目运行的情况, 左边的就相当于是React部分, 右边就相当于是Native的部分了. 看上去确实如官网介绍, 左边的开发代码是完全是使用React和JavaScript完成页面的层级, 布局, 事件响应等逻辑, 当它正常运行的时候, 里面的所有的页面都是native的, 右边看上去也是一个原生的App.

首先看一下实际的例子

在使用RN的时候, 官方已经给出的控件和功能是有限的, 很多时候还暂时满足不了我们项目中的需求, 这个时候我们可以利用native来自己实现. 比如当你需要直接通过NSUserDefaults的方式存取数据的时候, 你可能需要简单的做一些工作. 按照官方的文档指引, 这个时候你只是需要调用native的api就可以完成了.

@interface ZJRNUserDefaultsManager : NSObject<RCTBridgeModule>

@end

@implementation ZJRNUserDefaultsManager

// 注册类为module, 暴露给js, 这里可以指定module的名字, 不设置的时候默认为类名
RCT_EXPORT_MODULE();
// 注册方法, 暴露给js
RCT_EXPORT_METHOD(saveValue: (id)value forKey:(NSString *)key ) {

// native 端的处理
  NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
  [userDefaults setValue:value forKey:key];
  [userDefaults synchronize];

}

@end

上面的操作很简单, 首先新建一个类, 遵守RCTBridgeModule协议, 然后使用两个宏, 分别注册module和我们的方法, 方法里面的处理和native开发时类似. 然后在js端就可以这样的使用了(当然,动了native端的代码,这个时候需要重新编译运行了).

// 引入module
import { NativeModules } from 'react-native';
var ZJRNUserDefaultsManager = NativeModules.ZJRNUserDefaultsManager;

// 在需要保存的地方直接使用就可以了, 当然了js中是没有参数名(forKey...)这些了
ZJRNUserDefaultsManager.saveValue('hello', 'testKey')

仅需要完成上面的简单的操作, 我们就可以实现在js端调用native的功能了, 那么拥有这个功能是还不能很好的完成实际的开发工作的. 我们需要native端提供一些接口出来供React.js端调用, 比如, 创建指定类型的View, 更新指定的view的属性, 布局等操作. 还好, 这些工作Facebook已经帮我们完成了. 在RCTUIManager这个类中, 已经提供了createView, updateView, setChildren, findSubviewIn, removeSubviewsFromContainerWithID等需要的API供js端调用了. 我们现在如果要实现我们直接的控件在js端使用就很简单了.

上面我们所做的操作其实很简单, 按照官方指引就可以顺利的完成了, 甚至还可以加入方法的回调. 但是这一切是怎么工作起来的呢? 我们在native端做的这些操作起了什么作用, 然后js中又是怎么样正确的找到native端这些的class, 以及method, 然后正确的调用的呢? 这个时候, 我们就需要稍微的看下react-native的一些源码了. 主要是React-Native相关的初始化过程和oc和js的互相调用.

结构图.png

React-Native相关的初始化过程

  • [RCTRootView initWithBundleURL:...]
    • [RCTBridge initWithBundleURL:...]
      • [RCTBridge setUp]
        • 初始化batchedBridge
        • [RCTCxxBridge start]
          • 开启一个线程jsThread用于js
          • [RCTCxxBridge _initModulesWithDispatchGroup]
          • 初始化JSCExecutorFactory, 用于执行js代码以及处理回调 JSCExecutor::JSCExecutor()
            • JSCExecutor::initOnJSVMThread()
            • installGlobalProxy -> nativeModuleProxy
          • RCTJavaScriptLoader -> 加载js代码
          • [RCTCxxBridge executeSourceCode]
    • [RCTRootView initWithBridge:...]
      • [RCTRootView bundleFinishedLoading]
        • 初始化RCTRootContentView
        • [RCTRootView runApplication]

一般我们初始化的时候会只用到的就需要两句代码即可

  NSURL *jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index.ios" fallbackResource:nil];
  RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
                                                      moduleName:@"FlatListDemo"
                                               initialProperties:nil
                                                   launchOptions:launchOptions];

1 获取到app的js入口文件的NSURL
2 初始化RCTRootView, 传递的参数moduleName:@"FlatListDemo"是我们在入口的js文件中注册的, initialProperties:nil, 将作为js中的跟view的props, 可以用来传递需要的初始数据

进入初始化RCTRootView的方法中

  RCTBridge *bridge = [[RCTBridge alloc] initWithBundleURL:bundleURL
                                            moduleProvider:nil
                                             launchOptions:launchOptions];

  return [self initWithBridge:bridge moduleName:moduleName initialProperties:initialProperties];

主要的操作就是初始化了一个RCTBridge, 这个可以算是 OC <=> JS通信的桥梁, 所以最重要的部分就是初始化RCTBridge, [RCTBridge setUp]是重要的过程, 主要的是下面的几行代码

  Class bridgeClass = self.bridgeClass;
  self.batchedBridge = [[bridgeClass alloc] initWithParentBridge:self];
  [self.batchedBridge start];

RootView持有的RCTBridge它并没有做很多的事, 主要的工作都是由它持有的batchedBridge来完成的, 上面就是初始化batchedBridge的过程, 首先获取到了bridgeClass, 因为在RN之前的版本中使用的是RCTBatchedBridge这个class来处理的, 现在使用的是RCTCxxBridge, 所以在里面简单的处理了一下bridgeClass, 我们后面用到的batchedBridge就全部关注RCTCxxBridge它了.

self.batchedBridge = [[bridgeClass alloc] initWithParentBridge:self]

初始化batchedBridge的过程中设置了一些变量的初始化状态, 需要关注的是初始化了一个RCTDisplayLink -> 用于js中的 RCTTiming(定时器) frameUpdate(navigator...)等

[self.batchedBridge start] 在start的过程中, 进行了很多的操作

1 首先会创建一个JSThread, 在接触react-native的时候, 我们可能经常会听到有个js线程的说法, 实际上指的是会单独开一个线程给js用, js的执行会在这个线程(可以在其他线程回调)中完成, 名字就是com.facebook.react.JavaScript方便调试

// 创建jsThread 
static NSString *const RCTJSThreadName = @"com.facebook.react.JavaScript";
  _jsThread = [[NSThread alloc] initWithTarget:self
                                      selector:@selector(runJSRunLoop)
                                        object:nil];
  _jsThread.name = RCTJSThreadName;
  _jsThread.qualityOfService = NSOperationQualityOfServiceUserInteractive;
  [_jsThread start];
  


  执行js代码总是会被'保证'在jsThread上面完成
  
  - (void)ensureOnJavaScriptThread:(dispatch_block_t)block
{
  if ([NSThread currentThread] == _jsThread) {
    [self _tryAndHandleError:block];
  } else {
    [self performSelector:@selector(_tryAndHandleError:)
          onThread:_jsThread
          withObject:block
          waitUntilDone:NO];
  }
}

需要关注一下@selector(runJSRunLoop) -> 在里面使用了autoreleasepool开启了一个runloop, 让JSThread线程一直存在(关于runloop)
  - (void)runJSRunLoop
{
  @autoreleasepool {
    RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, @"-[RCTCxxBridge runJSRunLoop] setup", nil);

    // copy thread name to pthread name
    pthread_setname_np([NSThread currentThread].name.UTF8String);

    // Set up a dummy runloop source to avoid spinning
    CFRunLoopSourceContext noSpinCtx = {0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL};
    CFRunLoopSourceRef noSpinSource = CFRunLoopSourceCreate(NULL, 0, &noSpinCtx);
    CFRunLoopAddSource(CFRunLoopGetCurrent(), noSpinSource, kCFRunLoopDefaultMode);
    CFRelease(noSpinSource);

    RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @"");

    // run the run loop
    while (kCFRunLoopRunStopped != CFRunLoopRunInMode(kCFRunLoopDefaultMode, ((NSDate *)[NSDate distantFuture]).timeIntervalSinceReferenceDate, NO)) {
      RCTAssert(NO, @"not reached assertion"); // runloop spun. that's bad.
    }
  }
}

2 然后会创建一个CGDGroup dispatch_group_t prepareBridge, 在后面会用于modules和souceCode都准备好了之后在notify中, 执行js代码

.

3 初始化所有的注册的OC Class的信息为对应的module并保存下来[self _initModulesWithDispatchGroup:prepareBridge], 这个过程通过遍历每个module(分为两种, 一种是内部提前注册好的,如RCTJSCExecutor(现在弃用), 另一种是外部通过宏定义注册的)来保存它相关的'配置表'信息

  NSMutableArray<Class> *moduleClassesByID = [NSMutableArray new];
  NSMutableArray<RCTModuleData *> *moduleDataByID = [NSMutableArray new];
  NSMutableDictionary<NSString *, RCTModuleData *> *moduleDataByName = [NSMutableDictionary new];

遍历内部的modules我们就不关注了, 主要看下遍历外部注册的OC Class的处理

 for (Class moduleClass in RCTGetModuleClasses()) {
    NSString *moduleName = RCTBridgeModuleNameForClass(moduleClass);

// 初始化每个注册的class对应的RCTModuleData ---> 内部会检查是否有重写init, 
// constantsToExport等方法, 如果被重写将会在主线程被调用, 
// 因为RN认为, 我们可能需要进行UI相关的操作
    moduleData = [[RCTModuleData alloc] initWithModuleClass:moduleClass
                                                     bridge:self];
    moduleDataByName[moduleName] = moduleData;
    [moduleClassesByID addObject:moduleClass];
    [moduleDataByID addObject:moduleData];
  }
  // 保存 modules
  _moduleDataByID = [moduleDataByID copy];
  _moduleDataByName = [moduleDataByName copy];
  _moduleClassesByID = [moduleClassesByID copy];
  }

RCTGetModuleClasses()这个全局函数获取到的是一个全局的单例数组static NSMutableArray<Class> *RCTModuleClasses, 所以, 有个问题是这个数组中的数据是哪里来的. 其实是我们在自定义ViewManager(NSObject <RCTBridgeModule>)的时候的时候会使用一个宏 RCT_EXPORT_MODULE();, 这个宏展开后的情况是这样的

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


void RCTRegisterModule(Class moduleClass)
{
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    RCTModuleClasses = [NSMutableArray new];
  });

  RCTAssert([moduleClass conformsToProtocol:@protocol(RCTBridgeModule)],
            @"%@ does not conform to the RCTBridgeModule protocol",
            moduleClass);

  // Register module -> 添加到全局的单例数组中
  [RCTModuleClasses addObject:moduleClass];
}

上面的宏会为对应的class中定义一个返回moduleName(当没有设置的时候, 默认值为class的名字)的工厂方法
同时extern一个函数RCTRegisterModule(), 这个全局的函数中主要就是创建了一个全局的单例数组RCTModuleClasses, 并且将我们当前的Class添加进去
另外, 宏定义中重写了当前class的+load方法, 所以这个RCTRegisterModule()函数在类被加载的时候就已经被调用了, 这样就完成了注册的过程. 所以前面的数组的数据就是我们注册的Class

接着上面遍历分析, 后面有一个初始化RCTModuleData的操作, 稍微看一下它的初始化过程, 并没有做很多实际的事情, 在[RCTModuleData setUp]中检查是否有重写init, constantsToExport等方法, 如果被重写将会在主线程被调用, 因为RN认为, 我们可能需要进行UI相关的操作
其实在RCTModuleData初始化过程中没有很多的操作, 是因为, RCTModuleData是通过它持有的_instance来进行数据的操作的
所以在这个遍历完成之后, 进入了下一个遍历操作[self _prepareModulesWithDispatchGroup:dispatchGroup], 主要是为RCTModuleData设置好对应的_instance和bridge, 主要代码如下

  for (RCTModuleData *moduleData in _moduleDataByID) {
     (void)[moduleData instance]; 
     /*
      这是一个get方法, 如果没有setupComplete的时候会进行一系列的setUp操作
      [self setUpInstanceAndBridge] {
      1. 为instance赋值 bridge
      // Bridge must be set before methodQueue is set up, as methodQueue
      // initialization requires it (View Managers get their queue by calling
      // self.bridge.uiManager.methodQueue)
      [self setBridgeForInstance];
      
      2. 当_instance.methodQueue不存在的时候创建新的queue, 用于一些分发工作
      // @"com.facebook.react.%@Queue"
      [self setUpMethodQueue];
      3. 设置完成之后, 会给bridge注册实例, 用于frameUpdate---> CADisplayLink回调
      [self finishSetupForInstance]
      }
      */
     [moduleData gatherConstants]; //
  }

4 以上终于完成了Modules信息的保存, 接着会进行JSExecutorFactory的初始化和相关的设置, JSExecutorFactory内部会持有一个JSCExecutor 所有与JS的通信,一定都通过JSCExecutor来进行, 所以需要重点关注, 它的构造函数中调用了initOnJSVMThread(), 里面进行了很多的JS上下文的准备, 创建JSClass, 全局的context, 以及添加全局的回调.注意在这个里面使用到的JSC_JSXXX的宏的作用, 实际上是会转换为调用苹果的JavaScriptCore对应的方法(去掉JSC_), 同时还要留意JSCExecutor构造函数中为js的上下文中设置了一个Proxy, nativeModuleProxy.

void JSCExecutor::initOnJSVMThread()
  // 在js的上下文中注册全局的闭包回调 , 这两个是我们需要重点关注的回调.
  nativeFlushQueueImmediate
  nativeCallSyncHook
    
    installNativeHook<&JSCExecutor::nativeFlushQueueImmediate>("nativeFlushQueueImmediate");
    // 这一个注册的回调, 可以用于在js中直接调用oc, 注意, 在rn中js调用oc一般都是由oc发起的, 
    // 这就像我们的界面一般是有用户的操作才会触发系统调用, rn中类似的, 
    // 当oc调了js后, 在对应的js回调中才会实现调用oc的逻辑

 // 在react native MessageQueue.js中的源码中可以找到 这样一段代码, 
 // 就是当oc没有及时的在消息队列中调js的时候, 大于5ms后js就会调用这个回调, 主动调用oc
 
   MIN_TIME_BETWEEN_FLUSHES_MS = 5ms;
      if (global.nativeFlushQueueImmediate &&
        (now - this._lastFlush >= MIN_TIME_BETWEEN_FLUSHES_MS ||
         this._inCall === 0)) {
      var queue = this._queue;
      this._queue = [[], [], [], this._callID];
      this._lastFlush = now;
      global.nativeFlushQueueImmediate(queue);
    }
   // 
  
// 这个回调是用来调用查找native中对应的方法相关信息的

installNativeHook<&JSCExecutor::nativeCallSyncHook>("nativeCallSyncHook");
在react native的 NativeModules.js中有相关的调用来获取native方法的相关信息
function genMethod(moduleID: number, methodID: number, type: MethodType) {
//...
    return global.nativeCallSyncHook(moduleID, methodID, args);
}

为js的上下设置 nativeModuleProxy, 这一个属性很重要, js端用来获取所有注册的nativeModule. 在js中我们需要引入native端的时候回写这样的类似代码 var {NativeModules} from 'react-native', 实际上就是获取到我们这里设置的.

JSCExecutor::JSCExecutor()
  {
  // 这个函数在js的全局上下文中设置了`nativeModuleProxy`, 
// 使得在js端可以获取到, 同时注意这个函数的第三个参数中
// 还包装了一个方法用于调用, 里面涉及到JSCNativeModules这个类, 
// 就是获取NativeModule的操作, 后面分析
    installGlobalProxy(m_context, "nativeModuleProxy",
                       exceptionWrapMethod<&JSCExecutor::getNativeModule>());
  }

在react-native 的NativeModules.js底部有这样的代码, 就直接调用了上面在native端设置的全局属性了 

let NativeModules : {[moduleName: string]: Object} = {};
if (global.nativeModuleProxy) {
  NativeModules = global.nativeModuleProxy;
}
module.exports = NativeModules;

那么js中是怎么获取到我们之前在native端已经生成好的'配置表'信息的呢? 这个问题需要我们重点关注下, 在之前的react-native版本中, 是通过native端直接在js的上下文中注入这样一个__fbBatchedBridgeConfig配置表,
传过去一个json(remoteModuleConfig). 现在的版本中改了一些. 还是上面这段代码. 在如下的调用栈中,我们只需要关注最后一个函数.

  • JSCExecutor::getNativeModule()
    • JSCNativeModules::getModule()
      • JSCNativeModules::createModule()
        • ModuleRegistry::getConfig()
          • RCTNativeModule::getMethods()
            • NSStringFromSelector(selector) hasPrefix:@"rct_export"(初始化RCTModuleMethod信息)
folly::Optional<Object> JSCNativeModules::createModule(const std::string& name, JSContextRef context) {
  if (!m_genNativeModuleJS) {
// 获取js中contenxt中的global
    auto global = Object::getGlobalObject(context);  
// 获取 global中的__fbGenNativeModule对象
    m_genNativeModuleJS = global.getProperty("__fbGenNativeModule").asObject();
    m_genNativeModuleJS->makeProtected();
  }
 
 // 这个方法最终会调到 RCTNativeModule::getMethods()中,
 // 取得之前使用宏暴露的所有的方法  -> 前缀是 "__rct_export__"
 auto result = m_moduleRegistry->getConfig(name);
 
 
  if (!result.hasValue()) {
    return nullptr;
  }
// 调用 js中的__fbGenNativeModule方法, 并且传递过去native端的配置表, 用于端js处理
  Value moduleInfo = m_genNativeModuleJS->callAsFunction({
    Value::fromDynamic(context, result->config),
    Value::makeNumber(context, result->index)
  });
  return moduleInfo.asObject().getProperty("module").asObject();
}

// 下面是对应的js中的处理, NativeModules.js
// 暴露一个全局的属性给native(就是上面我们调用的js中的方法, 触发了后面的js后取到native端的配置表, 并且保存)
// export this method as a global so we can call it from native
global.__fbGenNativeModule = genModule;

function genModule(...) {
  // 获取到native端调用时传递过来的配置信息
  const [moduleName, constants, methods, promiseMethods, syncMethods] = config;
  const module = {};
  // 遍历 所有的native的方法列表, 获取所有native的方法的详细信息
  methods && methods.forEach((methodName, methodID) => {
    module[methodName] = genMethod(moduleID, methodID, methodType);
  });

}
function genMethod(...) {
  /// ...
  // 调用之前native端在js中注入的回调, 获取native中的所有方法的详细信息
  return global.nativeCallSyncHook(moduleID, methodID, args);
}


5 使用dispatch_group方式初始化Instance(执行代码需要), 和加载js代码RCTJavaScriptLoader

.

6 modules and source code都已经准备好了, 在notify中JSCExecutor专属的Thread内执行jsbundle代码执行js代码. 执行完毕后会将_displayLink添加到runloop(注意是在jsThread所在的runloop)中, 开始运行.

  • [RCTCxxBridge executeSourceCode: ]
  • [RCTCxxBridge enqueueApplicationScript:]
    • void Instance::loadScriptFromString()
    • void NativeToJsBridge::loadApplication()
      • void JSCExecutor::loadApplicationScript()
        • void JSCExecutor::flush()
          • void JSCExecutor::bindBridge()

上面的调用栈中我们主要关注后面几个函数

void JSCExecutor::loadApplicationScript(), 在这个函数中注意下面两行代码, 注意, RN中有个很明显的问题就是, 首次进入RN模块的时候, 加载的很慢, 会有几秒的加载时间, 其实就是在这个函数中加载jsBundle造成的, 如果要优化加载的时间, 以及不同模块页面切换流畅, 就需要对jsBundle文件进行精简, 缓存等操作.

  // 执行js代码, 加载jsBundle
  evaluateScript(m_context, jsScript, jsSourceURL);
  // 在加载完jsbundle后主动调用一次, 为js的上下文环境添加必要的全局属性和回调
  flush();

void JSCExecutor::bindBridge()保存js的上下文环境中必要的全局属性和回调, 这些在 react native的 MessageQueue.js中定义的调用方法, 当native需要调用js的方法的时候, 需要通过调用这些js方法, 在这些方法中, js端会根据传递的信息查找到在js中需要调用的方法.

    auto global = Object::getGlobalObject(m_context);
    auto batchedBridgeValue = global.getProperty("__fbBatchedBridge");
// 这些是在MessageQueue.js中定义的调用方法
    auto batchedBridge = batchedBridgeValue.asObject();
    m_callFunctionReturnFlushedQueueJS = batchedBridge.getProperty("callFunctionReturnFlushedQueue").asObject();
    m_invokeCallbackAndReturnFlushedQueueJS = batchedBridge.getProperty("invokeCallbackAndReturnFlushedQueue").asObject();
    m_flushedQueueJS = batchedBridge.getProperty("flushedQueue").asObject();
    m_callFunctionReturnResultAndFlushedQueueJS = batchedBridge.getProperty("callFunctionReturnResultAndFlushedQueue").asObject();
  }

7 上面的工作完成之后, 初始化bridge的工作就完成了, jsBundle已经加载完成, oc端的'配置表'也已经处理好, 并且成功已经传递给js端, js的上下文配置已经准备好, 下面就是开始执行我们的js代码了, React就会开始计算好所有的布局信息, 以及Component层级关系等, 等待native端完成对应的真正的页面渲染和布局. 回到RCTRootView的初始化方法中, 注意在[RCTRootView initWithBridge:...]的初始化方法中, 注册了几个js执行情况的通知, 我们重点关注js执行完毕后的通知RCTJavaScriptDidLoadNotification

- (instancetype)initWithBundleURL:(NSURL *)bundleURL
                       moduleName:(NSString *)moduleName
                initialProperties:(NSDictionary *)initialProperties
                    launchOptions:(NSDictionary *)launchOptions
{
  RCTBridge *bridge = [[RCTBridge alloc] initWithBundleURL:bundleURL
                                            moduleProvider:nil
                                             launchOptions:launchOptions];
// 上面完成了bridge的初始化工作 , 下面开始完成rootView的初始化
  return [self initWithBridge:bridge moduleName:moduleName initialProperties:initialProperties];
}

下面是js执行完毕后的通知RCTJavaScriptDidLoadNotification中的一系列的处理

  • [RCTRootView javaScriptDidLoad]
    • [RCTRootView bundleFinishedLoading]
      • RCTRootContentView (创建contentView)
      • [RCTRootView runApplication]

在创建RCTRootContentView的时候, 注意有个参数是reactTag, 这个属性很重要, 每一个reactTag都应该是唯一的, 从1开始, 每次递增10. RCTRootContentView初始化时, 还需要在RCTUIManager中通过reactTag去注册, 从而由RCTUIManager来统一管理所有的js端使用Component对应的每个原生view(_viewRegistry[tag]表), 有了这个, 我们就可以很方便的在其他地方通过reactTag获取到我们的Component所在的rootView.

    /**
     * Every root view that is created must have a unique react tag.
     * Numbering of these tags goes from 1, 11, 21, 31, etc
     *
     * NOTE: Since the bridge persists, the RootViews might be reused, so the
     * react tag must be re-assigned every time a new UIManager is created.
     */

- (NSNumber *)allocateRootTag
{
  NSNumber *rootTag = objc_getAssociatedObject(self, _cmd) ?: @1;
  objc_setAssociatedObject(self, _cmd, @(rootTag.integerValue + 10), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
  return rootTag;
}

然后才是[RCTRootView runApplication], 这里就会调用js里面AppRegistry对应的方法runApplication了, 在AppRegistry.js中有下面的一段注释, 已经解释得很清楚了


/**
 * `AppRegistry` is the JS entry point to running all React Native apps.  App
 * root components should register themselves with
 * `AppRegistry.registerComponent`, then the native system can load the bundle
 * for the app and then actually run the app when it's ready by invoking
 * `AppRegistry.runApplication`.
 *
 * To "stop" an application when a view should be destroyed, call
 * `AppRegistry.unmountApplicationComponentAtRootTag` with the tag that was
 * passed into `runApplication`. These should always be used as a pair.
 *
 * `AppRegistry` should be `require`d early in the `require` sequence to make
 * sure the JS execution environment is setup before other modules are
 * `require`d.
 */

- (void)runApplication:(RCTBridge *)bridge
{
  NSString *moduleName = _moduleName ?: @"";
  NSDictionary *appParameters = @{
    @"rootTag": _contentView.reactTag,
    @"initialProps": _appProperties ?: @{},
  };
// 调用AppRegistry.js中的runApplication开始运行
  RCTLogInfo(@"Running application %@ (%@)", moduleName, appParameters);
  [bridge enqueueJSCall:@"AppRegistry"
                 method:@"runApplication"
                   args:@[moduleName, appParameters]
             completion:NULL];
}

8 因为执行js代码的时候, js端会计算好每个view的布局, 属性等信息, 然后通过调用native的系统方法来完成, 页面的渲染, 页面的布局. 这个过程中就涉及到了js 和native的互调通信了. 这个过程分为两个部分.

OC 调 JS 部分

  • RCTEventDispatcher::sendXXEvent()
  • RCTEventDispatcher::dispatchEvent()
  • RCTCxxBridge:: enqueueJSCall()
  • Instance::callJSFunction()
  • NativeToJsBridge::callFunction()
  • JSCExecutor::callFunction()
    • bindBridge();
    • m_callFunctionReturnFlushedQueueJS->callAsFunction()

RCTEventDispatcher中的很多的sendXXEvent方法, 用来传递native的事件调用给js. 两种方式调用, 第一种是通过调用flushEventsQueue, 将dispatch的事件放在分发的队列中来处理, 另一种是直接调用, 但是最终每个事件都会调到RCTCxxBridge中的enqueueJSCall方法中去处理.
经过一系列的函数调用, 数据处理后, 最终会走到直接的执行函数中JSCExecutor::callFunction(), 这里面首先会检查js的上下文中的处理方法调用的回调是否获取到, 如果没有获取到, 将会再次调用bindBridge()来获取, 然后会使用m_callFunctionReturnFlushedQueueJS->callAsFunction(), 来调用, 注意这是封装的一个Object(Value.cpp)类中的方法.这个方法中会利用native端的js引擎执行MessageQueue.js中的callFunctionReturnFlushedQueue方法, js端在这个方法中, 会执行传递过去的特定的方法, 这样就完成了oc调用js了

Value Object::callAsFunction(JSObjectRef thisObj, int nArgs, const JSValueRef args[]) const {
  JSValueRef exn;
  // 这个宏才是真正的调用相关的方法的代码, 实际上是直接使用的JavaScriptCore中的JSObjectCallAsFunction方法完成的
  JSValueRef result = JSC_JSObjectCallAsFunction(m_context, m_obj, thisObj, nArgs, args, &exn);
  if (!result) {
    throw JSException(m_context, exn, "Exception calling object as function");
  }
  return Value(m_context, result);
}

完成上面的操作后, js端的BatchedBridge会处理接收到的方法调用信息, 然后查到js端的'配置表'完成对应的js方法调用. 并且会返回执行的结果, 这个结果中, 已经包括了对应的js回调中的相关信息, 用来直接调用native相关的方法.

JS 调 OC 部分

  • JSCExecutor::callNativeModules()
    • JsToNativeBridge :: callNativeModules()
    • ModuleRegistry::callNativeMethod()
      • RCTNativeModule::invoke()
      • RCTNativeModule::invokeInner()
        • RCTModuleMethod:: invokeWithBridge()
        • RCTModuleMethod:: processMethodSignature()
        • [_invocation invokeWithTarget:module]

注意, 因为在rn中js调用native一般都是由native触发的, 所以会先完成上面的native调用js的过程, 然后才会在js的回调的调用native. 所以我们这里就直接从js调用开始分析. 到这里, js端的BatchedBridge已经完成了, 将要调用的native的方法的信息封装, 并且传递到了native了(就是上面执行的结果).

上面的函数调用栈, 我们重点关注后面的函数.

//  运行时调用
- (id)invokeWithBridge:(RCTBridge *)bridge
                module:(id)module
             arguments:(NSArray *)arguments
{
  if (_argumentBlocks == nil) {
    [self processMethodSignature];  

// 配置invacation, 这个过程还比较复杂, 里面首先会调用一个全局的函数RCTParseMethodSignature(),
// 在里面会分析和解析有多少个参数, 以及参数的类型是什么, 以及参数类型的必要转化, 
// 后面就是根据JavaScript中传过来的callbackId(数字)生成对应的回调block, 在之后调用.
  }

  // Invoke method
// 上面已经设置好了_invocation的selector, 参数等信息, 
// 现在通过运行时调用对应的module(OC Class)里的方法, 完成js调用oc操作.
  [_invocation invokeWithTarget:module];

}

到这里, 我们就简单的探索了以下React-Native里面的初始化所做的一些工作, 初始化过程很关键, 里面为JS和OC通信做了很多的必要的操作. 同时初始化的过程中还是有一些可优化的. 比如RN首次加载模块的时候很慢, 可能会出现白屏, 就是初始化的时候加载jsBundle造成的, 我们可以预加载jsBundle, 在使用RN模块的时候, 直接渲染界面, 就能明显优化这个问题, 如果项目中的有多个RN模块的时候, 建议要处理一下RCTBridge, 尽量能公用一个RCTBridge.

参照文档
ReactNative iOS源码解析

推荐阅读更多精彩内容