Bridge and React Native App Execution

基于0.18.1

Async batched bridge used to communicate with the JavaScript application.

分析Objective-C和JavaScript的通信机制。

Bridge承担以下工作(或者提供接口):

A: 执行JavaScript代码

 - (void)enqueueJSCall:(NSString *)moduleDotMethod args:(NSArray *)args;

B: 管理"bridge module"

 - (id)moduleForName:(NSString *)moduleName;
- (id)moduleForClass:(Class)moduleClass;

C: 创建 JavaScript 执行器

 - (void)initModules
{
......
_javaScriptExecutor = [self moduleForClass:self.executorClass];

1. 寻找起点-RCTRootView

在React-Native Based的工程中, 我们看到在AppDelegate.m文件中有以下代码:

   RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
moduleName:@"AwesomeProject"
launchOptions:launchOptions]; self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
UIViewController *rootViewController = [[UIViewController alloc] init];
rootViewController.view = rootView;

之所以可以使用JS来进行iOS App开发,RCTRootView类是可以进行探索其原因的起点。

2. RCTBridge

接下来浏览类RCTRootView源码,RCTRootView是UIView的子类,并且很简单。其中有一个属性:

 @property (nonatomic, strong, readonly) RCTBridge *bridge;

通读类RCTBridge的代码,只有很少的200~300行。但是发现类RCTBatchedBridge继承自类RCTBridge。

类RCTBatchedBridge是Bridge模块的一个私有类,只被在类RCTBridge中被使用。这样设计使得接口和繁复的实现

分离。

3. RCTBatchedBridge

TODO: Rewrite this section to match the modification in version 0.18.1

观察类RCTBatchedBridge的initiailizer,发现对接口的'initJS'的调用。在这里终于和JS '发生关系'。

 - (instancetype)initWithParentBridge:(RCTBridge *)bridge
{
// 省略多行代码
// ......
// ......
/**
* Initialize and register bridge modules *before* adding the display link
* so we don't have threading issues
*/
[self registerModules]; // A /**
* Start the application script
*/
[self initJS]; // B }
return self;
}

在查看initJS方法的代码之前,我们先来关注比较重要的方法 registerModules。

3.0 Module是什么东西?

Module 在React Native中实际上是 可以被 JavaScript 代码调用的模块, 实现了接口RCTBridgeModule的类。

Module 包含有 Native类型, JavaScript源码类型。

A): Native类型:

由Objective-C来实现相应的功能,并将接口提供给JavaScript代码调用。

B): JavaScript源码类型:

也就是用JavaScript写的React Native App。这个类型的Module由 RCTSourceCode类 来代表。

3.1 registerModules 方法

