React Native原理 深入理解React Native核心原理(React Native的桥接(Bridge)

软件发布|下载排行|最新软件

当前位置:首页IT学院IT技术

React Native原理 深入理解React Native核心原理(React Native的桥接(Bridge)

Gavell   2021-04-06 我要评论
想了解深入理解React Native核心原理(React Native的桥接(Bridge)的相关内容吗,Gavell在本文为您仔细讲解React Native原理的相关知识和一些Code实例,欢迎阅读和指正,我们先划重点:React,Native原理,React,Native桥接,下面大家一起来学习吧。

在这篇文章之前我们假设你已经了解了React Native的基础知识,我们会重点关注当native和JavaScript进行信息交流时的内部运行原理。

主线程

在开始之前,我们需要知道在React Native中有三个主要的线程:

  • shadow queue:负责布局工作
  • main thread:UIKit 在这个线程工作(译者注:UI Manager线程,可以看成主线程,主要负责页面交互和控件绘制的逻辑)
  • JavaScript thread:运行JS代码的线程

另外,一般情况下每个native模块都有自己的GCD队列,除非有特殊说明(后面会解释)

*shadow queue其实更像一个GCD队列而不是线程

Native模块

如果你还不知道怎么创建一个Native模块,我推荐你去阅读一下文档

这是一个native模块Person的例子,它既受JavaScript的调用,也可以调用JavaScript

@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出你的module,但是在这个特定的上下文中export是什么意思呢,它意味着桥接知道你的模块。

它的定义实际上非常简单:

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

它做了以下工作:

  • 首先声明RCTRegisterModule为外部函数,意味着这个函数的实现对于编译器不可见,但是在链接阶段可用
  • 声明一个方法moduleName,返回可选的宏参数js_name,这样这个模块在JS中具有和Objective-C中不一样的类名
  • 声明一个load方法(当app加载到内存中后,每个类的load方法都会被调用),load方法调用RCTRegisterModule,然后桥接才知道这个暴露出来的模块

RCT_EXPORT_METHOD(method)

这个宏更有趣,它没有在你的method中增加任何东西,除了声明指定的方法外,它还创建了一个新方法。新方法如下所示:

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

它是通过将前缀(__rct_export__)和可选的js_name(本例子为空)和声明的行号以及__COUNTER__宏构成。

这个方法的目的是返回一个包含可选js_name和method签名的数组,这个js_name的作用是避免方法命名冲突。

Runtime

这整个设置仅仅是为了给桥接提供信息,让它可以找到export出来的所有东西,modules和methods,但是这些都是在加载的时候发生的,现在我们来看看运行的时候是怎么使用的。

这是桥接初始化时的依赖关系图:

初始化模块

RCTRegisterModule所做的事就是把类推进数组,这样在实例化一个新的桥接的时候就能找到这个类。桥接遍历数组中的所有模块,为每个模块创建一个实例,在桥接那边存储一个实例的引用,同时给这个模块实例一个桥接的引用(所以我们能两边都互相调用),然后检查这个模块实例是否有指定要在哪个队列运行,否则给它一个新队列,与其他模块分开:

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

配置模块

一旦我们有了这些modules,在后台线程中,我们列出每个module的所有methods,然后调用以__rct__export__开头的methods,我们得到一个method签名的字符串。这很重要因为我们现在知道了参数的实际类型,在运行的时候我们只知道其中一个参数是id,但是通过这个途径我们可以知道这个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执行器

JS执行器有一个 -setUp 方法允许它做更复杂的工作,例如在后台线程初始化JS代码,这同时节约了一些工作,因为只有活跃的执行器会接受 setUp 方法的调用,而不是所有的执行器:

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

注入JSON配置

JSON配置仅包含我们的module,例如:

这个配置信息作为全局变量存储在JavaScript虚拟机,所以当JS那边的桥接初始化后它可以用这个信息来创建modules

加载JavaScript代码 

这非常直观,只需要从指定的任何提供程序中加载源代码,通常在开发过程中从打包程序中加载源代码,在生产环境中从磁盘加载。

执行JavaScript代码

一旦所有事情准备就绪,我们可以在JS虚拟机中加载应用的源代码,复制代码,解析并执行它。在第一次执行时需要注册所有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);

JavaScript中的Modules

在JS侧我们现在可以通过react-native的NativeModules拿到前面的JSON配置信息构成的module:

它运行的方式是当你调用一个方法的时候它被放到一个队列,包括module的名称,method的名称以及所有的参数,在JavsScript执行的最后这个队列会给原生模块执行。

调用周期

现在如果我们用上面的代码调用module,它将会是这个样子的:

调用必须从native开始,native调用JS(这张图只是截取了JS运行的某个时刻),在执行过程中,因为JS调用NativeModules的方法,它把这个调用入队,因为这个调用必须在原生那边执行。当JS执行完后,原生模块遍历入队的所有调用,然后当它执行这些调用后,通过桥接进行回调(一个原生模块可以通过_bridge实例来调用enqueueJSCall:args:),来再次回调JS。

(如果您一直在关注该项目,过去也有来自native-> JS的调用队列,该调用队列会在每个vSYNC上分派,但为了缩短启动时间已将其删除)

参数类型

native到JS的调用很容易,参数被NSArray传递,我们将其编码为JSON数据,但是对于JS对native的调用,我们需要native的类型,为此我们检查基本类型(ints,floats,chars...)但是就像上边提及那样,对于任何对象(结构),运行时我们不会从NSMthodSignature获得足够的信息,所以我们把类型保存为字符串。

我们使用正则表达式从method签名中提取类型,并使用RCTConvert类来实际转换对象,默认情况下它为每种类型都提供了方法,并且尝试将JSON输入转换为所需要的类型。

除非是一个struct,否则我们使用objc_msgSend动态调用该方法,因为arm64上没有objc_msgSend_stret的版本,因此我们使用NSInvocation。

转换完所有参数后,我们将使用另一个NSInvocation来调用目标module和method。

例子:

// 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()

线程

正如以上提及那样,每个module默认都有一个GCD队列,除非它通过实现-methodQueue方法或将methodQueue属性与有效队列合并来指定要在哪个队列运行。ViewManagers*是例外(扩展了RCTViewManager),将默认使用Shadow Queue,而特殊目标RCTJSThread仅是一个占位符,因为它是线程而不是队列。

(其实View Managers不是真正的例外,因为基类显式的将Shadow Queue指定为目标队列了)

当前线程规则如下:

  • -init和-setBridge:保证在主线程执行
  • 所有export的方法保证在目标队列执行
  • 如果你实现了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); 
 } 
}

结尾

这就是React Native桥接工作原理的更深入概述。我希望者对想要构建更复杂modules或者想对核心框架有贡献的人有所帮助。

Copyright 2022 版权所有 软件发布 访问手机版

声明:所有软件和文章来自软件开发商或者作者 如有异议 请与本站联系 联系我们