深入理解React Native核心原理(React Native的橋接(Bridge)
在這篇文章之前我們假設(shè)你已經(jīng)了解了React Native的基礎(chǔ)知識(shí),我們會(huì)重點(diǎn)關(guān)注當(dāng)native和JavaScript進(jìn)行信息交流時(shí)的內(nèi)部運(yùn)行原理。
主線程
在開始之前,我們需要知道在React Native中有三個(gè)主要的線程:
- shadow queue:負(fù)責(zé)布局工作
- main thread:UIKit 在這個(gè)線程工作(譯者注:UI Manager線程,可以看成主線程,主要負(fù)責(zé)頁(yè)面交互和控件繪制的邏輯)
- JavaScript thread:運(yùn)行JS代碼的線程
另外,一般情況下每個(gè)native模塊都有自己的GCD隊(duì)列,除非有特殊說(shuō)明(后面會(huì)解釋)
*shadow queue其實(shí)更像一個(gè)GCD隊(duì)列而不是線程
Native模塊
如果你還不知道怎么創(chuàng)建一個(gè)Native模塊,我推薦你去閱讀一下文檔
這是一個(gè)native模塊Person的例子,它既受JavaScript的調(diào)用,也可以調(diào)用JavaScript
@interface Person : NSObject <RCTBridgeModule>
@end
@implementation Logger
RCT_EXPORT_MODULE()
RCT_EXPORT_METHOD(greet:(NSString *)name)
{
NSLog(@"Hi, %@!", name);
[_bridge.eventDispatcher sendAppEventWithName:@"greeted" body:@{ @"name": name }];
}
@end
我們重點(diǎn)關(guān)注RCT_EXPORT_MODULE和RCT_EXPORT_METHOD這兩個(gè)宏,它們擴(kuò)展成什么,它們的角色是什么,它們是如何運(yùn)行的。
RCT_EXPORT_MODULE([js_name])
正如這個(gè)方法的名字那樣,它export出你的module,但是在這個(gè)特定的上下文中export是什么意思呢,它意味著橋接知道你的模塊。
它的定義實(shí)際上非常簡(jiǎn)單:
#define RCT_EXPORT_MODULE(js_name) \
RCT_EXTERN void RCTRegisterModule(Class); \
+ (NSString \*)moduleName { return @#js_name; } \
+ (void)load { RCTRegisterModule(self); }
它做了以下工作:
- 首先聲明RCTRegisterModule為外部函數(shù),意味著這個(gè)函數(shù)的實(shí)現(xiàn)對(duì)于編譯器不可見,但是在鏈接階段可用
- 聲明一個(gè)方法moduleName,返回可選的宏參數(shù)js_name,這樣這個(gè)模塊在JS中具有和Objective-C中不一樣的類名
- 聲明一個(gè)load方法(當(dāng)app加載到內(nèi)存中后,每個(gè)類的load方法都會(huì)被調(diào)用),load方法調(diào)用RCTRegisterModule,然后橋接才知道這個(gè)暴露出來(lái)的模塊
RCT_EXPORT_METHOD(method)
這個(gè)宏更有趣,它沒有在你的method中增加任何東西,除了聲明指定的方法外,它還創(chuàng)建了一個(gè)新方法。新方法如下所示:
+ (NSArray *)__rct_export__120
{
return @[ @"", @"log:(NSString *)message" ];
}
它是通過(guò)將前綴(__rct_export__)和可選的js_name(本例子為空)和聲明的行號(hào)以及__COUNTER__宏構(gòu)成。
這個(gè)方法的目的是返回一個(gè)包含可選js_name和method簽名的數(shù)組,這個(gè)js_name的作用是避免方法命名沖突。
Runtime
這整個(gè)設(shè)置僅僅是為了給橋接提供信息,讓它可以找到export出來(lái)的所有東西,modules和methods,但是這些都是在加載的時(shí)候發(fā)生的,現(xiàn)在我們來(lái)看看運(yùn)行的時(shí)候是怎么使用的。
這是橋接初始化時(shí)的依賴關(guān)系圖:

初始化模塊
RCTRegisterModule所做的事就是把類推進(jìn)數(shù)組,這樣在實(shí)例化一個(gè)新的橋接的時(shí)候就能找到這個(gè)類。橋接遍歷數(shù)組中的所有模塊,為每個(gè)模塊創(chuàng)建一個(gè)實(shí)例,在橋接那邊存儲(chǔ)一個(gè)實(shí)例的引用,同時(shí)給這個(gè)模塊實(shí)例一個(gè)橋接的引用(所以我們能兩邊都互相調(diào)用),然后檢查這個(gè)模塊實(shí)例是否有指定要在哪個(gè)隊(duì)列運(yùn)行,否則給它一個(gè)新隊(duì)列,與其他模塊分開:
NSMutableDictionary *modulesByName; // = ...
for (Class moduleClass in RCTGetModuleClasses()) {
// ...
module = [moduleClass new];
if ([module respondsToSelector:@selector(setBridge:)]){
module.bridge = self;
modulesByName[moduleName] = module;
// ...
}
配置模塊
一旦我們有了這些modules,在后臺(tái)線程中,我們列出每個(gè)module的所有methods,然后調(diào)用以__rct__export__開頭的methods,我們得到一個(gè)method簽名的字符串。這很重要因?yàn)槲覀儸F(xiàn)在知道了參數(shù)的實(shí)際類型,在運(yùn)行的時(shí)候我們只知道其中一個(gè)參數(shù)是id,但是通過(guò)這個(gè)途徑我們可以知道這個(gè)id實(shí)際上是NSString *
unsigned int methodCount;
Method *methods = class_copyMethodList(moduleClass, &methodCount);
for (unsigned int i = 0; i < methodCount; i++) {
Method method = methods[i];
SEL selector = method_getName(method);
if ([NSStringFromSelector(selector) hasPrefix:@"__rct_export__"]) {
IMP imp = method_getImplementation(method);
NSArray *entries = ((NSArray *(*)(id, SEL))imp)(_moduleClass, selector);
//...
[moduleMethods addObject:/* Object representing the method */];
}
}
設(shè)置JavaScript執(zhí)行器
JS執(zhí)行器有一個(gè) -setUp 方法允許它做更復(fù)雜的工作,例如在后臺(tái)線程初始化JS代碼,這同時(shí)節(jié)約了一些工作,因?yàn)橹挥谢钴S的執(zhí)行器會(huì)接受 setUp 方法的調(diào)用,而不是所有的執(zhí)行器:
JSGlobalContextRef ctx = JSGlobalContextCreate(NULL); _context = [[RCTJavaScriptContext alloc] initWithJSContext:ctx];
注入JSON配置
JSON配置僅包含我們的module,例如:

這個(gè)配置信息作為全局變量存儲(chǔ)在JavaScript虛擬機(jī),所以當(dāng)JS那邊的橋接初始化后它可以用這個(gè)信息來(lái)創(chuàng)建modules
加載JavaScript代碼
這非常直觀,只需要從指定的任何提供程序中加載源代碼,通常在開發(fā)過(guò)程中從打包程序中加載源代碼,在生產(chǎn)環(huán)境中從磁盤加載。
執(zhí)行JavaScript代碼
一旦所有事情準(zhǔn)備就緒,我們可以在JS虛擬機(jī)中加載應(yīng)用的源代碼,復(fù)制代碼,解析并執(zhí)行它。在第一次執(zhí)行時(shí)需要注冊(cè)所有CommonJS模塊并且需要入口文件。
JSValueRef jsError = NULL; JSStringRef execJSString = JSStringCreateWithCFString((__bridge CFStringRef)script); JSStringRef jsURL = JSStringCreateWithCFString((__bridge CFStringRef)sourceURL.absoluteString); JSValueRef result = JSEvaluateScript(strongSelf->_context.ctx, execJSString, NULL, jsURL, 0, &jsError); JSStringRelease(jsURL); JSStringRelease(execJSString);
JavaScript中的Modules
在JS側(cè)我們現(xiàn)在可以通過(guò)react-native的NativeModules拿到前面的JSON配置信息構(gòu)成的module:

它運(yùn)行的方式是當(dāng)你調(diào)用一個(gè)方法的時(shí)候它被放到一個(gè)隊(duì)列,包括module的名稱,method的名稱以及所有的參數(shù),在JavsScript執(zhí)行的最后這個(gè)隊(duì)列會(huì)給原生模塊執(zhí)行。
調(diào)用周期
現(xiàn)在如果我們用上面的代碼調(diào)用module,它將會(huì)是這個(gè)樣子的:

調(diào)用必須從native開始,native調(diào)用JS(這張圖只是截取了JS運(yùn)行的某個(gè)時(shí)刻),在執(zhí)行過(guò)程中,因?yàn)镴S調(diào)用NativeModules的方法,它把這個(gè)調(diào)用入隊(duì),因?yàn)檫@個(gè)調(diào)用必須在原生那邊執(zhí)行。當(dāng)JS執(zhí)行完后,原生模塊遍歷入隊(duì)的所有調(diào)用,然后當(dāng)它執(zhí)行這些調(diào)用后,通過(guò)橋接進(jìn)行回調(diào)(一個(gè)原生模塊可以通過(guò)_bridge實(shí)例來(lái)調(diào)用enqueueJSCall:args:),來(lái)再次回調(diào)JS。
(如果您一直在關(guān)注該項(xiàng)目,過(guò)去也有來(lái)自native-> JS的調(diào)用隊(duì)列,該調(diào)用隊(duì)列會(huì)在每個(gè)vSYNC上分派,但為了縮短啟動(dòng)時(shí)間已將其刪除)
參數(shù)類型
native到JS的調(diào)用很容易,參數(shù)被NSArray傳遞,我們將其編碼為JSON數(shù)據(jù),但是對(duì)于JS對(duì)native的調(diào)用,我們需要native的類型,為此我們檢查基本類型(ints,floats,chars...)但是就像上邊提及那樣,對(duì)于任何對(duì)象(結(jié)構(gòu)),運(yùn)行時(shí)我們不會(huì)從NSMthodSignature獲得足夠的信息,所以我們把類型保存為字符串。
我們使用正則表達(dá)式從method簽名中提取類型,并使用RCTConvert類來(lái)實(shí)際轉(zhuǎn)換對(duì)象,默認(rèn)情況下它為每種類型都提供了方法,并且嘗試將JSON輸入轉(zhuǎn)換為所需要的類型。
除非是一個(gè)struct,否則我們使用objc_msgSend動(dòng)態(tài)調(diào)用該方法,因?yàn)閍rm64上沒有objc_msgSend_stret的版本,因此我們使用NSInvocation。
轉(zhuǎn)換完所有參數(shù)后,我們將使用另一個(gè)NSInvocation來(lái)調(diào)用目標(biāo)module和method。
例子:
// If you had the following method in a given module, e.g. `MyModule`
RCT_EXPORT_METHOD(methodWithArray:(NSArray *) size:(CGRect)size) {}
// And called it from JS, like:
require('NativeModules').MyModule.method(['a', 1], {
x: 0,
y: 0,
width: 200,
height: 100
});
// The JS queue sent to native would then look like the following:
// ** Remember that it's a queue of calls, so all the fields are arrays **
@[
@[ @0 ], // module IDs
@[ @1 ], // method IDs
@[ // arguments
@[
@[@"a", @1],
@{ @"x": @0, @"y": @0, @"width": @200, @"height": @100 }
]
]
];
// This would convert into the following calls (pseudo code)
NSInvocation call
call[args][0] = GetModuleForId(@0)
call[args][1] = GetMethodForId(@1)
call[args][2] = obj_msgSend(RCTConvert, NSArray, @[@"a", @1])
call[args][3] = NSInvocation(RCTConvert, CGRect, @{ @"x": @0, ... })
call()
線程
正如以上提及那樣,每個(gè)module默認(rèn)都有一個(gè)GCD隊(duì)列,除非它通過(guò)實(shí)現(xiàn)-methodQueue方法或?qū)ethodQueue屬性與有效隊(duì)列合并來(lái)指定要在哪個(gè)隊(duì)列運(yùn)行。ViewManagers*是例外(擴(kuò)展了RCTViewManager),將默認(rèn)使用Shadow Queue,而特殊目標(biāo)RCTJSThread僅是一個(gè)占位符,因?yàn)樗蔷€程而不是隊(duì)列。
(其實(shí)View Managers不是真正的例外,因?yàn)榛愶@式的將Shadow Queue指定為目標(biāo)隊(duì)列了)
當(dāng)前線程規(guī)則如下:
- -init和-setBridge:保證在主線程執(zhí)行
- 所有export的方法保證在目標(biāo)隊(duì)列執(zhí)行
- 如果你實(shí)現(xiàn)了RCTInvalidating協(xié)議,則還可以確保在目標(biāo)隊(duì)列上調(diào)用了invalidate
- 無(wú)法保證在哪個(gè)線程調(diào)用-dealloc
當(dāng)接收到JS的一批調(diào)用時(shí),這些調(diào)用會(huì)按目標(biāo)隊(duì)列進(jìn)行分組,并行調(diào)用:
// group `calls` by `queue` in `buckets`
for (id queue in buckets) {
dispatch_block_t block = ^{
NSOrderedSet *calls = [buckets objectForKey:queue];
for (NSNumber *indexObj in calls) {
// Actually call
}
};
if (queue == RCTJSThread) {
[_javaScriptExecutor executeBlockOnJavaScriptQueue:block];
} else if (queue) {
dispatch_async(queue, block);
}
}
結(jié)尾
這就是React Native橋接工作原理的更深入概述。我希望者對(duì)想要構(gòu)建更復(fù)雜modules或者想對(duì)核心框架有貢獻(xiàn)的人有所幫助。
到此這篇關(guān)于深入理解React Native核心原理(React Native的橋接(Bridge)的文章就介紹到這了,更多相關(guān)React Native原理內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
React.InputHTMLAttributes實(shí)踐和注意事項(xiàng)
文章討論了如何在封裝組件中使用React.InputHTMLAttributes,以及如何通過(guò) {...inputProps} 動(dòng)態(tài)傳遞屬性,最后,文章總結(jié)了使用React.InputHTMLAttributes的最佳實(shí)踐和注意事項(xiàng),感興趣的朋友一起看看吧2024-11-11
react 權(quán)限樹形結(jié)構(gòu)實(shí)現(xiàn)代碼
這篇文章主要介紹了react 權(quán)限樹形結(jié)構(gòu)實(shí)現(xiàn)代碼,項(xiàng)目背景react + ant design,本文結(jié)合實(shí)例代碼給大家介紹的非常詳細(xì),感興趣的朋友一起看看吧2024-05-05
React模擬實(shí)現(xiàn)Vue的keepAlive功能
Vue中,keep-alive組件可以緩存組件狀態(tài),在路由切換時(shí)重新掛載,實(shí)現(xiàn)這一功能在React中并不簡(jiǎn)單,但我們可以借助一個(gè)第三方庫(kù)——react-activation 來(lái)模擬Vue的keep-alive功能,需要的朋友可以參考下2024-10-10
使用react-virtualized實(shí)現(xiàn)圖片動(dòng)態(tài)高度長(zhǎng)列表的問(wèn)題
一般我們?cè)趯憆eact項(xiàng)目中,同時(shí)渲染很多dom節(jié)點(diǎn),會(huì)造成頁(yè)面卡頓, 空白的情況。為了解決這個(gè)問(wèn)題,今天小編給大家分享一篇教程關(guān)于react-virtualized實(shí)現(xiàn)圖片動(dòng)態(tài)高度長(zhǎng)列表的問(wèn)題,感興趣的朋友跟隨小編一起看看吧2021-05-05
記錄React使用connect后,ref.current為null問(wèn)題及解決
記錄React使用connect后,ref.current為null問(wèn)題及解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-05-05
在React中實(shí)現(xiàn)Vue的插槽功能的示例代碼
在?Vue?中,插槽(Slots)允許父組件向子組件傳遞?HTML?結(jié)構(gòu),從而實(shí)現(xiàn)更靈活的組件復(fù)用,具名插槽允許父組件向子組件傳遞多個(gè)不同的?HTML?結(jié)構(gòu),在?React?中,我們沒有直接的插槽概念,但可以通過(guò)?props.children?和函數(shù)作為?props?來(lái)實(shí)現(xiàn)類似的功能2025-01-01
React Native基礎(chǔ)入門之調(diào)試React Native應(yīng)用的一小步
這篇文章主要給大家介紹了關(guān)于React Native基礎(chǔ)入門之調(diào)試React Native應(yīng)用的相關(guān)資料,文中通過(guò)圖文介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2018-07-07