TODO: Rewrite this section to match the modification in version 0.18.1

 - (void)registerModules
{
RCTAssertMainThread(); // Register passed-in module instances
NSMutableDictionary *preregisteredModules = [[NSMutableDictionary alloc] init];
for (id<RCTBridgeModule> module in self.moduleProvider ? self.moduleProvider() : nil) { // A
preregisteredModules[RCTBridgeModuleNameForClass([module class])] = module;
} // Instantiate modules
_moduleDataByID = [[NSMutableArray alloc] init];
NSMutableDictionary *modulesByName = [preregisteredModules mutableCopy];
for (Class moduleClass in RCTGetModuleClasses()) { // B
NSString *moduleName = RCTBridgeModuleNameForClass(moduleClass); // Check if module instance has already been registered for this name
id<RCTBridgeModule> module = modulesByName[moduleName]; if (module) {
// Preregistered instances takes precedence, no questions asked
if (!preregisteredModules[moduleName]) {
// It's OK to have a name collision as long as the second instance is nil
RCTAssert([[moduleClass alloc] init] == nil,
@"Attempted to register RCTBridgeModule class %@ for the name "
"'%@', but name was already registered by class %@", moduleClass,
moduleName, [modulesByName[moduleName] class]);
}
if ([module class] != moduleClass) {
RCTLogInfo(@"RCTBridgeModule of class %@ with name '%@' was encountered "
"in the project, but name was already registered by class %@."
"That's fine if it's intentional - just letting you know.",
moduleClass, moduleName, [modulesByName[moduleName] class]);
}
} else {
// Module name hasn't been used before, so go ahead and instantiate
module = [[moduleClass alloc] init];
}
if (module) {
modulesByName[moduleName] = module;
}
} // Store modules
_modulesByName = [[RCTModuleMap alloc] initWithDictionary:modulesByName]; /**
* The executor is a bridge module, wait for it to be created and set it before
* any other module has access to the bridge
*/
_javaScriptExecutor = _modulesByName[RCTBridgeModuleNameForClass(self.executorClass)]; // C
RCTLatestExecutor = _javaScriptExecutor; [_javaScriptExecutor setUp]; // Set bridge
for (id<RCTBridgeModule> module in _modulesByName.allValues) { // D
if ([module respondsToSelector:@selector(setBridge:)]) {
module.bridge = self;
} RCTModuleData *moduleData = [[RCTModuleData alloc] initWithExecutor:_javaScriptExecutor
uid:@(_moduleDataByID.count)
instance:module];
[_moduleDataByID addObject:moduleData]; if ([module conformsToProtocol:@protocol(RCTFrameUpdateObserver)]) {
[_frameUpdateObservers addObject:moduleData];
}
}
// E
[[NSNotificationCenter defaultCenter] postNotificationName:RCTDidCreateNativeModules
object:self];
}

A): A部分用来注册外部传递进来的Module。由于RCTBridge创建RCTBatchedBridge对象时,传入的参数导致

属性 self.moduleProvider 的值为nil,故我们先跳过这部分,直接跳到B部分。

B): B部分的循环是将加载的ModuleClass进行注册,注册到成员变量 '_modulesByName'中。

(其中的 RCTGetModuleClasses()和 RCTBridgeModuleNameForClass()

参见 iOS.ReactNative-3-about-viewmanager-uimanager-and-bridgemodule 中的说明)

C): 从'_modulesByName'中获取javaScriptExecutor,JS Executor是关键所在,JS Executor是执行JS代码的。

在 initJS 方法中会用到。

D): 为ModuleObject(模块对象/模块实例, 或者简称: 模块)设置bridge以及ModuleData(模块元数据),最后将实现接口RCTFrameUpdateObserver

的模块对象添加到'_frameUpdateObservers' 中。

E): 发送通知, NativeModules已创建完毕。TODO: 该通知的观察者做了哪些工作?

3.2 initJS 方法

