原文位址:ReactNative與iOS原生通信原了解析-JS加載及執行篇
導語: 其實原本是想編寫一篇
(下文簡稱 rn) 在
react-native
中如何實作
iOS
的文章;相信看過官方文檔的同學都清楚 rn 和 iOS 通信使用了一個叫
jsbridge
的子產品去實作。但是不知怎麼呢?為了查閱其通信的原理,編寫了一篇 ReactNative 與 iOS 原生通信原了解析-初始化 ; 由于篇幅過長,我們還未講解 JS 代碼的加載和執行;下面我們就開始講解第二個部分【ReactNative 與 iOS 原生通信原了解析-JS 加載及執行篇】。
RCTBridgeModule
聲明: 本文所使用的 rn 版本為
0.63.0
;本文篇幅較長,由于涉及到原理是以本文存在大量 RN 的源碼,還請諒解。
緣起
此篇是上一篇
ReactNative 與 iOS 原生通信原了解析-初始化的姊妹篇,建議對 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
- 使用
讀取本地 bundle;fopen
- 通過
的前 4 個位元組來判斷 bundle 屬于什麼類型: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 中實際需要的部分,之後再逐漸按需加載更多的包。來自 官網的描述
-
:RAMBundle
RAM bundle
bundle
,它除了生成整合的 js 檔案bundle
index.ios.bundle
目錄下, bundle 頭四個位元組固定為js-modules
. RAMBundle 的使用及設定,詳情請見 官網0xFB0BD1E5
-
:BCBundle
是 js 位元組碼BCBundle
類型; 不允許使用;bundle
-
: 普通的 bundleString
異步加載 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 則執行個體化一個
下載下傳任務;異步下載下傳 Bundle;RCTMultipartDataTask
對于 Bundle 的下載下傳相信講到這裡大家已經了然于心;在這裡簡單做一個總結:
- RN 預設 優先判斷是否支援本地 Bundle
;如何可以則:同步加載
- 判斷
是否在本地,因為同步加載隻加載本地 bundle;否則直接報錯;bundle
- 使用
讀取本地 bundle;fopen
- 通過
的前 4 個位元組來判斷 bundle 屬于什麼類型:bundle
,RAMBundle
,String
;BCBundle
- 傳回
的二進制資料.bundle
- 否則使用 異步加載 Bundle 的方式
- 如果是本地的 Bundle 則使用異步加載本地的方式;
- 如果不是本地 Bundle 則執行個體化一個
下載下傳任務;異步下載下傳 Bundle;RCTMultipartDataTask
加載完成 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 的 MessageQueue,并設定到 global 的BatchedBridge.js
屬性裡;__fbBatchedBridge
- 如果
的JSIExecutor
函數不為空,則通過函數flushedQueue
擷取待調用的方法 queue,然後執行flushedQueue
callNativeModules;
- 通過
(在上一篇已經講過)作為屬性 key 去 global 中取對應的值也就是__fbBatchedBridge
本質上是 JS 側的batchedBridge,batchedBridge
類執行個體化的一個對象;MessageQueue
- 如果擷取到的 batchedBridge 為空或者還未綁定,則先将 js 函數和 native 進行綁定,然後執行
;callNativeModules
-
的主要作用其實就是通過callNativeModules
的方式執行 native modules 中方法。invoke
// 根據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
- 使用
讀取本地 bundle;fopen
- 通過
的前 4 個位元組來判斷 bundle 屬于什麼類型:bundle
,RAMBundle
,String
;BCBundle
- 傳回
的資料.bundle
- 否則使用 異步加載 Bundle 的方式
- 如果是本地的 Bundle 則使用異步加載本地的方式;
- 如果不是本地 Bundle 則執行個體化一個
下載下傳任務;異步下載下傳 Bundle;RCTMultipartDataTask
JSBundle 的加載和運作較為複雜,其主要步驟如下:
- 代碼的執行統一使用了
中的RCTxxBridge
去執行;其内部統一使用了executeSourceCode
去處理;executeApplicationScript
-
擁有RCTxxBridge
; 利用其執行個體去執行對應的reactInstance
; 然而我們之前也已經講到jsbundle
reactInstance
;NativeToJsBridge
會利用 factory 初始化NativeToJsBridge
;其實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 的 MessageQueue,并設定到 global 的BatchedBridge.js
屬性裡;__fbBatchedBridge
- 如果
的JSIExecutor
函數不為空,則通過函數flushedQueue
擷取待調用的方法 queue,然後執行flushedQueue
callNativeModules;
- 通過
(在上一篇已經講過)作為屬性 key 去 global 中取對應的值也就是__fbBatchedBridge
本質上是 JS 側的batchedBridge,batchedBridge
類執行個體化的一個對象;MessageQueue
- 如果擷取到的
為空或者還未綁定,則先将 js 函數和 native 進行綁定,然後執行batchedBridge
;callNativeModules
-
的主要作用其實就是通過callNativeModules
的方式執行 native modules 中方法。invoke
現在我們已經清楚了 RN 的初始化以及 JS 加載和執行的整個流程;是時候着手下一部分
ReactNative 與 iOS 原生通信原了解析-通信篇去了解 js 如何和
native
通信了。
ReactNative 與 iOS 原生通信原了解析系列- ReactNative 與 iOS 原生通信原了解析-初始化
- ReactNative 與 iOS 原生通信原了解析-JS 加載及執行篇
- ReactNative 與 iOS 原生通信原了解析-通信篇