移動客戶端跨平臺開發(fā)方案探索

本文是 Piasy 原創(chuàng),發(fā)表于 https://blog.piasy.com,請閱讀原文支持原創(chuàng) https://blog.piasy.com/2017/12/16/Mobile-Client-Cross-Platform-Development/

跨平臺開發(fā)想必很多朋友都聽說過,甚至實踐過,這里我就不過多介紹相關的背景了,Java 的 Slogan 完美詮釋了這一愿景:Write once, run anywhere!

提到這個話題,大家首先想到的可能是 React Native,不過本文并不是 RN 教程。本文旨在探索我關注到的幾種比較靠譜的移動客戶端跨平臺開發(fā)方案,當然 RN 是其中必不可少的一部分。

完美的跨平臺開發(fā)方案自然是一行平臺相關的代碼都沒有,但這個要求稍顯苛刻,很多時候退一步海闊天空,在移動客戶端跨平臺開發(fā)這個場景下也是這樣。所以我們的目標應該是盡可能地減少平臺相關代碼的開發(fā),最大程度復用核心邏輯代碼。

此外,客戶端的開發(fā)可以分為兩部分:業(yè)務邏輯與界面交互。由于不同的平臺(Android、iOS、Web)GUI 系統(tǒng)差異較大,通常來說界面交互共享代碼更加困難。這對于 APP 開發(fā)確實是個不小的問題,但對于 SDK 開發(fā)來說卻影響較小。

可選方案

接下來我們先看看有哪些可選方案:

其實如果要把小程序列入其中,也未嘗不可,畢竟能在不同平臺的微信和支付寶里運行,也堪稱跨平臺了。

注:上述各項技術的基本概念本文不會過多介紹,請大家自行查閱官方文檔

2017.12.22 更新:之前誤以為 Weex 是和小程序一樣的技術,但它是和 RN 對標的呀,實在汗顏,日后再做補充……

React Native

RN 由 Facebook 開源,F(xiàn)acebook 內部很多項目在用,工業(yè)界也有不少公司和團隊都在使用。這樣的項目,坑肯定有,但一定不會沒人填。

RN 的主要優(yōu)勢有兩大部分,一是 React 本身的優(yōu)勢,二是將「React」「Native 化」之后跨平臺的優(yōu)勢。這里我主要總結下 React 本身的優(yōu)勢(或者說特性):

  • 組件化:組件根據(jù)狀態(tài)(state)和屬性(props)確定 UI,各種組件組成 view tree;
  • 單向數(shù)據(jù)流:props 只允許自頂向下傳遞,父組件設置子組件的 props;交互事件則自底向上拋出,父組件收到子組件內的交互事件后更新 state,觸發(fā)整個 view tree 的更新(重新設置子組件的 props);
  • Virtual DOM:view tree 和實際 DOM tree 獨立,通過計算差量,每次刷新界面只更新變化的 DOM 元素,提高效率;
  • JSX 語法、render 函數(shù)讓代碼更易于理解和維護;

我覺得下面這幅圖形象地表達了 React 的設計精髓:building large applications with data that changes over time

image

內部原理:

  • 寫的是 JS 代碼,實際運行的也是 JS 代碼,在 JavaScriptCore VM 中運行(而不是 WebView);

  • JS 代碼里的 View 實際會使用 native View(在 RN 的上下文中,native 指的是原生),映射關系是可以控制的;

  • 支持 JS 代碼和 native 代碼互相調用,三大特點:異步,批量,序列化;

    • 實際上 native 化靠的正是 JS 和 native 互相調用(RN 內部實現(xiàn));
    • 雙向調用都是異步的;
    • 跨邊界調用實際上是消息收發(fā)和處理,由于跨邊界存在開銷,所以批量處理減小開銷;
    • 不跨邊界共享可變對象,而是跨邊界交換可序列化的消息;
  • 跨邊界調用通過 bridge 層實現(xiàn);

    • 把 native 模塊對象注冊到 bridge,之前 JS 代碼里面的函數(shù)調用就可以執(zhí)行了(JS 是動態(tài)語言,運行時有函數(shù)/屬性,就能訪問);
    • 跨邊界調用實際上是消息收發(fā)和處理,例如 JS 調用 native 的一個函數(shù),就往一個特殊的隊列里放入一條消息,bridge 取出消息后,利用反射,調用 native 函數(shù),反之亦然;
    • bridge 可以替換 JS runtime,這樣就變得很靈活了,例如可以在單獨的 WebKit 進程中運行,可以在 Chrome 里面調試 RN APP;
    • 下圖形象地展示了跨邊界調用的過程:
    image

