跨平臺一直是老生常談的話題,cordova、ionic、react-native、weex、kotlin-native、flutter等跨平臺框架的百花齊放,頗有一股推倒原生開發者的勢頭。本文將對當下跨平臺移動開發的現狀、實現原理、框架的選擇等進行深度解析。
為什麼我們需要跨平臺開發? 本質上,跨平臺開發是為了增加程式碼複用,減少開發者對多個平臺差異適配的工作量,降低開發成本,提高業務專注的同時,提供比web更好的體驗。通俗了說就是:省錢、偷懶。
本篇主要以react-native、weex、flutter,結合資訊展望,深入聊聊當前跨平臺移動開發的實現原理、現狀與未來。至於為什麼只講它們,因為對比ionic、phoneGap,它們更於 “naive”。
一、原理與特性
目前移動端跨平臺開發中,大致歸納為以下幾種情況:
react native、weex均使用Java作為程式語言,目前Java在跨平臺開發中,可謂佔據半壁江山,大有“一統天下”的趨勢。
kotlin-native開始支援iOS和Web開發,(kotlin已經成為android的一級語言)也想嘗試“一統天下”。
flutter是Google跨平臺移動UI框架,Dart作為谷歌的親兒子,毫無疑問Dart成為flutter的程式語言,如下圖,作為巨頭新生兒,在flutter官網也可以看出,flutter同樣“心懷天下”。
1、React Native
Facebook 出品,Java語言,JSCore引擎,React設計模式,原生渲染
1.1、理念架構
“Learn once, write anywhere
”,代表著 Facebook對 react native 的定義:學習 react ,同時掌握web與app兩種開發技能。 react native 用了 react 的設計模式,但UI渲染、動畫效果、網路請求等均由原生端實現。開發者編寫的js程式碼,通過 react native 的中間層轉化為原生控制元件和操作,比ionic等跨平臺應用,大大提高了的使用者體驗。
總結起來其實就是利用 JS 來呼叫 Native 端的元件,從而實現相應的功能。
如下圖所示,react native 的跨平臺是實現主要由三層構成,其中 C++ 實現的動態連結庫(.so),作為中間適配層橋接,實現了js端與原生端的雙向通訊互動。這裡最主要是封裝了JavaCore執行js的解析,而 react native 執行在JavaCore中,所以不存在瀏覽器相容的問題。
其中在IOS上直接使用內建的javacore,在Android則使用webkit.org官方開源的jsc.so
。
1.2、實現原理
和前端開發不同,react native所有的標籤都不是真實控制元件,JS程式碼中所寫控制元件的作用,類似 Map 中的key值。JS端通過這個key組合的Dom,最後Native端會解析這個 Dom ,得到對應的Native控制元件渲染,如 Android 中
在 react native 中,JS端是執行在獨立的執行緒中(稱為JS Thread )。JS Thread 作為單執行緒邏輯,不可能處理耗時的操作。那麼如fetch 、圖片載入、資料持久化等操作,在 Android 中實際對應的是okhttp 、Fresco 、SharedPreferences等。而跨執行緒通訊,也意味著 Js Thread 和原生之間互動與通訊是非同步的。
可以看出,跨平臺的關鍵在於C++ 層,開發人員大部分時候,只專注於JS 端的程式碼實現。 在原生端提供的各種 Native Module 模組(如網路請求,ViewGroup控制元件),和 JS 端提供的各種 JS Module(如JS EventEmiter模組),都會在C++ 實現的so中儲存起來,雙方的通訊通過C++中的儲存的對映,最終實現兩端的互動。通訊的資料和指令,在中間層會被轉為String字串傳輸,雙向的呼叫流程如下圖。
1.3、打包載入
最終,JS程式碼會被打包成一個 bundle 檔案,自動新增到 App 的資源目錄下。react native 的打包指令碼目錄為/node_modules/react-native/local-cli,打包最後會通過 metro 模組壓縮 bundle 檔案。而bundle檔案只會打包js程式碼,自然不會包含圖片等靜態資源,所以打包後的靜態資源,其實是被拷貝到對應的平臺資原始檔夾中。
其中圖片等存在資源的對映規則,比如放在 react native 專案根目錄下的 img/pic/logo.png 的資源,編譯時,會被重新命名後,根據大小 merged 到對應的是drawable目錄下,修改名稱為img_pic_logo.png。
打包Android和IOS,肯定需要相應的平臺專案存在,在 react-native init 時建立的專案,就已經包含了 android 和 ios 的模版工程,打包完的工程會載入bundle檔案,然後啟動專案,如下圖。這裡就不展(tou)開(lan)了,有興趣的可以看:React Native For Android 架構初探 。
2、WEEX
Alibaba 出品,Java語言,JS V8引擎,Vue設計模式,原生渲染
2.1、理念架構
“Write once, run everywhere”, weex的定義就像是:寫個 vue 前端,順便幫你編譯成效能還不錯的 apk 和 ipa(當然,現實有時很骨感)。基於 Vue 設計模式,支援 web、android、ios 三端,原生端同樣通過中間層轉化,將控制元件和操作轉化為原生邏輯來提高使用者體驗。
在 weex 中,主要包括三大部分:JS Bridge、Render、Dom,分別對應WXBridgeManager、WXRenderManager、WXDomManager,三部分通過WXSDKManager統一管理。其中 JS Bridge 和 Dom 都執行在獨立的 HandlerThread 中,而 Render 執行在 UI 執行緒。
JS Bridge 主要用來和 JS 端實現進行雙向通訊,比如把 JS 端的 dom 結構傳遞給 Dom 執行緒。Dom 主要是用於負責 dom 的解析、對映、新增等等的操作,最後通知UI執行緒更新。而 Render 負責在UI執行緒中對 dom 實現渲染。
2.2、實現原理
和 react native一樣,weex 所有的標籤也不是真實控制元件,JS 程式碼中所生成存的 dom,最後都是由 Native 端解析,再得到對應的Native控制元件渲染,如 Android 中
weex 中檔案預設為 .vue ,而 vue 檔案是被無法直接執行的,所以 vue 會被編譯成 .js 格式的檔案,Weex SDK會負責載入渲染這個js檔案。Weex可以做到跨三端的原理在於:在開發過程中,程式碼模式、編譯過程、模板元件、資料繫結、生命週期等上層語法是一致的。不同的是在JS Framework層的最後,web 平臺和 Native 平臺,對 Virtual DOM 執行的解析方法是有區別的。
實際上,在 Native 中對 bundle 檔案的載入大致經歷以下階段:
-
weex 接收到 js 檔案以後,JS Framework 根據檔案為 Vue 模式,會呼叫weex-vue-framework中提供的createInstance方法建立例項。
-
createInstance中會執行 Js Entry 程式碼裡new Vue()建立一個元件,通過其 render 函式建立出 Virtual DOM 節點。
-
由JS V8 引擎上解析 Virtual DOM ,得到 Json 資料傳送至 Dom 線,這裡輸出 Json 也是方便跨端的資料傳輸。
-
Dom 執行緒解析 Json 資料,得到對應的WxDomObject,然後建立對應的WxComponent提交 Render 。
-
Render在原生端最終處理處理渲染任務,並回撥裡JS方法。
得益於上層的統一性,只是通過weex-vue-framework判斷是由Vue.js生成真實的 Dom ,還是通過 Native Api 渲染元件,weex 一定程度上上,用JS 實現了vue一統天下的效果。
weex 在原生渲染 Render 時,在接收到渲染指令後,會逐步將資料渲染成原生元件。Render 通過解析渲染資料的描述,然後分發給不同的模組。
比如 控制元件渲染屬於dom模組中,頁面跳轉屬於navigator模組等。模組的渲染過程並非一個執行完,再執行另一個的流程,而是類似流式的過程。如上一個
承當了重要的職責,使得上層具備統一性,可以支援跨三個平臺。總的來說它主要負責是:管理Weex的生命週期;解析JS Bundle,轉為Virtual DOM,再通過所在平臺不同的API方法,構建頁面;進行雙向的資料互動和響應。
2.3、打包
weex 作為 react-native 之後出現的跨平臺實現方案,自然可以站在前人的肩膀上優化問題,比如:Bundle檔案過大問題。
Bundle檔案的大小,很大程度上影響了框架的效能,而 weex 選擇將JS Framework整合到 WeexSDK 中,一定程度減少了JS Bundle的體積,使得 bundle 裡面只保留業務程式碼。
打包時,weex 是通過 webpack 打包出 bundle 檔案的。bundle 檔案的打包和 entry.js檔案的配置數量有關,預設情況下之後一個 entry 檔案,自然也就只有一個bundle檔案。
在 weex 專案的 webpack.common.conf.js中可以看到,其實打包也是區分了 webConfig和weexConfig的不同打包方式。如下圖,其中weexEntry 就是 weex 打包配置的地方,可以看到本來已經有index和 entry.js存在了。如果有需要,可配置上你需要的打包頁面,具體這裡就不詳細展開了。有興趣可看:Weex原理之帶你去蹲坑 。
3、Flutter
Google 出品,Dart語言,Flutter Engine引擎,響應式設計模式,原生渲染
Flutter 是谷歌2018年釋出的跨平臺移動UI框架。相較於本人已經在專案中使用過 react native 和 Weex,Flutter目前僅僅是簡單執行過Demo,畢竟還是beta 階段,這裡更多的聊一下它的實現機制和效果。
與 react native 和 weex 的通過 Java 開發不同,Flutter 的程式語言是Drat,(谷歌親兒子,據說是因為 Drat 專案組就在 Flutter 隔壁而被選上( ))所以執行時並不需要 Java 引擎,但實際效果最終也通過原生渲染。
如上圖,Flutter 主要分為Framework和Engine,我們基於Framework 開發App,執行在 Engine 上。Engine 是 Flutter 的獨立虛擬機器,由它適配和提供跨平臺支援,目前猜測 Flutter 應用程式在 Android 上,是直接執行 Engine 上 所以在是不需要Dalvik虛擬機器。(這是比kotlin更徹底,拋棄JVM的糾纏?)
如下圖,得益於 Engine 層,Flutter 甚至不使用移動平臺的原生控制元件, 而是使用自己 Engine 來繪製 Widget (Flutter的顯示單元),而 Dart 程式碼都是通過 AOT 編譯為平臺的原生程式碼,所以 Flutter 可以 直接與平臺通訊,不需要JS引擎的橋接。同時 Flutter 唯一要求系統提供的是 canvas,以實現UI的繪製。咦?這麼想來,支援web端也沒問題吧!
在Flutter中,大多數東西都是widget,而widget是不可變的,僅支援一幀,並且在每一幀上不會直接更新,要更新而必須使用Widget的狀態。無狀態和有狀態 widget 的核心特性是相同的,每一幀它們都會重新構建,有一個State物件,它可以跨幀儲存狀態資料並恢復它。
Flutter 上 Android 自帶了 Skia,Skia是一個 2D的繪圖引擎庫,跨平臺,所以可以被嵌入到 Flutter的 iOS SDK中,也使得 Flutter Android SDK要比 iOS SDK小很多。
二、對比
這算是互相傷害的環節了吧。
1、大小
上面Apk大小是通過react-native init、weex create 和 flutter 建立出的工程後,直接不新增任何程式碼,打包出來的 release 簽名 apk 大小。從下圖可以看出,其中大比例都是在so庫。
2、社群
react native 作為 Facebook 主力開源專案之一,至今已有各類豐富的第三方庫,甚至如realm、lottie 等開源專案也有 react native 相關的版本,社群實際無需質疑。當然,因為並完全正統開發平臺,第三庫的健壯性和相容性有時候總是良莠不齊。
weex 其實有種生錯在國內的感覺。其實 weex 的設計和理念都很優秀,效能也不錯,但是對比 react native 的第三方支援,就顯得有點後媽養的。2016年開源至今,社群和各類文件都顯得有點疲弱,作為跨平臺開發人員,大多時候肯定不會希望,需要頻繁的自己增加原生功能支援,因為這樣的工作一多,反而會與跨平臺開發的理念背道而馳,帶來開發成本被維護難度增加。
Flutter目前還處理beta階段,但是谷歌的號召力一直很可觀,這一點無需質疑。
3、效能
理論上 flutter 的效能應該是最好的,但是目前實際體驗中,卻並沒有感受出來太大的差距,和 react native(0.5.0之後)、weex 在效能上個人體驗差異不是很大。當然,這裡並沒有實測渲染的毫秒時間和幀率資料。
4、其他區別
Weex的多頁面實現問題
weex 在 native 端是不支援 的,這一點和 react-native 不同在與,如果在 native 需要實現頁面跳轉,使用 vue-router 將會慘不忍睹:返回後頁面不做特別處理時,是會空白一片。參考官方Demo playground ,native 端 的採用weex.requireModule('navigator')跳轉 Activity 是才正確實現。
同時,weex中 navigator 跳轉的設計,也導致了多頁面的頁面間通訊的差異。weex在多頁面下的資料通訊,是通過url實現的,比如file://assets/dist/SecondPage.js params=0,而vuex和vue-router在跨頁面是無法共用的;而 react native 在跨 Actvity 使用時,因為是同一個bundle檔案,只要 manager 相同,那麼 router 和 store 時可以照樣使用的,資料通訊方式也和當個 Actvity 沒區別。
專案模板
weex 和 react native 模板程式碼模式也不同。weex 的模板是從 cordova 模式修改過來的,根據platform需求,用命令新增固定模組,而在 .gitignore 對 platforms資料夾是忽略跟蹤。 react native 在專案建立時模版就存在了,特別是新增第三方外掛原生端支援時,會直接修改模板程式碼,git程式碼中也會新增跟蹤修改。
三、未來趨勢
我們選擇框架的時候,很多時候會關注框架的成熟度和生命力不是麼。
1、React Native
“Airbnb 宣佈放棄使用 React Native,迴歸使用原生技術” : Airbnb 作為 react native 平臺上最大的支持者之一,其開源的lottie同樣是支援原生和 react native。
Airbnb 在宣佈放棄的文中,也對 react native 的表示了很大量的肯定,而使得他們放棄的理由,其實主要還是集中於專案龐大之後的維護困難,第三方庫的良莠不齊,相容上需要耗費更多的精力導致放棄。
ps:( Lottie庫Airbnb出的是一個能夠幫助解析AE匯出的包含動畫資訊的json檔案。這很好的解決了一個矛盾,設計師可以更專注的設計出各種炫酷的動畫效果,而開發只需要將其加入支援即可。)
Facebook 正在重構 React Native,將重寫大量底層。 在經歷了開源協議風波後,可以看出 Facebook 對於 react native 還是很看重的, 這些底層重構優化的地方,主要集中於:
首先,改變執行緒模型。UI 更新不再需要在三個不同的執行緒上執行,而是可以在任意執行緒上同步呼叫 Java 進行優先更新,同時將低優先順序工作推出主執行緒,以便保持對 UI 的響應。其次,將非同步渲染功能引入 React Native 中,允許執行多個渲染並簡化非同步資料處理。最後,簡化橋接,讓它更快、更輕量。原生和 Java 之間的直接呼叫效率更高,並且可以更輕鬆地構建除錯工具,如跨語言堆疊跟蹤。
2、Weex
沒有死!阿里公開Weex技術架構,還開源了一大波元件。 從 2018年初的新聞可以看出,weex 的遭遇有點類似曾經的 Duddo(Dubbo因為內部競爭被阿里一度放棄維護),這波詐屍後weex被託管到了Apache,而github的weexteam 如今也還保持著更新,希望後續能有多好的發展,拭目以待吧。
3、Flutter
Flutter 是 Google 跨平臺移動UI框架,Dart作為谷歌的親兒子在 Flutter 中使用,並且谷歌新作業系統 Fuchsia 支援 Dart,使用 Flutter 作為操作UI框架。這些集合到一起難免讓你懷疑 Android 是否要被谷歌拋棄的想法。
或者如今先 Android 等平臺上推廣 Flutter 與 Dart,就是為了以後跟好的過渡到新系統上,畢竟開發者是作業系統的生命源泉之一。而 Java 與 JVM 或者可以被谷歌完全拋開。當然,目前看起來 Flutter 貌似還缺少一些語法糖,巢狀下來的程式碼有點不忍直視,或者到正式版之後,我們更能感受出它的美麗吧。
本文作者戀貓月亮