天天看點

React Native架構分析

facebook 于2015年9月15日推出react native for android 版本, 加上2014年底已經開源的ios版本,至此rn (react-native)真正成為跨平台的用戶端架構。本篇主要是從分析代碼入手,探讨一下rn在安卓平台上是如何建構一套js的運作架構。

rn 這套架構讓 js開發者可以大部分使用js代碼就可以建構一個跨平台app。 facebook官方說法是learn once, run everywhere, 即在android 、 ios、 browser各個平台,程式畫ui和寫邏輯的方式都大緻相同。因為js 可以動态加載,進而理論上可以做到write once, run everywhere, 當然要做額外的适配處理。如圖:

<a href="http://img3.tbcdn.cn/l1/461/1/04602301662556708a669ff6685f8978ce668b3d?spm=5176.100239.blogcont.6.noep5t" target="_blank"></a>

React Native架構分析

rn需要一個js的運作環境, 在ios上直接使用内置的javascriptcore, 在android 則使用webkit.org官方開源的jsc.so。 此外還內建了其他開源元件,如fresco圖檔元件,okhttp網絡元件等。

rn 會把應用的js代碼(包括依賴的framework)編譯成一個js檔案(一般命名為index.android.bundle), , rn的整體架構目标就是為了解釋運作這個js 腳本檔案,如果是js 擴充的api, 則直接通過bridge調用native方法; 如果是ui界面, 則映射到virtual dom這個虛拟的js資料結構中,通過bridge 傳遞到native , 然後根據資料屬性設定各個對應的真實native的view。 bridge是一種js 和 java代碼通信的機制, 用bridge函數傳入對方module 和 method即可得到異步回調的結果。

對于js開發者來說, 畫ui隻需要畫到virtual dom 中,不需要特别關心具體的平台, 還是原來的單線程開發,還是原來html 組裝ui(jsx),還是原來的樣式模型(部分相容 )。rn的界面處理除了實作view 增删改查的接口之外,還自定義一套樣式表達csslayout,這套csslayout也是跨平台實作。 rn 擁有畫ui的跨平台能力,主要是加入virtual dom程式設計模型,該方法一方面可以照顧到js開發者在html dom的部分傳承, 讓js 開發者可以用類似dom程式設計模型就可以開發原生app , 另一方面則可以讓virtual dom适配實作到各個平台,實作跨平台的能力,并且為未來增加更多的想象空間, 比如react-cavas, react-opengl。而實際上react-native也是從react-js演變而來。

對于 android 開發者來說, rn是一個普通的安卓程式加上一堆事件響應, 事件來源主要是js的指令。主要有二個線程,ui main thread, js thread。 ui thread建立一個app的事件循環後,就挂在looper等待事件 , 事件驅動各自的對象執行指令。 js thread 運作的腳本相當于底層資料采集器, 不斷上傳資料,轉化成ui 事件, 通過bridge轉發到ui thread, 進而改變真實的view。 後面再深一層發現, ui main thread 跟 js thread更像是cs 模型,js thread更像服務端, ui main thread是用戶端, ui main thread 不斷詢問js thread并且請求資料,如果資料有變,則更新ui界面。

<a href="http://img2.tbcdn.cn/l1/461/1/23bbf9d71a527b7c15fbc6f01526b0ca0ce560e9?spm=5176.100239.blogcont.7.noep5t" target="_blank"></a>

React Native架構分析

對于js開發者來說, 整個rn app就隻有一個js檔案, 而開發者需要編寫的就隻有如上部分。主要是四個部分:

require 所有依賴到的元件, 相當于java中的import 或者 c++ 中的include。

