庖丁解牛!帶你深刻了解React Native下一代架構重構

概述

Facebook在2018年6月官方宣佈了大規模重構React Native的計劃及重構路線圖。目的是爲了讓React Native更加輕量化、更適應混合開發,接近甚至達到原生的體驗。(也有多是React Native團隊感覺到了Google Flutter的追趕壓力,必須從架構上作出重大革新,將來纔有可能和Flutter進行全面的競爭)。從Facebook公佈的官方信息來看,這是一次革命性的架構重構,主要的重構內容以下:前端

  1. 改變線程模式。UI 更新再也不同時須要在三個不一樣的線程上觸發執行,而是能夠在任意線程上同步調用 JavaScript 進行優先更新,同時將低優先級工做推出主線程,以便保持對 UI 的響應。
  2. 引入異步渲染能力。容許多個渲染並簡化異步數據處理。
  3. 簡化JSBridge,讓它更快、更輕量。

1. 目前React Native有哪些問題?

目前業內React Native框架已經有了普遍的應用。京東在這個方面起步比較早,相對來講總體解決方案也比較成熟。目前京東深度定製和擴展的JDReact解決方案已經累計接入了200+個RN業務和20+的獨立APP,而且承擔了千萬級的DAU。從業務實際開發中仍是遇到了很多坑,其中性能問題比較明顯,具體有如下幾類問題: 加載性能偏慢,由於系統或者自定義的原生UI組件和API的註冊加載過程當中須要驗證全部屬性和JS API的一致性,影響加載性能,甚至直接致使主UI線程很容易阻塞。 JSBridge,React Native總體生命週期和JSBridge綁定太緊,全部的原生和JS之間操做所有是經過這個Bridge,並且每次的事件通信是有時間間隔的,致使總體渲染過程是異步的。 手勢問題,React Native目前的架構,從JS側很難解決不少複雜的手勢問題,須要從新定製SDK來解決問題。 返回事件的處理,目前的返回事件不能像原生同樣,在組件中監聽。 Layout的計算,總體的UI計算必需要在shadow layout中完成,沒有辦法在總體的平臺框架中計算。java

現有的Native & JS Component組件以下,經過這些組件能夠完成原生UI渲染和API調用。這些組件都是經過packageManger註冊到系統的,當RN業務啓動後,須要對總體的屬性和方法作一些校驗,存在性能損失;另外RN是容許多個packagemanger同時註冊的,當API數量偏大時,致使的問題須要循環遍歷,調用過程也存在性能損耗。node

  1. Native Modules ,原生端API接口。
  2. ViewManager,原生UI組件。
  3. Native Navigation,原生導航組件。
  4. ComponentKit & Litho,原生端基於yoga UI組件。
  5. RCTSurface,原生端Surface實現。 加載過程當中首先須要加載初始化React Native Core Bridge,包含以上的一些組件功能,而後才能運行業務的JS Code,只有這步完成後,React Native才能將JS的組件渲染成原生的組件:

因此目前的架構下這些組件和API太過依賴JSBridge的初始化,並且通信能力也侷限於這一條通道。從渲染的層次來看,React Native是多線程運行的,最多見的是JS線程和原生端的線程,一旦線程間異常,JSBridge總體將會阻塞,咱們常常也能看到JS運行異常了,實際JS線程已經無響應了,但原生端還能響應滾動事件。react

2. 如何完全解決這些問題?

針對先有框架的一些問題Facebook在最近的版本中嘗試過不少優化工做,從2013年發佈到目前已經更新到了V0.58,去年一年發佈了10多個版本。從版本更新能夠看出,除了一些組件的更新和BUG修復外,Facebook作了性能優化方面的嘗試,讓其在加載和渲染性能上儘量的達到原生。c++

重大性能優化的版本: 0.33 Lazy module 0.40 RAM bundle/unbundle 0.43 FlatList/SectionList/VirtualizedList 0.50 SwipeableFlatList/Fibernpm

如下是目前官方建議的一些優化性能的方案:react-native

  1. 組件的懶加載註冊,原生端能夠採用懶註冊,在業務使用到該組件時註冊。
  2. 按需打包,直接減小業務包大小,去掉一些不須要的module,提升渲染速度。
  3. 業務的懶加載,直接減小業務渲染過程當中require各個組件的時間。
  4. UNBundle,將業務分解成小的模塊,提供性能。
  5. 移走初始化過程當中沒必要要的JS module模塊。
  6. 提供prepack工具優化JS代碼。 最新的架構又提出了Fibe/Relay Modern架構,總體渲染性能相比之前有了很大的提升,最新的JDReact SDK已經升級到這個架構,目標是將加載JSBridge的開銷降到最低,可是文章前面提到的瓶頸問題仍是沒有突破。 咱們和跨端平臺框架Flutter啓動和渲染作了對比,在啓動性能上React Native稍微優於Flutter,但渲染方面明顯不如Flutter,也就是咱們說的瓶頸問題,對好比下圖:

