React native for Android 初步實踐[原理剖析]

摘要: Facebook 於2015年9月15日推出React Native for Android 版本, 加上2014年底已經開源的iOS版本,至此RN (React-native)真正成爲跨平臺的客戶端框架。本篇主要是從分析代碼入手,探討一下RN在安卓平臺上是如何構建一套JS的運行框架。 一、 ...

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, 當然要做額外的適配處理。如圖:

_1
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界面。

二、 代碼流程

1、JS入口

_2015_10_26_4_40_39

對於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', () => AwesomeProject); 以上三個更像是參數,這個纔是JS 程序的入口。即把當前APP的對象註冊到AppRegistry組件中, AppRegistry組件是js module。

接着就等待Native事件驅動渲染JS端定義的APP組件。

2、Native 入口

_2015_10_26_5_01_52

對於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。
    _2015_10_27_8_02_04

ReactRootView第一次onMeasured計算完成, 然後會利用ReactInstanceManager創建 ReactContext上下文環境。重要的是初始化bridge以及加載js文件, 利用JSBundleLoader方法加載index.android.bundle. 如圖

_2015_10_27_1_41_19

此刻進入JS 世界, 開發者的js 語句連同react js框架層被執行。該步驟最終語句是執行AppRegistry.registerComponent註冊一個APP組件,但還沒有到開始渲染。

當運行環境準備完畢, 則調用bridge方法運行上步註冊的APP組件,觸發一連串JS 和 Native相互通信,配合事件驅動, 從而完成native世界的渲染。如圖利用bridge方法運行上面註冊的JS APP組件的runApplication方法: 
_2015_10_27_1_37_35

3、事件循環

所有的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 的消息隊列循環:

_2015_10_27_10_13_36

分析代碼可知,消息線程創建於ReactContext環境初始化時, MessageQueueThread.java當中, 該消息隊列主要接收系統事件(如 Vsync、timer、doFrame、backkey)、UI事件(如鍵盤彈起、滾動等)以及 callback事件(JS 的回調函數)。
如圖即ReactRootView往JS 傳遞鍵盤彈出的事件:

_2015_10_27_11_20_27

而對於Android 開發者, Android 已經爲APP創建一個默認的 Main Looper, 不管是Android System 還是JS 事件都是發送到Main thread通過UI渲染出來。如圖即是MessageQueueThread.java直接使用主線程Looper。

_2015_10_27_11_08_11

跟普通APP不同是,此時JS thread相當於work thread, JS會把對應的事件或者數據通過bridge發送到UI thread。 如圖即是native Java層收到的JS事件的處理函數:
_2015_10_27_1_47_12

三、 通信機制

RN框架最主要的就是實現了一套JAVA和 JS通信的方案,該方案可以做到比較簡便的互調對方的接口。一般的JS運行環境是直接擴展JS接口,然後JS通過擴展接口發送信息到主線程。但RN的通信的實現機制是單向調用,Native線程定期向JS線程拉取數據, 然後轉成JS的調用預期,最後轉交給Native對應的調用模塊。這樣最終同樣也可以達到Java和 JS 定義的Module互相調用的目的。

1、JS調用java

JS調用java 使用通過擴展模塊require('NativeModules')獲取native模塊,然後直接調用native公開的方法,比如require('NativeModules').UIManager.manageChildren()。 JS 調用require('NativeModules')實際上是獲取MessageQueue裏面的一個native模塊列表的屬性, 如:
_2015_10_27_4_03_35

_2015_10_27_2_00_49

使用_genModules 加載所有native module到 RemoteModules數組。RemoteModules每項都是一個映射到native module的JS對象。

_2015_10_27_4_08_02

調用RemoteModules 的方法, 實際是把moduleID、methodId、args放入三個queue保存。

_2015_10_27_4_10_34

至此, JS端調用完畢, queue中數據要等待Native層通過bridge來取。

native層會在一定條件下觸發事件, 通過bridge調用callFunctionReturnFlushedQueue
和 invokeCallbackAndReturnFlushedQueue ,得到的返回值就是這三個queue。

_2015_10_27_4_39_12

bridge會把這三個queue交給parseMethodCalls解析, 然後通過JNI回調函數轉發到Java層
_2015_10_27_4_42_17

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

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

_2015_10_27_4_53_41

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

2、java調用JS

之前ReactInstanceManager 中運行JS APP組件,JAVA 是調用catalystInstance.getJSModule 方法獲取JS 對象,然後直接訪問對象方法runApplication。實際上getJSModule 返回的是js對象在java層的映射對象。

java層可以調用的JS模塊主要在CoreModulesPackage.createJSModules方法配置,有:

_2015_10_27_4_23_46

如果調用JSModules對象的方法,則會動態代理跳轉到(mBridge).callFunction(moduleId, methodId, arguments);

_2015_10_27_4_27_15

接着調用ReactBridge中聲明的JNI 函數,
public native void callFunction(int moduleId, int methodId, NativeArray arguments);

_2015_10_27_4_31_32

_2015_10_27_5_01_42

通過JS 的require和 apply函數拼接一段JS 代碼, 然後用javascriptCore的腳本運行接口執行,並得到返回值。

_2015_10_27_5_03_09

這樣就在JS引擎中運行了一段JS代碼並得到返回值,實現了JAVA層到JS層的調用。每次有JAVA對JS的訪問, 則在返回值中從JS層的messageQueue.js中抓取之前累積的一堆JS calls。因爲JAVA層要把時間同步、 系統幀繪製等事件傳遞給JS, 因此queue中的JS calls都會在很短的時間內被抓取。

四、 擴展機制

1、 模塊擴展(native module)
官方文檔操作:
https://facebook.github.io/react-native/docs/native-modules-android.html#content

2、 組件擴展(UI component)
官方文檔操作:
https://facebook.github.io/react-native/docs/native-components-android.html#content

因爲react模塊加載主要在ReactPackage類配置,因此擴展可以通過反射、外部依賴注入等機制,可以做到跟H5容器一樣實現動態插拔的插件式擴展。比如API擴展, 通過外部傳入擴展模塊的類名即可反射構造函數創建新的API:

    @Override
    public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
        List<NativeModule> modules = new ArrayList();
        modules.addAll(Arrays.<NativeModule>asList(
                new AsyncStorageModule(reactContext),
                new FrescoModule(reactContext),
                new NetworkingModule(reactContext),
                new WebSocketModule(reactContext),
                new ToastModule(reactContext)));
        if (mModuleList != null && mModuleList.size() > 0) {
            for (int i = 0; i < 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.Constructor constructor = c.getConstructor(parameterTypes);
                    Object[] parameters = {reactContext};
                    NativeModule module = (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,對接到應用管理髮布平臺。
    _2015_10_27_6_02_57

  • 分離react 框架代碼和應用業務代碼。目前官方的生產工具是把框架代碼和業務代碼弄成一個bundle。 但框架代碼很大,需要共用, 因此要分離出框架代碼單獨前置加載。 應用業務代碼變成很小一段JS代碼單獨發佈。如果每次都加載框架代碼, 啓動業務代碼會比較慢,一個helloworld都需要4秒左右。初步實踐方案是把ReactInstanceManager設置成全局變量共享,在Native APP 啓動初始化或者第一次進入RN APP時初始化ReactInstanceManager。這個可能會導致多個RN APP全局變量衝突。

  • 在線更新
    離線包更新主要依賴應用管理髮布平臺,大致可以做到跟H5離線包一致。

資源離線

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

通過source屬性設置圖片資源路徑, 映射到native層:
_2015_10_27_5_46_33

_2015_10_27_5_46_52

因此不管是離線包內資源還是系統資源,只要能轉換成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

性能比較數據

  • Demo還在實現當中,等抓完再補充。

環境搭建

參考: 

https://yq.aliyun.com/articles/2757#

https://yq.aliyun.com/articles/8184?spm=5176.100239.bloglist.31.NvPMfC

https://yq.aliyun.com/articles/3208?spm=5176.100239.bloglist.36.NvPMfC

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章