# ReactNative与iOS原生通信原理解析-JS加载及执行篇
导语: 其实原本是想编写一篇
react-native
(下文简称 rn) 在iOS
中如何实现jsbridge
的文章;相信看过官方文档的同学都清楚 rn 和 iOS 通信使用了一个叫RCTBridgeModule
的模块去实现。但是不知怎么呢?为了查阅其通信的原理,编写了一篇ReactNative 与 iOS 原生通信原理解析-初始化 (opens new window); 由于篇幅过长,我们还未讲解 JS 代码的加载和执行;下面我们就开始讲解第二个部分【ReactNative 与 iOS 原生通信原理解析-JS 加载及执行篇】。
声明: 本文所使用的 rn 版本为0.63.0
;本文篇幅较长,由于涉及到原理所以本文存在大量 RN 的源码,还请谅解。
# 缘起
此篇是上一篇ReactNative 与 iOS 原生通信原理解析-初始化 (opens new window)的姊妹篇,建议对 RN 初始化的整个流程还不清楚的同学可以先行查阅。
上一篇已经讲到在RCTxxBridge.mm
的 start 方法中进行了七个步骤:
我们已经详细讲了前面的五个部分;还剩余两个部分:
- 加载 js 代码
- 执行 js 代码
下面我们就来看看,RN 是如何加载 JS 代码和执行 JS 代码的。
# 加载 JS 代码
让我们再来回顾一下RCTxxBridge.mm
的 start 方法:
```java
// RCTxxBridge.mm
- (void)start
{
//1. 发送RCTJavaScriptWillStartLoadingNotification消息通知以供RCTRootView接收并处理
[[NSNotificationCenter defaultCenter] postNotificationName:RCTJavaScriptWillStartLoadingNotification
object:_parentBridge
userInfo:@{@"bridge" : self}];
//2. 提前设置并开启JS线程 _jsThread
_jsThread = [[NSThread alloc] initWithTarget:[self class] selector:@selector(runRunLoop) object:nil];
_jsThread.name = RCTJSThreadName;
_jsThread.qualityOfService = NSOperationQualityOfServiceUserInteractive;
#if RCT_DEBUG
_jsThread.stackSize *= 2;
#endif
// 启动JS线程
[_jsThread start];
dispatch_group_t prepareBridge = dispatch_group_create();
//3. 注册native modules,主要是在初始化RCTBridge使用initWithBundleURL_moduleProvider_launchOptions中的moduleProvider block返回值的native modules;
[self registerExtraModules];
// 重点:注册所遇的自定义Native Module;包括你在rn官网上看到的原生模块定义以及RN自带的Text,Date等原生组件
(void)[self _initializeModules:RCTGetModuleClasses() withDispatchGroup:prepareBridge lazilyDiscovered:NO];
// 初始化所有懒加载的native module
[self registerExtraLazyModules];
// 其实这里不会做任何事情,详情请见initializeBridge
_reactInstance.reset(new Instance);
__weak RCTCxxBridge *weakSelf = self;
//4. 准备executor factory; 看RCTBridge是否指定了executorClass
std::shared_ptr<JSExecutorFactory> executorFactory;
if (!self.executorClass) {// 如果没有指定executorClass 但是实现了RCTCxxBridgeDelegate协议,那么就使用jsExecutorFactoryForBridge的方式 准备 executor factory 否则就使用make_shared初始化一个空的JSCExecutorFactory
if ([self.delegate conformsToProtocol:@protocol(RCTCxxBridgeDelegate)]) {
id<RCTCxxBridgeDelegate> cxxDelegate = (id<RCTCxxBridgeDelegate>)self.delegate;
executorFactory = [cxxDelegate jsExecutorFactoryForBridge:self];
}
if (!executorFactory) {
executorFactory = std::make_shared<JSCExecutorFactory>(nullptr);
}
} else {// 如果指定了 executorClass 就使用指定的executorClass 初始化;一般RCTObjcExecutorFactory为开发环境使用的
id<RCTJavaScriptExecutor> objcExecutor = [self moduleForClass:self.executorClass];
executorFactory.reset(new RCTObjcExecutorFactory(objcExecutor, ^(NSError *error) {
if (error) {
[weakSelf handleError:error];
}
}));
}
// 5. module初始化完成就初始化底层Instance实例,也就是_reactInstance
dispatch_group_enter(prepareBridge);
[self ensureOnJavaScriptThread:^{
// 利用executorFactory来initializeBridge 方法;完成初始化_reactInstance(也就是Instance)
[weakSelf _initializeBridge:executorFactory];
dispatch_group_leave(prepareBridge);
}];
//6. 异步加载js代码
dispatch_group_enter(prepareBridge);
__block NSData *sourceCode;
[self
loadSource:^(NSError *error, RCTSource *source) {
if (error) {
[weakSelf handleError:error];
}
sourceCode = source.data;
dispatch_group_leave(prepareBridge);
}
onProgress:^(RCTLoadingProgress *progressData) {
#if (RCT_DEV | RCT_ENABLE_LOADING_VIEW) && __has_include(<React/RCTDevLoadingViewProtocol.h>)
id<RCTDevLoadingViewProtocol> loadingView = [weakSelf moduleForName:@"DevLoadingView"
lazilyLoadIfNecessary:YES];
[loadingView updateProgress:progressData];
#endif
}];
// 7. 等待native moudle 和 JS 代码加载完毕后就执行JS; dispatch_group_t和dispatch_group_notify联合使用保证异步代码同步按顺序执行
dispatch_group_notify(prepareBridge, dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
RCTCxxBridge *strongSelf = weakSelf;
if (sourceCode && strongSelf.loading) {
// 重点,执行JS代码;后面我们会具体展开分析
[strongSelf executeSourceCode:sourceCode sync:NO];
}
});
}
// 真正的加载逻辑
- (void)loadSource:(RCTSourceLoadBlock)_onSourceLoad onProgress:(RCTSourceLoadProgressBlock)onProgress
{
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
[center postNotificationName:RCTBridgeWillDownloadScriptNotification object:_parentBridge];
// 声明加载成功的回调
RCTSourceLoadBlock onSourceLoad = ^(NSError *error, RCTSource *source) {
// 加载成功之后发送RCTBridgeDidDownloadScriptNotification通知RCTRootView准备执行JavaScript中的方法AppRegistry.runApplication
NSDictionary *userInfo = @{
RCTBridgeDidDownloadScriptNotificationSourceKey : source ?: [NSNull null],
RCTBridgeDidDownloadScriptNotificationBridgeDescriptionKey : self->_bridgeDescription ?: [NSNull null],
};
[center postNotificationName:RCTBridgeDidDownloadScriptNotification object:self->_parentBridge userInfo:userInfo];
_onSourceLoad(error, source);
};
if ([self.delegate respondsToSelector:@selector(loadSourceForBridge:onProgress:onComplete:)]) {
[self.delegate loadSourceForBridge:_parentBridge onProgress:onProgress onComplete:onSourceLoad];
} else if ([self.delegate respondsToSelector:@selector(loadSourceForBridge:withBlock:)]) {
[self.delegate loadSourceForBridge:_parentBridge withBlock:onSourceLoad];
} else if (!self.bundleURL) {
onSourceLoad(error, nil);
} else {
// 重点来了,使用RCTJavaScriptLoader异步加载JS代码
__weak RCTCxxBridge *weakSelf = self;
[RCTJavaScriptLoader loadBundleAtURL:self.bundleURL
onProgress:onProgress
onComplete:^(NSError *error, RCTSource *source) {
if (error) {
[weakSelf handleError:error];
return;
}
onSourceLoad(error, source);
}];
}
}
通过上面的部分我们了解到 RN 代码加载使用的是 RCTJavaScriptLoader
的loadBundleAtURL_onProgress_onComplete
的方法;我们可以使用 onProgress 监听加载进度,在 onComplete
中根据 error 是否为空判断加载是否成功;并且可以使用 source.data
获取到加载的二进制 JS 代码。
那么 RCTJavaScriptLoader 又是如何加载的?
// RCTJavaScriptLoader.mm
+ (void)loadBundleAtURL:(NSURL *)scriptURL
onProgress:(RCTSourceLoadProgressBlock)onProgress
onComplete:(RCTSourceLoadBlock)onComplete
{
int64_t sourceLength;
NSError *error;
// 尝试使用同步加载的方式加载jsbundle
NSData *data = [self attemptSynchronousLoadOfBundleAtURL:scriptURL
runtimeBCVersion:JSNoBytecodeFileFormatVersion
sourceLength:&sourceLength
error:&error];
if (data) {
onComplete(nil, RCTSourceCreate(scriptURL, data, sourceLength));
return;
}
const BOOL isCannotLoadSyncError = [error.domain isEqualToString:RCTJavaScriptLoaderErrorDomain] &&
error.code == RCTJavaScriptLoaderErrorCannotBeLoadedSynchronously;
// 否则尝试使用异步加载jsbundle
if (isCannotLoadSyncError) {
attemptAsynchronousLoadOfBundleAtURL(scriptURL, onProgress, onComplete);
} else {
onComplete(error, nil);
}
}
从上面的代码我们知道,jsbundle 的加载分为两种情况:
- 同步加载
- 异步加载
默认的情况,会尝试使用同步加载的方式,如果同步加载失败则使用异步加载的方式。
# 同步加载 JS 及三种 Bundle
由于同步加载代码较长笔者暂且保留重要部分,有兴趣同学可自行查阅。
+ (NSData *)attemptSynchronousLoadOfBundleAtURL:(NSURL *)scriptURL
runtimeBCVersion:(int32_t)runtimeBCVersion
sourceLength:(int64_t *)sourceLength
error:(NSError **)error
{
// ... 此处部分进行了scriptURL非空判断
// 如果bundle不再本地,那么就报错,不能同步加载Bundle
if (!scriptURL.fileURL) {
if (error) {
*error = [NSError errorWithDomain:RCTJavaScriptLoaderErrorDomain
code:RCTJavaScriptLoaderErrorCannotBeLoadedSynchronously
userInfo:@{
NSLocalizedDescriptionKey :
[NSString stringWithFormat:@"Cannot load %@ URLs synchronously", scriptURL.scheme]
}];
}
return nil;
}
// 通过bundle的前4个字节,可以判断出当前的bundle是普通的Bundle还是RAM bundle(RAM bundle前四个字节的值为0xFB0BD1E5)
// RAM bundle 相比普通的bundle好处在于可以使用【懒加载】的方式将 module注入到JSC中
// 使用fopen读取文件
FILE *bundle = fopen(scriptURL.path.UTF8String, "r");
if (!bundle) {
if (error) {
*error = [NSError
errorWithDomain:RCTJavaScriptLoaderErrorDomain
code:RCTJavaScriptLoaderErrorFailedOpeningFile
userInfo:@{
NSLocalizedDescriptionKey : [NSString stringWithFormat:@"Error opening bundle %@", scriptURL.path]
}];
}
return nil;
}
// 读取header
facebook::react::BundleHeader header;
size_t readResult = fread(&header, sizeof(header), 1, bundle);
// 文件读取之后记得关闭哦
fclose(bundle);
// ....
// 通过header就可以知道是什么类型的Bundle了(请见下面的pareseTyoeFromHeader)
facebook::react::ScriptTag tag = facebook::react::parseTypeFromHeader(header);
switch (tag) {
case facebook::react::ScriptTag::RAMBundle:
break;
case facebook::react::ScriptTag::String: {
NSData *source = [NSData dataWithContentsOfFile:scriptURL.path options:NSDataReadingMappedIfSafe error:error];
if (sourceLength && source != nil) {
*sourceLength = source.length;
}
return source;
}
case facebook::react::ScriptTag::BCBundle:
// ...
return nil;
}
break;
}
struct stat statInfo;
if (sourceLength) {
*sourceLength = statInfo.st_size;
}
// 返回jsbundle的二进制数据
return [NSData dataWithBytes:&header length:sizeof(header)];
}
// JSBundleType.cpp
static uint32_t constexpr RAMBundleMagicNumber = 0xFB0BD1E5;
static uint32_t constexpr BCBundleMagicNumber = 0x6D657300;
ScriptTag parseTypeFromHeader(const BundleHeader &header) {
switch (folly::Endian::little(header.magic)) {
case RAMBundleMagicNumber:
return ScriptTag::RAMBundle;
case BCBundleMagicNumber:
return ScriptTag::BCBundle;
default:
return ScriptTag::String;
}
}
通过上面的代码我们知道同步加载 jsbundle
一共做了 4 件事情:
- 判断
bundle
是否在本地,因为同步加载只加载本地 bundle;否则直接报错; - 使用
fopen
读取本地 bundle; - 通过
bundle
的前 4 个字节来判断 bundle 属于什么类型:RAMBundle
,String
,BCBundle
; - 返回
bundle
的二进制数据.
读到这里,四个步骤是不是对 RAMBundle
, String
, BCBundle
这三种类型有疑问,他们分别是干嘛的;笔者就在此处给大家解答疑惑。
正常情况下我们使用react-native bundle
打的包是普通的包也就是String
类型;如果你使用react-native ram-bundle
则是打的 RAMBundle;
那么相比普通的 Bundle
,RAMBundle
有什么好处呢?
在
react-native
执行 JS 代码之前,必须将代码加载到内存中并进行解析。如果你加载了一个 50MB 的普通 Bundle,那么所有的 50MB 都必须被加载和解析才能被执行。RAM 格式的 Bundle 则对此进行了优化,即启动时只加载 50MB 中实际需要的部分,之后再逐渐按需加载更多的包。来自官网的描述 (opens new window)
RAMBundle
:RAM bundle
相比普通的bundle
好处在于可以使用【懒加载】的方式将 module 注入到 JSC 中;RAMBundle 是用 ram-bundle 命令打出来的bundle
,它除了生成整合的 js 文件index.ios.bundle
外,还会生成各个单独的未整合 js 文件,全部放在js-modules
目录下, bundle 头四个字节固定为0xFB0BD1E5
. RAMBundle 的使用及设置,详情请见官网 (opens new window)BCBundle
:BCBundle
是 js 字节码bundle
类型; 不允许使用;String
: 普通的 bundle
# 异步加载 jsbundle
上面介绍了同步加载 bundle 就是读取本地磁盘预置或预先下载的 bundle 数据,所以不难判断异步加载 bundle 就是下载网络上的 bundle。下面我们来看下源码:
static void attemptAsynchronousLoadOfBundleAtURL(
NSURL *scriptURL,
RCTSourceLoadProgressBlock onProgress,
RCTSourceLoadBlock onComplete)
{
// 如果是本地的url则进行异步加载本地的Bundle
if (scriptURL.fileURL) {
// Reading in a large bundle can be slow. Dispatch to the background queue to do it.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSError *error = nil;
NSData *source = [NSData dataWithContentsOfFile:scriptURL.path options:NSDataReadingMappedIfSafe error:&error];
onComplete(error, RCTSourceCreate(scriptURL, source, source.length));
});
return;
}
// 启动一个下载打Task
RCTMultipartDataTask *task = [[RCTMultipartDataTask alloc] initWithURL:scriptURL
partHandler:^(NSInteger statusCode, NSDictionary *headers, NSData *data, NSError *error, BOOL done) {
if (!done) {
if (onProgress) {
onProgress(progressEventFromData(data));
}
return;
}
// ... 处理下载的异常请见
onComplete(error, nil);
return;
}
// 对于有多个请求头的,包含X-Http-Status请求头判断其值是否为200,如果不是则直接报错
NSString *statusCodeHeader = headers[@"X-Http-Status"];
if (statusCodeHeader) {
statusCode = [statusCodeHeader integerValue];
}
if (statusCode != 200) {
error =
[NSError errorWithDomain:@"JSServer"
code:statusCode
userInfo:userInfoForRawResponse([[NSString alloc] initWithData:data
encoding:NSUTF8StringEncoding])];
onComplete(error, nil);
return;
}
// 校验服务器返回的是否为text/javascript
NSString *contentType = headers[@"Content-Type"];
NSString *mimeType = [[contentType componentsSeparatedByString:@";"] firstObject];
if (![mimeType isEqualToString:@"application/javascript"] && ![mimeType isEqualToString:@"text/javascript"]) {
NSString *description = [NSString
stringWithFormat:@"Expected MIME-Type to be 'application/javascript' or 'text/javascript', but got '%@'.",
mimeType];
// ... error初始
onComplete(error, nil);
return;
}
// 把返回的数据包装成RCTSource并返回
RCTSource *source = RCTSourceCreate(scriptURL, data, data.length);
parseHeaders(headers, source);
onComplete(nil, source);
}
progressHandler:^(NSDictionary *headers, NSNumber *loaded, NSNumber *total) {
// Only care about download progress events for the javascript bundle part.
if ([headers[@"Content-Type"] isEqualToString:@"application/javascript"]) {
onProgress(progressEventFromDownloadProgress(loaded, total));
}
}];
// 启动下载任务
[task startTask];
}
果不其然,异步加载主要做了两件事情:
- 如果是本地的 Bundle 则使用异步加载本地的方式;
- 如果不是本地 Bundle 则实例化一个
RCTMultipartDataTask
下载任务;异步下载 Bundle;
对于 Bundle 的下载相信讲到这里大家已经了然于心;在这里简单做一个总结:
RN 默认优先判断是否支持本地 Bundle
同步加载
;如何可以则:- 判断
bundle
是否在本地,因为同步加载只加载本地 bundle;否则直接报错; - 使用
fopen
读取本地 bundle; - 通过
bundle
的前 4 个字节来判断 bundle 属于什么类型:RAMBundle
,String
,BCBundle
; - 返回
bundle
的二进制数据.
- 判断
否则使用异步加载 Bundle的方式
- 如果是本地的 Bundle 则使用异步加载本地的方式;
- 如果不是本地 Bundle 则实例化一个
RCTMultipartDataTask
下载任务;异步下载 Bundle;
加载完成 Bundle 就该执行 JS 代码咯。
# 执行 JS 代码
在 RCTxxBridge 的 start 方法中;js 代码的执行需要等到 jsbundle 以及加载且 native modules 也已加载完成才会进行执行。
// RCTxxBridge.mm
// 等待 jsbundle的和native modules完成加载后则开始执行代码
dispatch_group_notify(prepareBridge, dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
RCTCxxBridge *strongSelf = weakSelf;
if (sourceCode && strongSelf.loading) {
[strongSelf executeSourceCode:sourceCode sync:NO];
}
});
// js代码的执行
- (void)executeSourceCode:(NSData *)sourceCode sync:(BOOL)sync{
// js代码执行回调
dispatch_block_t completion = ^{
// 当js代码执行完成,需要刷新js执行事件队列
[self _flushPendingCalls];
// 在主线程中通知RCTRootView; js代码已经执行完毕;当RCTRootView接收到通知就会挂在并展示
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:RCTJavaScriptDidLoadNotification
object:self->_parentBridge
userInfo:@{@"bridge" : self}];
[self ensureOnJavaScriptThread:^{
// 定时器继续执行
[self->_displayLink addToRunLoop:[NSRunLoop currentRunLoop]];
}];
});
};
if (sync) {
// 同步执行js代码
[self executeApplicationScriptSync:sourceCode url:self.bundleURL];
completion();
} else {
// 异步执行js代码
[self enqueueApplicationScript:sourceCode url:self.bundleURL onComplete:completion];
}
[self.devSettings setupHotModuleReloadClientIfApplicableForURL:self.bundleURL];
}
通过上面我们简单了解到:
- 根据 sync 来选择是同步执行 js 还是异步执行 js;
- js 执行完成之后会进入事件回调 completion;在事件回调中我们会刷新当前的 js 执行队列并发送通知给 RCTRootView;
上面讲到同步执行 js 和异步执行 js 的两个方法;enqueueApplicationScript
和executeApplicationScriptSync
查阅源码,我们知道这两个方法都是调用的同一个方法executeApplicationScript
;
// RCTxxBridge.mm
- (void)executeApplicationScript:(NSData *)script url:(NSURL *)url async:(BOOL)async
{
[self _tryAndHandleError:^{
NSString *sourceUrlStr = deriveSourceURL(url);
// 发送 将要执行JS的通知 RCTJavaScriptWillStartExecutingNotification
[[NSNotificationCenter defaultCenter] postNotificationName:RCTJavaScriptWillStartExecutingNotification
object:self->_parentBridge
userInfo:@{@"bridge" : self}];
// 如果是RAMBundle则调用_reactInstance的loadRAMBundle:方法
// 否则调用_reactInstance的loadScriptFromString:方法
// 还记得吗reactInstance就是我们在上一篇文章中讲到的Instance初始化;
auto reactInstance = self->_reactInstance;
if (isRAMBundle(script)) {
[self->_performanceLogger markStartForTag:RCTPLRAMBundleLoad];
auto ramBundle = std::make_unique<JSIndexedRAMBundle>(sourceUrlStr.UTF8String);
std::unique_ptr<const JSBigString> scriptStr = ramBundle->getStartupCode();
[self->_performanceLogger markStopForTag:RCTPLRAMBundleLoad];
[self->_performanceLogger setValue:scriptStr->size() forTag:RCTPLRAMStartupCodeSize];
if (reactInstance) {
auto registry =
RAMBundleRegistry::multipleBundlesRegistry(std::move(ramBundle), JSIndexedRAMBundle::buildFactory());
reactInstance->loadRAMBundle(std::move(registry), std::move(scriptStr), sourceUrlStr.UTF8String, !async);
}
} else if (reactInstance) {
reactInstance->loadScriptFromString(std::make_unique<NSDataBigString>(script), sourceUrlStr.UTF8String, !async);
} else {
std::string methodName = async ? "loadBundle" : "loadBundleSync";
throw std::logic_error("Attempt to call " + methodName + ": on uninitialized bridge");
}
}];
}
如果您看了上一篇的 RN 初始化文章,是不是豁然开朗;js 代码的执行利用的是_reactInstance->loadRAMBundle
或者reactInstance->loadScriptFromString
方法; 然而我们之前也已经讲到 reactInstance
会初始化 NativeToJsBridge
;NativeToJsBridge
会利用 factory 初始化 JSIExecutor
;其实 Instance
是 NativeToJsBridged
的包装,NativeToJsBridge
又是 JSIExecutor
的包装;
为了证实这一点,我们一起来看一下具体的源码:(此处拿 loadScriptFromString
为例)
//Instance.cpp
void Instance::loadScriptFromString(
std::unique_ptr<const JSBigString> string,
std::string sourceURL,
bool loadSynchronously) {
SystraceSection s("Instance::loadScriptFromString", "sourceURL", sourceURL);
if (loadSynchronously) {
// 同步加载Bundle
loadBundleSync(nullptr, std::move(string), std::move(sourceURL));
} else {
// 异步加载Bundle
loadBundle(nullptr, std::move(string), std::move(sourceURL));
}
}
void Instance::loadBundle(
std::unique_ptr<RAMBundleRegistry> bundleRegistry,
std::unique_ptr<const JSBigString> string,
std::string sourceURL) {
callback_->incrementPendingJSCalls();
SystraceSection s("Instance::loadBundle", "sourceURL", sourceURL);
// 最终还是调用的NativeToJsBridge的加载方法
nativeToJsBridge_->loadBundle(
std::move(bundleRegistry), std::move(string), std::move(sourceURL));
}
void Instance::loadBundleSync(
std::unique_ptr<RAMBundleRegistry> bundleRegistry,
std::unique_ptr<const JSBigString> string,
std::string sourceURL) {
std::unique_lock<std::mutex> lock(m_syncMutex);
m_syncCV.wait(lock, [this] { return m_syncReady; });
// 最终还是调用的NativeToJsBridge的加载方法
nativeToJsBridge_->loadBundleSync(
std::move(bundleRegistry), std::move(string), std::move(sourceURL));
}
// NativeToJsBridge.cpp
void NativeToJsBridge::loadBundleSync(
std::unique_ptr<RAMBundleRegistry> bundleRegistry,
std::unique_ptr<const JSBigString> startupScript,
std::string startupScriptSourceURL) {
if (bundleRegistry) {
m_executor->setBundleRegistry(std::move(bundleRegistry));
}
try {
// 调用是JSIExecutor的加载方法
m_executor->loadBundle(
std::move(startupScript), std::move(startupScriptSourceURL));
} catch (...) {
m_applicationScriptHasFailure = true;
throw;
}
}
相信看到这里已经证实了笔者上面所说的观点;下面我们就来具体看看 JSIExecutor 中的加载方法:
// JSIExecutor.cpp
void JSIExecutor::loadBundle(
std::unique_ptr<const JSBigString> script,
std::string sourceURL) {
// 没错执行就是这么简单粗暴,使用了JSCRuntime的执行js方法
runtime_->evaluateJavaScript(
std::make_unique<BigStringBuffer>(std::move(script)), sourceURL);
// 不要忘记还有一个刷新的操作
flush();
}
// JSCRuntime.cpp
jsi::Value JSCRuntime::evaluateJavaScript(
const std::shared_ptr<const jsi::Buffer> &buffer,
const std::string &sourceURL) {
std::string tmp(
reinterpret_cast<const char *>(buffer->data()), buffer->size());
JSStringRef sourceRef = JSStringCreateWithUTF8CString(tmp.c_str());
JSStringRef sourceURLRef = nullptr;
if (!sourceURL.empty()) {
sourceURLRef = JSStringCreateWithUTF8CString(sourceURL.c_str());
}
JSValueRef exc = nullptr;
// 使用JavaScriptCore执行js代码
JSValueRef res =
JSEvaluateScript(ctx_, sourceRef, nullptr, sourceURLRef, 0, &exc);
JSStringRelease(sourceRef);
if (sourceURLRef) {
JSStringRelease(sourceURLRef);
}
checkException(res, exc);
return createValue(res);
}
在 ctx 中执行 JS 源码后,会初始化 JS 环境,BatchedBridge.js
,NativeModules.js
中的初始化代码也会执行。在 BatchedBridge.js
中,创建了一个名为 BatchedBridge
的 MessageQueue,并设置到
global
的__fbBatchedBridge
属性里,这个属性后面会用到。在初始化 JS 环境的时候,会加载到某些 NativeModule
,这些 module 才会被初始化,即调用到 native 侧 JSINativeModules
的 getModule
方法。当相关的 Module
都加载完之后,evaluateScript
方法执行完,JS 环境初始化完毕。
不知您注意到在 JSIExecutor 执行 jsbundle 之后有一个 flush 方法没?
# flush 方法
flush 方法中内容比较多。
void JSIExecutor::flush() {
// 如果JSIExecutor的flushedQueue_函数不为空,则通过函数flushedQueue_获取待调用的方法queue,然后执行callNativeModules
if (flushedQueue_) {
callNativeModules(flushedQueue_->call(*runtime_), true);
return;
}
// 通过__fbBatchedBridge(在上一篇已经讲过)作为属性key去global中取对应的值也就是batchedBridge,batchedBridge本质上是JS侧的MessageQueue类实例化的一个对象
Value batchedBridge =
runtime_->global().getProperty(*runtime_, "__fbBatchedBridge");
// 如果 js侧的batchedBridge对象为空(表示还未有任何Native modules被执行),那么就执行bindBridge操作; bindBridge的主要工作是将js侧的属性/方法绑定给native,以便于后续Native调用js方法
if (!batchedBridge.isUndefined()) {
bindBridge();
callNativeModules(flushedQueue_->call(*runtime_), true);
} else if (delegate_) {
callNativeModules(nullptr, true);
}
}
// 各种js方法向native的绑定
void JSIExecutor::bindBridge() {
std::call_once(bindFlag_, [this] {
// 通过js侧的__fbBatchedBridge获取对应的batchedBridge
Value batchedBridgeValue =
runtime_->global().getProperty(*runtime_, "__fbBatchedBridge");
if (batchedBridgeValue.isUndefined()) {
throw JSINativeException(
"Could not get BatchedBridge, make sure your bundle is packaged correctly");
}
// 把batchedBridge中的callFunctionReturnFlushedQueue 和 JSIExecutor对象的callFunctionReturnFlushedQueue_进行绑定
Object batchedBridge = batchedBridgeValue.asObject(*runtime_);
callFunctionReturnFlushedQueue_ = batchedBridge.getPropertyAsFunction(
*runtime_, "callFunctionReturnFlushedQueue");
// 把batchedBridge中的invokeCallbackAndReturnFlushedQueue 和 JSIExecutor中的invokeCallbackAndReturnFlushedQueue_进行绑定;
invokeCallbackAndReturnFlushedQueue_ = batchedBridge.getPropertyAsFunction(
*runtime_, "invokeCallbackAndReturnFlushedQueue");
// 把batchedBridge中的flushedQueue 和 JSIExecutor中的flushedQueue_进行绑定。
flushedQueue_ =
batchedBridge.getPropertyAsFunction(*runtime_, "flushedQueue");
});
}
void JSIExecutor::callNativeModules(const Value &queue, bool isEndOfBatch) {
delegate_->callNativeModules(
*this, dynamicFromValue(*runtime_, queue), isEndOfBatch);
}
// BatchedBridge.js
const MessageQueue = require("./MessageQueue");
const BatchedBridge: MessageQueue = new MessageQueue();
Object.defineProperty(global, "__fbBatchedBridge", {
configurable: true,
value: BatchedBridge,
});
module.exports = BatchedBridge;
总结一下 flush 方法的作用:
在 ctx 中执行 JS 源码,会初始化 JS 环境,
BatchedBridge.js,NativeModules.js
中的初始化代码也会执行。在BatchedBridge.js
中,创建了一个名为 BatchedBridge 的 MessageQueue,并设置到 global 的__fbBatchedBridge
属性里;如果
JSIExecutor
的flushedQueue
函数不为空,则通过函数flushedQueue
获取待调用的方法 queue,然后执行callNativeModules;
通过
__fbBatchedBridge
(在上一篇已经讲过)作为属性 key 去 global 中取对应的值也就是batchedBridge,batchedBridge
本质上是 JS 侧的MessageQueue
类实例化的一个对象;如果获取到的 batchedBridge 为空或者还未绑定,则先将 js 函数和 native 进行绑定,然后执行
callNativeModules
;callNativeModules
的主要作用其实就是通过invoke
的方式执行 native modules 中方法。
// 根据callNativeModules追溯到,后面会调用ModuleRegistry
// ModuleRegistry.cpp
void ModuleRegistry::callNativeMethod(
unsigned int moduleId,
unsigned int methodId,
folly::dynamic &¶ms,
int callId) {
modules_[moduleId]->invoke(methodId, std::move(params), callId);
}
# 总结
至此,JSBundle 的加载以及执行以及讲解完毕,现在我们来总结一下各个部分都做了什么。
JSBundle 的加载
RN 默认优先判断是否支持本地 Bundle
同步加载
;如何可以则:- 判断
bundle
是否在本地,因为同步加载只加载本地 bundle;否则直接报错; - 使用
fopen
读取本地 bundle; - 通过
bundle
的前 4 个字节来判断 bundle 属于什么类型:RAMBundle
,String
,BCBundle
; - 返回
bundle
的数据.
- 判断
否则使用异步加载 Bundle的方式
- 如果是本地的 Bundle 则使用异步加载本地的方式;
- 如果不是本地 Bundle 则实例化一个
RCTMultipartDataTask
下载任务;异步下载 Bundle;
JSBundle 的加载
JSBundle 的加载和运行较为复杂,其主要步骤如下:
代码的执行统一使用了
RCTxxBridge
中的executeSourceCode
去执行;其内部统一使用了executeApplicationScript
去处理;RCTxxBridge
拥有reactInstance
; 利用其实例去执行对应的jsbundle
;然而我们之前也已经讲到reactInstance
会初始化NativeToJsBridge
;NativeToJsBridge
会利用 factory 初始化JSIExecutor
;其实Instance
是NativeToJsBridged
的包装,NativeToJsBridge
又是JSIExecutor
的包装 所以最终会使用JSIExecutor
的loadBundle
去加载;
在 JSIExecutor 中的 loadBundle 会使用 JSCRuntime 去利用 JavaScriptCore 在 iOS 程序中取执行 js 代码;
执行 js 代码的之前会初始化整个 JS 的上下文 JSContext;并执行 js 和 native 通信所需的 js 代码;
执行完成 js 代码后会调用 flush 方法,去调用 native modules;
由于初始化了 JSContext,
BatchedBridge.js,NativeModules.js
中的初始化代码也会执行。在BatchedBridge.js
中,创建了一个名为 BatchedBridge 的 MessageQueue,并设置到 global 的__fbBatchedBridge
属性里;如果
JSIExecutor
的flushedQueue
函数不为空,则通过函数flushedQueue
获取待调用的方法 queue,然后执行callNativeModules;
通过
__fbBatchedBridge
(在上一篇已经讲过)作为属性 key 去 global 中取对应的值也就是batchedBridge,batchedBridge
本质上是 JS 侧的MessageQueue
类实例化的一个对象;如果获取到的
batchedBridge
为空或者还未绑定,则先将 js 函数和 native 进行绑定,然后执行callNativeModules
;callNativeModules
的主要作用其实就是通过invoke
的方式执行 native modules 中方法。
现在我们已经清楚了 RN 的初始化以及 JS 加载和执行的整个流程;是时候着手下一部分ReactNative 与 iOS 原生通信原理解析-通信篇 (opens new window)去理解 js 如何和 native
通信了。
ReactNative 与 iOS 原生通信原理解析系列