ReactNative設計與實現之三:整體架構

來自專欄跨平台開發小站2 人贊了文章

在介紹完RN的背景與實踐之後,接下來我們將介紹RN的整體架構。

一、RN的整體架構

一個RN工程可以分為三大部分,JS域、native域以及負責兩個域之間通信的C++ Bridge。如下圖所示:

其中,JS域為單線程,使用的編程語言是JavaScript,JS代碼運行在JavaScriptCore上。JS域主要負責實現APP的業務邏輯、並指定需要渲染的組件以及組件的布局。

Native域是一個多線程的環境,它有負責UI渲染的主UI線程,以及其他後台任務線程。值得注意的是,負責運行JS代碼的線程是native域中多條後台線程中的一條。熟悉JS事件循環的同學應該都很清楚,JS本身沒有線程,線程由宿主環境提供。native域的主要作用是提供宿主環境,並負責UI渲染與交互。native域的編程語言因平台而異,Android主要是Java和Kotlin,iOS主要是OC和Swift。

C++ Bridge主要負責JS域與Native域的通信,而通信則是指JS與Java、OC等語言之間的相互調用。在背景與實踐兩篇文章中提到的NativeModule,就是通過C++ Bridge來實現的。

二、RN性能

性能是我們考量一個框架或一套技術方案的重要指標之一,下面我們將分別討論RN三大部分的性能問題。

相對於Java、OC等編成語言,一直以來JS都留給我們執行慢的印象,其實隨著JS語言的發展以及JS引擎的不斷優化,現代JS代碼的執行速度已經非常快了,性能問題一般不會出在JS域。Android與iOS對其各自的性能都有保證,性能問題一般也不會出現在native域。RN的性能瓶頸往往會出現在C++ Bridge上。

我們知道,在JS域和Native域中運行的是不同的語言,因此在不同域中定義的變數是不能相互訪問的,所有跨語言的通信都需要通過C++ Bridge來完成。事實上,這與客戶端與伺服器間通過web通信的方式類似 -- 數據必須序列化後才能通過。而數據的序列化與反序列化是非常耗時的。同時,這裡有一件非常酷的事 -- 當你在Chrome中調試RN的JS代碼時,JS域和native域實際是運行在不同的計算機設備中的(你的PC和你的移動設備),它們之間通過WebSocket來橋接。

因此,為了構建一個高性能的RN APP,我們必須將橋上傳遞的數據量保持在最低限度

三、UI的非同步更新

有一個很明顯的性能陷阱:跨JS與native域進行UI的同步更新。如下圖所示:

我們在JS域通過Button.setColor來改變一個button的顏色,然後由native側來完成實際的改變button顏色的操作。當native在執行時,JS線程會被阻塞,直到native側更新button顏色的操作執行完成後,JS線程才會繼續執行。這是一個很明顯的性能陷阱,問題在於,我們真的需要這份同步嗎?

讓我們來看看RN是怎麼解決這個問題的,事實上,RN本身並沒去解決這個問題。React.js在Web上解決了一個類似的問題,RN直接復用了它的解決方案。Web與RN有著類似的結構,我們有JS腳本,它運行在JS線程上,同時我們有DOM,DOM是瀏覽器native結構的一部分,每次通過DOM API來更新DOM時,都是一次跨JS與native域的UI的同步更新操作。React為了解決這個問題,提出了虛擬DOM的概念,結合一個智能的diff演算法,將我們在JS側對組件的更改批量、非同步地發送到native側 -- 同時頁最小化了需要通過Bridge傳遞的數據量(diff演算法)。

因此,RN中的UI更新是非同步完成的。如下圖所示:

JS側的更新指令被批量、非同步地發送到native側,在native執行實際的更新操作時,JS線程不會被阻塞。這裡需要注意一點,當我們在JS側調用Button.setColor來更改button的顏色,緊接著調用Button.getColor來獲取button的最新顏色,我們可能獲取不到button的最新的顏色。因為,此時native側的更新操作可能還沒有完成,對於這種場景,我們需要特別注意。

四、RN中的線程(Android)

RN的Android端主要有三個線程,負責UI渲染的main_ui線程,負責執行JS代碼的mqt_js線程,以及與NativeModule相關的mqt_native_modules線程,每個線程都有與其綁定的消息隊列。

RN Android端在ReactQueueConfigurationImpl.java中定義並創建了mqt_js和mqt_native_modules線程,main_ui線程由Android本身創建。我們可以通過ReactContext來取得對這些線程的引用,代碼如下:

CatalystInstance catalystInstance = reactContext.getCatalystInstance();if (catalystInstance == null) { return;}ReactQueueConfiguration queueCfg = catalystInstance.getReactQueueConfiguration();if (queueCfg == null) { return;}MessageQueueThreadImpl jsQueueThread = (MessageQueueThreadImpl) queueCfg.getJSQueueThread();MessageQueueThreadImpl nativeModulesQueueThread = (MessageQueueThreadImpl) queueCfg.getNativeModulesQueueThread();

這三個線程之間的交互主要藉助Android的Handler來完成。如下圖所示:

mqt_js線程中運行的是JS代碼,它位於JS域;而main_ui與mqt_native_modules線程中運行的是native代碼,它們位於native域。native側(main_ui和mqt_native_modules線程)通過調用jniCallJSFunctionjniCallJSCallback方法來執行JS側的代碼,從而實現與mqt_js線程交互;mqt_js線程通過調用NativeModule暴露給JS側的方法與mqt_native_modules線程交互;而main_ui與mqt_native_modules可通過Handler直接交互。

關於JS域與native域代碼互調的細節,我們將在下一篇文章《ReactNative設計與實現之四:Android端源碼分析》中詳細介紹。

五、Android APK的結構

下圖是基於React-Native 0.53.3打出的Android APK結構:

上圖中,所有與RN相關的動態鏈接文件,都給出了其在源碼中的位置,一共有八個;同時,有三個文件名被加粗。第一個是assets下面的index.android.bundle,我們編寫的JS代碼以及這些代碼所需要的依賴都會被打包到這個文件中。在APP的啟動過程中,首先會載入這個bundle文件,並交由JavaScriptCore來執行。與業務相關的邏輯都在這個bundle中,如果我們能通過某種機制將這個bundle文件替換為新的bundle文件,在下次啟動APP時,就會載入這個新的bundle文件,這樣就達到更新業務邏輯的目的。這就是RN的熱更新,CodePush等熱更新方案就是通過這一機制來實現的。注意:熱更新只能更新JS部分的改動,而不能更新native部分的改動,所有native部分的改動都需要通過正常的發版機制來完成。

第二個文件是libjsc.so,上文中我們有提到,RN中編譯並繼承了一個全功能的JS引擎:JavaScriptCore。libjsc.so就是JavaScriptCore的動態鏈接文件,它比較大,有4M左右,且每一個由RN工程打包出來的apk都包含一個獨立的libjsc.so。

第三個文件是libreactnativejni.so,它是所有與RN相關的so文件的入口文件。

六、小結

本文首先介紹了RN的整體架構,其分為JS域、native域與C++ Bridge三大部分;隨後介紹的RN的性能以及其UI非同步更新的特性;接著又介紹了RN Android端的線程模型以及它們之間的交互,並給出了獲取這些線程的引用的方法;最後簡單分析了RN Android APK的結構。接下來,我們將開始分析Android端的源碼。


推薦閱讀:
相关文章