作為 GSY 開源系列的作者,在去年也整理過 《移動端跨平臺開發的深度解析》 的對比文章,時隔一年之後,本篇將重新由 環境搭建、實現原理、程式設計開發、外掛開發、編譯執行、效能穩定、發展未來 等七個方面,對當前的 React Native 和 Flutter 進行全面的分析對比,希望能給你更有價值的參考。
是的,這次沒有了 Weex,超長內容預警,建議收藏後閱。
前言
臨冬之際,移動端跨平臺在經歷數年沉浮之後,如今還能在舞臺聚光燈下雀躍的, 也只剩下 React Native 和 Flutter 了,作為沉澱了數年的 “豪門” 與 19 年當紅的 “新貴” ,它們之間的 “針鋒相對” 也成了開發者們關心的事情。
過去曾有人問我:“他即寫 Java 又會 Object-C ,在 Android 和 IOS 平臺上可以同時開發,為什麼還要學跨平臺呢?”
而我的回答是:跨平臺的市場優勢不在於效能或學習成本,甚至平臺適配會更耗費時間,但是它最終能讓程式碼邏輯(特別是業務邏輯),無縫的複用在各個平臺上,降低了重複程式碼的維護成本,保證了各平臺間的統一性, 如果這時候還能保證一定的效能,那就更完美了。
型別 | React Native | Flutter |
---|---|---|
語言 | JavaScript | dart |
環境 | JSCore | Flutter Engine |
釋出事件 | 2015 | 2017 |
star | 67k+ | 78k+ |
對比版本 | 0.59.9 | 1.6.3 |
空專案打包大小 | Android 20M(可調整至 7.3M) / IOS 1.6M | Android 5.2M / IOS 10.1M |
GSY專案大小 | Android 28.6M / IOS 9.1M | Android 11.6M / IOS 21.5M |
程式碼產物 | JS Bundle 檔案 | 二進位制檔案 |
維護者 | ||
風格 | 響應式,Learn once, write anywhere | 響應式,一次編寫多平臺執行 |
支援 | Android、IOS、(PC) | Android、IOS、(Web/PC) |
使用代表 | 京東、攜程、騰訊課堂 | 閒魚、美團 |
一、環境搭建
無論是 React Native 還是 Flutter ,都需要 Android 和 IOS 的開發環境,也就是 JDK 、Android SDK、Xcode 等環境配置,而不同點在於 :
- React Native 需要
npm
、node
、react-native-cli
等配置 。 - Flutter 需要
flutter sdk
和 Android Studio / VSCode 上的 Dart 與 Flutter 外掛。
從配置環境上看, Flutter 的環境搭配相對簡單,而 React Native 的環境配置相對複雜,而且由於 node_module
的“黑洞”屬性和依賴複雜度等原因,目前在個人接觸的例子中,首次配置執行成功率 Flutter 是高於 React Native 的,且 Flutter 失敗的原因則大多歸咎於網路。
同時跨平臺開發首選 Mac ,沒有為什麼。
二、實現原理
在 Android 和 IOS 上,預設情況下 Flutter 和 React Native 都需要一個原生平臺的
Activity
/ ViewController
支援,且在原生層面屬於一個“單頁面應用”, 而它們之間最大的不同點其實在於 UI 構建 :
- React Native :
React Native 是一套 UI 框架,預設情況下 React Native 會在 Activity
下載入 JS 檔案,然後執行在 JavaScriptCore
中解析 Bundle 檔案佈局,最終堆疊出一系列的原生控制元件進行渲染。
簡單來說就是 通過寫 JS 程式碼配置頁面佈局,然後 React Native 最終會解析渲染成原生控制元件,如 <View>
標籤對應 ViewGroup/UIView
,<ScrollView>
標籤對應 ScrollView/UIScrollView
,<Image>
標籤對應 ImageView/UIImageView
等。
所以相較於如 Ionic
等框架而言, React Native 讓頁面的效能能得到進一步的提升。
- Flutter :
如果說 React Native 是為開發者做了平臺相容,那 Flutter 則更像是為開發者遮蔽平臺的概念。
Flutter 中只需平臺提供一個
Surface
和一個Canvas
,剩下的 Flutter 說:“你可以躺下了,我們來自己動”。
Flutter 中絕大部分的 Widget
都與平臺無關, 開發者基於 Framework
開發 App ,而 Framework
執行在 Engine
之上,由 Engine
進行適配和跨平臺支援。這個跨平臺的支援過程,其實就是將 Flutter UI 中的 Widget
“資料化” ,然後通過 Engine
上的 Skia
直接繪製到螢幕上 。
所以從以上可以看出:React Native 的 Learn once, write anywhere 的思路,就是隻要你會 React ,那麼你可以用寫 React 的方式,再去開發一個效能不錯的App;而 Flutter 則是讓你忘掉平臺,專注於 Flutter UI 就好了。
- DOM:
額外補充一點,React 的虛擬 DOM 的概念相信大家都知道,這是 React 的效能保證之一,而 Flutter 其實也存在類似的虛擬 DOM 概念。
看過我 Flutter 系列文章可能知道,Flutter 中我們寫的
Widget
, 其實並非真正的渲染控制元件,這一點和 React Native 中的標籤類似,Widget
更像配置檔案, 由它組成的Widget
樹並非真正的渲染樹。
Widget
在渲染時會經過 Element
變化, 最後轉化為 RenderObject
再進行繪製, 而最終組成的 RenderObject
樹才是 “真正的渲染 Dom” , 每次 Widget
樹觸發的改變,並不一定會導致RenderObject
樹的完全更新。
所以在實現原理上 React Native 和 Flutter 是完全不同的思路,雖然都有類似“虛擬 DOM 的概念” ,但是React Native 帶有較強的平臺關聯性,而 Flutter UI 的平臺關聯性十分薄弱。
三、 程式設計開發
React Native 使用的 JavaScrpit 相信大家都不陌生,已經 24 歲的它在多年的發展過程中,各端各平臺中都出沒著它的身影,在 Facebook 的 React 開始風靡之後,15 年移動浪潮下推出的 React Native ,讓前端的 JS 開發者擁有了技能的擴充。
Flutter 的首選語言 Dart 語言誕生於 2011 年,而 2018 年才釋出了 2.0,原本是為了用來對抗 JavaScrpit 而釋出的開發語言,卻在 Web 端一直不溫不火,直到 17年 才因為 Flutter 而受關注起來,之後又因為 Flutter For Web 繼續嘗試後迴歸 Web 領域。
程式設計開發所涉及的點較多,後面主要從 開發語言
、介面開發
、狀態管理
、原生控制元件
四個方面進行對比介紹。
至於最多吐槽之一就是為什麼 Flutter 團隊不選擇 JS ,有說因為 Dart 團隊就在 Flutter 團隊隔壁,也有說谷歌不想和 Oracle 相關的東西沾上邊。 同時 React Native 更新快 4 年了,版本號依舊沒有突破 1.0 。
3.1、 語言
因為起初都是為了 Web 而生,所以 Dart 和 JS 在一定程度上有很大的通識性。
如下程式碼所示, 它們都支援通過 var
定義變數,支援 async/await
語法糖,支援 Promise
(Future
) 等鏈式非同步處理,甚至 *
/yield
的語法糖都類似(雖然這個對比不大準確),但可以看出它們確實存在“近親關係” 。
/// JS
var a = 1
async function doSomeThing() {
var result = await xxxx()
doAsync().then((res) => {
console.log("ffff")
})
}
function* _loadUserInfo () {
console.log("**********************");
yield put(UpdateUserAction(res.data));
}
/// Dart
var a = 1;
void doSomeThing() async {
var result = await xxxx();
doAsync().then((res) {
print('ffff');
});
}
_loadUserInfo() async* {
print("**********************");
yield UpdateUserAction(res.data);
}
複製程式碼
但是它們之間的差異性也很多,而最大的區別就是: JS 是動態語言,而 Dart 是偽動態語言的強型別語言。
如下程式碼中,在 Dart
中可以直接宣告 name
為 String
型別,同時 otherName
雖然是通過 var
語法糖宣告,但在賦值時其實會通過自推匯出型別 ,而 dynamic
宣告的才是真的動態變數,在執行時才檢測型別。
// Dart
String name = 'dart';
var otherName = 'Dart';
dynamic dynamicName = 'dynamic Dart';
複製程式碼
如下圖程式碼最能體現這個差異,在下圖例子中:
-
var i
在全域性中未宣告型別時,會被指定為dymanic
,從而導致在init()
方法中編譯時不會判斷型別,這和 JS 內的現象會一致。 -
如果將
var i = "";
定義在init()
方法內,這時候i
已經是強型別String
了 ,所以編譯器會在i++
報錯,但是這個寫法在 JS 動態語言裡,預設編譯時是不會報錯的。
動態語言和非動態語言都有各種的優缺點,比如 JS 開發便捷度明顯會高於 Dart ,而 Dart 在型別安全和重構程式碼等方面又會比 JS 更穩健。
3.2、介面開發
React Native 在介面開發上延續了 React 的開發風格,支援 scss/sass 、樣式程式碼分離、在 0.59 版本開始支援 React Hook 函數語言程式設計 等等,而不同 React 之處就是更換標籤名,並且樣式和屬性支援因為平臺相容做了刪減。
如下圖所示,是一個普通 React Native 元件常見實現方式,繼承 Component
類,通過 props
傳遞引數,然後在 render
方法中返回需要的佈局,佈局中每個控制元件通過 style
設定樣式 等等,這對於前端開發者基本上沒有太大的學習成本。
如下所示,如果再配合 React Hooks 的加持,函式式的開發無疑讓整個程式碼結構更為簡潔。
Flutter 最大的特點在於: Flutter 是一套平臺無關的 UI 框架,在 Flutter 宇宙中萬物皆 Widget
。
如下圖所示,Flutter 開發中一般是通過繼承 無狀態 StatelessWidget
控制元件或者 有狀態 StatefulWidget
控制元件 來實現頁面,然後在對應的 Widget build(BuildContext context)
方法內實現佈局,利用不同 Widget
的 child
/ children
去做巢狀,通過控制元件的構造方法傳遞引數,最後對佈局裡的每個控制元件設定樣式等。
而對於 Flutter 控制元件開發,目前最多的吐槽就是 控制元件巢狀和樣式程式碼不分離 ,樣式程式碼分離這個問題我就暫不評價,這個真要實際開發才能更有體會,而關於巢狀這裡可以做一些 “洗白” :
Flutter 中把一切皆為 Widget
貫徹得很徹底,所以 Widget
的顆粒度控制得很細 ,如 Padding
、Center
都會是一個單獨的 Widget
,甚至狀態共享都是通過 InheritedWidget
共享 Widget
去實現的,而這也是被吐槽的程式碼巢狀樣式難看的原因。
事實上正是因為顆粒度細,所以你才可以通過不同的 Widget
, 自由組合出多種業務模版, 比如 Flutter 中常用的 Container
,它就是官方幫你組合好的模板之一, Container
內部其實是由 Align
、 ConstrainedBox
、DecoratedBox
、Padding
、Transform
等控制元件組合而成 ,所以巢狀深度等問題完全是可以人為控制,甚至可以在幀率和繪製上做到更細緻的控制。
當然,官方也在不斷地改進優化編寫和視覺化的體驗,如下圖所示,從目前官方放出的訊息上看,未來這個問題也會被進一步改善。
最後總結一下,拋開上面的開發風格,React Native 在 UI 開發上最大的特點就是平臺相關,而 Flutter 則是平臺無關,比如下拉重新整理,在 React Native 中, <RefreshControl>
會自帶平臺的不同下拉重新整理效果,而在 Flutter 中,如果需要平臺不同下拉重新整理效果,那麼你需要分別使用 RefreshIndicator
和 CupertinoSliverRefreshControl
做顯示,不然多端都會呈現出一致的效果。
3.3、狀態管理
前面說過, Flutter 在很多方面都借鑑了 React Native ,所以在狀態管理方面也極具“即視感”,比如都是呼叫 setState
的方式去更新,同時操作都不是立即生效的 ,當然它們也有著差異的地方,如下程式碼所示:
- 正常情況下 React Native 需要在
Component
內初始化一個this.state
變數,然後通過this.state.name
訪問 。 - Flutter 繼承
StatefulWidget
,然後在其的State
物件內通過變數直接訪問和setState
觸發更新。
/// JS
this.state = {
name: ""
};
···
this.setState({
name: "loading"
});
···
<Text>this.state.name</Text>
/// Dart
var name = "";
setState(() {
name = "loading";
});
···
Text(name)
複製程式碼
當然它們兩者的內部實現也有著很大差異,比如 React Native 受 React diff 等影響,而 Flutter 受 isRepaintBoundary
、markNeedsBuild
等影響。
而在第三方狀態管理上,兩者之間有著極高的相似度,如早期在 Flutter 平臺就湧現了很多前端的狀態管理框架如:flutter_redux 、fish_redux 、 dva_flutter 、flutter_mobx 等等,它們的設計思路都極具 React 特色。
同時 Flutter 官方也提供了 scoped_model 、provider 等具備 Flutter 特色的狀態管理。
所以在狀態管理上 React Native 和 Flutter 是十分相近的,甚至是在跟著 React 走。
3.4、原生控制元件
在跨平臺開發中,就不得不說到接入原有平臺的支援,比如 在 Android 平臺上接入 x5 瀏覽器 、接入視訊播放框架、接入 Lottie 動畫框架等等。
這一需求 React Native 先天就支援,甚至在社群就已經提供了類似 lottie-react-native 的專案。 因為 React Native 整個渲染過程都在原生層中完成,所以接入原有平臺控制元件並不會是難事 ,同時因為發展多年,雖然各類第三方庫質量參差不齊,但是數量上的優勢還是很明顯的。
而 Flutter 在就明顯趨於弱勢,甚至官方在開始的時候,連 WebView
都不支援,這其實涉及到 Flutter 的實現原理問題。
因為 Flutter 的整體渲染脫離了原生層面,直接和 GPU 互動,導致了原生的控制元件無法直接插入其中 ,而在視訊播放實現上, Flutter 提供了外界紋理的設計去實現,但是這個過程需要的資料轉換,很明顯的限制了它的通用性, 所以在後續版本中 Flutter 提供了 PlatformView
的模式來實現整合。
以 Android 為例子,在原生層 Flutter 通過
Presentation
副屏顯示的原理,利用VirtualDisplay
的方式,讓 Android 控制元件在記憶體中繪製到Surface
層。VirtualDisplay
繪製在Surface
的 textureId ,之後會通知到 Dart 層,在 Dart 層利用AndroidView
定義好的Widget
並帶上 textureId ,那麼 Engine 在渲染時,就會在記憶體中將 textureId 對應的資料渲染到AndroidView
上。
PlatformView
的設計必定導致了效能上的缺陷,最大的體現就是記憶體佔用的上漲,同時也引導了諸如鍵盤無法彈出#19718和黑屏等問題,甚至於在 Android 上的效能還可能不如外界紋理。
所以目前為止, Flutter 原生控制元件的接入上是仍不如 React Native 穩定。
四、 外掛開發
React Native 和 Flutter 都是支援外掛開發,不同在於 React Native 開發的是 npm 外掛,而 Flutter 開發的是 pub 外掛。
React Native 使用 npm 外掛的好處就是:可以使用豐富的 npm 外掛生態,同時減少前端開發者的學習成本。
但是使用 npm 的問題就是太容易躺坑,因為 npm 包依賴的複雜度和深度所惑,以至於你都可能不知道 npm 究竟裝了什麼東西,拋開安全問題,這裡最直觀的感受就是 :“為什麼別人跑得起來,而我的跑不起來?” 同時每個專案都獨立一個 node_module ,對於硬碟空間較小的 Mac 使用者略顯心酸。
Flutter 的 pub 外掛預設統一管理在 pub 上,類似於 npm 同樣支援 git 連結安裝,而 flutter packages get
檔案一般儲存在電腦的統一位置,多個專案都引用著同一份外掛。
- win 一般是在 C:\Users\xxxxx\AppData\Roaming\Pub\Cache 路徑下
- mac 目錄在 ~/.pub-cache
如果找不到外掛目錄,也可以通過檢視 .flutter-plugins
檔案,或如下圖方式開啟外掛目錄,至於為什麼需要開啟這個目錄,感興趣的可以看看這個問題 13# 。
最後說一下 Flutter 和 React Native 外掛,在帶有原生程式碼時不同的處理方法:
-
React Native 在安裝完帶有原生程式碼的外掛後,需要執行
react-native link
指令碼去引入支援,具體如 Android 會在setting.gradle
、build.gradle
、MainApplication.java
等地方進行侵入性修改而達到引用。 -
Flutter 則是通過
.flutter-plugins
檔案,儲存了帶有原生程式碼的外掛 key-value 路徑 ,之後 Flutter 的指令碼會通過讀取的方式,動態將原生程式碼引入,最後通過生成GeneratedPluginRegistrant.java
這個忽略檔案完成匯入,這個過程開發者基本是無感的。
所以在外掛這一塊的體驗, Flutter 是略微優於 React Native 的。
五、 編譯和產物
React Native 編譯後的檔案主要是 bundle
檔案,在 Android 中是 index.android.bunlde
檔案,而在 IOS 下是 main.jsbundle
。
Flutter 編譯後的產物在 Android 主要是 :
isolate_snapshot_instr
應用程式指令段isolate_snapshot_data
應用程式資料段vm_snapshot_data
虛擬機器資料段vm_snapshot_instr
虛擬機器指令段等產物
在 IOS 主要是 App.framework ,其內部也包含了 kDartVmSnapshotData
、kDartVmSnapshotInstructions
、 kDartIsolateSnapshotData
、kDartIsolateSnapshotInstructions
四個部分。
接著看完整結果,如下圖所示,是空專案下 和 GSY 實際專案下, React Native 和 Flutter 的 Release 包大小對比。
可以看出在 React Native 同等條件下, IOS 比 Android 大很多 ,這是因為 IOS 自帶了 JSCore ,而 Android 需要各類動態 so 內建支援,而且這裡 Android 的動態庫 so 是經過了 ndk
過濾後的大小,不然還會更大。
Flutter 和 React Native 則是相反,因為 Android 自帶了 skia ,所以比沒有自帶 skia 的 IOS 會小得多。
以上的特點在 GSY 專案中的 Release 包也呈同樣狀態。
型別 | React Native | Flutter |
---|---|---|
空專案 Android | ||
空專案 IOS | ||
GSY Android | ||
GSY IOS |
值得注意的是,Google Play 最近釋出了 《8月不支援 64 位,App 將無法上架 Google Play!》 的通知 ,同時也表示將停止 Android Studio 32 位的維護,而 arm64-v8a
格式的支援,React Native 需要在 0.59 以後的版本才支援。
至於 Flutter ,在打包時通過指定 flutter build apk --release --target-platform android-arm64
即可。
六、效能
說到效能,這是一個大家都比較關心的概念,但是有一點需要注意,拋開場景說效能顯然是不合適的,因為效能和程式碼質量與複雜度是有一定聯絡的。
先說理論效能,在理論上 Flutter 的設計效能是強於 React Native ,這是框架設計的理念導致的,Flutter 在少了 OEM Widget ,直接與 CPU / GPU 互動的特性,決定了它先天效能的優勢。
這裡注意不要用模擬器測試效能,特別是IOS模擬器做效能測試,因為 Flutter 在 IOS模擬器中純 CPU ,而實際裝置會是 GPU 硬體加速,同時只在 Release 下對比效能。
程式碼的實現方式不同,也可能會導致效能的損失,比如 Flutter 中 skia 在繪製時,
saveLayer
是比較消耗效能的,比如 透明合成、clipRRect 等等,都會可能需要saveLayer
的呼叫, 而saveLayer
會清空GPU繪製的快取,導致效能上的損耗,從而導致開發過程中如果掉幀嚴重。
最後如下圖所示,是去年閒魚用 GSY 專案做測試對比的資料,原文在《流言終結者- Flutter和RN誰才是更好的跨端開發方案?》 ,可以看出在去年的時候, Flutter的整體幀率和繪製就有了明顯的優勢。
額外補充一點,JS 和 Dart 都是單執行緒應用,利用了協程的概念實現非同步效果,而在 Flutter 中 Dart 支援的
isolate
,卻是屬於完完全全的非同步執行緒處理,可以通過 Port 快捷地進行非同步互動,這大大擴充了 Flutter 在 Dart 層面的效能優勢。
七、發展未來
之前一篇 《為什麼 Airbnb 放棄了 React Native?》 文章,讓眾多不明所以的吃瓜群眾以為 React Native 已經被放棄,之後官方釋出的 《Facebook 正在重構 React Native,將重寫大量底層》 公示,又再一次穩定了軍心。
同時 React Native 在 0.59 版本開始支援 React Hook 等特性,並將原本平臺的特性控制元件從 React Native 內部剝離到社群,這樣控制元件的單獨升級維護可以更加便捷,同時讓 React Native 與 React 之間的界限越發模糊。
Flutter UI 平臺的無關能力,讓 Flutter 在跨平臺的擴充上更為迅速,儘管 React Native 也有 Web 和 PC 等第三方實現擴充支援,但是由於平臺關聯性太強,這些年發展較為緩慢, 而 Flutter 則是短短時間又宣佈 Web 支援,甚至擴充到 PC 和嵌入式裝置當中。
這裡面對於 Flutter For Web 相信是大家最為關心的話題, 如下圖所示,在 Flutter 的設計邏輯下,開發 Flutter Web 的過程中,你甚至感知不出來你在開發的是 Web 應用。
Flutter Web 保留了 大量原本已有的移動端邏輯,只是在 Engine 層利用 Dart2Js 的能力實現了差異化, 不過現階段而言,Flutter Web 仍處在技術預覽階段,不建議在生產環境中使用 。
由此可以推測,不管是 Flutter 或者 React Native,都會努力將自己擴充到更多的平臺,同時在自己的領域內進一步簡化開發。
- 其他參考資料 :
《Facebook 正在重構 React Native,將重寫大量底層》
《React Native 的未來與React Hooks》
《庖丁解牛!深入剖析 React Native 下一代架構重構》
自此,本文終於結束了,長呼一口氣。
資源推薦
- Github : github.com/CarGuo
- 開源 Flutter 專案:github.com/CarGuo/GSYG…
- 開源 React Native 專案:github.com/CarGuo/GSYG…
- 開源 Fluttre 實戰電子書專案:github.com/CarGuo/GSYF…