總結:RN 利用 JavascriptCore VM 在 iOS/Android 系統(tǒng)上執(zhí)行 JS 代碼,實現(xiàn)了業(yè)務邏輯跨平臺;通過 JS 和 native 互調完成 UI native 化,實現(xiàn)了界面交互跨平臺。雖然 JS VM 解釋執(zhí)行、跨邊界調用存在一定的性能開銷,但大部分情況下它們都不會成為瓶頸,如果開發(fā)任務大多集中在界面交互、非計算密集的業(yè)務邏輯上,RN 是不錯的選擇。

Flutter

Flutter 由 Google 開源,項目年齡比 RN 短大半年,目前仍處于 alpha 狀態(tài),沒有正式發(fā)布版(RN 雖然更新頻繁,而且還處于 0.x 版本,但好歹也算發(fā)布了)。

優(yōu)勢:

  • 自帶 beautiful UI
  • UI 代碼的編寫,借鑒了 React 的思想:組件化,聲明式,集中的 build 函數(shù)(React 的 render)……
  • 利用 Flutter 自己的 GUI 引擎,高效實現(xiàn)所有的 2D GUI 開發(fā);
  • 相機(拍照)、傳感器、GPS、網絡、持久化,都可以在 Flutter 里使用(利用 Dart 和 native 互調);
  • Dart package 市場,也可以發(fā)現(xiàn)輪子、發(fā)布輪子;

內部原理:

  • 架構圖:
image
  • 核心引擎部分是純 C++ 代碼,iOS/Android 都直接支持,Dart 代碼則會預編譯為機器碼,在各個系統(tǒng)里直接執(zhí)行,沒有解釋器 賺差價
  • UI 部分 Flutter 不使用任何系統(tǒng) OEM 控件,只用在自己的 GUI 體系內打造的控件,因為 Google 信不過 OEM,F(xiàn)lutter 自己完全實現(xiàn)了一套跨平臺的 GUI 系統(tǒng),包括圖層組合渲染、觸摸事件、手勢、動畫、控件……這個 GUI 系統(tǒng)承載于一個 FlutterView(Android 繼承自 SurfaceView,iOS 繼承自 UIView);
  • 支持 Dart 和 native 代碼雙向通訊(和 RN 一樣,這里的 native 指的是原生):通過 Channel 實現(xiàn)完全異步的消息收發(fā);
    • Channel 是 Flutter 里 Dart 和 native 通訊的通道,類似于一個網絡連接,通過名字進行區(qū)分,是全雙工的通道,創(chuàng)建 Channel 時使用相同的名字即可互相收發(fā)消息;
    • MethodChannel/FlutterMethodChannel 是專門用來實現(xiàn)類似 RPC 的通道;
    • BasicMessageChannel/FlutterBasicMessageChannel 可以用來實現(xiàn)普通的消息收發(fā),全雙工使用示例(Dart 端Android 端iOS 端
    • EventChannel/FlutterEventChannel 用來實現(xiàn)雙向 Event 通訊(看起來和 MessageChannel 功能重疊);

總結:Flutter 利用 Dart 代碼可以預編譯為機器碼這一特性,實現(xiàn)了業(yè)務邏輯的跨平臺;通過自己實現(xiàn)一套跨平臺的 GUI 系統(tǒng),實現(xiàn)了界面交互的跨平臺。由于最終執(zhí)行的代碼都是機器碼(C++,Dart),而且跨平臺的實現(xiàn)基本不涉及 Dart 代碼與 native 代碼互相調用,所以 Flutter 的效率肯定是有保障的。在涉及到動畫、手勢等強交互 UI 特性時,F(xiàn)lutter 的性能應該比 RN 要高不少。Flutter 還自帶了一套 Material Design 的跨平臺控件庫,簡直良心。

Web 端呢?Dart 可是能運行在瀏覽器里的呢!

Dart runs fast in every modern browser, on the command line, on servers, and on mobile.

不過遺憾的是,雖然 Dart 代碼可以在瀏覽器運行,但卻無法和 Flutter APP 共用 UI,Flutter 團隊也不打算開發(fā) Web 支持

J2ObjC + GWT

J2ObjC 和 GWT 都由 Google 開源,分別用來把 Java 代碼編譯為 ObjC 代碼和 JS 代碼,用來在 Android、iOS、Web 端共用業(yè)務邏輯代碼,界面交互代碼則需要單獨開發(fā)。

這個特性簡直就是 SDK 開發(fā)者的福音,事實上這也正是我在最近兩個月在公司項目中實踐的方案。這套跨平臺方案沒有什么炫酷的技術,直接把代碼編譯為各個平臺開發(fā)語言的代碼,簡單粗暴但行之有效。

這里我簡單總結下我在實踐中感受到的一些要點:

  • 利用 interface 隔離 UI 操作;
  • 利用 interface 隔離其他 SDK 調用;
  • JSON 解析作為一個特殊的 SDK,除了要隔離,還有別的坑:ObjC 中可能需要手動實現(xiàn)序列化與反序列化,我們項目使用的 MJExtension 就是如此;
  • 支持 try-catch,會被翻譯為 ObjC 的異常機制;
  • 循環(huán)引用問題:Java 內存回收做得比 ObjC 好,即便有循環(huán)引用,但如果整體不被其他對象引用,也可以回收,但 ObjC 就不行,所以要么代碼里得顯式設置 null 切斷循環(huán)引用,要么就得用 WeakReference@Weak 注解會被翻譯為 __unsafe_unretained,用起來還是有些擔心的);
  • J2ObjC 的靜態(tài)庫 libjre_emu.a 有好幾百兆,如果發(fā)布的 SDK 是靜態(tài)庫,那讓開發(fā)者用戶也攜帶這個 .a 就會比較惱人;

