ReactNative 原理解析-通信

理解React

React 是一套可以用简洁的语法高效绘制 DOM 的框架。

  1. JSX 允许我们写 HTML 标签或 React 标签,封装成component使用,它们终将被转换成原生的 JavaScript 并创建 DOM。

  2. React 独创了 Virtual DOM 机制,高效绘制DOM。

我们可以暂时放下 HTML 和 CSS,只关心如何用 JavaScript 构造页面。

ReactNative

类比React,我们也可以暂时放下Native的代码(OC/JAVA),只关心如何用 JavaScript 构造页面。

这是一个面向前端开发者的框架。它的宗旨是让前端开发者像用 React 写网页那样,用 React Native 写移动端应用,能够用同样的语法、工具等,分别开发安卓和 iOS 平台的应用并且不用一行原生代码。

如果用一个词概括 React Native,那就是:Native 版本的 React

原理概述

首先要明白的一点是,即使使用了 React Native,我们依然需要 UIKit 等框架,调用的是 Objective-C 代码。总之,JavaScript 只是辅助,它只是提供了配置信息和逻辑的处理结果。React Native 与 Hybrid 完全没有关系,它只不过是以 JavaScript 的形式告诉 Objective-C 该执行什么代码。

JavaScript 是一种单线程的语言,它不具备自运行的能力,因此总是被动调用。很多介绍 React Native 的文章都会提到 “JavaScript 线程” 的概念,实际上,它表示的是 Objective-C 创建了一个单独的线程,这个线程只用于执行 JavaScript 代码,而且 JavaScript 代码只会在这个线程中执行。

其次,React Native 能够运行起来,全靠 Objective-C 和 JavaScript 的交互。

React Native通信机制

由于 JavaScriptCore 是一个面向 Native 的框架,在 Objective-C 这一端,我们对 JavaScript 上下文知根知底,可以很容易的获取到对象,方法等各种信息,当然也包括调用 JavaScript 函数。举个例子:

JSContext *context = [[JSContext alloc] init];
JSValue *jsVal = [context evaluateScript:@"21+7"];
int iVal = [jsVal toInt32];

真正复杂的问题在于,JavaScript 不知道 Objective-C 有哪些方法可以调用。

接下来我们举个🌰,来观察通信flow,OC定义了一个模块RCTSQLManager,里面有个方法-query:successCallback:,JS可以直接调用RCTSQLManager.query并通过回调获取执行结果:

//OC:
@implement RCTSQLManager
RCT_EXPORT_MODULE();
RCT_EXPORT_METHOD(query:(NSString *)queryData successCallback:(RCTResponseSenderBlOCk)responseSender) {
    RCT_EXPORT();
    NSString *ret = @"ret"
    responseSender(ret);
}
@end
//JS:
RCTSQLManager.query("SELECT * FROM table", function(result) {
    //result == "ret";
});

 

1. 模块配置表

React Native 解决这个问题的方案是在 Objective-C 和 JavaScript 两端都保存了一份配置表,里面标记了所有 Objective-C 暴露给 JavaScript 的模块和方法。这样,无论是哪一方调用另一方的方法,实际上传递的数据只有 ModuleId、MethodId 和 Arguments 这三个元素,它们分别表示类、方法和方法参数,当 Objective-C 接收到这三个值后,就可以通过 runtime 唯一确定要调用的是哪个函数,然后调用这个函数。(先不考虑CallBack)

1.1 模块配置表怎么生成和同步

Module

每一个需要暴露给 JavaScript 的类(也称为 Module)都会标记一个宏:RCT_EXPORT_MODULE,这个宏的具体实现并不复杂:

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

这样,这个类在 load 方法中就会调用 RCTRegisterModule 方法注册自己:

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

因此,React Native 可以通过 RCTModuleClasses 拿到所有暴露给 JavaScript 的类。下一步操作是遍历这个数组,然后生成 RCTModuleData 对象:

