React Native深入理解Native与RN通信原理

Posted 骑着代马去流浪

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了React Native深入理解Native与RN通信原理相关的知识,希望对你有一定的参考价值。

        在使用 React Native 开发应用程序的时候,有时候需要使用 javascript 中默认不可用的 iosandroid 的原生 API。 也许你想复用一些现有的 OC、Swift、Java 或 C++ 库,而不必在 JavaScript 中重新实现它,或者为图像处理之类的事情编写一些高性能、多线程的代码。那么此时就不得不与Native打交道了。

        幸运的是,React Native已经提供了这样的能力来供你使用,在JS Core的强大支持下,我们RN侧可以使用NativeModule 来将Native代码作为 JS 对象暴露给 JavaScript来调用,从而允许您从 JS 内可以调用Native代码。

        作为非原生开发,虽然我们不希望此功能成为通常开发过程的一部分,但它的存在至关重要。 并且如果 React Native 没有导出你的应用程序需要的原生 API,那么你应该能够自己封装并且导出它!

下面,我们来深入聊一聊RN的初始化阶段,以及原生端与JS端互相通信的过程。

JS Core

        开始之前,我觉得有必要简单介绍一下JS Core,众所周知,RN是通过JavaScriptCore提供的能力来与native交互的,JavaScript Core(简称 JSCore)是一个开源的框架,是 WebKit 的一部分,用最简单的话描述这个框架,它大概提供了两种能力:

  1. 在Native的环境下执行JS代码,不需要浏览器或Node的环境。
  2. 把原生代码注入到JS中,提供给JS调用原生代码的能力。

        JSExport 是整个 JSCore 里面最神奇的部分,也正是有了 JSExport 才让我们把 Native 对象暴露给 JS 环境非常的容易,简单说,通过JSExport我们可以把 Native 对象的属性和方法暴露给 JS 环境,先来看一段OC的接口代码:

@protocol NativeObjectExport <JSExport>

@interface NativeObject : NSObject<NativeObjectExport>
@property (nonatomic, assign) BOOL property1;
@property (nonatomic, strong) id property2;

- (void)method1:(JSValue *)arguments;
- (void)method2;
@end

        NativeObject 可以被实现为任意的对象,只要他实现了NativeObjectExport那么这个协议里面的属性和对象就可以直接被 JS 环境使用,例如上面的 property1 和 method1。我们只要在 context 里面注入一个NativeObject的对象,就可以在 JS 环境放肆的与 Native 进行交互了:

context[@"helper"] = [NativeObject new];
[arguments[@"handler"] callWithArguments:@[object]];

在JS的环境中

// 可以通过property1访问native
var prop = helper.property1;
// 可以通过method1调用native的方法
helper.method1(
  handler: function(object) 
);

        当Native代码执行完 method1之后,可以通过这个 handler 回调到 JS 环境,JS 环境通过 function 的 object 拿到返回结果,这就是一个完整的流程。

初始化阶段

        在RN初始化阶段,首先原生端会遍历开发者自定义的原生模块与RN框架提供的原生模块,将其注册到一张原生模块映射表中,然后,原生端也会将需要调用的JS模块注册到一张JS模块映射表中,需要注意的是,原生端并没有实现JS Module,只是有一份接口而已(Android需要实现JS接口,但是IOS不需要)。

        紧接着JS Core会将两份映射表传入到JS侧,在JS侧,原生模块映射表会绑定到RN的提供的NativeModule上面,这样JS就可以通过NativeModule来调用Native提供的API了,对于JS模块映射表来说,JS侧会实现对应的JS方法,并注册进去。这样,Native就可以调用JS提供的方法了。并且JS可以以回调参数的形式来接受Native传来的数据。

请看源码:

import  NativeModules  from 'react-native';

const _ActionSheet = 
  showActionSheet: createNoop('ActionSheet.showActionSheet'),
  ...NativeModules.GAActionSheet,
;

        这是一段从Native SDK里拿出来的源码,可以看到,GAActionSheet就是native暴露的api,createNoop方法只是为了在native api还没加载完成时就调用时的一种兜底方案。

const ViewEventEmitter = new NativeEventEmitter(NativeModules.GAViewEventEmitter)

ViewEventEmitter.addListener(self_callback);

        相应地,上述代码展示了怎样通过ReactNative提供的NativeEventEmitter来订阅一个JS事件。

        附一段react native源码来解释NativeModules到底是什么,由于React Native源码较多,已被我删减了很多,感兴趣的同学可以去react-native/Libraries/BatchedBridge/NativeModules.js底下查看,可以看到,NativeModules正是所谓的“原生模块映射表”。

        另外,可以看到,通过global全局对象可以注册JS函数供Native调用,JS可以通过global来获取Native传递过来的数据。

function genModule(
  config: ?ModuleConfig,
  moduleID: number,
): ?
  name: string,
  module?: Object,
 
  const [moduleName, constants, methods, promiseMethods, syncMethods] = config;
  const module = ;
  methods &&
    methods.forEach((methodName, methodID) => 
    ...
    );
  return name: moduleName, module;


global.__fbGenNativeModule = genModule;