因此咱們的結論是,在現有架構下的各類優化都很難完全解決性能問題。promise

3. 惟有架構重構纔是王道

在最近的開發者大會中,Facebook對下一代架構重構的進展進行了介紹,咱們也對master分支上提交的部分源碼進行了分析,能夠了解新架構的一些雛型設計,總體架構還在不斷優化中,相信還會有更多驚喜。從現有的信息和代碼來看,JS層業務的影響較小,不會所以次大規模架構重構後須要大量適配業務代碼。此次的重構主要是JSBridge及原生架構的重構,下面咱們從幾個層面對比介紹總體框架:緩存

3.1現有架構渲染原理

UI的渲染過程分爲三層:JS業務層、shadow tree、原生UI層。其中JS和shadow tree是經過JSBridge來同步數據的,JS層會將全部UI node生成一串JSON數據,傳遞到原生shadow層,原生shadow層經過傳入node數據,新增新UI或者刪除一些不須要的UI組件,這就完成了下圖這三個層次之間的驅動關係:安全

帶來的問題是總體UI渲染異步且太過於依賴JSBrige,很容易出現阻塞而影響總體UI體驗,從JDReact的業務開發經驗來看,好比初始化過程當中UI複雜度太高,點擊UI時響應時間會很長,就是由於UI被阻塞了很難響應touch事件,另外UI大小計算JS framework沒有辦法直接計算,須要依賴原生計算完成後的回調。

再看看SrollView的例子,這是業務或者社區反饋性能和體驗問題最大的組件。最第一版本的ScollView是一次渲染的不做任何回收,因此啓動性能慢且內存佔用較大。後續版本Flatlist做了組件的回收,內存基本穩定了,可是快速滑動過程當中出現了體驗問題,容易白屏且容易卡頓。你們看下面的流程圖就能明白爲何Flatlist(基於ScollView實現)/ScrollView 快速滑動下會有長時間的白屏或者卡頓。

在Flatlist快速滑動過程當中JS層會根據滑動的事件,觸發Flatlist item的render渲染每一條數據,可是由於JSBridge的異步關係致使了shadow層最終呈現到原生的UI是異步的,並且滑動太快後會有大量的UI事件會阻塞在JSBridge,也致使了長時間的白屏出現,同時當部分item滑出可視區域必定的範圍後UI內容會被回收等待下次滑到該區域後從新渲染。

3.2新架構Fabric渲染原理

UI的渲染過程仍是和以前介紹的架構同樣分爲三層:JS業務層、shadow tree、原生UI層。解耦了JS層到shadow層對於JSBridge的過分依賴,其中JS和shadow tree是經過新架構JSI(後面會介紹原理)來同步數據的,能夠作到對單個node組件的同步更新,這樣JS render到原生能夠採用同步操做也能採用異步操做,同時由於shadow層和JS層是一一對應的,因此能夠作到對UI更細的控制,大概的原理圖以下:

回到以前ScrollView的例子,看看Fabric是怎麼解決快速滑動過程當中的性能問題的。

  1. 初始化:JS到Shadow層已是同步操做了,而shadow層到原生UI變成了能夠異步也能夠同步操做了,組件能夠根據本身的業務場景來適配不一樣的操做。

  2. 滑動過程:原生端直接控制JS層渲染,同時建立shadow層的node節點,node節點會採用同步的方式渲染原生UI,整個過程當中滑動的渲染是採用的同步操做,因此不會出現舊架構下白屏的問題。

3.3 Fabric -新的UI架構

  1. React Fabric Renderer (JS) ,JS端的Render架構。
  2. FabricUIManager (JS, C++) ,JS端和原生端UI管理模塊。
  3. ComponentDescriptor (C++) ,原生端組件的惟一描述及組件屬性定義。
  4. Platform-specific Component Impl (ObjC++, Java) ,原生端組件。
  5. RCTSurface (ObjC++, Java),Surface組件。

從這些組件的結構描述來看,新的Fabric架構大體以下:

  1. shadow層從原有的Java層,挪到了C++層。
  2. 由C++層來管理總體的UI組件,原有的Java層UIManager換到C++層,管理這些C++層到虛擬組件。
  3. 而原生的組件透過JNI層會在C++層生成對應的實例,綁定一些屬性和方法。
  4. JS層FabricUIManager透過JSI,喚起C++層去生產node節點,並最終對應到咱們的ComponentDescriptor。 從總體來看JS端的node節點能夠完整的和C++端的node節點一一對應,透過JSI能夠完成同步的調用和屬性同步,一樣C++到原生java層到組件是經過JNI來完成的,並且也是同步操做。

下面咱們參考下目前Facebook開放出來的部分代碼:

  1. ComponentDescriptor,原生和原生UI對應的一層抽象層,這邊實現了原生端組件的屬性和事件,並經過惟一標示註冊到comonentRegister中,如下是已經開放出來的switch組件的代碼架構。

總體的Fabric的UIManger 組件和消息通道是怎麼創建的呢?你們能夠參考文件Scheduler.cpp,JS會經過JSI調用該接口來初始化。

  1. Fabric component 註冊。
  2. 消息通道註冊。
  3. 初始化UIManager和UIManagerbinding,其中UIManager提供了建立node、clone node,添加shadow node、關閉surface等功能,而UIManagerbinding是基於JSI接口直接實現了和JS端UIManger的直接調用,你們能夠參考源碼JS端是經過JSI的get方法,經過屬性的方式通知UIManagerbinding執行C++層的UIManger,而UImanger最終會根據生成的shadow node生成對應的UI。

下面咱們看看JS端是如何生成原生組件的,你們能夠對照源碼,在JS端咱們有FabricUIManager,在初始化UIManagerBinding過程當中,註冊到運行的JS環境,由於UIManagerBinding是JSI實現的,因此能夠理解爲咱們建立了一個Host代理對象,註冊到了JS,而JS側也對應一樣的數據結構來一一對應。

下面是建立一個node的例子:

從目前的結構來看,後續Fabric UI開發,須要從C++ component層、shadow層、原生Java層,三個層次開發,並且建立的shadow層也是經過JSI的方式和JS層的node節點一一對應的。

3.4 JSI介紹

上面介紹Fabric架構時提到了JSI,那到底什麼是JSI呢?如何能作到更原子級的控制每一個模塊和API呢?他是架起JS和原生java或者Objc的橋樑,相似於現有架構的JSBridge,可是採用的是內存共享、代理類的方式,JSI全部的運行環境都是在JSCRuntime環境下的,爲了實現和JS端直接通信,咱們須要在C++層實現一個JSI::HostObject,該數據結構只有get、set兩個接口,經過prop來區分不一樣接口的調用。

而後經過JSI接口生成一個JSObject,能夠看到生成的代理對象和咱們的HostObjectProxy是共享內存的,而且proxy中也實現了set和get方法。如下是具體的流程:

在JS端對應的是LazyObject,經過對這些Object的set、get,來完成對應C++實現的hostobject方法的調用:

3.5 TurboModule架構

這是基於新的JSI架構實現的Native module架構。JS層經過JSI獲取到對應的C++層的module對象的代理,最終經過JNI回調Java層module。 C++層NativeMoudleProxy是經過JSI實現的對象,能夠經過它傳入module的名字獲取C++層註冊的module,已經這個module封裝的全部的API method name。因此在JS業務加載的時候,會將這個proxy生成JS object代理對象到JS層。

2.JS層經過getNativeModule API並傳遞prop到JSI,最終會經過JSI接口找到Host object NativeModuleProxy,因NativeModuleProxy主要做用是將註冊在C++層的JSI module經過名字生成JS Object傳遞的JS層調用, 因此其get方法中只有一個屬性,就是經過JSINativeModules獲取對應的module,而JSINativeModules是有緩存機制的,若是沒有緩存的就直接解析該module中全部的API,若是有直接讀取緩存的module信息.

你們能夠看到在解析過程當中,新版本增長了同步和異步的方法,也就是promise和sync。因此JSI module實際是能夠同步操做API的,不像以前JSBridge的API都是異步操做的,同步操做的好處就是能作到線程間的同步。

全部的JSI module都是經過JSIMoudleregistry來註冊的,固然這裏註冊的都是C++層的moulde,而全部的C++module最終是經過descriptor綁定到java層的turbomoudle中註冊的Java層module,也就是咱們最終在原生端實現的API,因此C++層module會經過對應的method prop來觸發Java層的方法調用。

