React Native 是最近非?;鸬囊粋€(gè)話題,介紹如何利用 React Native 進(jìn)行開發(fā)的文章和書籍多如牛毛,但面向入門水平并介紹它工作原理的文章卻寥寥無幾。本文分為兩個(gè)部分:上半部分用通俗的語言解釋了相關(guān)的名詞,重點(diǎn)介紹 React Native 出現(xiàn)的背景和試圖解決的問題。適合新手對 React Native 形成初步了解。(事實(shí)證明,女票能看懂這段)下半部分則通過源碼(0.27 版本)分析 React Native 的工作原理,適合深入學(xué)習(xí)理解 React Native 的運(yùn)行機(jī)制。最后則是我個(gè)人對 React Native 的分析與前景判斷。動態(tài)配置由于 AppStore 審核周期的限制,如何動態(tài)的更改 app 成為了永恒的話題。無論采用何種方式,我們的流程總是可以歸結(jié)為以下三部曲:“從 Server 獲取配置 --> 解析 --> 執(zhí)行native代碼”。很多時(shí)候,我們自覺或者不自覺的利用 JSON 文件實(shí)現(xiàn)動態(tài)配置的效果,它的核心流程是:通過 HTTP 請求獲取 JSON 格式的配置文件。配置文件中標(biāo)記了每一個(gè)元素的屬性,比如位置,顏色,圖片 URL 等。解析完 JSON 后,我們調(diào)用 Objective-C 的代碼,完成 UI 控件的渲染。通過這種方法,我們實(shí)現(xiàn)了在后臺配置 app 的展示樣式。從本質(zhì)上來說,移動端和服務(wù)端約定了一套協(xié)議,但是協(xié)議內(nèi)容嚴(yán)重依賴于應(yīng)用內(nèi)要展示的內(nèi)容,不利于拓展。也就是說,如果業(yè)務(wù)要求頻繁的增加或修改頁面,這套協(xié)議很難應(yīng)付。最重要的是,JSON 只是一種數(shù)據(jù)交換的格式,說白了,我們就是在解析文本數(shù)據(jù)。這就意味著它只適合提供一些配置信息,而不方便提供邏輯信息。舉個(gè)例子,我們從后臺可以配置顏色,位置等信息,但如果想要控制 app 內(nèi)的業(yè)務(wù)邏輯,就非常復(fù)雜了。記住,我們只是在解析字符串,它完全不具備運(yùn)行和調(diào)試的能力。React不妨?xí)簳r(shí)拋棄移動端的煩惱,來看看前端的“新玩意”。背景作為前端小白,我以前對前端的理解是這樣的:用 HTML 創(chuàng)建 DOM,構(gòu)建整個(gè)網(wǎng)頁的布局、結(jié)構(gòu)用 CSS 控制 DOM 的樣式,比如字體、字號、顏色、居中等用 JavaScript 接受用戶事件,動態(tài)的操控 DOM在這三者的配合下,幾乎所有頁面上的功能都能實(shí)現(xiàn)。但也有比較不爽地方,比如我想動態(tài)修改一個(gè)按鈕的文字,我需要這樣寫:old button然后在 JavaScript 中操作 DOM:function onClick() {
document.getElementById('button').innerHTML='new button';
}可以看到,在 HTML 和 JavaScript 代碼中,id 和 onclick 事件觸發(fā)的函數(shù)必須完全對應(yīng),否則就無法正確的響應(yīng)事件。如果想知道一個(gè) HTML 標(biāo)簽會如何被響應(yīng),我們還得跑去 JavaScript 代碼中查找,這種原始的配置方式讓我覺得非常不爽。初識 React隨著 FaceBook 推出了 React 框架,這個(gè)問題得到了大幅度改善。我們可以把一組相關(guān)的 HTML 標(biāo)簽,也就是 app 內(nèi)的 UI 控件,封裝進(jìn)一個(gè)組件(Component)中,我從阮一峰的 React 教程中摘錄了一段代碼:var MyComponent = React.createClass({? handleClick: function() {? ? this.refs.myTextInput.focus();? },? render: function() {? ? return (
);? }});如果你想問:“為什么 JavaScript 代碼里面出現(xiàn)了 HTML 的語法”,那么恭喜你已經(jīng)初步體會到 React 的奧妙了。這種語法被稱為 JSX,它是一種 JavaScript 語法拓展。JSX 允許我們寫 HTML 標(biāo)簽或 React 標(biāo)簽,它們終將被轉(zhuǎn)換成原生的 JavaScript 并創(chuàng)建 DOM。在 React 框架中,除了可以用 JavaScript 寫 HTML 以外,我們甚至可以寫 CSS,這在后面的例子中可以看到。理解 React前端界總是喜歡創(chuàng)造新的概念,仿佛誰說的名詞更晦澀,誰的水平就越高。如果你和當(dāng)時(shí)的我一樣,聽到 React 這個(gè)概念一臉懵逼的話,只要記住以下定義即可:React 是一套可以用簡潔的語法高效繪制 DOM 的框架上文已經(jīng)解釋過了何謂“簡潔的語法”,因?yàn)槲覀兛梢詴簳r(shí)放下 HTML 和 CSS,只關(guān)心如何用 JavaScript 構(gòu)造頁面。所謂的“高效”,是因?yàn)?React 獨(dú)創(chuàng)了 Virtual DOM 機(jī)制。Virtual DOM 是一個(gè)存在于內(nèi)存中的 JavaScript 對象,它與 DOM 是一一對應(yīng)的關(guān)系,也就是說只要有 Virtual DOM,我們就能渲染出 DOM。當(dāng)界面發(fā)生變化時(shí),得益于高效的 DOM Diff 算法,我們能夠知道 Virtual DOM 的變化,從而高效的改動 DOM,避免了重新繪制 DOM。當(dāng)然,React 并不是前端開發(fā)的全部。從之前的描述也能看出,它專注于 UI 部分,對應(yīng)到 MVC 結(jié)構(gòu)中就是 View 層。要想實(shí)現(xiàn)完整的 MVC 架構(gòu),還需要 Model 和 Controller 的結(jié)構(gòu)。在前端開發(fā)時(shí),我們可以采用 Flux 和 Redux 架構(gòu),它們并非框架(Library),而是和 MVC 一樣都是一種架構(gòu)設(shè)計(jì)(Architecture)。如果不從事前端開發(fā),就不用深入的掌握 Flux 和 Redux 架構(gòu),但理解這一套體系結(jié)構(gòu)對于后面理解 React Native 非常重要。React Native分別介紹完了移動端和前端的背景知識后,本文的主角——React Native 終于要登場了。融合前面我們介紹了移動端通過 JSON 文件傳遞信息的不足之處:只能傳遞配置信息,無法表達(dá)邏輯。從本質(zhì)上講,這是因?yàn)?JSON 畢竟只是純文本,它缺乏像編程語言那樣的運(yùn)行能力。而 React 在前端取得突破性成功以后,JavaScript 布道者們開始試圖一統(tǒng)三端。他們利用了移動平臺能夠運(yùn)行 JavaScript 代碼的能力,并且發(fā)揮了 JavaScript 不僅僅可以傳遞配置信息,還可以表達(dá)邏輯信息的優(yōu)點(diǎn)。當(dāng)痛點(diǎn)遇上特點(diǎn),兩者一拍即合,于是乎:一個(gè)基于 JavaScript,具備動態(tài)配置能力,面向前端開發(fā)者的移動端開發(fā)框架,React Native,誕生了!看到了么,這是一個(gè)面向前端開發(fā)者的框架。它的宗旨是讓前端開發(fā)者像用 React 寫網(wǎng)頁那樣,用 React Native 寫移動端應(yīng)用。這就是為什么 React Native 自稱:Learn once,Write anywhere!而非很多跨平臺語言,項(xiàng)目所說的:Write once, Run anywhere!React Native 希望前端開發(fā)者學(xué)習(xí)完 React 后,能夠用同樣的語法、工具等,分別開發(fā)安卓和 iOS 平臺的應(yīng)用并且不用一行原生代碼。如果用一個(gè)詞概括 React Native,那就是:Native 版本的 React。原理概述React Native 不是黑科技,我們寫的代碼總是以一種非常合理,可以解釋的方式的運(yùn)行著,只是絕大多數(shù)人沒有理解而已。接下來我以 iOS 平臺為例,簡單的解釋一下 React Native 的原理。首先要明白的一點(diǎn)是,即使使用了 React Native,我們依然需要 UIKit 等框架,調(diào)用的是 Objective-C 代碼??傊琂avaScript 只是輔助,它只是提供了配置信息和邏輯的處理結(jié)果。React Native 與 Hybrid 完全沒有關(guān)系,它只不過是以 JavaScript 的形式告訴 Objective-C 該執(zhí)行什么代碼。其次,React Native 能夠運(yùn)行起來,全靠 Objective-C 和 JavaScript 的交互。對于沒有接觸過 JavaScript 的人來說,非常有必要理解 JavaScript 代碼如何被執(zhí)行。我們知道 C 系列的語言,經(jīng)過編譯,鏈接等操作后,會得到一個(gè)二進(jìn)制格式的可執(zhí)行文,所謂的運(yùn)行程序,其實(shí)是運(yùn)行這個(gè)二進(jìn)制程序。而 JavaScript 是一種腳本語言,它不會經(jīng)過編譯、鏈接等操作,而是在運(yùn)行時(shí)才動態(tài)的進(jìn)行詞法、語法分析,生成抽象語法樹(AST)和字節(jié)碼,然后由解釋器負(fù)責(zé)執(zhí)行或者使用 JIT 將字節(jié)碼轉(zhuǎn)化為機(jī)器碼再執(zhí)行。整個(gè)流程由 JavaScript 引擎負(fù)責(zé)完成。蘋果提供了一個(gè)叫做 JavaScript Core 的框架,這是一個(gè) JavaScript 引擎。通過下面這段代碼可以簡單的感受一下 Objective-C 如何調(diào)用 JavaScript 代碼:JSContext *context = [[JSContext alloc] init];JSValue *jsVal = [context evaluateScript:@"21+7"];int iVal = [jsVal toInt32];這里的 JSContext 指的是 JavaScript 代碼的運(yùn)行環(huán)境,通過 evaluateScript 即可執(zhí)行 JavaScript 代碼并獲取返回結(jié)果。JavaScript 是一種單線程的語言,它不具備自運(yùn)行的能力,因此總是被動調(diào)用。很多介紹 React Native 的文章都會提到 “JavaScript 線程” 的概念,實(shí)際上,它表示的是 Objective-C 創(chuàng)建了一個(gè)單獨(dú)的線程,這個(gè)線程只用于執(zhí)行 JavaScript 代碼,而且 JavaScript 代碼只會在這個(gè)線程中執(zhí)行。Objective-C 與 JavaScript 交互提到 Objective-C 與 JavaScript 的交互,不得不推薦 bang神的這篇文章:React Native通信機(jī)制詳解 。雖然其中不少細(xì)節(jié)都已經(jīng)過時(shí),但是整體的思路值得學(xué)習(xí)。本節(jié)主要分析 Objective-C 與 JavaScript 交互時(shí)的整理邏輯與流程,下一節(jié)將通過源碼來分析具體原理。JavaScript 調(diào)用 Objective-C由于 JavaScript Core 是一個(gè)面向 Objective-C 的框架,在 Objective-C 這一端,我們對 JavaScript 上下文知根知底,可以很容易的獲取到對象,方法等各種信息,當(dāng)然也包括調(diào)用 JavaScript 函數(shù)。真正復(fù)雜的問題在于,JavaScript 不知道 Objective-C 有哪些方法可以調(diào)用。React Native 解決這個(gè)問題的方案是在 Objective-C 和 JavaScript 兩端都保存了一份配置表,里面標(biāo)記了所有 Objective-C 暴露給 JavaScript 的模塊和方法。這樣,無論是哪一方調(diào)用另一方的方法,實(shí)際上傳遞的數(shù)據(jù)只有 ModuleId、MethodId 和 Arguments 這三個(gè)元素,它們分別表示類、方法和方法參數(shù),當(dāng) Objective-C 接收到這三個(gè)值后,就可以通過 runtime 唯一確定要調(diào)用的是哪個(gè)函數(shù),然后調(diào)用這個(gè)函數(shù)。再次重申,上述解決方案只是一個(gè)抽象概念,可能與實(shí)際的解決方案有微小差異,比如實(shí)際上 Objective-C 這一端,并沒有直接保存這個(gè)模塊配置表。具體實(shí)現(xiàn)將在下一節(jié)中隨著源碼一起分析。閉包與回調(diào)既然說到函數(shù)互調(diào),那么就不得不提到回調(diào)了。對于 Objective-C 來說,執(zhí)行完 JavaScript 代碼再執(zhí)行 Objective-C 回調(diào)毫無難度,難點(diǎn)依然在于 JavaScript 代碼調(diào)用 Objective-C 之后,如何在 Objective-C 的代碼中,回調(diào)執(zhí)行 JavaScript 代碼。目前 React Native 的做法是:在 JavaScript 調(diào)用 Objective-C 代碼時(shí),注冊要回調(diào)的 Block,并且把 BlockId 作為參數(shù)發(fā)送給 Objective-C,Objective-C 收到參數(shù)時(shí)會創(chuàng)建 Block,調(diào)用完 Objective-C 函數(shù)后就會執(zhí)行這個(gè)剛剛創(chuàng)建的 Block。Objective-C 會向 Block 中傳入?yún)?shù)和 BlockId,然后在 Block 內(nèi)部調(diào)用 JavaScript 的方法,隨后 JavaScript 查找到當(dāng)時(shí)注冊的 Block 并執(zhí)行。圖解好吧,如果你是新手,并且堅(jiān)持讀到了這里,估計(jì)已經(jīng)懵逼了。不要擔(dān)心,與 JavaScript 的交互確實(shí)不是一下子能夠完全理清楚的,你可以先參考這個(gè)示意圖:交互流程注:本圖由 bang 的文章中的圖片修改而來本圖只是一個(gè)簡單的示意圖,不建議當(dāng)做時(shí)序圖使用,請參考下一節(jié)源碼分析。Objective-C 和 JavaScript 的交互總是由前者發(fā)起,本圖為了簡化,省略了這一步驟。React Native 源碼分析要想深入理解 React Native 的工作原理,有兩個(gè)部分有必要閱讀一下,分別是初始化階段和方法調(diào)用階段。為了提煉出代碼的核心含義,我會在不改變代碼意圖的基礎(chǔ)上對它做一些刪改,以便閱讀。寫這篇文章是,React Native 還處于 0.27 版本,由于在 1.0 之前的變動幅度相對較大,因此下面的源碼分析很可能隨著 React Native 的演變而過時(shí)。但不管何時(shí),把下面的源碼讀一遍都有助于你加深對 React Native 原理的理解。初始化 React Native每個(gè)項(xiàng)目都有一個(gè)入口,然后進(jìn)行初始化操作,React Native 也不例外。一個(gè)不含 Objective-C 代碼的項(xiàng)目留給我們的唯一線索就是位于 AppDelegate 文件中的代碼:RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? moduleName:@"PropertyFinder"? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? initialProperties:nil? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? launchOptions:launchOptions];用戶能看到的一切內(nèi)容都來源于這個(gè) RootView,所有的初始化工作也都在這個(gè)方法內(nèi)完成。在這個(gè)方法內(nèi)部,在創(chuàng)建 RootView 之前,React Native 實(shí)際上先創(chuàng)建了一個(gè) Bridge 對象。它是 Objective-C 與 JavaScript 交互的橋梁,后續(xù)的方法交互完全依賴于它,而整個(gè)初始化過程的最終目的其實(shí)也就是創(chuàng)建這個(gè)橋梁對象。初始化方法的核心是 setUp 方法,而 setUp 方法的主要任務(wù)則是創(chuàng)建 BatchedBridge。BatchedBridge 的作用是批量讀取 JavaScript 對 Objective-C 的方法調(diào)用,同時(shí)它內(nèi)部持有一個(gè) JavaScriptExecutor,顧名思義,這個(gè)對象用來執(zhí)行 JavaScript 代碼。創(chuàng)建 BatchedBridge 的關(guān)鍵是 start 方法,它可以分為五個(gè)步驟:讀取 JavaScript 源碼初始化模塊信息初始化 JavaScript 代碼的執(zhí)行器,即 RCTJSCExecutor 對象生成模塊列表并寫入 JavaScript 端執(zhí)行 JavaScript 源碼我們逐個(gè)分析每一步完成的操作:讀取 JavaScript 源碼這一部分的具體代碼實(shí)現(xiàn)沒有太大的討論意義。我們只要明白,JavaScript 的代碼是在 Objective-C 提供的環(huán)境下運(yùn)行的,所以第一步就是把 JavaScript 加載進(jìn)內(nèi)存中,對于一個(gè)空的項(xiàng)目來說,所有的 JavaScript 代碼大約占用 1.5 Mb 的內(nèi)存空間。需要說明的是,在這一步中,JSX 代碼已經(jīng)被轉(zhuǎn)化成原生的 JavaScript 代碼。初始化模塊信息這一步在方法 initModulesWithDispatchGroup: 中實(shí)現(xiàn),主要任務(wù)是找到所有需要暴露給 JavaScript 的類。每一個(gè)需要暴露給 JavaScript 的類(也成為 Module,以下不作區(qū)分)都會標(biāo)記一個(gè)宏:RCT_EXPORT_MODULE,這個(gè)宏的具體實(shí)現(xiàn)并不復(fù)雜:#define RCT_EXPORT_MODULE(js_name) \RCT_EXTERN void RCTRegisterModule(Class); \+ (NSString *)moduleName { return @#js_name; } \+ (void)load { RCTRegisterModule(self); }這樣,這個(gè)類在 load 方法中就會調(diào)用 RCTRegisterModule 方法注冊自己:void RCTRegisterModule(Class moduleClass){? static dispatch_once_t onceToken;? dispatch_once(&onceToken, ^{? ? RCTModuleClasses = [NSMutableArray new];? });? [RCTModuleClasses addObject:moduleClass];}因此,React Native 可以通過 RCTModuleClasses 拿到所有暴露給 JavaScript 的類。下一步操作是遍歷這個(gè)數(shù)組,然后生成 RCTModuleData 對象:for (Class moduleClass in RCTGetModuleClasses()) {? ? RCTModuleData *moduleData = [[RCTModuleData alloc]initWithModuleClass:moduleClass? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? bridge:self];? ? [moduleClassesByID addObject:moduleClass];? ? [moduleDataByID addObject:moduleData];}可以想見,RCTModuleData 對象是模塊配置表的主要組成部分。如果把模塊配置表想象成一個(gè)數(shù)組,那么每一個(gè)元素就是一個(gè) RCTModuleData 對象。這個(gè)對象保存了 Module 的名字,常量等基本信息,最重要的屬性是一個(gè)數(shù)組,保存了所有需要暴露給 JavaScript 的方法。暴露給 JavaScript 的方法需要用 RCT_EXPORT_METHOD 這個(gè)宏來標(biāo)記,它的實(shí)現(xiàn)原理比較復(fù)雜,有興趣的讀者可以自行閱讀。簡單來說,它為函數(shù)名加上了 __rct_export__ 前綴,再通過 runtime 獲取類的函數(shù)列表,找出其中帶有指定前綴的方法并放入數(shù)組中:- (NSArray> *)methods{? ? unsigned int methodCount;? ? Method *methods = class_copyMethodList(object_getClass(_moduleClass), &methodCount); // 獲取方法列表? ? for (unsigned int i = 0; i < methodCount; i++) {? ? ? ? RCTModuleMethod *moduleMethod = /* 創(chuàng)建 method */? ? ? ? [_methods addObject:moduleMethod];? ? ? }? ? }? ? return _methods;}因此 Objective-C 管理模塊配置表的邏輯是:Bridge 持有一個(gè)數(shù)組,數(shù)組中保存了所有的模塊的 RCTModuleData 對象。只要給定 ModuleId 和 MethodId 就可以唯一確定要調(diào)用的方法。初始化 JavaScript 代碼的執(zhí)行器,即 RCTJSCExecutor 對象通過查看源碼可以看到,初始化 JavaScript 執(zhí)行器的時(shí)候,addSynchronousHookWithName 這個(gè)方法被調(diào)用了多次,它其實(shí)向 JavaScript 上下文中添加了一些 Block 作為全局變量:- (void)addSynchronousHookWithName:(NSString *)name usingBlock:(id)block {? ? self.context.context[name] = block;}有些同學(xué)讀源碼時(shí)可能會走進(jìn)一個(gè)誤區(qū),如果在 Block 中打一個(gè)斷點(diǎn)就會發(fā)現(xiàn),Block 其實(shí)是被執(zhí)行了,但卻找不到任何能夠執(zhí)行 Block 的代碼。這其實(shí)是因?yàn)檫@個(gè) Block 并非由 Objective-C 主動調(diào)用,而是在第五步執(zhí)行 JavaScript 代碼時(shí),由 JavaScript 在上下文中獲取到 Block 對象并調(diào)用,有興趣的讀者可以自行添加斷點(diǎn)并驗(yàn)證。這里我們需要重點(diǎn)注意的是名為 nativeRequireModuleConfig 的 Block,它在 JavaScript 注冊新的模塊時(shí)調(diào)用:get: () => {? ? let module = RemoteModules[moduleName];? ? const json = global.nativeRequireModuleConfig(moduleName); // 調(diào)用 OC 的 Block? ? const config = JSON.parse(json); // 解析 json? ? module = BatchedBridge.processModuleConfig(config, module.moduleID); // 注冊 config? ? return module;},這就是模塊配置表能夠加載到 JavaScript 中的原理。另一個(gè)值得關(guān)注的 Block 叫做 nativeFlushQueueImmediate。實(shí)際上,JavaScript 除了把調(diào)用信息放到 MessageQueue 中等待 Objective-C 來取以外,也可以主動調(diào)用 Objective-C 的方法:if (global.nativeFlushQueueImmediate &&? ? now - this._lastFlush >= MIN_TIME_BETWEEN_FLUSHES_MS) {? ? global.nativeFlushQueueImmediate(this._queue); // 調(diào)用 OC 的代碼}目前,React Native 的邏輯是,如果消息隊(duì)列中有等待 Objective-C 處理的邏輯,而且 Objective-C 超過 5ms 都沒有來取走,那么 JavaScript 就會主動調(diào)用 Objective-C 的方法:[self addSynchronousHookWithName:@"nativeFlushQueueImmediate" usingBlock:^(NSArray*calls){? ? [self->_bridge handleBuffer:calls batchEnded:NO];}];這個(gè) handleBuffer 方法是 JavaScript 調(diào)用 Objective-C 方法的關(guān)鍵,在下一節(jié)——方法調(diào)用中,我會詳細(xì)分析它的實(shí)現(xiàn)原理。一般情況下,Objective-C 會定時(shí)、主動的調(diào)用 handleBuffer 方法,這有點(diǎn)類似于輪詢機(jī)制:// 每個(gè)一段時(shí)間發(fā)生一次:Objective-C:嘿,JavaScript,有沒有要調(diào)用我的方法呀?JavaScript:有的,你從 MessageQueue 里面取出來。然而由于卡頓或某些特殊原因,Objective-C 并不能總是保證能夠準(zhǔn)時(shí)的清空 MessageQueue,這就是為什么 JavaScript 也會在一定時(shí)間后主動的調(diào)用 Objective-C 的方法。查看上面 JavaScript 的代碼可以發(fā)現(xiàn),這個(gè)等待時(shí)間是 5ms。請牢牢記住這個(gè) 5ms,它告訴我們 JavaScript 與 Objective-C 的交互是存在一定開銷的,不然就不會等待而是每次都立刻發(fā)起請求。其次,這個(gè)時(shí)間開銷大約是毫秒級的,不會比 5ms 小太多,否則等待這么久就意義不大了。生成模塊配置表并寫入 JavaScript 端復(fù)習(xí)一下 nativeRequireModuleConfig 這個(gè) Block,它可以接受 ModuleName 并且生成詳細(xì)的模塊信息,但在前文中我們沒有提到 JavaScript 是如何知道 Objective-C 要暴露哪些類的(目前只是 Objective-C 自己知道)。這一步的操作就是為了讓 JavaScript 獲取所有模塊的名字:- (NSString *)moduleConfig{? ? NSMutableArray*config = [NSMutableArray new];? ? for (RCTModuleData *moduleData in _moduleDataByID) {? ? ? [config addObject:@[moduleData.name]];? ? }}查看源碼可以發(fā)現(xiàn),Objective-C 把 config 字符串設(shè)置成 JavaScript 的一個(gè)全局變量,名字叫做:__fbBatchedBridgeConfig。執(zhí)行 JavaScript 源碼這一步也沒什么技術(shù)難度可以,代碼已經(jīng)加載進(jìn)了內(nèi)存,該做的配置也已經(jīng)完成,只要把 JavaScript 代碼運(yùn)行一遍即可。運(yùn)行代碼時(shí),第三步中所說的那些 Block 就會被執(zhí)行,從而向 JavaScript 端寫入配置信息。至此,JavaScript 和 Objective-C 都具備了向?qū)Ψ浇换サ哪芰Γ瑴?zhǔn)備工作也就全部完成了。畫了一個(gè)簡陋的時(shí)序圖以供參考:初始化過程方法調(diào)用如前文所述,在 React Native 中,Objective-C 和 JavaScript 的交互都是通過傳遞 ModuleId、MethodId 和 Arguments 進(jìn)行的。以下是分情況討論:調(diào)用 JavaScript 代碼也許你在其他文章中曾經(jīng)多次聽說 JavaScript 代碼總是在一個(gè)單獨(dú)的線程上面調(diào)用,它的實(shí)際含義是 Objective-C 會在單獨(dú)的線程上運(yùn)行 JavaScript 代碼:- (void)executeBlockOnJavaScriptQueue:(dispatch_block_t)block{? if ([NSThread currentThread] != _javaScriptThread) {? ? [self performSelector:@selector(executeBlockOnJavaScriptQueue:)? ? ? ? ? ? ? ? onThread:_javaScriptThread withObject:block waitUntilDone:NO];? } else {? ? block();? }}調(diào)用 JavaScript 代碼的核心代碼如下:- (void)_executeJSCall:(NSString *)method? ? ? ? ? ? arguments:(NSArray *)arguments? ? ? ? ? ? ? callback:(RCTJavaScriptCallback)onComplete{? ? [self executeBlockOnJavaScriptQueue:^{? ? ? ? // 獲取 contextJSRef、methodJSRef、moduleJSRef? ? ? ? resultJSRef = JSObjectCallAsFunction(contextJSRef, (JSObjectRef)methodJSRef, (JSObjectRef)moduleJSRef, arguments.count, jsArgs, &errorJSRef);? ? ? ? objcValue = /*resultJSRef 轉(zhuǎn)換成 Objective-C 類型*/? ? ? ? onComplete(objcValue, nil);? ? }];}需要注意的是,這個(gè)函數(shù)名是我們要調(diào)用 JavaScript 的中轉(zhuǎn)函數(shù)名,比如 callFunctionReturnFlushedQueue。也就是說它的作用其實(shí)是處理參數(shù),而非真正要調(diào)用的 JavaScript 函數(shù)。這個(gè)中轉(zhuǎn)函數(shù)接收到的參數(shù)包含了 ModuleId、MethodId 和 Arguments,然后由中轉(zhuǎn)函數(shù)查找自己的模塊配置表,找到真正要調(diào)用的 JavaScript 函數(shù)。在實(shí)際使用的時(shí)候,我們可以這樣發(fā)起對 JavaScript 的調(diào)用:[_bridge.eventDispatcher sendAppEventWithName:@"greeted"? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? body:@{ @"name": @"nmae"}];這里的 Name 和 Body 參數(shù)分別表示要調(diào)用的 JavaScript 的函數(shù)名和參數(shù)。JavaScript 調(diào)用 Objective-C在調(diào)用 Objective-C 代碼時(shí),如前文所述,JavaScript 會解析出方法的 ModuleId、MethodId 和 Arguments 并放入到 MessageQueue 中,等待 Objective-C 主動拿走,或者超時(shí)后主動發(fā)送給 Objective-C。Objective-C 負(fù)責(zé)處理調(diào)用的方法是 handleBuffer,它的參數(shù)是一個(gè)含有四個(gè)元素的數(shù)組,每個(gè)元素也都是一個(gè)數(shù)組,分別存放了 ModuleId、MethodId、Params,第四個(gè)元素目測用處不大。函數(shù)內(nèi)部在每一次方調(diào)用中調(diào)用 _handleRequestNumber:moduleID:methodID:params 方法。,通過查找模塊配置表找出要調(diào)用的方法,并通過 runtime 動態(tài)的調(diào)用:[method invokeWithBridge:self module:moduleData.instance arguments:params];在這個(gè)方法中,有一個(gè)很關(guān)鍵的方法:processMethodSignature,它會根據(jù) JavaScript 的 CallbackId 創(chuàng)建一個(gè) Block,并且在調(diào)用完函數(shù)后執(zhí)行這個(gè) Block。實(shí)戰(zhàn)應(yīng)用俗話說:“思而不學(xué)則神棍”,下面舉一個(gè)例子來演示 Objective-C 是如何與 JavaScript 進(jìn)行交互的。首先新建一個(gè)模塊:// .h 文件#import#import "RCTBridgeModule.h"@interface Person : NSObject@end
Person 這個(gè)類是一個(gè)新的模塊,它有兩個(gè)方法暴露給 JavaScript:
#import "Person.h"
#import "RCTEventDispatcher.h"
#import "RCTConvert.h"
@implementation Person
@synthesize bridge = _bridge;
RCT_EXPORT_MODULE();
RCT_EXPORT_METHOD(greet:(NSString *)name)
{
NSLog(@"Hi, %@!", name);
[_bridge.eventDispatcher sendAppEventWithName:@"greeted"
body:@{ @"name": @"nmae"}];
}
RCT_EXPORT_METHOD(greetss:(NSString *)name name2:(NSString *)name2 callback:(RCTResponseSenderBlock)callback)
{
NSLog(@"Hi, %@! %@!!!", name, name2);
callback(@[@[@12,@23,@34]]);
}
@end
在 JavaScript 中,可以這樣調(diào)用:
Person.greet('Tadeu');
Person.greetss('Haha', 'Heihei', (events) => {
for (var i = 0; i < events.length; i++) {
console.log(events[i]);
}
});
有興趣的同學(xué)可以復(fù)制以上代碼并自行調(diào)試。
React Native 優(yōu)缺點(diǎn)分析
經(jīng)過一長篇的討論,其實(shí) React Native 的優(yōu)缺點(diǎn)已經(jīng)不難分析了,這里簡單總結(jié)一下:
優(yōu)點(diǎn)
復(fù)用了 React 的思想,有利于前端開發(fā)者涉足移動端。
能夠利用 JavaScript 動態(tài)更新的特性,快速迭代。
相比于原生平臺,開發(fā)速度更快,相比于 Hybrid 框架,性能更好。
缺點(diǎn)
做不到 Write once, Run everywhere,也就是說開發(fā)者依然需要為 iOS 和 Android 平臺提供兩套不同的代碼,比如參考官方文檔可以發(fā)現(xiàn)不少組件和API都區(qū)分了 Android 和 iOS 版本。即使是共用組件,也會有平臺獨(dú)享的函數(shù)。
不能做到完全屏蔽 iOS 端或 Android 的細(xì)節(jié),前端開發(fā)者必須對原生平臺有所了解。加重了學(xué)習(xí)成本。對于移動端開發(fā)者來說,完全不具備用 React Native 開發(fā)的能力。
由于 Objective-C 與 JavaScript 之間切換存在固定的時(shí)間開銷,所以性能必定不及原生。比如目前的官方版本無法做到 UItableview(ListView) 的視圖重用,因?yàn)榛瑒舆^程中,視圖重用需要在異步線程中執(zhí)行,速度太慢。這也就導(dǎo)致隨著 Cell 數(shù)量的增加,占用的內(nèi)存也線性增加。
綜上,我對 React Native 的定位是:
利用腳本語言進(jìn)行原生平臺開發(fā)的一次成功嘗試,降低了前端開發(fā)者入門移動端的門檻,一定業(yè)務(wù)場景下具有獨(dú)特的優(yōu)勢,幾乎不可能取代原生平臺開發(fā)。
參考資料
React Native 官方文檔、React Native 官方文檔中文版
React Native通信機(jī)制詳解
React 入門實(shí)例教程
OC的基礎(chǔ)知識 ? 著作權(quán)歸作者所有 舉報(bào)文章
144 關(guān)注bestswifter
寫了 264251 字,被 8083 人關(guān)注,獲得了 6859 個(gè)喜歡
個(gè)人博客:https://www.bestswifter.com 全棧計(jì)劃:http://f...
如果覺得我的文章對您有用,請隨意打賞。您的支持將鼓勵(lì)我繼續(xù)創(chuàng)作!
贊賞支持
喜歡 400? ? 更多分享
66條評論 只看作者 按喜歡排序按時(shí)間正序按時(shí)間倒序
請叫我留胡渣
19樓 · 2016.09.12 12:09
要用rn做出比較優(yōu)秀的app的話必須要學(xué)習(xí)oc,但是學(xué)習(xí)oc為什么還要用rn,哈哈,只是說在一些業(yè)務(wù)場景下可以用起來,我們小組有倆個(gè)rn app在做,挺不錯(cuò)的,對于最后老板那句不可能取代oc,個(gè)人覺得,分情況來看,哈哈。
2人贊? 回復(fù)
bestswifter: @請叫我留胡渣 學(xué)習(xí)oc和使用rn不沖突,就像你說的,分情況來
2016.09.12 12:42? 回復(fù)
請叫我留胡渣: @bestswifter 看了下bang的博客,然后又看到了gmtc全球移動技術(shù)大會,一個(gè)叫郭曉銘的架構(gòu)師驚呆了,
2016.09.12 15:13? 回復(fù)
RemisKrlet: @請叫我留胡渣 我們公司的
2017.02.19 14:05? 回復(fù)
添加新評論
AidenRao
2樓 · 2016.06.11 16:35
博主高產(chǎn):smile:先回再看
贊? 回復(fù)
bestswifter: @饒志臻 額。。。半個(gè)月一篇而已。
2016.06.11 16:37? 回復(fù)
AidenRao: @bestswifter 可是長啊,內(nèi)容多。感覺寫這么一篇得花很多時(shí)間。
2016.06.11 21:29? 回復(fù)
bestswifter: @饒志臻 嗯,光寫就花了很久很久。。。還畫了兩個(gè)圖
2016.06.11 21:32? 回復(fù)
添加新評論
asce1885
3樓 · 2016.06.12 10:08
介紹 React Native 的書籍寥寥無幾
贊? 回復(fù)
chacha: @asce1885 官方文檔最清楚了
2016.08.27 23:54? 回復(fù)
4a5a47577505: 看到Android高級進(jìn)階的作者。 :grin:
2016.12.11 15:00? 回復(fù)
asce1885: @4a5a47577505 :v:
2016.12.11 18:18? 回復(fù)
添加新評論
柴茝
4樓 · 2016.06.12 18:58
講的很贊啊~
贊? 回復(fù)
夜殤丶夜逝
5樓 · 2016.06.12 21:49
nice
贊? 回復(fù)
Ellian
7樓 · 2016.06.13 11:20
可以轉(zhuǎn)載嗎?
贊? 回復(fù)
summer_liu的自留地
8樓 · 2016.06.13 18:45
為什么不直接用moduleName,methodName呢,要用id來中轉(zhuǎn)?
贊? 回復(fù)
bestswifter: @summer_liu 感謝提出如此優(yōu)秀的問題,最近比較忙,抱歉回復(fù)慢了。我的理解是:如果要傳字符串,那么兩端其實(shí)保存的是一個(gè)字典(JS<->OC映射關(guān)系),如果模塊名和方法名非常多的話,這樣可能存在性能問題。而現(xiàn)在的方案,兩端其實(shí)存了不同的兩個(gè)數(shù)組,不存在性能問題。另外,對于CallBackId這樣的東西,用字符串似乎不方便表示,所以為了統(tǒng)一,就都用ID了。
以上是我的理解,歡迎交流
2016.06.14 21:03? 回復(fù)
summer_liu的自留地: @bestswifter
找了下配置表的對應(yīng)關(guān)系。
"ModuleName1": {
"moduleID": 0,
"methods": {
"methodName1": {
"methodID": 0,
"type": "remote"
},
"methodName2": {
"methodID": 1,
"type": "remote"
},
etc...
},
"constants": {
...
}
},
我猜可能是因?yàn)閕d傳起來比較方便清晰,不會顯得那么冗余。
感覺直接用modulename也是可以的。
另外,性能問題是指哪里的?
2016.06.14 21:29? 回復(fù)
bestswifter: @summer_liu 之前理解錯(cuò)了,應(yīng)該不涉及性能問題?!扒逦睉?yīng)該只是最顯而易見的有點(diǎn),但如果沒有別的好處,我是不會弄出配置表這種東西來把傳遞的信息簡化成ID的。我猜測,有了配置表以后,JavaScript 這一端就可以知道模塊和方法是否存在。既然配置表是有必要的,那么利用配置表傳遞ID就比較合理了。此外,OC的類名并不一定就是暴露給JS的模塊名,這也許是不傳moduleName的原因之一吧。