Djinni + WebAssembly

Djinni 由 Dropbox 開源,它的核心思想是用 C++ 開發(fā)業(yè)務邏輯,利用 JNI、Objective-C++ 實現(xiàn) C++ 和 Java、ObjC 的互相調用。

那這是 JNI 和 Objective-C++ 實現(xiàn)的跨平臺呀,和 Djinni 有啥關系呢?做過 NDK 開發(fā)的朋友肯定知道,要實現(xiàn) C++ 調用 Java 非常麻煩,boilerplate code 很多,Djinni 的發(fā)力點就在于減少 boilerplate code,聲明跨邊界的數(shù)據(jù)結構和接口,自動生成 C++、Java、ObjC 跨邊界調用相關的代碼,讓我們可以聚焦于真正的業(yè)務邏輯開發(fā)中。

Djinni 生成代碼的思路和 J2ObjC 編譯代碼的思路異曲同工,也都是簡單粗暴的辦法。接觸到 Djinni 是半年前,最近趁著總結這篇文章的機會,簡單實踐了一把,還談不上有太深的感悟,唯一一點是 Djinni 生成的 record 在跨邊界時,是 immutable 的,因為很難保證跨邊界狀態(tài)維護,設計接口時需要考慮這一點。

這個方案是在 Dropbox 內部大量實踐后再開源的,而且生成的代碼并不多,穩(wěn)定性不存在太多的風險,靠譜程度還是很高的。如果 SDK 的業(yè)務邏輯想在 C/C++ 代碼中實現(xiàn),那這套方案就是良策了。

理論上來說,我們的 C++ 代碼也是可以編譯為可以被 WebAssembly 加載的代碼的,那在 Web 端使用應該也是可行的,當然,一層 JS wrapper 必不可少。

Demo time

光說不練假把式,demo 自然是需要的。但逼近年關,公司項目太忙,只來得及做完后兩種方案實現(xiàn) Android iOS 跨平臺的 demo,剩下的部分,只能來年再補了。

畢竟「done is better then perfect」

Caveat time

綠色守護的開發(fā)者馮森林老師曾在一次 MDCC 上說:如果一個項目只說自己如何如何牛,卻對自己的限制、坑閉口不提,那這個項目一定不靠譜。

坑肯定是少不了的,這里記錄下我踩過的那些坑。

React Native

ReactNative 某個標簽對應的 native view,如果修改 child view(增減、調整大小),都不會觸發(fā)這個 native view 自身的 layout,所以需要自己手動觸發(fā)。參考:GitHub issue

J2ObjC

包裝類型使用注意:對 List<Long> 調用 .contains(long),判斷將失效,生成的 ObjC 代碼在比較指針。

參考文章

React Native:

Flutter:

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,546評論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,570評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,505評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,017評論 1 313
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,786評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,219評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,287評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,438評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 48,971評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,796評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,995評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,540評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,230評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,918評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,697評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內容