for (Class moduleClass in RCTGetModuleClasses()) { 
RCTModuleData *moduleData = [[RCTModuleData alloc]initWithModuleClass:moduleClass bridge:self];
  [moduleClassesByID addObject:moduleClass];
  [moduleDataByID addObject:moduleData]; 
}

这个对象保存了 Module 的名字,常量等基本信息,最重要的属性是一个数组,保存了所有需要暴露给 JavaScript 的方法。

Method

暴露给 JavaScript 的方法需要用 RCT_EXPORT_METHOD 这个宏来标记,简单来说,它为函数名加上了 rct_export 前缀,再通过 runtime 获取类的函数列表,找出其中带有指定前缀的方法并放入数组中:

(NSArray<id<RCTBridgeMethod>> *)methods{
unsigned int methodCount; // 获取方法列表
Method *methods = class_copyMethodList(object_getClass(_moduleClass), &methodCount);
  for (unsigned int i = 0; i < methodCount; i++) { 
  /* 创建 method */ RCTModuleMethod *moduleMethod = [_methods addObject:moduleMethod];
  }
  return _methods;
}

因此 Objective-C 管理模块配置表的逻辑是:Bridge 持有一个数组,数组中保存了所有的模块的 RCTModuleData 对象。只要给定 ModuleIdMethodId 就可以唯一确定要调用的方法。

生成模块配置表并写入 JavaScript 端

在前文中我们没有提到 JavaScript 是如何知道 Objective-C 要暴露哪些类的(目前只是 Objective-C 自己知道)。 这一步的操作就是为了让 JavaScript 获取所有模块的名字:

class RCTObjcExecutor : public JSExecutor {
public:
RCTObjcExecutor(
    id<RCTJavaScriptExecutor> jse,
    RCTJavaScriptCompleteBlock errorBlock,
    std::shared_ptr<MessageQueueThread> jsThread,
    std::shared_ptr<ExecutorDelegate> delegate)
    : m_jse(jse), m_errorBlock(errorBlock), m_delegate(std::move(delegate)), m_jsThread(std::move(jsThread))
{
  ...

  folly::dynamic nativeModuleConfig = folly::dynamic::array;
  auto moduleRegistry = m_delegate->getModuleRegistry();
  for (const auto &name : moduleRegistry->moduleNames()) {
    auto config = moduleRegistry->getConfig(name);
    nativeModuleConfig.push_back(config ? config->config : nullptr);
  }

  folly::dynamic config = folly::dynamic::object("remoteModuleConfig", std::move(nativeModuleConfig));

  setGlobalVariable("__fbBatchedBridgeConfig", std::make_unique<JSBigStdString>(folly::toJson(config)));
}

查看源码可以发现,Objective-C 把 config 字符串设置成 JavaScript 的一个全局变量,名字叫做:__fbBatchedBridgeConfig

void JSIExecutor::setGlobalVariable(
  std::string propName,
  std::unique_ptr<const JSBigString> jsonValue) {
SystraceSection s("JSIExecutor::setGlobalVariable", "propName", propName);
runtime_->global().setProperty(
    *runtime_,
    propName.c_str(),
    Value::createFromJsonUtf8(
        *runtime_,
        reinterpret_cast<const uint8_t *>(jsonValue->c_str()),
        jsonValue->size()));
}

2. JS 调用 Native

看起来有点复杂,不过一步步说明,应该很容易弄清楚整个流程,图中每个流程都标了序号,从发起调用到执行回调总共有11个步骤,详细说明下这些步骤:

  1. JS端调用某个OC模块暴露出来的方法。

  2. 把上一步的调用分解为ModuleName,MethodName,arguments,再扔给MessageQueue处理。

在初始化时模块配置表上的每一个模块都生成了对应的remoteModule对象,对象里也生成了跟模块配置表里一一对应的方法,这些方法里可以拿到自身的模块名,方法名,并对callback进行一些处理,再移交给MessageQueue。具体实现在BatchedBridgeFactory.js的_createBridgedModule里,整个实现区区24行代码,感受下JS的魔力吧。