而C++層Native module是在java層instance manger初始化過程當中註冊的,遍歷並註冊java層和C++module。

全部理解Native Module的調用實際就是JSI的調用,而運行返回結果是基礎的數據類型或者JSI object的,因此一個turbo module的method調用,返回值是能夠是JSI object。開發者能夠根據本身業務須要,將一些完整的數據結構封裝成JSI host object,這樣就能夠作到,JS端一樣能夠獲取到該對象,並同原生端對象造成了代理關係,能夠同步完成一系列該Object porp功能操做,舉個例子: 之前的調用方式經過JSBridge獲取到一個picture,這個數據類型對應到JS端只能是一些基礎的數據類型,好比咱們參見的圖片地址String類型,因此若是如今要上傳這個圖片的話,咱們將JS端的數據再回傳到原生端,以下圖:

但有了JSI後就不同了,咱們在JS端透過JSI獲取到的是JS Object,也就是picture,可是這個picture再也不是簡單的數據類型,而是和原生端造成綁定關係的結構,可以支持不少屬性的同步設置,如改變alpha值等等,會直接觸發host object的屬性和函數調用,因此咱們再也不須要像以前同樣改變alpha須要不少的JSBridge的調用,一樣上傳過程能夠直接操做c++層的Object執行上傳操做。

下面簡單列舉一下相關層次的調用關係:

3.6 社區化

React Native 目前有52個直接依賴包,而後這些包間接遞歸依賴了589個包,你們能夠在http://npm.anvaka.com/#/view/2d/react-native網站看看總體的依賴關係,是一個很是複雜的關係圖。 從目前React Native開源的代碼來看,總體是個很大的repo工程,包含了各類各樣的Native組件、API、JS module,如何讓這些組件和API維護更簡單,讓接入React Native架構的APP能將API或者組件快速裁剪變小也成了此次架構重構的目標。 刪除掉一些不須要的modules,將全部的module分離成小的repo,相似於組件化模塊。 減少業務開發的bundle代碼的大小(按需引入和編譯須要的component),進而提高業務的渲染和啓動速度。 分離後將這些組件或API開放給社區,社區能夠貢獻本身的資源,這樣可讓組件的維護和迭代更快,幫助減小組件維護成本。 減小目前React Native SDK開發的依賴,簡化代碼結構,讓SDK升級起來更輕量、更快速。 增長了社區的貢獻讓PR的修復加快,讓更多的開發者集中在更合理的位置,減小重複開發,下降開放的複雜度。 將來facebook計劃:

  1. 刪除全部以爲使用率低或者無價值的組件或者API。
  2. 將現有維護的模塊移到外部的repo,單獨維護。 具體組件的規劃和歸類,你們能夠同步參考Facebook提供的列表,有不少組件已經明確要刪掉或者移到開源社區來,好比WebView:docs.google.com/spreadsheet… 如下是React Native組件社區的規劃圖:

之後原生端Native組件主要分三部分,一部份會放到系統的SDK,一部分移到開源社區維護,開發者再貢獻一部分組件。對開發者而言帶來的影響:

  1. 全部組件不能像以前同樣一次就同步下來了,須要根據拆分的repo由開發者本身按需安裝repo,碎片化比較嚴重。
  2. 由於社區運營後開發者貢獻會愈來愈快,版本迭代也會加速,因此版本的控制和代碼的安全性也是一個重要的問題。
  3. 不少社區化的組件是對原生Native module是有依賴的,因此增長了前端開發人員的集成開發難度。

4. 總結

咱們經過源碼分析給你們簡單介紹了Facebook的React Native下一代框架的設計,相信無論從性能體驗和功能上都會有很大的變化。雖然總體變化很大但對於前端開發者而言JS的變化微乎其微,而重點改造在原生端組件和API架構,封裝起來變得更加複雜,須要封裝C++ shadow層,因此從之前的JAVA開發擴展到了C++和JAVA開發,對於開發者知識結構和儲備要求更高,但對於提高性能而言,這些都是值得的。社區化運營後Facebook官方能夠從之前的組件和框架一塊兒開發,簡化到只需關注總體框架能力和性能了,讓開發者貢獻維護如今組件,大大提升了框架的迭代週期。 京東多端融合技術團隊也會持續保持關注,待React Native新架構穩定後也會對渲染引擎進行升級並引入新的架構和特性。將來也會打通JDReact和JDFlutter雙引擎的底層和組件,提供更爲全面的跨端解決方案。

相關文章
相關標籤/搜索