使用App.png
本文結構
- 目前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源碼解析