閱讀本文大約需要8分鐘
本文帶你了解React Native出現的背景、解決的問題以及深入學習理解React Native的運行機制。
本文投稿人bestswifter:
博客:https://bestswifter.com/react-native/
本文所講知識點
React Native問世解決的痛點
React Native運行原理
React Native源碼分析
React Native優缺點分析
1.React Native問世解決的痛點
1.1.動態配置
由於 AppStore 審核周期的限制,如何動態的更改 app 成為了永恆的話題。無論採用何種方式,我們的流程總是可以歸結為以下三部曲:「從 Server 獲取配置 --> 解析 --> 執行native代碼」。
很多時候,我們自覺或者不自覺的利用 JSON 文件實現動態配置的效果,它的核心流程是:
通過 HTTP 請求獲取 JSON 格式的配置文件。
配置文件中標記了每一個元素的屬性,比如位置,顏色,圖片 URL 等。
解析完 JSON 後,我們調用 Objective-C 的代碼,完成 UI 控制項的渲染。
通過這種方法,我們實現了在後臺配置 app 的展示樣式。從本質上來說,移動端和服務端約定了一套協議,但是協議內容嚴重依賴於應用內要展示的內容,不利於拓展。也就是說,如果業務要求頻繁的增加或修改頁面,這套協議很難應付。
最重要的是,JSON 只是一種數據交換的格式,說白了,我們就是在解析文本 數據。這就意味著它只適合提供一些配置信息,而不方便提供邏輯信息。舉個例子,我們從後臺可以配置顏色,位置等信息,但如果想要控制 app 內的業務邏輯,就非常複雜了。
記住,我們只是在解析字符串,它完全不具備運行和調試的能力。
2.React
不妨暫時拋棄移動端的煩惱,來看看前端的「新玩意」。
2.1.背景
作為前端小白,我以前對前端的理解是這樣的:
用 HTML 創建 DOM,構建整個網頁的布局、結構
用 CSS 控制 DOM 的樣式,比如字體、字號、顏色、居中等
用 JavaScript 接受用戶事件,動態的操控 DOM
在這三者的配合下,幾乎所有頁面上的功能都能實現。但也有比較不爽地方,比如我想動態修改一個按鈕的文字,我需要這樣寫:
<button type="button" id="button" onclick="onClick()">old button</button>
然後在 JavaScript 中操作 DOM:
可以看到,在 HTML 和 JavaScript 代碼中,id 和 onclick 事件觸發的函數必須完全對應,否則就無法正確的響應事件。如果想知道一個 HTML 標籤會如何被響應,我們還得跑去 JavaScript 代碼中查找,這種原始的配置方式讓我覺得非常不爽。
2.2.初識React
隨著 FaceBook 推出了 React 框架,這個問題得到了大幅度改善。我們可以把一組相關的 HTML 標籤,也就是 app 內的 UI 控制項,封裝進一個組件(Component)中,我從阮一峰的 React 教程中摘錄了一段代碼:
如果你想問:「為什麼 JavaScript 代碼裡面出現了 HTML 的語法」,那麼恭喜你已經初步體會到 React 的奧妙了。這種語法被稱為 JSX,它是一種 JavaScript 語法拓展。JSX 允許我們寫 HTML 標籤或 React 標籤,它們終將被轉換成原生的 JavaScript 並創建 DOM。
在 React 框架中,除了可以用 JavaScript 寫 HTML 以外,我們甚至可以寫 CSS,這在後面的例子中可以看到。
2.3.理解React
前端界總是喜歡創造新的概念,仿佛誰說的名詞更晦澀,誰的水平就越高。如果你和當時的我一樣,聽到 React 這個概念一臉懵逼的話,只要記住以下定義即可:
React 是一套可以用簡潔的語法高效繪製 DOM 的框架
上文已經解釋過了何謂「簡潔的語法」,因為我們可以暫時放下 HTML 和 CSS,只關心如何用 JavaScript 構造頁面。
所謂的「高效」,是因為 React 獨創了 Virtual DOM 機制。Virtual DOM 是一個存在於內存中的 JavaScript 對象,它與 DOM 是一一對應的關係,也就是說只要有 Virtual DOM,我們就能渲染出 DOM。
當界面發生變化時,得益於高效的 DOM Diff 算法,我們能夠知道 Virtual DOM 的變化,從而高效的改動 DOM,避免了重新繪製 DOM。
當然,React 並不是前端開發的全部。從之前的描述也能看出,它專注於 UI 部 分,對應到 MVC 結構中就是 View 層。要想實現完整的 MVC 架構,還需要 Model 和 Controller 的結構。在前端開發時,我們可以採用 Flux 和 Redux 架構,它們並非框架(Library),而是和 MVC 一樣都是一種架構設計(Architecture)。
如果不從事前端開發,就不用深入的掌握 Flux 和 Redux 架構,但理解這一套體系結構對於後面理解 React Native 非常重要。
3.React Native
分別介紹完了移動端和前端的背景知識後,本文的主角——React Native 終於要登場了。
3.1.融合
前面我們介紹了移動端通過 JSON 文件傳遞信息的不足之處:只能傳遞配置信息,無法表達邏輯。從本質上講,這是因為 JSON 畢竟只是純文本,它缺乏像程式語言那樣的運行能力。
而 React 在前端取得突破性成功以後,JavaScript 布道者們開始試圖一統三端。他們利用了移動平臺能夠運行 JavaScript 代碼的能力,並且發揮了 JavaScript 不僅僅可以傳遞配置信息,還可以表達邏輯信息的優點。
當痛點遇上特點,兩者一拍即合,於是乎:
一個基於 JavaScript,具備動態配置能力,面向前端開發者的移動端開發框架,React Native,誕生了!
看到了麼,這是一個面向前端開發者的框架。它的宗旨是讓前端開發者像用 React 寫網頁那樣,用 React Native 寫移動端應用。這就是為什麼 React Native 自稱:
Learn once,Write anywhere!
而非很多跨平臺語言,項目所說的:
Write once, Run anywhere!
React Native 希望前端開發者學習完 React 後,能夠用同樣的語法、工具等,分別開發安卓和 iOS 平臺的應用並且不用一行原生代碼。
如果用一個詞概括 React Native,那就是:Native 版本的 React。
3.2.原理概述
React Native 不是黑科技,我們寫的代碼總是以一種非常合理,可以解釋的方式的運行著,只是絕大多數人沒有理解而已。接下來我以 iOS 平臺為例,簡單的解釋一下 React Native 的原理。
首先要明白的一點是,即使使用了 React Native,我們依然需要 UIKit 等框架,調用的是 Objective-C 代碼。總之,JavaScript 只是輔助,它只是提供了配置信息和邏輯的處理結果。React Native 與 Hybrid 完全沒有關係,它只不過是以 JavaScript 的形式告訴 Objective-C 該執行什麼代碼。
其次,React Native 能夠運行起來,全靠 Objective-C 和 JavaScript 的交互。對於沒有接觸過 JavaScript 的人來說,非常有必要理解 JavaScript 代碼如何被執行。
我們知道 C 系列的語言,經過編譯,連結等操作後,會得到一個二進位格式的可執行文,所謂的運行程序,其實是運行這個二進位程序。
而 JavaScript 是一種腳本語言,它不會經過編譯、連結等操作,而是在運行時 才動態的進行詞法、語法分析,生成抽象語法樹(AST)和字節碼,然後由解釋器負責執行或者使用 JIT 將字節碼轉化為機器碼再執行。整個流程由 JavaScript 引擎負責完成。
蘋果提供了一個叫做 JavaScript Core 的框架,這是一個 JavaScript 引擎。通過下面這段代碼可以簡單的感受一下 Objective-C 如何調用 JavaScript 代碼:
可執行 JavaScript 代碼並獲取返回結果。
JavaScript 是一種單線程的語言,它不具備自運行的能力,因此總是被動調用。很多介紹 React Native 的文章都會提到 「JavaScript 線程」 的概念,實際上,它表示的是 Objective-C 創建了一個單獨的線程,這個線程只用於執行 JavaScript 代碼,而且 JavaScript 代碼只會在這個線程中執行。
4.Objective-C 與 JavaScript 交互
本節主要分析 Objective-C 與 JavaScript 交互時的整理邏輯與流程,下一節將通過源碼來分析具體原理。
4.1.JavaScript 調用 Objective-C
由於 JavaScript Core 是一個面向 Objective-C 的框架,在 Objective-C 這一端,我們對 JavaScript 上下文知根知底,可以很容易的獲取到對象,方法等各種信息,當然也包括調用 JavaScript 函數。
真正複雜的問題在於,JavaScript 不知道 Objective-C 有哪些方法可以調用。
React Native 解決這個問題的方案是在 Objective-C 和 JavaScript 兩端都保存了一份配置表,裡面標記了所有 Objective-C 暴露給 JavaScript 的模塊和方法。這樣,無論是哪一方調用另一方的方法,實際上傳遞的數據只有 ModuleId、MethodId 和 Arguments 這三個元素,它們分別表示類、方法和方法參數,當 Objective-C 接收到這三個值後,就可以通過 runtime 唯一確定要調用的是哪個函數,然後調用這個函數。
再次重申,上述解決方案只是一個抽象概念,可能與實際的解決方案有微小差異,比如實際上 Objective-C 這一端,並沒有直接保存這個模塊配置表。具體實現將在下一節中隨著源碼一起分析。
4.2.閉包與回調
既然說到函數互調,那麼就不得不提到回調了。對於 Objective-C 來說,執行完 JavaScript 代碼再執行 Objective-C 回調毫無難度,難點依然在於 JavaScript 代碼調用 Objective-C 之後,如何在 Objective-C 的代碼中,回調執行 JavaScript 代碼。
目前 React Native 的做法是:在 JavaScript 調用 Objective-C 代碼時,註冊要回調的 Block,並且把 BlockId 作為參數發送給 Objective-C,Objective-C 收到參數時會創建 Block,調用完 Objective-C 函數後就會執行這個剛剛創建的 Block。
Objective-C 會向 Block 中傳入參數和 BlockId,然後在 Block 內部調用 JavaScript 的方法,隨後 JavaScript 查找到當時註冊的 Block 並執行。
4.3.圖解
如果你是新手,並且堅持讀到了這裡,估計已經懵逼了。不要擔心,與 JavaScript 的交互確實不是一下子能夠完全理清楚的,你可以先參考這個示意圖:
註:
5.React Native源碼分析
要想深入理解 React Native 的工作原理,有兩個部分有必要閱讀一下,分別是初始化階段和方法調用階段。
為了提煉出代碼的核心含義,我會在不改變代碼意圖的基礎上對它做一些刪改,以便閱讀。
寫這篇文章是,React Native 還處於 0.27 版本,由於在 1.0 之前的變動幅度相對較大,因此下面的源碼分析很可能隨著 React Native 的演變而過時。但不管何時,把下面的源碼讀一遍都有助於你加深對 React Native 原理的理解。
5.1.初始化React Native
每個項目都有一個入口,然後進行初始化操作,React Native 也不例外。一個不含 Objective-C 代碼的項目留給我們的唯一線索就是位於 AppDelegate 文件中的代碼:
用戶能看到的一切內容都來源於這個 RootView,所有的初始化工作也都在這個方法內完成。
在這個方法內部,在創建 RootView 之前,React Native 實際上先創建了一個 Bridge 對象。它是 Objective-C 與 JavaScript 交互的橋梁,後續的方法交互完全依賴於它,而整個初始化過程的最終目的其實也就是創建這個橋梁對象。
初始化方法的核心是 setUp 方法,而 setUp 方法的主要任務則是創建 BatchedBridge。
BatchedBridge 的作用是批量讀取 JavaScript 對 Objective-C 的方法調用,同時它內部持有一個 JavaScriptExecutor,顧名思義,這個對象用來執行 JavaScript 代碼。
創建 BatchedBridge 的關鍵是 start 方法,它可以分為五個步驟:
我們逐個分析每一步完成的操作:
①.讀取 JavaScript 源碼
這一部分的具體代碼實現沒有太大的討論意義。我們只要明白,JavaScript 的代碼是在 Objective-C 提供的環境下運行的,所以第一步就是把 JavaScript 加載進內存中,對於一個空的項目來說,所有的 JavaScript 代碼大約佔用 1.5 Mb 的內存空間。
需要說明的是,在這一步中,JSX 代碼已經被轉化成原生的 JavaScript 代碼。
②.初始化模塊信息
這一步在方法 initModulesWithDispatchGroup: 中實現,主要任務是找到所有需要暴露給 JavaScript 的類。每一個需要暴露給 JavaScript 的類(也成為 Module,以下不作區分)都會標記一個宏:RCT_EXPORT_MODULE,這個宏的具體實現並不複雜:
這樣,這個類在 load 方法中就會調用 RCTRegisterModule 方法註冊自己:
因此,React Native 可以通過 RCTModuleClasses 拿到所有暴露給 JavaScript 的類。下一步操作是遍歷這個數組,然後生成 RCTModuleData 對象:
可以想見,RCTModuleData 對象是模塊配置表的主要組成部分。如果把模塊配置表想像成一個數組,那麼每一個元素就是一個 RCTModuleData 對象。
這個對象保存了 Module 的名字,常量等基本信息,最重要的屬性是一個數組,保存了所有需要暴露給 JavaScript 的方法。
暴露給 JavaScript 的方法需要用 RCT_EXPORT_METHOD 這個宏來標記,它的實現原理比較複雜,有興趣的讀者可以自行閱讀。簡單來說,它為函數名加上了 __rct_export__ 前綴,再通過 runtime 獲取類的函數列表,找出其中帶有指定前綴的方法並放入數組中:
因此 Objective-C 管理模塊配置表的邏輯是:Bridge 持有一個數組,數組中保存了所有的模塊的 RCTModuleData 對象。只要給定 ModuleId 和 MethodId 就可以唯一確定要調用的方法。
③.初始化 JavaScript 代碼的執行器,即 RCTJSCExecutor 對象
通過查看源碼可以看到,初始化 JavaScript 執行器的時候,addSynchronousHookWithName 這個方法被調用了多次,它其實向 JavaScript 上下文中添加了一些 Block 作為全局變量:
有些同學讀源碼時可能會走進一個誤區,如果在 Block 中打一個斷點就會發現,Block 其實是被執行了,但卻找不到任何能夠執行 Block 的代碼。
這其實是因為這個 Block 並非由 Objective-C 主動調用,而是在第五步執行 JavaScript 代碼時,由 JavaScript 在上下文中獲取到 Block 對象並調用,有興趣的讀者可以自行添加斷點並驗證。
這裡我們需要重點注意的是名為 nativeRequireModuleConfig 的 Block,它在 JavaScript 註冊新的模塊時調用:
這就是模塊配置表能夠加載到 JavaScript 中的原理。
另一個值得關注的 Block 叫做 nativeFlushQueueImmediate。實際上,JavaScript 除了把調用信息放到 MessageQueue 中等待 Objective-C 來取以外,也可以主動調用 Objective-C 的方法:
目前,React Native 的邏輯是,如果消息隊列中有等待 Objective-C 處理的邏輯,而且 Objective-C 超過 5ms 都沒有來取走,那麼 JavaScript 就會主動調用 Objective-C 的方法:
這個 handleBuffer 方法是 JavaScript 調用 Objective-C 方法的關鍵,在下一節——方法調用中,我會詳細分析它的實現原理。
一般情況下,Objective-C 會定時、主動的調用 handleBuffer 方法,這有點類似於輪詢機制:
然而由於卡頓或某些特殊原因,Objective-C 並不能總是保證能夠準時的清空 MessageQueue,這就是為什麼 JavaScript 也會在一定時間後主動的調用 Objective-C 的方法。查看上面 JavaScript 的代碼可以發現,這個等待時間是 5ms。
請牢牢記住這個 5ms,它告訴我們 JavaScript 與 Objective-C 的交互是存在一定開銷的,不然就不會等待而是每次都立刻發起請求。其次,這個時間開銷大約是毫秒級的,不會比 5ms 小太多,否則等待這麼久就意義不大了。
④.生成模塊配置表並寫入 JavaScript 端
複習一下 nativeRequireModuleConfig 這個 Block,它可以接受 ModuleName 並且生成詳細的模塊信息,但在前文中我們沒有提到 JavaScript 是如何知道 Objective-C 要暴露哪些類的(目前只是 Objective-C 自己知道)。
這一步的操作就是為了讓 JavaScript 獲取所有模塊的名字:
查看源碼可以發現,Objective-C 把 config 字符串設置成 JavaScript 的一個全局變量,名字叫做:__fbBatchedBridgeConfig。
⑤.執行 JavaScript 源碼
這一步也沒什麼技術難度可以,代碼已經加載進了內存,該做的配置也已經完成,只要把 JavaScript 代碼運行一遍即可。
運行代碼時,第三步中所說的那些 Block 就會被執行,從而向 JavaScript 端寫入配置信息。
至此,JavaScript 和 Objective-C 都具備了向對方交互的能力,準備工作也就全部完成了。
畫了一個簡陋的時序圖以供參考:
6.方法調用
如前文所述,在 React Native 中,Objective-C 和 JavaScript 的交互都是通過傳遞 ModuleId、MethodId 和 Arguments 進行的。以下是分情況討論:
6.1.調用 JavaScript 代碼
也許你在其他文章中曾經多次聽說 JavaScript 代碼總是在一個單獨的線程上面 調用,它的實際含義是 Objective-C 會在單獨的線程上運行 JavaScript 代碼:
調用 JavaScript 代碼的核心代碼如下:
需要注意的是,這個函數名是我們要調用 JavaScript 的中轉函數名,比如 callFunctionReturnFlushedQueue。也就是說它的作用其實是處理參數,而非真正要調用的 JavaScript 函數。
這個中轉函數接收到的參數包含了 ModuleId、MethodId 和 Arguments,然後由中轉函數查找自己的模塊配置表,找到真正要調用的 JavaScript 函數。
在實際使用的時候,我們可以這樣發起對 JavaScript 的調用:
這裡的 Name 和 Body 參數分別表示要調用的 JavaScript 的函數名和參數。
6.2.JavaScript 調用 Objective-C
在調用 Objective-C 代碼時,如前文所述,JavaScript 會解析出方法的 ModuleId、MethodId 和 Arguments 並放入到 MessageQueue 中,等待 Objective-C 主動拿走,或者超時後主動發送給 Objective-C。
Objective-C 負責處理調用的方法是 handleBuffer,它的參數是一個含有四個元素的數組,每個元素也都是一個數組,分別存放了 ModuleId、MethodId、Params,第四個元素目測用處不大。
函數內部在每一次方調用中調用 _handleRequestNumber:moduleID:methodID:params 方法。,通過查找模塊配置表找出要調用的方法,並通過 runtime 動態的調用:
在這個方法中,有一個很關鍵的方法:processMethodSignature,它會根據 JavaScript 的 CallbackId 創建一個 Block,並且在調用完函數後執行這個 Block。
7.實戰運用
俗話說:「思而不學則神棍」,下面舉一個例子來演示 Objective-C 是如何與 JavaScript 進行交互的。首先新建一個模塊:
Person 這個類是一個新的模塊,它有兩個方法暴露給 JavaScript:
在 JavaScript 中,可以這樣調用:
8.React Native優缺點分析
經過一長篇的討論,其實 React Native 的優缺點已經不難分析了,這裡簡單總結一下:
8.1.優點
復用了 React 的思想,有利於前端開發者涉足移動端。
能夠利用 JavaScript 動態更新的特性,快速迭代。
相比於原生平臺,開發速度更快,相比於 Hybrid 框架,性能更好。
8.2.缺點
做不到 Write once, Run everywhere,也就是說開發者依然需要為 iOS 和 Android 平臺提供兩套不同的代碼,比如參考官方文檔可以發現不少組件和API都區分了 Android 和 iOS 版本。即使是共用組件,也會有平臺獨享的函數。
不能做到完全屏蔽 iOS 端或 Android 的細節,前端開發者必須對原生平臺有所了解。加重了學習成本。對於移動端開發者來說,完全不具備用 React Native 開發的能力。
由於 Objective-C 與 JavaScript 之間切換存在固定的時間開銷,所以性能必定 不及原生。比如目前的官方版本無法做到 UItableview(ListView) 的視圖重用,因為滑動過程中,視圖重用需要在異步線程中執行,速度太慢。這也就導致隨著 Cell 數量的增加,佔用的內存也線性增加。
本公眾號歡迎大家投稿,如果你希望你的文章可以被更多人看到,直接將md、doc等格式的文章到我郵箱即可(781931404@qq.com),也可以加我QQ/微信(781931404)好友,需要註明(投稿),謝謝。