TODO: Rewrite this section to match the modification in version 0.18.1

 - (void)initJS
{
RCTAssertMainThread(); // Inject module data into JS context
NSMutableDictionary *config = [[NSMutableDictionary alloc] init]; // A
for (RCTModuleData *moduleData in _moduleDataByID) {
config[moduleData.name] = moduleData.config;
}
NSString *configJSON = RCTJSONStringify(@{
@"remoteModuleConfig": config,
}, NULL);
[_javaScriptExecutor injectJSONText:configJSON
asGlobalObjectNamed:@"__fbBatchedBridgeConfig"
callback:^(NSError *error) {
if (error) {
[[RCTRedBox sharedInstance] showError:error];
}
}]; NSURL *bundleURL = _parentBridge.bundleURL;
if (_javaScriptExecutor == nil) { /**
* HACK (tadeu): If it failed to connect to the debugger, set loading to NO
* so we can attempt to reload again.
*/
_loading = NO; } else if (!bundleURL) { // Allow testing without a script
dispatch_async(dispatch_get_main_queue(), ^{
_loading = NO;
[[NSNotificationCenter defaultCenter] postNotificationName:RCTJavaScriptDidLoadNotification
object:_parentBridge
userInfo:@{ @"bridge": self }];
});
} else {
RCTProfileBeginEvent();
RCTPerformanceLoggerStart(RCTPLScriptDownload);
RCTJavaScriptLoader *loader = [[RCTJavaScriptLoader alloc] initWithBridge:self]; // B
[loader loadBundleAtURL:bundleURL onComplete:^(NSError *error, NSString *script) {
RCTPerformanceLoggerEnd(RCTPLScriptDownload);
RCTProfileEndEvent(@"JavaScript download", @"init,download", @[]); _loading = NO;
if (!self.isValid) {
return;
} static BOOL shouldDismiss = NO;
if (shouldDismiss) {
[[RCTRedBox sharedInstance] dismiss];
}
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
shouldDismiss = YES;
}); RCTSourceCode *sourceCodeModule = self.modules[RCTBridgeModuleNameForClass([RCTSourceCode class])];
sourceCodeModule.scriptURL = bundleURL;
sourceCodeModule.scriptText = script;
if (error) { NSArray *stack = [error userInfo][@"stack"];
if (stack) {
[[RCTRedBox sharedInstance] showErrorMessage:[error localizedDescription]
withStack:stack];
} else {
[[RCTRedBox sharedInstance] showErrorMessage:[error localizedDescription]
withDetails:[error localizedFailureReason]];
} NSDictionary *userInfo = @{@"bridge": self, @"error": error};
[[NSNotificationCenter defaultCenter] postNotificationName:RCTJavaScriptDidFailToLoadNotification
object:_parentBridge
userInfo:userInfo]; } else { [self enqueueApplicationScript:script url:bundleURL onComplete:^(NSError *loadError) { // C

if (loadError) {
[[RCTRedBox sharedInstance] showError:loadError];
return;
} /**
* Register the display link to start sending js calls after everything
* is setup
*/
NSRunLoop *targetRunLoop = [_javaScriptExecutor isKindOfClass:[RCTContextExecutor class]] ? [NSRunLoop currentRunLoop] : [NSRunLoop mainRunLoop];
[_jsDisplayLink addToRunLoop:targetRunLoop forMode:NSRunLoopCommonModes]; // D
      // E
[[NSNotificationCenter defaultCenter] postNotificationName:RCTJavaScriptDidLoadNotification
object:_parentBridge
userInfo:@{ @"bridge": self }];
}];
}
}];
}
}

A): 将每个ModuleObject 元数据中的config 注册到 JS Executor中。(config 参见 5. RCTModuleData)

B): 拉取JS Bundler, 可以将JS Bundler看作JS代码的'包'。

C): 执行Application JS code。(参见 4. JS Executor)

D): 将'_jsDisplayLink'添加到runloop中。'_jsDisplayLink'周期性的触发的工作是什么?

E): 发送通知 RCTJavaScriptDidLoadNotification。该通知的观察者进行了哪些处理?

到此,焦点会集中到JS Executor上面,接下来进行JS Executor的代码阅读。

3.3 invalidate 方法

TODO: Rewrite this section to match the modification in version 0.18.1

3.4 React Native App源码的执行

3.4.1 执行步骤

1: RCTBatchedBridge类的init方法中调用start方法来启动React Native App

 - (instancetype)initWithParentBridge:(RCTBridge *)bridge
{
....... [self start]; // 1
}
return self;
}

2: 在start方法的最后,模块初始化完毕,并且(React Native App的)源码加载完毕后执行源码。

模块的初始化包含两个部分:

A) JavaScript 模块的初始化