var awesomeproject = react.createclass 建立app, 并且在render函數中傳回ui界面結構(采用jsx ), 實際經過編譯, 都會變成js 代碼, 比如 變成 react.createelement(view,{style:{flex:1}},

var styles = stylesheet.create({, 建立css 樣式,實際上會直接當做參數直接回報到上面的react.createelement

appregistry.registercomponent('awesomeproject', () =&gt; awesomeproject); 以上三個更像是參數,這個才是js 程式的入口。即把目前app的對象注冊到appregistry元件中, appregistry元件是js module。

接着就等待native事件驅動渲染js端定義的app元件。

<a href="http://img1.tbcdn.cn/l1/461/1/3ab928a93376a5df39d67dec7ce07e6cbc0eabb5?spm=5176.100239.blogcont.8.noep5t" target="_blank"></a>

React Native架構分析

對于android 開發者, 普通安卓程式入口是activity.oncreate()方法 , 主要有三個對象

reactrootview, android 标準的framelayout對象,另外一個功能是提供react 世界的入口,函數startreactapplication實際調用attachmeasuredrootview觸發react世界的初始化。

myreactpackage, 配置目前app 需要加載的子產品,rn 的js架構會在初始化階段就會把native的子產品按照配置加載到js資料結構中(messagequeue), 進而才能在js 層即可直接判斷native是否支援某個子產品。支援三種類型子產品配置, native module(實際就是不需要操作view結構的api), view managers(實際是映射到virtual dom中的view元件), js module 。

reactinstancemanager, 建構react世界的運作環境,發送事件到js世界, 驅動整個react世界運轉。 通過builder可以建立不同的react環境, 比如内置js 路徑, 開發環境dev的js名字,是否支援調試等。doinbackground會加載指定的js檔案, onpostexecute會調用runapplication接口運作js app。

<a href="http://img4.tbcdn.cn/l1/461/1/c3e9e520a8d98f4b61b7a2bfe4b39dd787b7c008?spm=5176.100239.blogcont.9.noep5t" target="_blank"></a>

React Native架構分析

reactrootview第一次onmeasured計算完成, 然後會利用reactinstancemanager建立 reactcontext上下文環境。重要的是初始化bridge以及加載js檔案, 利用jsbundleloader方法加載index.android.bundle. 如圖

<a href="http://img4.tbcdn.cn/l1/461/1/7c594f647aa51a0080435689022b84613ae82657?spm=5176.100239.blogcont.10.noep5t" target="_blank"></a>

React Native架構分析

此刻進入js 世界, 開發者的js 語句連同react js架構層被執行。該步驟最終語句是執行appregistry.registercomponent注冊一個app元件,但還沒有到開始渲染。

當運作環境準備完畢, 則調用bridge方法運作上步注冊的app元件,觸發一連串js 和 native互相通信,配合事件驅動, 進而完成native世界的渲染。如圖利用bridge方法運作上面注冊的js app元件的runapplication方法:

<a href="http://img2.tbcdn.cn/l1/461/1/c136ec2fbc26629ac7bfcfa717fa830d5adec0e9?spm=5176.100239.blogcont.11.noep5t" target="_blank"></a>

React Native架構分析

所有的app在作業系統中, 最終都會使用一個事件循環來運作。

一般來說,js 開發者隻需要開發各個元件對象,監聽元件事件, 然後利用framework接口調用render方法渲染元件。

而實際上,js 也是單線程事件循環,不管是 api調用, virtural dom同步, 還是系統事件監聽, 都是異步事件,采用observer(觀察者)模式監聽java層事件, java層會把js 關心的事件通過bridge直接使用javascriptcore的接口執行固定的腳本, 比如"requrire (test_module).test_methode(test_args)"。此時,ui main thread相當于work thread, 把系統事件或者使用者事件往js層抛,同時,js 層也不斷調用子產品api或者ui元件 , 驅動java層完成實際的view渲染。js開發者隻需要監聽js層framework定義的事件即可。如圖即js thread 的消息隊列循環:

<a href="http://img4.tbcdn.cn/l1/461/1/1d667a068698fb723f7d838b20ded8d28d29fc09?spm=5176.100239.blogcont.12.noep5t" target="_blank"></a>

React Native架構分析

分析代碼可知,消息線程建立于reactcontext環境初始化時, messagequeuethread.java當中, 該消息隊列主要接收系統事件(如 vsync、timer、doframe、backkey)、ui事件(如鍵盤彈起、滾動等)以及 callback事件(js 的回調函數)。

如圖即reactrootview往js 傳遞鍵盤彈出的事件:

<a href="http://img2.tbcdn.cn/l1/461/1/6aa974701ca9fa045102e708a0201ecdbeac01e2?spm=5176.100239.blogcont.13.noep5t" target="_blank"></a>

React Native架構分析

而對于android 開發者, android 已經為app建立一個預設的 main looper, 不管是android system 還是js 事件都是發送到main thread通過ui渲染出來。如圖即是messagequeuethread.java直接使用主線程looper。

<a href="http://img3.tbcdn.cn/l1/461/1/38939e065f76cee9815dea24237ee4040cd4931c?spm=5176.100239.blogcont.14.noep5t" target="_blank"></a>

React Native架構分析

跟普通app不同是,此時js thread相當于work thread, js會把對應的事件或者資料通過bridge發送到ui thread。 如圖即是native java層收到的js事件的處理函數:

<a href="http://img4.tbcdn.cn/l1/461/1/c7ae031da1a4ebd43225f63a59c0437345f1bb70?spm=5176.100239.blogcont.15.noep5t" target="_blank"></a>

React Native架構分析

rn架構最主要的就是實作了一套java和 js通信的方案,該方案可以做到比較簡便的互調對方的接口。一般的js運作環境是直接擴充js接口,然後js通過擴充接口發送資訊到主線程。但rn的通信的實作機制是單向調用,native線程定期向js線程拉取資料, 然後轉成js的調用預期,最後轉交給native對應的調用子產品。這樣最終同樣也可以達到java和 js 定義的module互相調用的目的。

js調用java 使用通過擴充子產品require('nativemodules')擷取native子產品,然後直接調用native公開的方法,比如require('nativemodules').uimanager.managechildren()。 js 調用require('nativemodules')實際上是擷取messagequeue裡面的一個native子產品清單的屬性, 如:

<a href="http://img1.tbcdn.cn/l1/461/1/088ced5ec9cdc54f869075ccb8fe67e46985c484?spm=5176.100239.blogcont.16.noep5t" target="_blank"></a>

React Native架構分析

<a href="http://img4.tbcdn.cn/l1/461/1/cd563e3f4489f8fa31a75f17f947333c17aa55c3?spm=5176.100239.blogcont.17.noep5t" target="_blank"></a>

React Native架構分析

使用_genmodules 加載所有native module到 remotemodules數組。remotemodules每項都是一個映射到native module的js對象。

<a href="http://img1.tbcdn.cn/l1/461/1/7e18b499a98e384cc9f7b24b982bdfd3436e8ad1?spm=5176.100239.blogcont.18.noep5t" target="_blank"></a>

React Native架構分析

調用remotemodules 的方法, 實際是把moduleid、methodid、args放入三個queue儲存。

<a href="http://img4.tbcdn.cn/l1/461/1/cd3fd4786a0e8f6a06d154bc316108faae7340f3?spm=5176.100239.blogcont.19.noep5t" target="_blank"></a>

React Native架構分析

至此, js端調用完畢, queue中資料要等待native層通過bridge來取。

native層會在一定條件下觸發事件, 通過bridge調用callfunctionreturnflushedqueue

和 invokecallbackandreturnflushedqueue ,得到的傳回值就是這三個queue。

<a href="http://img4.tbcdn.cn/l1/461/1/4c233eddcad914dd3ed85e09f25be9be95c80e13?spm=5176.100239.blogcont.20.noep5t" target="_blank"></a>

React Native架構分析

bridge會把這三個queue交給parsemethodcalls解析, 然後通過jni回調函數轉發到java層

<a href="http://img4.tbcdn.cn/l1/461/1/53def2b0168f1256ffa684644484eaa7b72f14b5?spm=5176.100239.blogcont.21.noep5t" target="_blank"></a>

React Native架構分析

m_callback 函數是在bridge初始化的時候設定到c++層, 如:

<a href="http://img3.tbcdn.cn/l1/461/1/cbab5b561f4da3e4efdd64f616d707bad2730f6e?spm=5176.100239.blogcont.22.noep5t" target="_blank"></a>

React Native架構分析

然後在回調函數中,陸續調用reactcallback對象的call方法,weakcallback就是java層初始化bridge時傳入的nativemodulesreactcallback對象,也就是reactcallback的子類。

<a href="http://img2.tbcdn.cn/l1/461/1/ba6de61200d5114f967ebf0197af14c3e923af61?spm=5176.100239.blogcont.23.noep5t" target="_blank"></a>

React Native架構分析

到此,轉入java層. 從native module配置表中,取到對應module和method,并執行。

<a href="http://img4.tbcdn.cn/l1/461/1/9fec1799cf6c6e05cb6a60c5395dea1d572012ed?spm=5176.100239.blogcont.24.noep5t" target="_blank"></a>

React Native架構分析

之前reactinstancemanager 中運作js app元件,java 是調用catalystinstance.getjsmodule 方法擷取js 對象,然後直接通路對象方法runapplication。實際上getjsmodule 傳回的是js對象在java層的映射對象。

java層可以調用的js子產品主要在coremodulespackage.createjsmodules方法配置,有:

<a href="http://img2.tbcdn.cn/l1/461/1/18dce11fe3eb5d62475c522bc3700ec6ba1fca80?spm=5176.100239.blogcont.25.noep5t" target="_blank"></a>

React Native架構分析

如果調用jsmodules對象的方法,則會動态代理跳轉到(mbridge).callfunction(moduleid, methodid, arguments);

<a href="http://img2.tbcdn.cn/l1/461/1/54bf7411607b6da8da9c5a1b0485ece5bc5f61d1?spm=5176.100239.blogcont.26.noep5t" target="_blank"></a>

React Native架構分析

接着調用reactbridge中聲明的jni 函數,

public native void callfunction(int moduleid, int methodid, nativearray arguments);

<a href="http://img4.tbcdn.cn/l1/461/1/b03f9b5b5dbe64be49ebe11f65002470170bc2bd?spm=5176.100239.blogcont.27.noep5t" target="_blank"></a>

React Native架構分析

<a href="http://img3.tbcdn.cn/l1/461/1/03022fded732c7b439b0c055bbdd411d706993a8?spm=5176.100239.blogcont.28.noep5t" target="_blank"></a>

React Native架構分析

通過js 的require和 apply函數拼接一段js 代碼, 然後用javascriptcore的腳本運作接口執行,并得到傳回值。

<a href="http://img2.tbcdn.cn/l1/461/1/19e44379ffb7fdff0473b5fe8cbb8b4ba096d106?spm=5176.100239.blogcont.29.noep5t" target="_blank"></a>

React Native架構分析

這樣就在js引擎中運作了一段js代碼并得到傳回值,實作了java層到js層的調用。每次有java對js的通路, 則在傳回值中從js層的messagequeue.js中抓取之前累積的一堆js calls。因為java層要把時間同步、 系統幀繪制等事件傳遞給js, 是以queue中的js calls都會在很短的時間内被抓取。

1、 子產品擴充(native module)

官方文檔操作:

<a href="https://facebook.github.io/react-native/docs/native-modules-android.html?spm=5176.100239.blogcont.30.noep5t#content" target="_blank">https://facebook.github.io/react-native/docs/native-modules-android.html#content</a>

2、 元件擴充(ui component)

<a href="https://facebook.github.io/react-native/docs/native-components-android.html?spm=5176.100239.blogcont.31.noep5t#content" target="_blank">https://facebook.github.io/react-native/docs/native-components-android.html#content</a>

因為react子產品加載主要在reactpackage類配置,是以擴充可以通過反射、外部依賴注入等機制,可以做到跟h5容器一樣實作動态插拔的插件式擴充。比如api擴充, 通過外部傳入擴充子產品的類名即可反射構造函數建立新的api:

@overridepubliclist createnativemodules(reactapplicationcontext reactcontext){

list modules = new arraylist();

modules.addall(arrays.aslist(

new asyncstoragemodule(reactcontext),

new frescomodule(reactcontext),

new networkingmodule(reactcontext),

new websocketmodule(reactcontext),

new toastmodule(reactcontext)));

if (mmodulelist != null &amp;&amp; mmodulelist.size() &gt; 0) {

for (int i = 0; i &lt; mmodulelist.size(); i++) {

try {

log.i("myreactpackage", "add module:" + mmodulelist.get(i));

class c = class.forname(mmodulelist.get(i));

class[] parametertypes = {reactapplicationcontext.class};                    java.lang.reflect.constructorconstructor=c.getconstructor(parametertypes);object[] parameters ={reactcontext};                    nativemodulemodule= (nativemodule)constructor.newinstance(parameters);modules.add(module);                }catch (exception e){

log.i("myreactpackage", "add module exeception:" + e);

e.printstacktrace();

}}        }        return modules;    }

離線包支援。 目前rn官方支援内置apk打包以及dev server線上更新。而實際上,一般的容器都會實作一套離線包釋出平台。大緻的實作方案是自定義一個jsbundleloader,對接到應用管理釋出平台。

<a href="http://img2.tbcdn.cn/l1/461/1/d3845c7b892c37b661cd29976208b74252960bfc?spm=5176.100239.blogcont.32.noep5t" target="_blank"></a>

React Native架構分析

分離react 架構代碼和應用業務代碼。目前官方的生産工具是把架構代碼和業務代碼弄成一個bundle。 但架構代碼很大,需要共用, 是以要分離出架構代碼單獨前置加載。 應用業務代碼變成很小一段js代碼單獨釋出。如果每次都加載架構代碼, 啟動業務代碼會比較慢,一個helloworld都需要4秒左右。初步實踐方案是把reactinstancemanager設定成全局變量共享,在native app 啟動初始化或者第一次進入rn app時初始化reactinstancemanager。這個可能會導緻多個rn app全局變量沖突。

線上更新

離線包更新主要依賴應用管理釋出平台,大緻可以做到跟h5離線包一緻。

一般說的是圖檔資源比較多, rn 使用控件顯示圖檔,如:

<a href="http://img3.tbcdn.cn/l1/461/1/d5349c523fc4c025299f8c01c4a56bd1b9432053?spm=5176.100239.blogcont.33.noep5t" target="_blank"></a>

React Native架構分析

通過source屬性設定圖檔資源路徑, 映射到native層:

<a href="http://img2.tbcdn.cn/l1/461/1/cbabf45903b94408359cab339bb4c11fd6e54705?spm=5176.100239.blogcont.34.noep5t" target="_blank"></a>

React Native架構分析

<a href="http://img1.tbcdn.cn/l1/461/1/b0241b605f2e6bea8b14cd5125fe859d801679b8?spm=5176.100239.blogcont.35.noep5t" target="_blank"></a>

React Native架構分析

是以不管是離線包内資源還是系統資源,隻要能轉換成android 統一資源定位uri對象,即可擷取到圖檔。

如果是靜态資源,則直接uri統一定位。如果是動态資源, 比如要通過網關擷取到base64格式的圖檔,則需要native擴充特别接口。

1、 可能瓶頸

*因為bridge,  js和 java是異步互通,如果實作複雜多api的邏輯,可能會導緻部分效率損耗在多線程通信。js 異步的程式設計方式多多少少帶來一些不便。*因為bridge,  可能某些場景做不到及時響應。比如幀動畫的實時控制。*android版本剛推出不完善,并且目前rn版本還在不停的更新中, 可能存在暗坑。*加入js引擎, 記憶體的控制比較麻煩,會比普通native增加不少。

2、 待研究

動态注入的api插件實作方案,能跟h5容器共用實作。

因為rn已經具備很多的靈活, js也可以做到很多大型控件,是以native ui擴充需要定義js 和 native邊界, 哪些是js 實作, 哪些是native實作。

動畫的實作方式。

h5容器和rn容器融合方案

write once, 完全跨平台。

js 層支援 fragment manager