在这篇文章之前我们假设你已经了解了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或者想对核心框架有贡献的人有所帮助。

到此这篇关于深入理解React Native核心原理(React Native的桥接(Bridge)的文章就介绍到这了,更多相关React Native原理内容请搜索Devmax以前的文章或继续浏览下面的相关文章希望大家以后多多支持Devmax!

深入理解React Native核心原理(React Native的桥接(Bridge)的更多相关文章

  1. ios – React native链接到另一个应用程序

    如果是错误的,有人知道如何调用正确的吗?

  2. ios – React Native – 在异步操作后导航

    我正在使用ReactNative和Redux开发移动应用程序,我正面临着软件设计问题.我想调用RESTAPI进行登录,如果该操作成功,则导航到主视图.我正在使用redux和thunk所以我已经实现了异步操作,所以我的主要疑问是:我应该把逻辑导航到主视图?我可以直接从动作访问导航器对象并在那里执行导航吗?.我对组件中的逻辑没有信心.似乎不是一个好习惯.有没有其他方法可以做到这一点?

  3. 在ios中使用带有React Native(0.43.4)的cocoapods的正确方法是什么?

    我已经挖掘了很多帖子试图使用cocoapods为本地ios库设置一个反应原生项目,但我不可避免地在#import中找到了丢失文件的错误.我的AppDelegate.m文件中的语句.什么是使用反应原生的可可豆荚的正确方法?在这篇文章发表时,我目前的RN版本是0.43.4,而我正在使用Xcode8.2.1.这是我的过程,好奇我可能会出错:1)

  4. ios – React Native WebView滚动行为无法按预期工作

    如何确保滚动事件的行为与ReactNative应用程序中的浏览器相同?

  5. ios – React Native – BVLinearGradient – 找不到’React/RCTViewManager.h’文件

    谢谢.解决方法几天前我遇到了完全相同的问题.问题是在构建应用程序时React尚未链接.试试这个:转到Product=>Scheme=>管理方案…=>点击你的应用程序Scheme,然后点击Edit=>转到Build选项卡=>取消选中ParallelizeBuild然后点击标志添加目标=>搜索React,选择第一个名为React的目标,然后单击Add然后在目标列表中选择React并将其向上拖动到该列表中的第一个.然后转到Product=>再次清理并构建项目.这应该有所帮助.

  6. ios – React Native – NSNumber无法转换为NSString

    解决方法在你的fontWeight()函数中也许变成:

  7. ios – React native error – react-native-xcode.sh:line 45:react-native:command not found命令/ bin/sh失败,退出代码127

    尝试构建任何(新的或旧的)项目时出现此错误.我的节点是版本4.2.1,react-native是版本0.1.7.我看过其他有相同问题的人,所以我已经更新了本机的最新版本,但是我仍然无法通过xcode构建任何项目.解决方法要解决此问题,请使用以下步骤:>使用节点版本v4.2.1>cd进入[你的应用]/node_modules/react-native/packager>$sh./packager.s

  8. 反应原生 – 如何通过Xcode构建React Native iOS应用程序到设备?

    我试图将AwesomeProject应用程序构建到设备上.构建成功并启动屏幕显示,但后来我看到一个红色的“无法连接到开发服务器”屏幕.它表示“确保节点服务器正在运行–从Reactroot运行”npmstart“.看起来节点服务器已经运行,因为当我做npm启动时,我收到一个EADDRINUSE消息,表示该端口已经在使用.解决方法从设备访问开发服务器您可以使用开发服务器快速迭代设备.要做到这一点,你的

  9. 静音iOS推送通知与React Native应用程序在后台

    我有一个ReactNative应用程序,我试图获得一个发送到JavaScript处理程序的静默iOS推送通知.我看到的行为是AppDelegate中的didReceiveRemoteNotification函数被调用,但是我的JavaScript中的处理程序不会被调用,除非应用程序在前台,或者最近才被关闭.我很困惑的事情显然是应用程序正在被唤醒,并且它的didReceiveRemoteNotifi

  10. 如何为iOS的React Native设置分析

    所以我已经完成了一个针对iOS的ReactNative项目,但是我想在其中分析.我尝试了react-native-google-analytics软件包,但是问题阻止了它的正常工作.此外,react-native-cordova-plugin软件包只适用于Android,因此插入Cordova插件进行分析的能力现在已成为问题.我也没有Swift/ObjectiveC的经验,所以将完全失去GA的插入.有没有人有任何建议如何连接GoogleAnalytics的ReactNativeforiOS?

随机推荐

  1. js中‘!.’是什么意思

  2. Vue如何指定不编译的文件夹和favicon.ico

    这篇文章主要介绍了Vue如何指定不编译的文件夹和favicon.ico,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教

  3. 基于JavaScript编写一个图片转PDF转换器

    本文为大家介绍了一个简单的 JavaScript 项目,可以将图片转换为 PDF 文件。你可以从本地选择任何一张图片,只需点击一下即可将其转换为 PDF 文件,感兴趣的可以动手尝试一下

  4. jquery点赞功能实现代码 点个赞吧!

    点赞功能很多地方都会出现,如何实现爱心点赞功能,这篇文章主要为大家详细介绍了jquery点赞功能实现代码,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  5. AngularJs上传前预览图片的实例代码

    使用AngularJs进行开发,在项目中,经常会遇到上传图片后,需在一旁预览图片内容,怎么实现这样的功能呢?今天小编给大家分享AugularJs上传前预览图片的实现代码,需要的朋友参考下吧

  6. JavaScript面向对象编程入门教程

    这篇文章主要介绍了JavaScript面向对象编程的相关概念,例如类、对象、属性、方法等面向对象的术语,并以实例讲解各种术语的使用,非常好的一篇面向对象入门教程,其它语言也可以参考哦

  7. jQuery中的通配符选择器使用总结

    通配符在控制input标签时相当好用,这里简单进行了jQuery中的通配符选择器使用总结,需要的朋友可以参考下

  8. javascript 动态调整图片尺寸实现代码

    在自己的网站上更新文章时一个比较常见的问题是:文章插图太宽,使整个网页都变形了。如果对每个插图都先进行缩放再插入的话,太麻烦了。

  9. jquery ajaxfileupload异步上传插件

    这篇文章主要为大家详细介绍了jquery ajaxfileupload异步上传插件,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  10. React学习之受控组件与数据共享实例分析

    这篇文章主要介绍了React学习之受控组件与数据共享,结合实例形式分析了React受控组件与组件间数据共享相关原理与使用技巧,需要的朋友可以参考下

返回
顶部