  1. 在这一步把JS的callback函数缓存在MessageQueue的一个成员变量里,用CallbackID代表callback。在通过保存在MessageQueue的模块配置表把上一步传进来的ModuleName和MethodName转为ModuleID和MethodID。

  2. 把上述步骤得到的ModuleID,MethodId,CallbackID和其他参数argus传给OC。至于具体是怎么传的,后面再说。

  3. OC接收到消息,通过模块配置表拿到对应的模块和方法。

实际上模块配置表已经经过处理了,跟JS一样,在初始化时OC也对模块配置表上的每一个模块生成了对应的实例并缓存起来,模块上的每一个方法也都生成了对应的RCTModuleMethod对象,这里通过ModuleID和MethodID取到对应的Module实例和RCTModuleMethod实例进行调用。具体实现在_handleRequestNumber:moduleID:methodID:params:。

  1. RCTModuleMethod对JS传过来的每一个参数进行处理。

RCTModuleMethod可以拿到OC要调用的目标方法的每个参数类型,处理JS类型到目标类型的转换,所有JS传过来的数字都是NSNumber,这里会转成对应的int/long/double等类型,更重要的是会为block类型参数的生成一个block。

例如-(void)select:(int)index response:(RCTResponseSenderBlock)callback 这个方法,拿到两个参数的类型为int,block,JS传过来的两个参数类型是NSNumber,NSString(CallbackID),这时会把NSNumber转为int,NSString(CallbackID)转为一个block,block的内容是把回调的值和CallbackID传回给JS。

这些参数组装完毕后,通过NSInvocation动态调用相应的OC模块方法。

  1. OC模块方法调用完,执行block回调。

  2. 调用到第6步说明的RCTModuleMethod生成的block。

  3. block里带着CallbackID和block传过来的参数去调JS里MessageQueue的方法invokeCallbackAndReturnFlushedQueue。

  4. MessageQueue通过CallbackID找到相应的JS callback方法。

  5. 调用callback方法,并把OC带过来的参数一起传过去,完成回调。

整个流程就是这样,简单概括下,差不多就是:JS函数调用转ModuleID/MethodID -> callback转CallbackID -> OC根据ID拿到方法 -> 处理参数 -> 调用OC方法 -> 回调CallbackID -> JS通过CallbackID拿到callback执行

2.1 JS数据传输

上述第4步留下一个问题,JS是怎样把数据传给OC,让OC去调相应方法的?

答案是通过返回值。JS不会主动传递数据给OC,在调OC方法时,会在上述第4步把ModuleID,MethodID等数据加到一个队列里,等OC过来调JS的任意方法时,再把这个队列返回给OC,此时OC再执行这个队列里要调用的方法。

一开始不明白,设计成JS无法直接调用OC,需要在OC去调JS时才通过返回值触发调用,整个程序还能跑得通吗。后来想想纯native开发里的事件响应机制,就有点理解了。native开发里,什么时候会执行代码?只在有事件触发的时候,这个事件可以是启动事件,触摸事件,timer事件,系统事件,回调事件。而在React Native里,这些事件发生时OC都会调用JS相应的模块方法去处理,处理完这些事件后再执行JS想让OC执行的方法,而没有事件发生的时候,是不会执行任何代码的,这跟native开发里事件响应机制是一致的。

说到OC调用JS,再补充一下,实际上模块配置表除了有上述OC的模块remoteModules外,还保存了JS模块localModules,OC调JS某些模块的方法时,也是通过传递ModuleID和MethodID去调用的,都会走到-enqueueJSCall:args:方法把两个ID和参数传给JS的BatchedBridge.callFunctionReturnFlushedQueue,跟JS调OC原理差不多,就不再赘述了。

posted @ 2021-08-18 16:31  6度XZ  阅读(545)  评论(0编辑  收藏  举报