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

在这篇文章之前我们假设你已经了解了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原理内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • ReactRouter的实现方法

    ReactRouter的实现 ReactRouter是React的核心组件,主要是作为React的路由管理器,保持UI与URL同步,其拥有简单的API与强大的功能例如代码缓冲加载.动态路由匹配.以及建立正确的位置过渡处理等. 描述 React Router是建立在history对象之上的,简而言之一个history对象知道如何去监听浏览器地址栏的变化,并解析这个URL转化为location对象,然后router使用它匹配到路由,最后正确地渲染对应的组件,常用的history有三种形式: Brow

  • React.Children的用法详解

    React.Children 是顶层API之一,为处理 this.props.children 这个封闭的数据结构提供了有用的工具. this.props 对象的属性与组件的属性一一对应,但是有一个例外,就是 this.props.children 属性.它表示组件的所有子节点. 1.React.Children.map object React.Children.map(object children, function fn [, object context]) 使用方法: React.C

  • 一看就懂的ReactJs基础入门教程-精华版

    一.ReactJS简介 React 起源于 Facebook 的内部项目,因为该公司对市场上所有 JavaScript MVC 框架,都不满意,就决定自己写一套,用来架设 Instagram 的网站.做出来以后,发现这套东西很好用,就在2013年5月开源了.由于 React 的设计思想极其独特,属于革命性创新,性能出众,代码逻辑却非常简单.所以,越来越多的人开始关注和使用,认为它可能是将来 Web 开发的主流工具. ReactJS官网地址:http://facebook.github.io/re

  • React.cloneElement的使用详解

    因为要接手维护一些项目,团队的技术栈最近从 vue 转向 react ,作为一个 react 新手,加上一向喜欢通过源码来学习新的东西,就选择了通过阅读 antd 这个大名鼎鼎的项目源码来学习一些 react 的用法. 在阅读源码的过程中,发现好些组件都使用了 React.cloneElement 这个 api ,虽然通过名字可以猜测它做了什么,但是并不知道具体的作用:然后去看官方文档,文档很清晰地描述了它的作用,却没有告诉我们什么场景下需要使用它.于是我根据文档的描述,结合源码的使用,面向 g

  • 基于react hooks,zarm组件库配置开发h5表单页面的实例代码

    最近使用React Hooks结合zarm组件库,基于js对象配置方式开发了大量的h5表单页面.大家都知道h5表单功能无非就是表单数据的收集,验证,提交,回显编辑,通常排列方式也是自上向下一行一列的方式显示 , 所以一开始就考虑封装一个配置化的页面生成方案,目前已经有多个项目基于此方式配置开发上线,思路和实现分享一下. 使用场景 任意包含表单的h5页面(使用zarm库,或自行适配自己的库) 目标 代码实现简单和简洁 基于配置 新手上手快,无学习成本 老手易扩展和维护 写之前参考了市面上的一些方案

  • React+Koa实现文件上传的示例

    背景 最近在写毕设的时候,涉及到了一些文件上传的功能,其中包括了普通文件上传,大文件上传,断点续传等等 服务端依赖 koa(node.js框架) koa-router(Koa路由) koa-body(Koa body 解析中间件,可以用于解析post请求内容) koa-static-cache(Koa 静态资源中间件,用于处理静态资源请求) koa-bodyparser(解析 request.body 的内容) 后端配置跨域 app.use(async (ctx, next) => { ctx.

  • React antd tabs切换造成子组件重复刷新

    描述: Tabs组件在来回切换的过程中,造成TabPane中包含的相同子组件重复渲染,例如: <Tabs activeKey={tabActiveKey} onChange={(key: string) => this.changeTab(key)} type="card" > <TabPane tab={"对外授权"} key="/authed-by-me"> <AuthedCollections colle

  • React Hook的使用示例

    这篇文章分享两个使用React Hook以及函数式组件开发的简单示例. 一个简单的组件案例 Button组件应该算是最简单的常用基础组件了吧.我们开发组件的时候期望它的基础样式能有一定程度的变化,这样就可以适用于不同场景了.第二点是我在之前做项目的时候写一个函数组件,但这个函数组件会写的很死板,也就是上面没有办法再绑定基本方法.即我只能写入我已有的方法,或者特性.希望编写Button组件,即使没有写onClick方法,我也希望能够使用那些自带的默认基本方法. 对于第一点,我们针对不同的class

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

    在这篇文章之前我们假设你已经了解了React Native的基础知识,我们会重点关注当native和JavaScript进行信息交流时的内部运行原理. 主线程 在开始之前,我们需要知道在React Native中有三个主要的线程: shadow queue:负责布局工作 main thread:UIKit 在这个线程工作(译者注:UI Manager线程,可以看成主线程,主要负责页面交互和控件绘制的逻辑) JavaScript thread:运行JS代码的线程 另外,一般情况下每个native模

  • React Hooks核心原理深入分析讲解

    目录 Hooks 闭包 开始动手实现 将useState应用到组件中 过期闭包 模块模式 实现useEffect 支持多个Hooks Custom Hooks 重新理解Hooks规则 React Hooks已经推出一段时间,大家应该比较熟悉,或者多多少少在项目中用过.写这篇文章简单分析一下Hooks的原理,并带大家实现一个简易版的Hooks. 这篇写的比较细,相关的知识点都会解释,给大家刷新一下记忆. Hooks Hooks是React 16.8推出的新功能.以这种更简单的方式进行逻辑复用.之前

  • React中Redux核心原理深入分析

    目录 一.Redux是什么 二.Redux的核心思想 三.Redux中间件原理 四.手写一个Redux 总结 一.Redux是什么 众所周知,Redux最早运用于React框架中,是一个全局状态管理器.Redux解决了在开发过程中数据无限层层传递而引发的一系列问题,因此我们有必要来了解一下Redux到底是如何实现的? 二.Redux的核心思想 Redux主要分为几个部分:dispatch.reducer.state. 我们着重看下dispatch,该方法是Redux流程的第一步,在用户界面中通过

  • 深入理解React 三大核心属性

    目录 1.State属性 2.Props 属性 3.Refs 属性 (1)字符串形式 (2)函数回调形式 (3)createRef创建ref容器 1.State 属性 React 把组件看成是一个状态机(State Machines).通过与用户的交互,实现不同状态,然后渲染 UI,让用户界面和数据保持一致. React 里,只需更新组件的 state,然后根据新的 state 重新渲染用户界面(不要操作 DOM). import React from 'react '; import Reac

  • 深入理解React调度(Scheduler)原理

    目录 异步调度 时间分片 异步调度原理 总结 异步调度 问题:由于对于大型的 React 应用,会存在一次更新,递归遍历大量的虚拟 DOM ,造成占用 js 线程,使得浏览器没有时间去做一些动画效果,伴随项目越来越大,项目会越来越卡. 对比Vue: Vue 有这 template 模版收集依赖的过程,轻松构建响应式,使得在一次更新中,Vue 能够迅速响应,找到需要更新的范围,然后以组件粒度更新组件,渲染视图. React 中,一次更新 React 无法知道此次更新的波及范围,所以 React 选

  • 深入理解JavaScript的React框架的原理

    如果你在两个月前问我对React的看法,我很可能这样说: 我的模板在哪里?javascript中的HTML在做些什么疯狂的事情?JSX开起来非常奇怪!快向它开火,消灭它吧! 那是因为我没有理解它. 我发誓,React 无疑是在正确的轨道上, 请听我道来. Good old MVC 在一个交互式应用程序一切罪恶的根源是管理状态. "传统"的方式是MVC架构,或者一些变体. MVC提出你的模型是检验真理的唯一来源 - 所有的状态住在那里. 视图是源自模型,并且必须保持同步. 当模式的转变,

  • 浅谈React底层实现原理

    目录 1. props,state与render函数关系,数据和页面如何实现互相联动? 2. React中的虚拟DOM 常规思路 改良思路(仍使用DOM) React的思路 深入理解虚拟DOM 3. 虚拟DOM的diff算法 4. React中ref的使用 5. React中的生命周期函数 6. 生命周期函数的使用场景 1. props,state与render函数关系,数据和页面如何实现互相联动? 当组件的state或者props发生改变的时候,自己的render函数就会重新执行.注意:当父组

  • react  Suspense工作原理解析

    目录 Suspense 基本应用 Suspense 原理 基本流程 源码解读 - primary 组件 源码解读 - 异常捕获 源码解读 - 添加 promise 回调 源码解读-Suspense 首次渲染 primary 组件加载完成前的渲染 primary 组件加载完成时的渲染 利用 Suspense 自己实现数据加载 Suspense 基本应用 Suspense 目前在 react 中一般配合 lazy 使用,当有一些组件需要动态加载(例如各种插件)时可以利用 lazy 方法来完成.其中

  • ES6 class类链式继承,实例化及react super(props)原理详解

    本文实例讲述了ES6 class类链式继承,实例化及react super(props)原理.分享给大家供大家参考,具体如下: class定义类是es6提供的新的api,比较直观,class类继承也有着一定的规律性,在egg, webpack等库的源码中有着很多的应用场景.结合一些初学者可能遇到的难点,本文主要对其链式继承进行总结,关于super关键字的使用请参考我的其他文章es6中super关键字的理解. class定义 class App { constructor(options){ su

  • 浅谈React双向数据绑定原理

    目录 什么是双向数据绑定 实现双向数据绑定 数据影响视图 视图影响数据 如果已经学过Vue,并且深入了解过Vue的双向数据绑定的话,就会明白 Vue 2.0 双向数据绑定的核心其实是通过Object.defineProperty来实现数据劫持和监听. 在 Vue 3.0 中则通过 Proxy来实现对整体对象的监听,对 Vue2.0 的优化. 什么是双向数据绑定 数据模型和视图之间的双向绑定. 当数据发生变化的时候,视图也就发生变化,当视图发生变化的时候,数据也会跟着同步变化:可以这样说用户在视图

随机推荐