React Native桥接器初探

本文假设你已经有必定的React Native基础,而且想要了解React Native的JS和原生代码之间是如何交互的。javascript

React Native的工做线程

  • shadow queue:布局在这个线程工做html

  • main thread:UIKit在这里工做java

  • Javascript thread:Js代码在这里工做react

另外每个原生模块都有本身的一个工做GCD queue,除非你明确指定它的工做队列ios

*shadow queue*实际是一个GCD queue,而不是一个线程。

原生模块

若是你还不知道如何建立原声模块,我建议你看看官方文档git

下面是一个叫作Person的原生模块,既能够被js调用,也能够调用js代码。github

@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_MODULERCT_EXPORT_METHOD。看看他们是如何工做的。正则表达式

RCT_EXPORT_MODULE([js_name])

这个宏的功能就和它名字说的同样,处处一个模块。可是export是什么意思呢?它的意思是让React Native的bridge(桥接)感知到原生模块。objective-c

它的定义其实很是的简单:算法

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

它的做用:

  • 首先它声明了RCTRegisterModuleextern方法,也就是说这个方法的实如今编译的时候不可知,而在link的时候才可知。

  • 声明了一个方法moduleName,这个方法返回可选的宏定义参数js_name,通常是你但愿有一个专门的模块名称,而不是默认的ObjC类名的时候使用。

  • 最后,声明了一个load方法(当app被加载进内存的时候,load方法也会被调用)。在这个方法里调用RCTRegisterModule方法来让RN的bridge感知到这个模块。

RCT_EXPORT_METHOD(method)

这个宏更有意思,它并给你的模块添加任何实际的方法。它建立了一个新的方法,这个新的方法基本上是这样的:

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

这个被load方法生成的方法的名称由前缀(__rct_export__)和一个可选的js_name(如今是空的)和声明的行号(好比12)和__COUNTER__宏拼接在一块儿组成。

这个新生成的方法的做用就是返回一个数组,这个数组包含一个可选的js_name(在本例中是空的)和方法的签名。签名说的那一堆是为了不方法崩溃。

即便是这么复杂的生成算法,若是你使用了*category*的话也不免会有两个方法的名称是同样的。不过这个几率很是低,而且也不会产生什么不可控的行为。虽然Xcode会这么警告。

Runtime

这一步只作一件事,那就是给React Native的桥接模块提供信息。这样它就能够找到原生模块里export出来的所有信息:modulesmethods,并且这些所有发生在load的时候。

下图是React Native桥接的依赖图

初始化模块

方法RCTRegisterModule方法就是用来把类添加到一个数组里,这样React Native桥接器实例建立以后能够找到这个模块。它会遍历模块数组,建立每一个模块的实例,并在桥接器里保存它的引用,而且每一个模块的实例也会保留桥接器的实例。而且该方法还会检查模块是否指定了运行的队列,若是没有指定那么就运行在一个新建的队列上,与其余队列分割。

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

配置原生模块

一旦在后台线程里有了模块实例,咱们就列出每一个模块的所有方法,以后调用__rct_export__开始的方法,这样咱们就有一个该方法签名的字符串。这样咱们后续就能够得到参数的实际类型。在运行时,咱们只会知道参数的类型是id,按照上面的方法就能够得到参数的实际类型,好比本例的是NSString*

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 */];
  }
}

初始化Javascript执行器

JavaScript执行器有一个setUp方法。用这个方法能够执行不少耗费资源的任务,好比在后台线程里初始化JavaScriptCore。因为只有active的执行器才能够接受到setUp的调用,因此也节约了不少的资源。

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

注入Json配置

模块的配置都是Json形式的,如:

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

这些都做为全局变量存储在JavaScript VM里,所以当桥接器的Js侧代码初始化完毕的时候它能够用这些信息来建立原生模块。

加载JavaScript代码

能够得到代码的地方只有两个,在开发的时候从packager下载代码,在产品环境下从磁盘加载代码。

执行JavaScript代码

一旦全部的准备工做就绪,咱们就能够把App的代码都加载到JavaScript Core里解析,执行。在最开始执行的时候,全部的CommonJS模块都会被注册(如今你写的是ES6的模块,不是CommonJS,可是最后会转码为ES5),并require入口文件。

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);

JavaScript模块

这个时候,上例中的原生模块就能够在NativeModules对象里调用了。

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

Person.greet('Tadeu');

当你调用一个原生模块的方法的时候,它会在一个队列里执行。其中包含模块名、方法名和调用这个方法须要的所有参数。在JavaScript执行结束的时候原生代码继续执行。

调用周期

下面看看若是咱们调用上面的代码会发生什么:

代码的调用从Js开始,以后开始原生代码的执行。Js传入的回调会经过桥接器(原生模块使用_bridge实例调用enqueueJSCall:args:)传回到JS代码。

注意:你若是看过文档,或者亲自实践过的话你就会知道也有从原生模块调用JS的状况。这个是用vSYNC实现的。可是这些为了改善启动时间被删除了。

参数类型

从原生调用JS的状况更简单一些,参数是作为JSON例的一个数组传递的。可是从JS到原生的调用里,咱们须要原生的类型。可是,如上文所述,对于类的对象(结构体的对象),运行时并不能经过NSMethodSignature给咱们足够的信息,咱们只有字符串类型。

咱们使用正则表达式从方法的签名里提取类型,而后咱们使用RCTConvert工具类来实际转化参数的类型。这个工具类会把JSON里的数据转化成咱们须要的类型。

咱们使用objc_msgSend来动态调用方法。若是是struct的话,则使用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()

线程

默认状况下,每个模块都有本身的GCD queue。除非在模块中经过-methodQueue方法指定模块要运行的队列。有一个例外是View Managers(就是继承了RCTViewManager)的类,会默认运行在Shadow Queue里。

目前的线程规则是这样的:

  • -init-setBridge:保证会在main thread里执行

  • 全部导出的方法都会在目标队列里执行

  • 若是你实现了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);
  }
}

总结

本文还只是对桥接器如何工做的一个简单描述。但愿对各位能有所帮助。

原文:https://tadeuzagallo.com/blog...

相关文章
相关标签/搜索