一、JavaScriptCore
講react Native之前,了解JavaScriptCore會有幫助,也是必要的。react Native的核心驅動力就來自於js Engine. 你寫的所有js和JSX代碼都會被JS Engine來執行, 沒有JS Engine的參與,你是無法享受ReactJS給原生應用開發帶來的便利的。在iOS上,默認的就是JavaScriptCore, iOS 7之后的設備都支持. iOS 不允許用自己的JS Engine. JavaScriptCore來自於WebKit, 所以,安卓上默認也是用JavaScriptCore
你深入了解React Native的第一站應該是 JavaScriptCore
JavaScriptCore在iOS平台上給React Native提供的接口也僅限於那幾個接口,你弄明白了JavaScriptCore那幾個接口, React Native 剩下的魔法秘密都可以順藤摸瓜來分析了。
接下來要講解的就是Facebook圍繞這幾個接口以及用一個React來顛覆整個native開發所做的精妙設計和封裝
二、瀏覽器工作原理
瀏覽器通過Dom Render來渲染所有的元素.
瀏覽器有一整套的UI控件,樣式和功能都是按照html標准實現的
瀏覽器能讀懂html和css。
html告訴瀏覽器繪制什么控件(html tag),css告訴瀏覽器每個類型的控件(html tag)具體長什么樣。
瀏覽器的主要作用就是通過解析html來形成dom樹,然后通過css來點綴和裝飾樹上的每一個節點
三、React Native 架構
綠色的是我們應用開發的部分。我們寫的代碼基本上都是在這一層
藍色代表公用的跨平台的代碼和工具引擎,一般我們不會動藍色部分的代碼
黃色代碼平台相關的代碼,做定制化的時候會添加修改代碼。不跨平台,要針對平台寫不同的代碼。iOS寫OC, android寫java,web寫js. 每個bridge都有對應的js文件,js部分是可以共享的,寫一份就可以了。如果你想做三端融合,你就得理解這一個東西。如果你要自己定制原生控件,你就得寫bridge部分
紅色部分是系統平台的東西。紅色上面有一個虛線,表示所有平台相關的東西都通過bridge隔離開來了
大部分情況下我們只用寫綠色的部分,少部分情況下會寫黃色的部分。你如果對基礎架構和開源感興趣,你可以寫藍色部分,然后嘗試給那些大的開源項目提交代碼。紅色部分是獨立於React Native的
四、React Native、React和JavascriptCore的關系
React Native最重要的三個概念應該就是 React Native 、 React 和 JavascriptCore
React Native
React Native它可不一樣
第一點,驅動關系不一樣。前面我們說的是, JS Engine來解析執行React腳本, 所以,React由瀏覽器(最終還是JS Engine)來驅動. 到了React Native這里,RN的原生代碼(Timer和用戶事件)驅動JS Engine, 然后JS Engine解析執行React或者相關的JS代碼,然后把計算好的結果返回給Native code. 然后, Native code 根據JS計算出來的結果驅動設備上所有能驅動的硬件。重點,所有的硬件。也就是說,在RN這里,JS代碼已經擺脫JS Engine(瀏覽器)的限制,可以調用所有原生接口啦
第二點, 它利用React的Virtual Dom和數據驅動編程概念,簡化了我們原生應用的開發, 同時,它不由瀏覽器去繪制,只計算出繪制指令,最終的繪制還是由原生控件去負責,保證了原生的用戶體驗
React Native組件結構
驅動硬件的能力決定能一個軟件能做多大的事情,有多大的主控性。研究過操作系統底層東西或者匯編的同學明白,我們大部分時候寫的代碼是受限的代碼,很多特權指令我們是沒法使用的,很多設備我們是不允許直接驅動的。我們現在的編程里面幾乎已經沒有人提中斷了,沒有中斷,硬件的操作幾乎會成為一場災難.
在一定程度上,React Native和NodeJS有異曲同工之妙。它們都是通過擴展JavaScript Engine, 使它具備強大的本地資源和原生接口調用能力,然后結合JavaScript豐富的庫和社區和及其穩定的跨平台能力,把javascript的魔力在瀏覽器之外的地方充分發揮出來
JavaScriptCore + ReactJS + Bridges 就成了React Native
JavaScriptCore 負責JS代碼解釋執行
ReactJS 負責描述和管理 VirtualDom ,指揮原生組件進行繪制和更新,同時很多計算邏輯也在js里面進行。ReactJS自身是不直接繪制UI的,UI繪制是非常耗時的操作,原生組件最擅長這事情。
Bridges 用來翻譯ReactJS的繪制指令給原生組件進行繪制,同時把原生組件接收到的用戶事件反饋給 ReactJS 。
要在不同的平台實現不同的效果就可以通過定制 Bridges 來實現
深入 Bridge 前面有提到, RN厲害在於它能打通JS和Native Code, 讓JS能夠調用豐富的原生接口,充分發揮硬件的能力, 實現非常復雜的效果,同時能保證效率和跨平台性。
打通RN任督二脈的關鍵組件就是 Bridge . 在RN中如果沒有Bridge, JS還是那個JS,只能調用JS Engine提供的有限接口,繪制標准html提供的那些效果,那些攝像頭,指紋,3D加速,聲卡, 視頻播放定制等等,JS都只能流流口水,原生的、平台相關的、設備相關的效果做不了, 除非對瀏覽器進行定制
Bridge的作用就是給RN內嵌的JS Engine提供原生接口的擴展供JS調用。所有的本地存儲、圖片資源訪問、圖形圖像繪制、3D加速、網絡訪問、震動效果、NFC、原生控件繪制、地圖、定位、通知等都是通過Bridge封裝成JS接口以后注入JS Engine供JS調用。理論上,任何原生代碼能實現的效果都可以通過Bridge封裝成JS可以調用的組件和方法, 以JS模塊的形式提供給RN使用。
每一個支持RN的原生功能必須同時有一個原生模塊和一個JS模塊,JS模塊是原生模塊的封裝,方便Javascript調用其接口。Bridge會負責管理原生模塊和對應JS模塊之間的溝通, 通過Bridge, JS代碼能夠驅動所有原生接口,實現各種原生酷炫的效果。
RN中JS和Native分隔非常清晰,JS不會直接引用Native層的對象實例,Native也不會直接引用JS層的對象實例(所有Native和JS互掉都是通過Bridge層會幾個最基礎的方法銜接的)。
Bridge 原生代碼負責管理原生模塊並生成對應的JS模塊信息供JS代碼調用。每個功能JS層的封裝主要是針對ReactJS做適配,讓原生模塊的功能能夠更加容易被用ReactJS調用。 MessageQueue.js 是 Bridge 在JS層的代理,所有JS2N和N2JS的調用都會經過 MessageQueue.js 來轉發。JS和Native之間不存在任何指針傳遞,所有參數都是字符串傳遞。所有的instance都會被在JS和Native兩邊分別編號,然后做一個映射,然后那個數字/字符串編號會做為一個查找依據來定位跨界對象。
五、Bridge各模塊簡介
5.1 RCTRootView
RCTRootView 是 React Native 加載的地方,是萬物之源。從這里開始,我們有了JS Engine, JS代碼被加載進來,對應的原生模塊也被加載進來,然后js loop開始運行。 js loop的驅動來源是Timer和Event Loop(用戶事件). js loop跑起來以后應用就可以持續不停地跑下去了。
如果你要通過調試來理解RN底層原理,你也應該是從RCTRootView着手,順藤摸瓜。
每個項目的 AppDelegate.m 的- (BOOL)application:didFinishLaunchingWithOptions:里面都可以看到RCTRootView的初始化代碼,RCTRootView初始化完成以后,整個React Native運行環境就已經初始化好了,JS代碼也加載完畢,所有React的繪制都會有這個RCTRootView來管理。
RCTRootView做的事情如下
創建並且持有 RCTBridge
加載 JS Bundle 並且初始化JS運行環境.
初始化JS運行環境的時候在App里面顯示 loadingView , 注意不是屏幕頂部的那個下拉懸浮進度提示條. RN第一次加載之后每次啟動非常快,很少能意識到這個加載過程了。loadingView默認情況下為空, 也就是默認是沒有效果的。loadingView可以被自定義,直接覆蓋RCTRootView.loadingView就可以了.開發模式下RN app第一次啟動因為需要完整打包整個js所以可以很明顯看到加載的過程,加載第一次以后就看不到很明顯的加載過程了,可以執行下面的命令來觸發重新打包整個js來觀察 loadingView 的效果 watchman watch-del-all && rm -rf node_modules/ && yarn install && yarn start – –reset-cache , 然后殺掉 app 重啟你就會看到一個很明顯的進度提示.
JS 運行環境准備好以后把加載視圖用 RCTRootContentView 替換加載視圖
有准備工作就緒以后調用 AppRegistry.runApplication 正式啟動RN JS代碼,從 Root Component() 開始UI繪制
一個App可以有多個 RCTRootView , 初始化的時候需要手動傳輸 Bridge 做為參數,全局可以有多個 RCTRootView , 但是只能有一個 Bridge
如果你做過 React Native 和原生代碼混編,你會發現混編就是把 AppDelegate 里面那段初始化 RCTRootView 的代碼移動到需要混編的地方,然后把 RCTRootView 做為一個普通的 subview來加載到原生的 view 里面去,非常簡單。不過這地方也要注意處理好單Bridge實例的問題,同時,混編里面要注意 RCTRootView 如果銷毀過早可能會引發JS回調奔潰的問題
5.2 RCTRootContentView
RCTRootContentView reactTag 在默認情況下為1. 在 Xcode view Hierarchy debugger 下可以看到,最頂層為 RCTRootView , 里面嵌套的是 RCTRootContentView , 從 RCTRootContentView開始,每個View都有一個 reactTag
RCTRootView 繼承自UIView, RCTRootView主要負責初始化 JS Environment 和React代碼,然后管理整個運行環境的生命周期。 RCTRootContentView 繼承自 RCTView , RCTView 繼承自UIView, RCTView封裝了React Component Node更新和渲染的邏輯, RCTRootContentView會管理所有react ui components. RCTRootContentView 同時負責處理所有touch事件
5.3 RCTBridge
這是一個加載和初始化專用類,用於前期JS的初始化和原生代碼的加載
RCTBridgeModule protocol RCTBatchedBridge
5.4 RCTBatchedBridge
如果RCTBridge是總裁, 那么RCTBatchedBridge就是副總裁。前者負責發號施令,后者負責實施落地
負責Native和JS之間的相互調用(消息通信)
持有 JSExecutor
實例化所有在RCTBridge里面注冊了的 native node_modules
創建JS運行環境, 注入 native hooks 和 modules , 執行 JS bundle script
管理JS run loop, 批量把所有JS到native的調用翻譯成 native invocations
批量管理原生代碼到JS的調用,把這些調用翻譯成JS消息發送給 JS executor
5.5 RCTJavaScriptLoader
這是實現遠程代碼加載的核心。熱更新,開發環境代碼加載,靜態 jsbundle 加載都離不開這個工具。
從指定的地方( bundle , http server )加載 script bundle
把加載完成的腳本用 string 的形式返回
處理所有獲取代碼、打包代碼時遇到的錯誤
5.6 RCTContextExecutor
封裝了基礎的JS和原生代碼互掉和管理邏輯,是JS引擎切換的基礎。通過不同的RCTCOntextExecutor來適配不同的JS Engine,讓我們的React JS可以在iOS、Android、chrome甚至是自定義的js engine里面執行。這也是為何我們能在chrome里面直接調試js代碼的原因
管理和執行所有N2J調用
5.7 RCTModuleData
加載和管理所有和JS有交互的原生代碼。把需要和JS交互的代碼按照一定的規則自動封裝成JS模塊
收集所有橋接模塊的信息,供注入到JS運行環境
5.8 RCTModuleMethod
記錄所有原生代碼的導出函數地址(JS里面是不能直接持有原生對象的),同時生成對應的字符串映射到該函數地址。JS調用原生函數的時候會通過message的形式調用過來
- 記錄所有的原生代碼的函數地址,並且生成對應的字符串映射到該地址
- 記錄所有的block的地址並且映射到唯一的一個id
- 翻譯所有J2N call,然后執行對應的native方法
如果是原生方法的調用則直接通過方法名調用,MessageQueue會幫忙把Method翻譯成MethodID, 然后轉發消息給原生代碼,傳遞函數簽名和參數給原生MessageQueue, 最終給RCTModuleMethod解析調用最終的方法
如果JS調用的是一個回調block,MessageQueue會把回調對象轉化成一個一次性的block id, 然后傳遞給RCTModuleMethod, 最終由RCTModuleMethod解析調用。基本上和方法調用一樣,只不過生命周期會不一樣,block是動態生成的,要及時銷毀,要不然會導致內存泄漏
實際上是不存在原生MessageQueue對象模塊的,JS的MessageQueue對應到原生層就是RCTModuleData & RCTModuleMethod的組合, MessageQueue的到原生層的調用先經過RCTModuleData和RCTModuleMethod翻譯成原生代碼調用,然后執行
5.9 MessageQueue
這是核心中的核心。整個react native對瀏覽器內核是未做任何定制的,完全依賴瀏覽器內核的標准接口在運作。它怎么實現UI的完全定制的呢?它實際上未使用瀏覽器內核的任何UI繪制功能,注意是未使用UI繪制功能。它利用javascript引擎強大的DOM操作管理能力來管理所有UI節點,每次刷新前把所有節點信息更新完畢以后再給yoga做排版,然后再調用原生組件來繪制。javascript是整個系統的核心語言。
我們可以把瀏覽器看成一個盒子,javascript引擎是盒子里面的總管,DOM是javascript引擎內置的,javascript和javascript引擎也是無縫鏈接的。react native是怎么跳出這個盒子去調用外部原生組件來繪制UI的呢?秘密就在MessageQueue。
javascript引擎對原生代碼的調用都是通過一套固定的接口來實現,這套接口的主要作用就是記錄原生接口的地址和對應的javascript的函數名稱,然后在javascript調用該函數的時候把調用轉發給原生接口
六、React Native 初始化
React Native 的初始化從 RootView 開始,默認在 AppDelegate.m:- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 里面會有 RootViewd 的初始化邏輯,調試的時候可以從這里入手
React Native的初始化分為幾個步驟
原生代碼加載
JS Engine 初始化(生成一個空的JS引擎)
JS基礎設施初始化. 主要是require等基本模塊的加載並替換JS默認的實現。自定義 require , Warning window , Alert window , fetch 等都是在這里進行的。基礎設施初始化好以后就可以開始加載js代碼了
遍歷加載所有要導出給JS用的原生模塊和方法, 生成對應的JS模塊信息,打包成json的格式給JS Engine, 准確地說是給MessageQueue.
這里需要提一下的是
這里的導出是沒有對象的,只有方法和模塊。JS不是一個標准的面向對象語言,剛從Java轉JavaScript的同學都會在面向對象這個概念上栽跟頭,這里特別提醒一下
6.1 原生代碼初始化
這里討論的主要是RN相關的原生代碼和用戶自定義的RN模塊的原生代碼的加載和初始化。原生代碼初始化主要分兩步
靜態加載。iOS沒有動態加載原生代碼的接口,所有的代碼都在編譯的初期就已經編譯為靜態代碼並且鏈接好,程序啟動的時候所有的原生代碼都會加載好。這是原生代碼的靜態加載,iOS里面沒有動態加載原生代碼的概念,這也是為何沒有靜態代碼熱更新的原因
RN模塊解析和注入JS。這是加載的第二步。在RootView初始化的時候會遍歷所有被標記為RCTModule的原生模塊,生成一個json格式的模塊信息,里面包含模塊名稱和方法名稱,然后注入到JS Engine, 由MessageQueue記錄下來。原生代碼在生成json模塊信息的時候同時會在原生代碼這邊維護一個名稱字典,用來把模塊和方法的名稱映射到原生代碼的地址上去,用於JS調用原生代碼的翻譯
6.2 Javascript環境初始化
RN的初始化是從RCRootView開始的,所有的繪制都會在這個RootView里面進行(Alert除外)
RootView做的第一件事情就是初始化一個空的JS Engine。 這個空的JS Engine里面包含一些最基礎的模塊和方法(fetch, require, alert等), 沒有UI繪制模塊。 RN的工作就是替換這些基礎的模塊和方法,然后把RN的UI繪制模塊加載並注入到JS Engine.
JS Engine不直接管理UI的繪制
所有的繪制由原生控制的UI事件和Timer觸發
影響界面刷新的事件發生以后一部分直接由原生控件消化掉,直接更新原生控件。剩下的部分會通過 Bridge 派發給MessageQueue,然后在JS層進行業務邏輯的計算,再由 React 來進行Virtual Dom的管理和更新。 Virtual Dom 再通過MessageQueue發送重繪指令給對應的原生組件進行UI更新
6.3 NativeModules加載
在OC里面,所有NativeModules要加載進JS Engine都必須遵循一定的協議(protocol)。
模塊(OC里面的類)需要聲明為 , 然后在類里面還必須調用宏RCT_EXPORT_MODULE() 用來定義一個接口告訴JS當前模塊叫什么名字。這個宏可以接受一個可選的參數,指定模塊名,不指定的情況下就取類名。
對應的JS模塊在初始化的時候會調用原生類的[xxx new]方法- 模塊聲明為 <RCTBridgeModule>后只是告訴Native Modules這有一個原生模塊,是一個空的模塊。要導出任何方法給JS使用都必須手動用宏RCT_EXPORT_METHOD來導出方法給JS用.
所有的原生模塊都會注冊到 NativeModules 這一個JS模塊下面去,你如果想要讓自己的模塊成為一個頂級模塊就必須再寫一個JS文件封裝一遍NativeModules里面的方法。
你如果想自己的方法導出就默認成為頂級方法,那么你需要一個手動去調用JSC的接口,這個在前面章節有講解。 不建議這樣做,因為這樣你會失去跨JS引擎的便利性。
你可以導出常量到JS里面去, 模塊初始化的時候會堅持用戶是否有實現 constantsToExport 方法, 接受一個常量詞典
- (NSDictionary *)constantsToExport
{
return @{ @"firstDayOfTheWeek": @"Monday" };// JS里面可以直接調用 ModuleName.firstDayOfTheWeek獲取這個常量 }
常量只會在初始化的時候調用一次,動態修改該方法的返回值無效
所有標記為RCT_EXPORT_MODULE的模塊都會在程序啟動的時候自動注冊好這些模塊,主要是記錄模塊名和方法名。只是注冊,不一定會初始化。
Native Modules 導出宏具體使用方法見官方文檔 Native Modules
6.4 三個線程
React Native有三個重要的線程:
Javascript thread. javascript
可以看到Shadow queue是queue而不是thread, 在iOS里面queue是thread之上的一層抽象,GCD里面的一個概念,創建queue的時候可以指定是並行的還是串行的。也就是說,一個queue可能對應多個thread
七、內部機制
內部機制
JS用時序
八、總結
8.1 React Native 框架分析
8.2 層次架構
Java層 :該層主要提供了Android的UI渲染器 UIManager (將JavaScript映射成 Android Widget )以及一些其他的功能組件(例如:Fresco、Okhttp)等,在java層均封裝為Module,java層核心jar包是react-native.jar,封裝了眾多上層的interface,如Module,Registry,bridge等
C++層 :主要處理Java與JavaScript的通信以及執行JavaScript代碼工作,該層封裝了JavaScriptCore,執行對js的解析。基於 JavaScriptCore , Web 開發者可以盡情使用ES6的新特性,如class、箭頭操作符等,而且 React Native運行在 JavaScriptCore 中的,完全不存在瀏覽器兼容的情況。Bridge橋接了java , js 通信的核心接口。JSLoader主要是將來自assets目錄的或本地file加載javascriptCore,再通過 JSCExectutor 解析js文件
Js層 :該層提供了各種供開發者使用的組件以及一些工具庫。
Component :Js層通js/jsx編寫的 Virtual Dom 來構建 Component 或Module,Virtual DOM是DOM在內存中的一種輕量級表達方式,可以通過不同的渲染引擎生成不同平台下的UI。component的使用在 React 里極為重要, 因為component的存在讓計算 DOM diff 更高效。
ReactReconciler : 用於管理頂層組件或子組件的掛載、卸載、重繪
注:JSCore,即JavaScriptCore,JS解析的核心部分,IOS使用的是內置的 JavaScriptCore,Androis上使用的是 https://webkit.org 家的jsc.so。
PPT模板下載大全https://www.wode007.com
Java層核心類及原理,如下所示
ReactContext
ReactContext繼承於ContextWrapper,是ReactNative應用的上下文,通過getContext()去獲得,通過它可以訪問ReactNative核心類的實現。
ReactInstanceManager
ReactInstanceManager 是ReactNative應用總的管理類,創建 ReactContext 、 CatalystInstance 等類,解析 ReactPackage 生成映射表,並且配合 ReactRootView 管理View的創建與生命周期等功能。
ReactRootView
為啟動入口核心類,負責監聽及分發事件並重新渲染元素,App啟動后,其將作為App的 root view 。
CatalystInstance
CatalystInstance 是 ReactNative 應用Java層、C++層、JS層通信總管理類,總管Java層、JS層核心 Module 映射表與回調,三端通信的入口與橋梁。
JavaScriptModule
JavaScriptModule 是 JS Module ,負責JS到Java的映射調用格式聲明,由 CatalystInstance 統一管理。
NativeModule
NativeModule 是 java Module ,負責Java到Js的映射調用格式聲明,由 CatalystInstance統一管理。
JavascriptModuleRegistry
JS Module映射表,負責將所有JavaScriptModule注冊到CatalystInstance,通過Java動態代理調用到Js。
NativeModuleRegistry
是Java Module映射表,即暴露給Js的API集合。
CoreModulePackage
定義核心框架模塊,創建 NativeModules&JsModules
8.3 啟動過程的解析
ReactInstanceManager創建時會配置應用所需的java模塊與js模塊,通過ReactRootView的startReactApplication啟動APP。
在創建ReactInstanceManager同時會創建用於加載JsBundle的JSBundlerLoader,並傳遞給CatalystInstance。
CatalystInstance會創建Java模塊注冊表及Javascript模塊注冊表,並遍歷實例化模塊。
CatalystInstance通過JSBundlerLoader向Node Server請求Js Bundle,並傳遞給JSCJavaScriptExectutor,最后傳遞給javascriptCore,再通過ReactBridge通知ReactRootView完成渲染
8.4 Js與Java通信機制
Java與Js之間的調用,是以兩邊存在兩邊存在同一份模塊配置表,最終均是將調用轉化為{moduleID,methodID,callbackID,args},處理端在模塊配置表里查找注冊的模塊與方法並調用。
Java 調用Js
Java通過注冊表調用到CatalystInstance實例,透過ReactBridge的jni,調用到Onload.cpp中的callFunction,最后通過javascriptCore,調用BatchedBridge.js,根據參數{moduleID,methodID}require相應Js模塊執行。流程如下圖:
Js 調用Java
如果消息隊列中有等待Java 處理的邏輯,而且 Java 超過 5ms 都沒有來取走,那么 JavaScript 就會主動調用 Java 的方法,在需要調用調Java模塊方法時,會把參數{moduleID,methodID}等數據存在MessageQueue中,等待Java的事件觸發,把MessageQueue中的{moduleID,methodID}返回給Java,再根據模塊注冊表找到相應模塊處理。流程如下圖: