本文结构
- 目前App的几种常见的开发模式
- 关于React-Native的一点小看法
- React-Native相关的初始化过程源码探索
- OC 和 JS 互调部分
首先看一下, 目前App的几种常见的开发模式
Native App
- 直接依托于操作系统,交互性最强,功能最为强大,性能最好, 体验最好
- 开发成本高,无法跨平台,不同平台Android和iOS上都要各自独立开发
- 更新缓慢,特别是发布应用商店后,需要等待审核周期
- 维护成本高
Hybrid App
混合开发,也就是半原生半Web的开发模式,有跨平台效果,最终发布的是独立的原生APP
优点:
- 开发成本较低,可以跨平台,调试方便(chrome...)
- 维护成本低,功能可复用(基本上是维护一套前端代码)
- 更新较为自由
- 部分性能要求的页面可用原生实现
缺点:
- 基于webKit, 受限于webKit, 相比原生,性能仍然有较大损耗
- 动画体验差, 事件反馈不好, 不适用于交互性较强的app
React-Native App
优点:
- 基于JavaScriptCore, 所有的页面都是native, 体验接近原生, 解决了Hybrid的性能问题
- 拥有Hybrid和webApp的动态更新优势
- 使用React.JS理念开发原生App, 可以实现大多的代码多端复用
缺点:
- React Native 只能调用原生接口,但是不能对原生做扩展,要做扩展只能写 Native
- 学习曲线比较陡峭, 指导性文档少, 官方文档也不齐全, 开发中遇到的问题很难解决
- React Native 的首次加载很慢
- 只能部分跨平台,并不是像Hybrid中只需要编写一次前端代码, 即可多端运行,而是不同平台代码有所区别
关于React-Native的一点小看法
随着Apple官方禁用热更新之后, 看上去动态化是越来越火了, hybrid 和 React-Native
.
hybrid方案大多是基于webView来实现的动态化和即时更新功能,由Native通过JSBridge等方法提供统一的API,然后用Html5+JS来写实际的逻辑,调用API,这种模式下,由于Android,iOS的API一般有一致性,而且最终的页面也是在webview中显示, 所以能实现跨平台统一开发, 但是很多的功能体现受限于webView的方面. 比如动画的流畅性, 事件响应的及时反馈.
而Facebook开源的React-Native在保留hybrid的优势上,似乎解决了webView带来的交互体验问题, 因为React-Native的项目并非基于webView来实现的, App运行的时候全部都是原生的系统UI, 响应系统, 甚至动画(新版部分), 所以它可以带来原生App的体验
. 同时React-Native的官方介绍是: Build native mobile apps using JavaScript and React
. 使用JavaScript来开发原生的应用, 也就意味着, 它是同样具备有hybrid动态更新的优势的, 只要能够动态更新对应的js代码. 使用React来开发原生应用, 意味着可以使用React.JS的一整套的开发理念来开发原生App, 向写前端一样来开发原生App吧!
React-Native提出了Learn once, write anywhere
, 看上去有点像Java的write once, run anywhere
. 虽然React-Native并没有提出像Java一样的run anywhere的概念, 但是实际上, 在真实开发项目的时候(Android, iOS), 两个平台的JS代码的复用性是非常高的. 但是因为, React-Native的项目运行起来真实使用的是原生的系统UI, 响应系统等, 这些东西Android, iOS使用都是有差异的, 所以这些代码不能run anywhere
, 就需要在各个平台使用原生的开发方式来实现.
暂且不提React-Native自身的缺陷(官方的长列表可以用不能用来形容), 另一个更现实的问题是, 使用React-Native开发的任务应给在那些人. 前端? Native? 虽然说的是使用JS和React来开发原生App
, 但是这几乎是不可能的, 因为React-Native官方提供的Component和功能有限, 很多功能都是需要Native端自己来实现的, 所以前端开发人员大多也应该负责React部分了, Native部分的学习成本就有点太高了. 如果单独是Native端的人员的任务, 那么同时也是需要Android和iOS的开发人员都要学习React.JS相关的技能了, 学习的成本看上去比前端人员会少一些. 实际上, 看上去, 对于一个大一些的项目, 至少还是需要有Android和iOS的开发人员同时来参与比较现实.
上面的截图就是一个很简单的React-Native的iOS项目运行的情况, 左边的就相当于是React部分, 右边就相当于是Native的部分了. 看上去确实如官网介绍, 左边的开发代码是完全是使用React和JavaScript
完成页面的层级, 布局, 事件响应等逻辑, 当它正常运行的时候, 里面的所有的页面都是native的, 右边看上去也是一个原生的App.
首先看一下实际的例子
在使用RN的时候, 官方已经给出的控件和功能是有限的, 很多时候还暂时满足不了我们项目中的需求, 这个时候我们可以利用native来自己实现. 比如当你需要直接通过NSUserDefaults的方式存取数据的时候, 你可能需要简单的做一些工作. 按照官方的文档指引, 这个时候你只是需要调用native的api就可以完成了.
@interface ZJRNUserDefaultsManager : NSObject<RCTBridgeModule>
@end
@implementation ZJRNUserDefaultsManager
// 注册类为module, 暴露给js, 这里可以指定module的名字, 不设置的时候默认为类名
RCT_EXPORT_MODULE();
// 注册方法, 暴露给js
RCT_EXPORT_METHOD(saveValue: (id)value forKey:(NSString *)key ) {
// native 端的处理
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
[userDefaults setValue:value forKey:key];
[userDefaults synchronize];
}
@end
上面的操作很简单, 首先新建一个类, 遵守RCTBridgeModule
协议, 然后使用两个宏, 分别注册module和我们的方法, 方法里面的处理和native开发时类似. 然后在js端就可以这样的使用了(当然,动了native端的代码,这个时候需要重新编译运行了).
// 引入module
import { NativeModules } from 'react-native';
var ZJRNUserDefaultsManager = NativeModules.ZJRNUserDefaultsManager;
// 在需要保存的地方直接使用就可以了, 当然了js中是没有参数名(forKey...)这些了
ZJRNUserDefaultsManager.saveValue('hello', 'testKey')
仅需要完成上面的简单的操作, 我们就可以实现在js端调用native的功能了, 那么拥有这个功能是还不能很好的完成实际的开发工作的. 我们需要native端提供一些接口出来供React.js端调用, 比如, 创建指定类型的View, 更新指定的view的属性, 布局等操作. 还好, 这些工作Facebook已经帮我们完成了. 在RCTUIManager
这个类中, 已经提供了createView
, updateView
, setChildren
, findSubviewIn
, removeSubviewsFromContainerWithID
等需要的API供js端调用了. 我们现在如果要实现我们直接的控件在js端使用就很简单了.
上面我们所做的操作其实很简单, 按照官方指引就可以顺利的完成了, 甚至还可以加入方法的回调. 但是这一切是怎么工作起来的呢? 我们在native端做的这些操作起了什么作用, 然后js中又是怎么样正确的找到native端这些的class, 以及method, 然后正确的调用的呢? 这个时候, 我们就需要稍微的看下react-native的一些源码了. 主要是React-Native相关的初始化过程和oc和js的互相调用.
React-Native相关的初始化过程
- [RCTRootView initWithBundleURL:...]
- [RCTBridge initWithBundleURL:...]
- [RCTBridge setUp]
- 初始化batchedBridge
- [RCTCxxBridge start]
- 开启一个线程jsThread用于js
- [RCTCxxBridge _initModulesWithDispatchGroup]
- 初始化JSCExecutorFactory, 用于执行js代码以及处理回调 JSCExecutor::JSCExecutor()
- JSCExecutor::initOnJSVMThread()
- installGlobalProxy -> nativeModuleProxy
- RCTJavaScriptLoader -> 加载js代码
- [RCTCxxBridge executeSourceCode]
- [RCTBridge setUp]
- [RCTRootView initWithBridge:...]
- [RCTRootView bundleFinishedLoading]
- 初始化RCTRootContentView
- [RCTRootView runApplication]
- [RCTRootView bundleFinishedLoading]
- [RCTBridge initWithBundleURL:...]
一般我们初始化的时候会只用到的就需要两句代码即可
NSURL *jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index.ios" fallbackResource:nil];
RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
moduleName:@"FlatListDemo"
initialProperties:nil
launchOptions:launchOptions];
1 获取到app的js入口文件的NSURL
2 初始化RCTRootView, 传递的参数moduleName:@"FlatListDemo"
是我们在入口的js文件中注册的, initialProperties:nil
, 将作为js中的跟view的props, 可以用来传递需要的初始数据
进入初始化RCTRootView的方法中
RCTBridge *bridge = [[RCTBridge alloc] initWithBundleURL:bundleURL
moduleProvider:nil
launchOptions:launchOptions];
return [self initWithBridge:bridge moduleName:moduleName initialProperties:initialProperties];
主要的操作就是初始化了一个RCTBridge, 这个可以算是 OC <=> JS通信的桥梁, 所以最重要的部分就是初始化RCTBridge, [RCTBridge setUp]是重要的过程, 主要的是下面的几行代码
Class bridgeClass = self.bridgeClass;
self.batchedBridge = [[bridgeClass alloc] initWithParentBridge:self];
[self.batchedBridge start];
RootView持有的RCTBridge它并没有做很多的事, 主要的工作都是由它持有的batchedBridge来完成的, 上面就是初始化batchedBridge的过程, 首先获取到了bridgeClass, 因为在RN之前的版本中使用的是RCTBatchedBridge
这个class来处理的, 现在使用的是RCTCxxBridge
, 所以在里面简单的处理了一下bridgeClass, 我们后面用到的batchedBridge就全部关注RCTCxxBridge
它了.
self.batchedBridge = [[bridgeClass alloc] initWithParentBridge:self]
初始化batchedBridge的过程中设置了一些变量的初始化状态, 需要关注的是初始化了一个RCTDisplayLink -> 用于js中的 RCTTiming(定时器) frameUpdate(navigator...)等
[self.batchedBridge start]
在start的过程中, 进行了很多的操作
1 首先会创建一个JSThread, 在接触react-native的时候, 我们可能经常会听到有个js线程的说法, 实际上指的是会单独开一个线程给js用, js的执行会在这个线程(可以在其他线程回调)中完成, 名字就是
com.facebook.react.JavaScript
方便调试
// 创建jsThread
static NSString *const RCTJSThreadName = @"com.facebook.react.JavaScript";
_jsThread = [[NSThread alloc] initWithTarget:self
selector:@selector(runJSRunLoop)
object:nil];
_jsThread.name = RCTJSThreadName;
_jsThread.qualityOfService = NSOperationQualityOfServiceUserInteractive;
[_jsThread start];
执行js代码总是会被'保证'在jsThread上面完成
- (void)ensureOnJavaScriptThread:(dispatch_block_t)block
{
if ([NSThread currentThread] == _jsThread) {
[self _tryAndHandleError:block];
} else {
[self performSelector:@selector(_tryAndHandleError:)
onThread:_jsThread
withObject:block
waitUntilDone:NO];
}
}
需要关注一下@selector(runJSRunLoop) -> 在里面使用了autoreleasepool开启了一个runloop, 让JSThread线程一直存在(关于runloop)
- (void)runJSRunLoop
{
@autoreleasepool {
RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, @"-[RCTCxxBridge runJSRunLoop] setup", nil);
// copy thread name to pthread name
pthread_setname_np([NSThread currentThread].name.UTF8String);
// Set up a dummy runloop source to avoid spinning
CFRunLoopSourceContext noSpinCtx = {0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL};
CFRunLoopSourceRef noSpinSource = CFRunLoopSourceCreate(NULL, 0, &noSpinCtx);
CFRunLoopAddSource(CFRunLoopGetCurrent(), noSpinSource, kCFRunLoopDefaultMode);
CFRelease(noSpinSource);
RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @"");
// run the run loop
while (kCFRunLoopRunStopped != CFRunLoopRunInMode(kCFRunLoopDefaultMode, ((NSDate *)[NSDate distantFuture]).timeIntervalSinceReferenceDate, NO)) {
RCTAssert(NO, @"not reached assertion"); // runloop spun. that's bad.
}
}
}
2 然后会创建一个CGDGroup
dispatch_group_t prepareBridge
, 在后面会用于modules和souceCode都准备好了之后在notify中, 执行js代码
.
3 初始化所有的注册的OC Class的信息为对应的module并保存下来
[self _initModulesWithDispatchGroup:prepareBridge]
, 这个过程通过遍历每个module(分为两种, 一种是内部提前注册好的,如RCTJSCExecutor(现在弃用), 另一种是外部通过宏定义注册的)来保存它相关的'配置表'信息
NSMutableArray<Class> *moduleClassesByID = [NSMutableArray new];
NSMutableArray<RCTModuleData *> *moduleDataByID = [NSMutableArray new];
NSMutableDictionary<NSString *, RCTModuleData *> *moduleDataByName = [NSMutableDictionary new];
遍历内部的modules我们就不关注了, 主要看下遍历外部注册的OC Class的处理
for (Class moduleClass in RCTGetModuleClasses()) {
NSString *moduleName = RCTBridgeModuleNameForClass(moduleClass);
// 初始化每个注册的class对应的RCTModuleData ---> 内部会检查是否有重写init,
// constantsToExport等方法, 如果被重写将会在主线程被调用,
// 因为RN认为, 我们可能需要进行UI相关的操作
moduleData = [[RCTModuleData alloc] initWithModuleClass:moduleClass
bridge:self];
moduleDataByName[moduleName] = moduleData;
[moduleClassesByID addObject:moduleClass];
[moduleDataByID addObject:moduleData];
}
// 保存 modules
_moduleDataByID = [moduleDataByID copy];
_moduleDataByName = [moduleDataByName copy];
_moduleClassesByID = [moduleClassesByID copy];
}
RCTGetModuleClasses()
这个全局函数获取到的是一个全局的单例数组static NSMutableArray<Class> *RCTModuleClasses
, 所以, 有个问题是这个数组中的数据是哪里来的. 其实是我们在自定义ViewManager(NSObject <RCTBridgeModule>)的时候的时候会使用一个宏 RCT_EXPORT_MODULE();
, 这个宏展开后的情况是这样的
#define RCT_EXPORT_MODULE(js_name) \
RCT_EXTERN void RCTRegisterModule(Class); \
+ (NSString *)moduleName { return @#js_name; } \
+ (void)load { RCTRegisterModule(self); }
void RCTRegisterModule(Class moduleClass)
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
RCTModuleClasses = [NSMutableArray new];
});
RCTAssert([moduleClass conformsToProtocol:@protocol(RCTBridgeModule)],
@"%@ does not conform to the RCTBridgeModule protocol",
moduleClass);
// Register module -> 添加到全局的单例数组中
[RCTModuleClasses addObject:moduleClass];
}
上面的宏会为对应的class中定义一个返回moduleName(当没有设置的时候, 默认值为class的名字)的工厂方法
同时extern一个函数RCTRegisterModule(), 这个全局的函数中主要就是创建了一个全局的单例数组RCTModuleClasses, 并且将我们当前的Class添加进去
另外, 宏定义中重写了当前class的+load方法, 所以这个RCTRegisterModule()函数在类被加载的时候就已经被调用了, 这样就完成了注册的过程. 所以前面的数组的数据就是我们注册的Class
接着上面遍历分析, 后面有一个初始化RCTModuleData
的操作, 稍微看一下它的初始化过程, 并没有做很多实际的事情, 在[RCTModuleData setUp]中检查是否有重写init, constantsToExport等方法, 如果被重写将会在主线程被调用, 因为RN认为, 我们可能需要进行UI相关的操作
其实在RCTModuleData初始化过程中没有很多的操作, 是因为, RCTModuleData是通过它持有的_instance
来进行数据的操作的
所以在这个遍历完成之后, 进入了下一个遍历操作[self _prepareModulesWithDispatchGroup:dispatchGroup]
, 主要是为RCTModuleData设置好对应的_instance和bridge, 主要代码如下
for (RCTModuleData *moduleData in _moduleDataByID) {
(void)[moduleData instance];
/*
这是一个get方法, 如果没有setupComplete的时候会进行一系列的setUp操作
[self setUpInstanceAndBridge] {
1. 为instance赋值 bridge
// 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];
2. 当_instance.methodQueue不存在的时候创建新的queue, 用于一些分发工作
// @"com.facebook.react.%@Queue"
[self setUpMethodQueue];
3. 设置完成之后, 会给bridge注册实例, 用于frameUpdate---> CADisplayLink回调
[self finishSetupForInstance]
}
*/
[moduleData gatherConstants]; //
}
4 以上终于完成了Modules信息的保存, 接着会进行JSExecutorFactory的初始化和相关的设置, JSExecutorFactory内部会持有一个
JSCExecutor
所有与JS的通信,一定都通过JSCExecutor来进行, 所以需要重点关注, 它的构造函数中调用了initOnJSVMThread()
, 里面进行了很多的JS上下文的准备, 创建JSClass, 全局的context, 以及添加全局的回调.注意在这个里面使用到的JSC_JSXXX
的宏的作用, 实际上是会转换为调用苹果的JavaScriptCore对应的方法(去掉JSC_), 同时还要留意JSCExecutor构造函数中为js的上下文中设置了一个Proxy,nativeModuleProxy
.
void JSCExecutor::initOnJSVMThread()
// 在js的上下文中注册全局的闭包回调 , 这两个是我们需要重点关注的回调.
nativeFlushQueueImmediate
nativeCallSyncHook
installNativeHook<&JSCExecutor::nativeFlushQueueImmediate>("nativeFlushQueueImmediate");
// 这一个注册的回调, 可以用于在js中直接调用oc, 注意, 在rn中js调用oc一般都是由oc发起的,
// 这就像我们的界面一般是有用户的操作才会触发系统调用, rn中类似的,
// 当oc调了js后, 在对应的js回调中才会实现调用oc的逻辑
// 在react native MessageQueue.js中的源码中可以找到 这样一段代码,
// 就是当oc没有及时的在消息队列中调js的时候, 大于5ms后js就会调用这个回调, 主动调用oc
MIN_TIME_BETWEEN_FLUSHES_MS = 5ms;
if (global.nativeFlushQueueImmediate &&
(now - this._lastFlush >= MIN_TIME_BETWEEN_FLUSHES_MS ||
this._inCall === 0)) {
var queue = this._queue;
this._queue = [[], [], [], this._callID];
this._lastFlush = now;
global.nativeFlushQueueImmediate(queue);
}
//
// 这个回调是用来调用查找native中对应的方法相关信息的
installNativeHook<&JSCExecutor::nativeCallSyncHook>("nativeCallSyncHook");
在react native的 NativeModules.js中有相关的调用来获取native方法的相关信息
function genMethod(moduleID: number, methodID: number, type: MethodType) {
//...
return global.nativeCallSyncHook(moduleID, methodID, args);
}
为js的上下设置 nativeModuleProxy
, 这一个属性很重要, js端用来获取所有注册的nativeModule. 在js中我们需要引入native端的时候回写这样的类似代码 var {NativeModules} from 'react-native'
, 实际上就是获取到我们这里设置的.
JSCExecutor::JSCExecutor()
{
// 这个函数在js的全局上下文中设置了`nativeModuleProxy`,
// 使得在js端可以获取到, 同时注意这个函数的第三个参数中
// 还包装了一个方法用于调用, 里面涉及到JSCNativeModules这个类,
// 就是获取NativeModule的操作, 后面分析
installGlobalProxy(m_context, "nativeModuleProxy",
exceptionWrapMethod<&JSCExecutor::getNativeModule>());
}
在react-native 的NativeModules.js底部有这样的代码, 就直接调用了上面在native端设置的全局属性了
let NativeModules : {[moduleName: string]: Object} = {};
if (global.nativeModuleProxy) {
NativeModules = global.nativeModuleProxy;
}
module.exports = NativeModules;
那么js中是怎么获取到我们之前在native端已经生成好的'配置表'信息的呢? 这个问题需要我们重点关注下, 在之前的react-native版本中, 是通过native端直接在js的上下文中注入这样一个__fbBatchedBridgeConfig
配置表,
传过去一个json(remoteModuleConfig). 现在的版本中改了一些. 还是上面这段代码. 在如下的调用栈中,我们只需要关注最后一个函数.
- JSCExecutor::getNativeModule()
- JSCNativeModules::getModule()
- JSCNativeModules::createModule()
- ModuleRegistry::getConfig()
- RCTNativeModule::getMethods()
- NSStringFromSelector(selector) hasPrefix:@"rct_export"(初始化RCTModuleMethod信息)
- RCTNativeModule::getMethods()
- ModuleRegistry::getConfig()
- JSCNativeModules::createModule()
- JSCNativeModules::getModule()
folly::Optional<Object> JSCNativeModules::createModule(const std::string& name, JSContextRef context) {
if (!m_genNativeModuleJS) {
// 获取js中contenxt中的global
auto global = Object::getGlobalObject(context);
// 获取 global中的__fbGenNativeModule对象
m_genNativeModuleJS = global.getProperty("__fbGenNativeModule").asObject();
m_genNativeModuleJS->makeProtected();
}
// 这个方法最终会调到 RCTNativeModule::getMethods()中,
// 取得之前使用宏暴露的所有的方法 -> 前缀是 "__rct_export__"
auto result = m_moduleRegistry->getConfig(name);
if (!result.hasValue()) {
return nullptr;
}
// 调用 js中的__fbGenNativeModule方法, 并且传递过去native端的配置表, 用于端js处理
Value moduleInfo = m_genNativeModuleJS->callAsFunction({
Value::fromDynamic(context, result->config),
Value::makeNumber(context, result->index)
});
return moduleInfo.asObject().getProperty("module").asObject();
}
// 下面是对应的js中的处理, NativeModules.js
// 暴露一个全局的属性给native(就是上面我们调用的js中的方法, 触发了后面的js后取到native端的配置表, 并且保存)
// export this method as a global so we can call it from native
global.__fbGenNativeModule = genModule;
function genModule(...) {
// 获取到native端调用时传递过来的配置信息
const [moduleName, constants, methods, promiseMethods, syncMethods] = config;
const module = {};
// 遍历 所有的native的方法列表, 获取所有native的方法的详细信息
methods && methods.forEach((methodName, methodID) => {
module[methodName] = genMethod(moduleID, methodID, methodType);
});
}
function genMethod(...) {
/// ...
// 调用之前native端在js中注入的回调, 获取native中的所有方法的详细信息
return global.nativeCallSyncHook(moduleID, methodID, args);
}
5 使用dispatch_group方式初始化Instance(执行代码需要), 和加载js代码
RCTJavaScriptLoader
.
6 modules and source code都已经准备好了, 在notify中JSCExecutor专属的Thread内执行jsbundle代码执行js代码. 执行完毕后会将_displayLink添加到runloop(注意是在jsThread所在的runloop)中, 开始运行.
- [RCTCxxBridge executeSourceCode: ]
- [RCTCxxBridge enqueueApplicationScript:]
- void Instance::loadScriptFromString()
- void NativeToJsBridge::loadApplication()
- void JSCExecutor::loadApplicationScript()
- void JSCExecutor::flush()
- void JSCExecutor::bindBridge()
- void JSCExecutor::flush()
- void JSCExecutor::loadApplicationScript()
上面的调用栈中我们主要关注后面几个函数
void JSCExecutor::loadApplicationScript()
, 在这个函数中注意下面两行代码, 注意, RN中有个很明显的问题就是, 首次进入RN模块的时候, 加载的很慢, 会有几秒的加载时间, 其实就是在这个函数中加载jsBundle造成的, 如果要优化加载的时间, 以及不同模块页面切换流畅, 就需要对jsBundle文件进行精简, 缓存等操作.
// 执行js代码, 加载jsBundle
evaluateScript(m_context, jsScript, jsSourceURL);
// 在加载完jsbundle后主动调用一次, 为js的上下文环境添加必要的全局属性和回调
flush();
void JSCExecutor::bindBridge()
保存js的上下文环境中必要的全局属性和回调, 这些在 react native的 MessageQueue.js中定义的调用方法, 当native需要调用js的方法的时候, 需要通过调用这些js方法, 在这些方法中, js端会根据传递的信息查找到在js中需要调用的方法.
auto global = Object::getGlobalObject(m_context);
auto batchedBridgeValue = global.getProperty("__fbBatchedBridge");
// 这些是在MessageQueue.js中定义的调用方法
auto batchedBridge = batchedBridgeValue.asObject();
m_callFunctionReturnFlushedQueueJS = batchedBridge.getProperty("callFunctionReturnFlushedQueue").asObject();
m_invokeCallbackAndReturnFlushedQueueJS = batchedBridge.getProperty("invokeCallbackAndReturnFlushedQueue").asObject();
m_flushedQueueJS = batchedBridge.getProperty("flushedQueue").asObject();
m_callFunctionReturnResultAndFlushedQueueJS = batchedBridge.getProperty("callFunctionReturnResultAndFlushedQueue").asObject();
}
7 上面的工作完成之后, 初始化bridge的工作就完成了, jsBundle已经加载完成, oc端的'配置表'也已经处理好, 并且成功已经传递给js端, js的上下文配置已经准备好, 下面就是开始执行我们的js代码了, React就会开始计算好所有的布局信息, 以及Component层级关系等, 等待native端完成对应的真正的页面渲染和布局. 回到RCTRootView的初始化方法中, 注意在
[RCTRootView initWithBridge:...]
的初始化方法中, 注册了几个js执行情况的通知, 我们重点关注js执行完毕后的通知RCTJavaScriptDidLoadNotification
- (instancetype)initWithBundleURL:(NSURL *)bundleURL
moduleName:(NSString *)moduleName
initialProperties:(NSDictionary *)initialProperties
launchOptions:(NSDictionary *)launchOptions
{
RCTBridge *bridge = [[RCTBridge alloc] initWithBundleURL:bundleURL
moduleProvider:nil
launchOptions:launchOptions];
// 上面完成了bridge的初始化工作 , 下面开始完成rootView的初始化
return [self initWithBridge:bridge moduleName:moduleName initialProperties:initialProperties];
}
下面是js执行完毕后的通知RCTJavaScriptDidLoadNotification
中的一系列的处理
- [RCTRootView javaScriptDidLoad]
- [RCTRootView bundleFinishedLoading]
- RCTRootContentView (创建contentView)
- [RCTRootView runApplication]
- [RCTRootView bundleFinishedLoading]
在创建RCTRootContentView的时候, 注意有个参数是reactTag
, 这个属性很重要, 每一个reactTag都应该是唯一的, 从1开始, 每次递增10. RCTRootContentView
初始化时, 还需要在RCTUIManager
中通过reactTag去注册, 从而由RCTUIManager
来统一管理所有的js端使用Component对应的每个原生view(_viewRegistry[tag]表
), 有了这个, 我们就可以很方便的在其他地方通过reactTag获取到我们的Component所在的rootView.
/**
* Every root view that is created must have a unique react tag.
* Numbering of these tags goes from 1, 11, 21, 31, etc
*
* NOTE: Since the bridge persists, the RootViews might be reused, so the
* react tag must be re-assigned every time a new UIManager is created.
*/
- (NSNumber *)allocateRootTag
{
NSNumber *rootTag = objc_getAssociatedObject(self, _cmd) ?: @1;
objc_setAssociatedObject(self, _cmd, @(rootTag.integerValue + 10), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
return rootTag;
}
然后才是[RCTRootView runApplication]
, 这里就会调用js里面AppRegistry
对应的方法runApplication
了, 在AppRegistry.js
中有下面的一段注释, 已经解释得很清楚了
/**
* `AppRegistry` is the JS entry point to running all React Native apps. App
* root components should register themselves with
* `AppRegistry.registerComponent`, then the native system can load the bundle
* for the app and then actually run the app when it's ready by invoking
* `AppRegistry.runApplication`.
*
* To "stop" an application when a view should be destroyed, call
* `AppRegistry.unmountApplicationComponentAtRootTag` with the tag that was
* passed into `runApplication`. These should always be used as a pair.
*
* `AppRegistry` should be `require`d early in the `require` sequence to make
* sure the JS execution environment is setup before other modules are
* `require`d.
*/
- (void)runApplication:(RCTBridge *)bridge
{
NSString *moduleName = _moduleName ?: @"";
NSDictionary *appParameters = @{
@"rootTag": _contentView.reactTag,
@"initialProps": _appProperties ?: @{},
};
// 调用AppRegistry.js中的runApplication开始运行
RCTLogInfo(@"Running application %@ (%@)", moduleName, appParameters);
[bridge enqueueJSCall:@"AppRegistry"
method:@"runApplication"
args:@[moduleName, appParameters]
completion:NULL];
}
8 因为执行js代码的时候, js端会计算好每个view的布局, 属性等信息, 然后通过调用native的系统方法来完成, 页面的渲染, 页面的布局. 这个过程中就涉及到了js 和native的互调通信了. 这个过程分为两个部分.
OC 调 JS 部分
- RCTEventDispatcher::sendXXEvent()
- RCTEventDispatcher::dispatchEvent()
- RCTCxxBridge:: enqueueJSCall()
- Instance::callJSFunction()
- NativeToJsBridge::callFunction()
- JSCExecutor::callFunction()
- bindBridge();
- m_callFunctionReturnFlushedQueueJS->callAsFunction()
RCTEventDispatcher中的很多的sendXXEvent方法, 用来传递native的事件调用给js. 两种方式调用, 第一种是通过调用flushEventsQueue, 将dispatch的事件放在分发的队列中来处理, 另一种是直接调用, 但是最终每个事件都会调到RCTCxxBridge中的enqueueJSCall方法中去处理.
经过一系列的函数调用, 数据处理后, 最终会走到直接的执行函数中JSCExecutor::callFunction()
, 这里面首先会检查js的上下文中的处理方法调用的回调是否获取到, 如果没有获取到, 将会再次调用bindBridge()来获取, 然后会使用m_callFunctionReturnFlushedQueueJS->callAsFunction()
, 来调用, 注意这是封装的一个Object(Value.cpp)类中的方法.这个方法中会利用native端的js引擎执行MessageQueue.js中的callFunctionReturnFlushedQueue方法, js端在这个方法中, 会执行传递过去的特定的方法, 这样就完成了oc调用js了
Value Object::callAsFunction(JSObjectRef thisObj, int nArgs, const JSValueRef args[]) const {
JSValueRef exn;
// 这个宏才是真正的调用相关的方法的代码, 实际上是直接使用的JavaScriptCore中的JSObjectCallAsFunction方法完成的
JSValueRef result = JSC_JSObjectCallAsFunction(m_context, m_obj, thisObj, nArgs, args, &exn);
if (!result) {
throw JSException(m_context, exn, "Exception calling object as function");
}
return Value(m_context, result);
}
完成上面的操作后, js端的BatchedBridge会处理接收到的方法调用信息, 然后查到js端的'配置表'完成对应的js方法调用. 并且会返回执行的结果, 这个结果中, 已经包括了对应的js回调中的相关信息, 用来直接调用native相关的方法.
JS 调 OC 部分
- JSCExecutor::callNativeModules()
- JsToNativeBridge :: callNativeModules()
- ModuleRegistry::callNativeMethod()
- RCTNativeModule::invoke()
- RCTNativeModule::invokeInner()
- RCTModuleMethod:: invokeWithBridge()
- RCTModuleMethod:: processMethodSignature()
- [_invocation invokeWithTarget:module]
注意, 因为在rn中js调用native一般都是由native触发的, 所以会先完成上面的native调用js的过程, 然后才会在js的回调的调用native. 所以我们这里就直接从js调用开始分析. 到这里, js端的BatchedBridge已经完成了, 将要调用的native的方法的信息封装, 并且传递到了native了(就是上面执行的结果).
上面的函数调用栈, 我们重点关注后面的函数.
// 运行时调用
- (id)invokeWithBridge:(RCTBridge *)bridge
module:(id)module
arguments:(NSArray *)arguments
{
if (_argumentBlocks == nil) {
[self processMethodSignature];
// 配置invacation, 这个过程还比较复杂, 里面首先会调用一个全局的函数RCTParseMethodSignature(),
// 在里面会分析和解析有多少个参数, 以及参数的类型是什么, 以及参数类型的必要转化,
// 后面就是根据JavaScript中传过来的callbackId(数字)生成对应的回调block, 在之后调用.
}
// Invoke method
// 上面已经设置好了_invocation的selector, 参数等信息,
// 现在通过运行时调用对应的module(OC Class)里的方法, 完成js调用oc操作.
[_invocation invokeWithTarget:module];
}
到这里, 我们就简单的探索了以下React-Native里面的初始化所做的一些工作, 初始化过程很关键, 里面为JS和OC通信做了很多的必要的操作. 同时初始化的过程中还是有一些可优化的. 比如RN首次加载模块的时候很慢, 可能会出现白屏, 就是初始化的时候加载jsBundle造成的, 我们可以预加载jsBundle, 在使用RN模块的时候, 直接渲染界面, 就能明显优化这个问题, 如果项目中的有多个RN模块的时候, 建议要处理一下RCTBridge, 尽量能公用一个RCTBridge.
参照文档
ReactNative iOS源码解析