B) Native 模块的初始化。方法 initModules 完成了 Native模块的初始化。

 - (void)start
{
......
// Synchronously initialize all native modules that cannot be loaded lazily
[self initModules];

......
dispatch_group_notify(initModulesAndLoadSource, dispatch_get_main_queue(), ^{
RCTBatchedBridge *strongSelf = weakSelf;
if (sourceCode && strongSelf.loading) {
dispatch_async(bridgeQueue, ^{
[weakSelf executeSourceCode:sourceCode]; // 2
});
}
});

3: executeSourceCode方法调用方法enqueueApplicationScript来执行(React Native App的)JavaScript源码。

 - (void)executeSourceCode:(NSData *)sourceCode
{
...... RCTSourceCode *sourceCodeModule = [self moduleForClass:[RCTSourceCode class]];
sourceCodeModule.scriptURL = self.bundleURL;
sourceCodeModule.scriptData = sourceCode; [self enqueueApplicationScript:sourceCode url:self.bundleURL onComplete:^(NSError *loadError) { // 3
...... // Register the display link to start sending js calls after everything is setup
NSRunLoop *targetRunLoop = [_javaScriptExecutor isKindOfClass:[RCTJSCExecutor class]] ? [NSRunLoop currentRunLoop] : [NSRunLoop mainRunLoop];
[_jsDisplayLink addToRunLoop:targetRunLoop forMode:NSRunLoopCommonModes]; // Perform the state update and notification on the main thread, so we can't run into
// timing issues with RCTRootView
dispatch_async(dispatch_get_main_queue(), ^{
[self didFinishLoading];
[[NSNotificationCenter defaultCenter]
postNotificationName:RCTJavaScriptDidLoadNotification
object:_parentBridge userInfo:@{@"bridge": self}];
});
}]; }

4: 方法enqueueApplicationScript 最终依赖RCTJSCExecutor类型的实例 _javaScriptExecutor

来执行(React Native App的)JavaScript源码。

 - (void)enqueueApplicationScript:(NSData *)script
url:(NSURL *)url
onComplete:(RCTJavaScriptCompleteBlock)onComplete
{ [_javaScriptExecutor executeApplicationScript:script sourceURL:url onComplete:^(NSError *scriptLoadError) { // 4

....... [_javaScriptExecutor flushedQueue:^(id json, NSError *error)
{ [self handleBuffer:json batchEnded:YES]; onComplete(error);
}];
}];
}

下一节来走读 类 RCTJSCExecutor 和 接口RCTJavaScriptExecutor。

4. JS Executor

TODO: Rewrite this section to match the modification in version 0.18.1

4.0 接口RCTJavaScriptExecutor

接口RCTJavaScriptExecutor定义了JS Executor需要实现的接口。在React中提供了两个JS Executor的实现,

在React/Executors Group中:RCTWebViewExecutor、RCTContextExecutor。

WebSocket中也有一个实现: RCTWebSocketExecutor。

下面是接口RCTJavaScriptExecutor的方法声明:

typedef void (^RCTJavaScriptCompleteBlock)(NSError *error);
typedef void (^RCTJavaScriptCallback)(id json, NSError *error); /**
* Abstracts away a JavaScript execution context - we may be running code in a
* web view (for debugging purposes), or may be running code in a `JSContext`.
*/
@protocol RCTJavaScriptExecutor <RCTInvalidating, RCTBridgeModule> /**
* Used to set up the executor after the bridge has been fully initialized.
* Do any expensive setup in this method instead of `-init`.
*/
- (void)setUp; /**
* Executes given method with arguments on JS thread and calls the given callback
* with JSValue and JSContext as a result of the JS module call.
*/
- (void)executeJSCall:(NSString *)name
method:(NSString *)method
arguments:(NSArray *)arguments
callback:(RCTJavaScriptCallback)onComplete; /**
* Runs an application script, and notifies of the script load being complete via `onComplete`.
*/
- (void)executeApplicationScript:(NSString *)script
sourceURL:(NSURL *)sourceURL
onComplete:(RCTJavaScriptCompleteBlock)onComplete;

// 将由script表示的JavaScript脚本代表的object以objectName注册为全局变量。
- (void)injectJSONText:(NSString *)script
asGlobalObjectNamed:(NSString *)objectName
callback:(RCTJavaScriptCompleteBlock)onComplete; /**
* Enqueue a block to run in the executors JS thread. Fallback to `dispatch_async`
* on the main queue if the executor doesn't own a thread.
*/
- (void)executeBlockOnJavaScriptQueue:(dispatch_block_t)block; @optional /**
* Special case for Timers + ContextExecutor - instead of the default
* if jsthread then call else dispatch call on jsthread
* ensure the call is made async on the jsthread
*/
- (void)executeAsyncBlockOnJavaScriptQueue:(dispatch_block_t)block; @end

4.1 RCTJSCExecutor类

RCTJSCExecutor实现了接口RCTJavaScriptExecutor:

 /**
* Uses a JavaScriptCore context as the execution engine.
*/
@interface RCTJSCExecutor : NSObject <RCTJavaScriptExecutor>

5. RCTModuleData

RCTModuleData实例保存关于RCTBridgeModule实例的数据,这些数据包含:  "bridge module"模块的类(1),

"bridge module"模块在Javascript中名字(2), "bridge module"模块导出到JavaScript中的 Method(3),

"bridge module"模块实例(4), the module method dispatch queue(5), "bridge module"模块的配置信息(6)。

 @property (nonatomic, strong, readonly) Class moduleClass; // 1
@property (nonatomic, copy, readonly) NSString *name; // 2
 @property (nonatomic, copy, readonly) NSArray<id<RCTBridgeMethod>> *methods; // 3

@property (nonatomic, strong, readonly) id<RCTBridgeModule> instance; // 4
 @property (nonatomic, strong, readonly) dispatch_queue_t methodQueue; // 5

@property (nonatomic, copy, readonly) NSArray *config; // 6

RCTModuleData的方法instance 会创建"bridge module"模块实例:

 - (id<RCTBridgeModule>)instance
{
[_instanceLock lock];
if (!_setupComplete) {
if (!_instance) {
_instance = [_moduleClass new];
}
// Bridge must be set before methodQueue is set up, as methodQueue
// initialization requires it (View Managers get their queue by calling
// self.bridge.uiManager.methodQueue)
[self setBridgeForInstance];
[self setUpMethodQueue];
[_bridge registerModuleForFrameUpdates:_instance withModuleData:self];
_setupComplete = YES;
}
[_instanceLock unlock];
return _instance;
}

6. RCTJavaScriptLoader

从本地文件系统或者远程Server加载JavaScript。

 + (void)loadBundleAtURL:(NSURL *)scriptURL onComplete:(RCTSourceLoadBlock)onComplete

该接口实现使用了 NSURLSessionDataTask, React Native需要 iOS 7.0+ 的系统

7. RCTSourceCode

RCTSourceCode抽象JavaScript源码数据, 包含属性:

scriptData 和 scriptURL
 @interface RCTSourceCode : NSObject <RCTBridgeModule> // E

 @property (nonatomic, copy) NSData *scriptData;
@property (nonatomic, copy) NSURL *scriptURL; @end

E: 关于 RCTBridgeModule接口 参见 iOS.ReactNative-3-about-viewmanager-uimanager-and-bridgemodule


Reference

1. React-Native: RCTBridge.m/h

iOS.ReactNative-2-bridge-and-react-native-app-execution的更多相关文章

  1. iOS原生项目中集成React Native

    1.本文的前提条件是,电脑上已经安装了CocoaPods,React Native相关环境. 2.使用Xcode新建一个工程.EmbedRNMeituan [图1] 3.使用CocoaPods安装Re ...

  2. 利用 Create React Native App 快速创建 React Native 应用

    本文介绍的 Create-React-Native-App 是非常 Awesome 的工具,而其背后的 Expo 整个平台也让笔者感觉非常的不错.笔者目前公司是采用 APICloud 进行移动应用开发 ...

  3. React Native APP结构探索

    APP结构探索 我在Github上找到了一个有登陆界面,能从网上获取新闻信息的开源APP,想来研究一下APP的结构. 附上原网址:我的第一个React Native App 具体来讲,就是研究一个复杂 ...

  4. React Native App设置&Android版发布

    React Native系列 <逻辑性最强的React Native环境搭建与调试> <ReactNative开发工具有这一篇足矣> <解决React Native un ...

  5. [译] Facebook:我们是如何构建第一个跨平台的 React Native APP

    英文原文(需FQ):https://code.facebook.com/posts/1189117404435352/ 早些时候,我们介绍过iOS版的React Native. React Nativ ...

  6. React Native & app demos

    React Native & app demos https://github.com/ReactNativeNews/React-Native-Apps https://github.com ...

  7. 我的第一个React Native App

    我用了三天时间实现了一个相对比较完整的React Native 新闻发布类型的示例.应用做得很简单,但大多React Native的组件都有用到,今天做一个分享(由于我电脑是Windows系统,所以只 ...

  8. 用CodePush在React Native App中做热更新

    最近在学React Native,学到了CodePush热更新. 老师讲了两种实现的方法,现将其记录一下. 相比较原生开发,使用React Native开发App不仅能节约开发成本,还能做原生开发不能 ...

  9. 利用 Create React Native App 创建 React Native 应用

    $ npm i -g create-react-native-app $ create-react-native-app my-project $ cd my-project $ npm start

  10. React Native 系列(一) -- JS入门知识

    前言 本系列是基于React Native版本号0.44.3写的,最初学习React Native的时候,完全没有接触过React和JS,本文的目的是为了给那些JS和React小白提供一个快速入门,让 ...

随机推荐

  1. 07 Linux su和sudo命令的区别

    一. 使用 su 命令临时切换用户身份 1.su 的适用条件和威力 su命令就是切换用户的工具,怎么理解呢?比如我们以普通用户beinan登录的,但要添加用户任务,执行useradd ,beinan用 ...

  2. pg强制删库

    在某些时候,由于有别的连接,无法删除数据库,这时候用这个 SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity ...

  3. 黄聪:PHP页面跳转如何实现延时跳转

    php在用header重定向的时候,可以设置下延时跳转,代码如下: header("Refresh:5;url=index.php");

  4. HTML标签_01

    <!DOCTYPE html> <html> <body> <h1>我的第一个标题</h1> <p>我的第一个段落.</p ...

  5. sudo执行脚本找不到环境变量

    sudo执行脚本找不到变量 问题 当普通用户下,设置并export一个变量,然后利用sudo执行echo命令,能得到变量的值,但是如果把echo命令写入脚本,然后再sudo执行脚本,就找不到变量,未能 ...

  6. Windows下运行python脚本报错“ImportError: No Module named ...”的解决方法

    之前遇到一个问题,在Pycharm或IPython之类的IDE上运行脚本正常,但是直接运行或cmd命令行运行的时候报了模块未能找到的错误--ImportError: No Module named . ...

  7. JS settimeout 使用笔记

    无参数使用方法: setTimeout(function_name,delay_time); 基本使用方法是逗号前是函数名字,不能带有 xxxx(),不然不执行: 但是很多函数都要带参数的,以下是解决 ...

  8. 去除DEDECMS后台预览文章URL地址多余的数字信息

    在使用织梦模板时发现这样一个问题:在后台预览文章的时候,出现的文章网址尽管是静态URL,但是会在网址的尾部出现问号并跟随一个时间戳,在复制URL时就显得很不方便.那么如何解决这一问题呢? 经过查找资料 ...

  9. NHibernate系列文章二十六:NHibernate查询之SQL Query查询(附程序下载)

    摘要 NHibernate在很早的版本就提供了SQL Query(原生SQL查询),对于很复杂的查询,如果使用其他的查询方式实现比较困难的时候,一般使用SQL Query.使用SQL Query是基于 ...

  10. win7出现无法连接到代理服务器的错误,不能上网的问题的解决

    今天晚上突然停电,等我打开电脑发现不然上网,用google浏览器出现这个错误: 用IE诊断错误如下: 说是不能连到代理服务器,但是我没有连接到代理服务器啊,但是我的QQ能登,就是不能用浏览器上网,经过 ...