Skip to content

Bridging in React Native

Published: at 12:00 AM

这篇文章翻译自(https://tadeuzagallo.com/blog/react-native-bridge/)

如果您已经了解了rn(react-native)的基础,并且想了解js和native是如何交互的内部机制,那么欢迎看这篇文章.

Main Threads

再开始之前,需要了解rn中有3个主线程:

”shadow quene” 实际上如名字所示是GCD Queen而不是一个线程

Native Modules

如何你还不知道如何创建原生模块,那么建议读读 官方文档。 下面是一个Person原生模块的例子,既接收来自js的调用,也调用js。

    @interface Person : NSObject <RCTBridgeModule>
    @end

    @implementation Logger=============

    RCT_EXPORT_MODULE()

    RCT_EXPORT_METHOD(greet:(NSString *)name)
    {
      NSLog(@"Hi, %@!", name);
      [_bridge.eventDispatcher sendAppEventWithName:@"greeted"
                                               body:@{ @"name": name }];
    }

    @end

我们将着重分析 RCT_EXPORT_MODULE 和 RCT_EXPORT_METHOD,看看他们扩展层什么,他们的角色,以及他们是如何工作的。

RCT_EXPORT_MODULE([js_name])

正如名字暗示的,它导出你的模块,但是在这个环境中export真正的含义是什么呢?它意味着让桥梁注意到你注册的模块。

它的定义很简单:

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

它究竟做了什么了呢:

RCT_EXPORT_METHOD(method)

这个概念就更有意思了,它没有添加任何东西到你实际的方法上,它实际上创建了一个新方法,并且定义了指定的这个方法。

这个新的方法就长成下面这样:

      + (NSArray *)__rct_export__120
      {
        return @[ @"", @"log:(NSString *)message" ];
      }

它是由前缀(rct_export)和可选的 js_name (这里是空的)以及定义的行号(例如: 12),还有COUNTER__,这几部分拼接成的。 这个方法的目的仅仅是返回包含可选的 js_name 和方法签名的数组,这个名字的hack仅仅是避免命名冲突。

Runtime

上面这些全部的设置仅仅是给桥梁提供信息,所以桥能够找到导出的全部模块和相应的方法,然而这些都发生在加载的时候,现在让我们看看在运行时都发生了什么。

下面是桥的初始化依赖图:

Initialise Modules

RCTRegisterModule函数做的全部事情就是把class添加到一个数组中,所以稍后桥的实例创建成功的时候,让桥能够找到它。桥遍历模块数组,为每个模块创建一个实例,并且为桥建立一个引用,同时让模块也能引用到桥,这样就建立了一个双向的联系,并且检查模块是否指定了要在那个queue运行,如果没有指定的化,我们就为其建立一个独立与其他所有模块的单独的queue。

      NSMutableDictionary *modulesByName; // = ...
      for (Class moduleClass in RCTGetModuleClasses()) {
      // ...
      module = [moduleClass new];
      if ([module respondsToSelector:@selector(setBridge:)]) {
      module.bridge = self;
      }
      modulesByName[moduleName] = module;
      // ...
      }

Configure Modules

一旦我们在后台后台线程中有了模块,我们列出每个模块对应的所有方法,并且调用名字以rct_export开头的方法,所以我们得到了代表方法签名的字符串。所以我们就得到了参数的实际类型,也就是,在运行时我们仅仅能够知道一个参数是一个id,这样我们能知道,在这种情况下,它实际上是NSString*(iOS中的字符串类型)类型的。

      unsigned int methodCount;
      Method *methods = class_copyMethodList(moduleClass, &methodCount);
      for (unsigned int i = 0; i < methodCount; i++) {
        Method method = methods[i];
        SEL selector = method_getName(method);
        if ([NSStringFromSelector(selector) hasPrefix:@"__rct_export__"]) {
          IMP imp = method_getImplementation(method);
          NSArray *entries = ((NSArray *(*)(id, SEL))imp)(_moduleClass, selector);
          //...
          [moduleMethods addObject:/* Object representing the method */];
        }
      }

Setup JavaScript Executor

JavaScript执行器有个-setUp方法,这个方法允许其在后台线程中做类似于初始化JavaScriptCore这样的昂贵的任务。因为只有活跃的执行器才能接收setUp方法,导致也能省去一些工作。

      JSGlobalContextRef ctx = JSGlobalContextCreate(NULL);
      _context = [[RCTJavaScriptContext alloc] initWithJSContext:ctx];

Inject JSON Configuration

JSON Configuration仅仅包含我们的模块,下面是代码:

    {
      "remoteModuleConfig": {
        "Logger": {
          "constants": { /* If we had exported constants... */ },
          "moduleID": 1,
          "methods": {
            "requestPermissions": {
              "type": "remote",
              "methodID": 1
            }
          }
        }
      }
    }

这个对象作为全局变量存储在JavaScript VM中,所以当桥的js侧初始化的时候,就能获取相关信息创建模块。

Load JavaScript Code

这个方法很有新意,从任何容器中载入源码。通常开发过程是从包文件中下载源代码,或者在产品模式则从磁盘加载。

Execute JavaScript Code

一旦所有事情都准备就绪,我们在JavaScriptCore VM中加载应用源代码,通过拷贝,粘贴再执行。 首次执行的时候,它注册所有的CommonJS模块,然后获取入口文件。

    JSValueRef jsError = NULL;
    JSStringRef execJSString = JSStringCreateWithCFString((__bridge CFStringRef)script);
    JSStringRef jsURL = JSStringCreateWithCFString((__bridge CFStringRef)sourceURL.absoluteString);
    JSValueRef result = JSEvaluateScript(strongSelf->_context.ctx, execJSString, NULL, jsURL, 0, &jsError);
    JSStringRelease(jsURL);
    JSStringRelease(execJSString);

Modules in JavaScript

JavaScript通过rn的NativeModules对象可以获取到,通过上面的JSON configuration生成的模块。例如:

    var { NativeModules } = require('react-native');
    var { Person } = NativeModules;

    Person.greet('Tadeu');

它工作的方式是,当你带着模块名字,方法名字以及所有相关参数调用方法的时候,会进入一个队列。在JavaScript执行的尾声,这个队列就会通过执行这次调用返回到native端。

Call cycle

那么,当我们在js端通过上面的代码调用一个模块的时候,如下是将会发生的:

调用必须起始于native端,进入js端,在执行过程中,当调用NativeModules的方法的时候,它会把必须在native端执行的调用推入队列中。这样当js结束的时候,native端遍历所有的必须在native端执行的调用,并且当执行它的时候,经过桥(既是通过native模块调用enqueueJSCall:args:得到的_bridge 实例)的回调和调用就又用作回调进入js端。

Argument types

对于从native到js的调用容易,参数以NSArray的形式传递,这个NSArray我们编码成JSON格式,但是对于来自于js的调用,我们需要指定native类型,为此,我们显示的检查原生类型(例如ints,floats,chars等),但是如上所述,对于任何对象,运行时(runtime)并没有给我们提供足够的来自于方法签名的信息,况且我们把参数类型保存为字符串。 我们利用正则表达式来从方法签名中抽取出类型,然后用RCTConvert实用class明确转换对象,这个class默认的对于每个支持的类型提供一个方法,而且会尝试把JSON类型的输入转化为想要的类型。 我们用objc_msgSend动态调用方法,但是遇到struct类型的,由于在arm64上没有 objc_msgSend_stret的版本,所以我们就落在了NSInvocation。 一旦我们把所有的参数都转化好以后,我们用另一个NSInvocation来调用目标模块和方法。 下面是例子:

    // If you had the following method in a given module, e.g. `MyModule`
    RCT_EXPORT_METHOD(methodWithArray:(NSArray *) size:(CGRect)size) {}

    // And called it from JS, like:
    require('NativeModules').MyModule.method(['a', 1], {
    x: 0,
    y: 0,
    width: 200,
    height: 100
    });

    // The JS queue sent to native would then look like the following:
    // ** Remember that it's a queue of calls, so all the fields
    // are arrays **
    @[
    @[ @0 ], // module IDs
    @[ @1 ], // method IDs
    @[       // arguments
    @[
      @[@"a", @1],
      @{ @"x": @0, @"y": @0, @"width": @200, @"height": @100 }
    ]
    ]
    ];

    // This would convert into the following calls (pseudo code)
    NSInvocation call
    call[args][0] = GetModuleForId(@0)
    call[args][1] = GetMethodForId(@1)
    call[args][2] = obj_msgSend(RCTConvert, NSArray, @[@"a", @1])
    call[args][3] = NSInvocation(RCTConvert, CGRect, @{ @"x": @0, ... })
    call()

Threading

如上所述,每个模块默认会有自己的GCD quene,当然了除非是通过实施-methodQueue方法,或者利用有效队列来同步方法队列,来明确指定想要运行的队列。例外是View Managers _ (继承自RCTViewManager),它默认走了Shadow Quene,以及特殊目标RCTJSThread,因为它是一个队列而不是线程,所以它仅仅是一个占位。 当前的线程规则是这样的: _ -init 和 -setBridge: 确保在主线程被调用 _ 所有导出的方法确定的在目标队列被调用 _ 如果实施了RCTInvalidating协议,invalidate也确保在目标队列中调用 * 对于-dealloc来说,没有确定的队列来执行 当接收到一大批来自于js的调用时,这些调用会根据目标队列来分组,并且会并行的分发:

    // group `calls` by `queue` in `buckets`
    for (id queue in buckets) {
      dispatch_block_t block = ^{
        NSOrderedSet *calls = [buckets objectForKey:queue];
        for (NSNumber *indexObj in calls) {
          // Actually call
        }
      };
      if (queue == RCTJSThread) {
        [_javaScriptExecutor executeBlockOnJavaScriptQueue:block];
      } else if (queue) {
        dispatch_async(queue, block);
      }
    }

The end