let NativeModules: [moduleName: string]: Object, ... = ;
if (global.nativeModuleProxy) 
  NativeModules = global.nativeModuleProxy;
 else if (!global.nativeExtensions) 
  const bridgeConfig = global.__fbBatchedBridgeConfig;
  (bridgeConfig.remoteModuleConfig || []).forEach(
    (config: ModuleConfig, moduleID: number) => 
      const info = genModule(config, moduleID);
      if (!info) 
        return;
      
      if (info.module) 
        NativeModules[info.name] = info.module;
      
    ,
  );

        这里展示了JSCore中如何获取并执行JS中注册好的global.__fbGenNativeModule = genModule

folly::Optional<Object> JSINativeModules::createModule(
    Runtime &rt,
    const std::string &name) 
  bool hasLogger(ReactMarker::logTaggedMarker);
  if (hasLogger) 
    ReactMarker::logTaggedMarker(
        ReactMarker::NATIVE_MODULE_SETUP_START, name.c_str());
  

  if (!m_genNativeModuleJS) 
    m_genNativeModuleJS =
        rt.global().getPropertyAsFunction(rt, "__fbGenNativeModule");
  

  auto result = m_moduleRegistry->getConfig(name);
  if (!result.hasValue()) 
    return folly::none;
  

  Value moduleInfo = m_genNativeModuleJS->call(
      rt,
      valueFromDynamic(rt, result->config),
      static_cast<double>(result->index));
  CHECK(!moduleInfo.isNull()) << "Module returned from genNativeModule is null";

  folly::Optional<Object> module(
      moduleInfo.asObject(rt).getPropertyAsObject(rt, "module"));

  if (hasLogger) 
    ReactMarker::logTaggedMarker(
        ReactMarker::NATIVE_MODULE_SETUP_STOP, name.c_str());
  

  return module;

原生端调用JS

         接下来看一下原生端怎样调用JS,首先,Native在初始化时候创建好的JS模块映射表中找到需要被调用的JSModule_1,然后通过JSCore来传递被调用的模块名、方法名以及参数,传递到JS侧,JS在JS模块映射表中找到提前注册好的JSModule_1,然后执行并传入参数。这就是一个完整的原生端向JS端通信的过程。

JS端调用原生代码

        类似地,看一下JS怎样调用Native方法,首先,JS在初始化时传过来的原生模块映射表中找到需要调用的NativeModule_1,然后通过JSCore传递被调用的模块名方法名以及参数到Native侧,Native拿到方法名之后找到注册好的NativeModule_1,然后执行相应的Native方法并传入参数。

        那么,有人会想,在Native侧执行完之后,如果有返回值,怎么将返回值传到JS侧呢?当JS侧调用原生代码有返回值的时候,流程会相对复杂一些:

         如图,可以清晰的看到,如果有返回值的时候,在JS调用NativeModule_1的时候会生成一个回调ID,并将回调ID一起传递给Native,这个回调ID就是用来标识当Native传递执行结果时,JS拿到执行结果后,将执行结果分发给哪个JS变量。

接下来我们看一下JS是怎么注册到JS模块映射表中的:

const SharedStore = 
  pool: new Map(),
  setItem(key: String, value: any) 
    this.pool.set(key, value);
  ,
  getItem(key: string) 
    return this.pool.get(key);
  ,
;

const BatchedBridge = globle.__fbBatchedBridge;
BatchedBridge.registerCallableModule("SharedStore", SharedStore);

        可以看到JS方法是通过调用__fbBatchedBridge上面的registerCallableModule函数来注册的,翻到RN源码的react-native/Libraries/BatchedBridge/BatchedBridge.js处,你会发现,__fbBatchedBridge其实就是一个消息队列,registerCallableModule就是往表里添加一项而已。

class MessageQueue 
  registerCallableModule(name: string, module: Object) 
    this._lazyCallableModules[name] = () => module;
  


const BatchedBridge: MessageQueue = new MessageQueue();
Object.defineProperty(global, '__fbBatchedBridge', 
  configurable: true,
  value: BatchedBridge,
);

module.exports = BatchedBridge;

        下面看一下Android里是怎样调用JS方法的,可以看到在Android里先定义了JS模块的接口。

import com.facebook.react.bridge.JavaScriptModule
// 定义js模块的接口
public interface SharedStore extends JavaScriptModule 
  void setItem(String key, Object value)

// 在ReactNative上下文对象创建后,调用JavaScript模块
ReactInstanceManager reactInstanceManager = getReactNativeHost().getReactInstanceManager();
reactInstanceManager.addReactInstanceEventListener(
  new ReactInstanceManager.ReactInstanceEventListener() 
    @Override
    public void onReactContextInitialied(ReactContext context) 
      context.getCatalystInstance().getJSModule(SharedStore.class).setItem("example", "hello world!")
    
  
)

        在IOS内调用JS十分简单:

// 在ReactNative上下文创建后,直接调用JavaScript模块
RETBridge *bridge = //...
[bridge enqueueJSCall:@"SharedStore" method:@"setItem" args:@[@"example", @"hello world!"] completion:^
  // 调用JavaScript模块完成的回调函数
]

 

参考资料

极客时间 - 从原生方向探索React Native

知乎 - JavaScriptCore 整体介绍

知乎 - JavaScriptCore全面解析

React Native源码

以上是关于React Native深入理解Native与RN通信原理的主要内容,如果未能解决你的问题,请参考以下文章

React Native深入理解Native与RN通信原理

React Native深入理解Native与RN通信原理

《React-Native系列》3RN与native交互之CallbackPromise

《React-Native系列》RN与native交互与数据传递

React Native技术剖析

React Native技术剖析