介紹
最近出現的 React Native 再次讓跨平臺移動端開發這個話題火起來了,曾經大家以為在手機上可以像桌面那樣通過 Web 技術來實現跨平臺開發,卻大多因為性能或功能問題而放棄,不得不針對不同平臺開發多個版本。
但這并沒有阻止人們對跨平臺開發技術的探索,畢竟誰不想降低開發成本,一次編寫就處處運行呢?除了 React Native,這幾年還出現過許多其它解決方案,本文我將會對這些方案進行技術分析,供感興趣的讀者參考。
為了方便討論,我將它們分為了以下 4 大流派:
- Web 流:也被稱為 Hybrid 技術,它基于 Web 相關技術來實現界面及功能
- 代碼轉換流:將某個語言轉成 Objective-C、Java 或 C#,然后使用不同平臺下的官方工具來開發
- 編譯流:將某個語言編譯為二進制文件,生成動態庫或打包成 apk/ipa/xap 文件
- 虛擬機流:通過將某個語言的虛擬機移植到不同平臺上來運行
Web 流
Web 流是大家都比較了解的了,比如著名的 PhoneGap/Cordova,它將原生的接口封裝后暴露給 Javascript,可以運行在系統自帶的 WebView 中,也可以自己內嵌一個 Chrome 內核 。
作為這幾年爭論的熱點,網上已經有很多關于它的討論了,這里我重點聊聊大家最關心的性能問題。
Web 流最常被吐槽的就是性能慢(這里指內嵌 HTML 的性能,不考慮網絡加載時間),可為什么慢呢?常見的看法是認為「DOM 很慢」,然而從瀏覽器實現角度來看,其實 DOM 就是將對文檔操作的 API 暴露給了 Javascript,而 Javascript 的調用這些 API 后就進入內部的 C++ 實現了,這中間并沒有多少性能消耗,所以從理論上來說瀏覽器的 DOM 肯定比 Android 的「DOM」快,因為 Android 的展現架構大部分功能是用 Java 寫的,在實現相同功能的前提下,C++ 不大可能比 Java 慢(某些情況下 JIT 編譯優化確實有可能做得更好,但那只是少數情況)。
所以從字面意思上看「DOM 很慢」的說法是錯誤的,這個看法之所以很普遍,可能是因為大部分人對瀏覽器實現不了解,只知道瀏覽器有 DOM,所以不管什么問題都只能抱怨它了。
那么問題在哪呢?在我看來有三方面的問題:
- 早期瀏覽器實現比較差,沒有進行優化
- CSS 過于復雜,計算起來更耗時
- DOM 提供的接口太有限,使得難以進行優化
第一個問題是最關鍵也是最難解決的,現在說到 Web 性能差主要說的是 Android 下比較差,在 iOS 下已經很流暢了,在 Android 4 之前的 WebView 甚至都沒有實現 GPU 加速,每次重繪整個頁面,有動畫的時候不卡才怪。
瀏覽器實現的優化可以等 Android 4.4 慢慢普及起來,因為 4.4 以后就使用 Chrome 來渲染了。
而對于最新的瀏覽器來說,渲染慢的原因就主要是第二個問題:CSS 過于復雜,因為從實現原理上看 Chrome 和 Android View 并沒有本質上的差別,但 CSS 太靈活功能太多了,所以計算成本很高,自然就更慢了。
那是不是可以通過簡化 CSS 來解決?實際上還真有人這么嘗試了,比如 Famo.us,它最大的特色就是不讓你寫 CSS,只能使用固定的幾種布局方法,完全靠 Javascript 來寫界面,所以它能有效避免寫出低效的 CSS,從而提升性能。
而對于復雜的界面及手機下常見的超長的 ListView 來說,第三個問題會更突出,因為 DOM 是一個很上層的 API,使得 Javascript 無法做到像 Native 那樣細粒度的控制內存及線程,所以難以進行優化,則在硬件較差的機器上會比較明顯。對于這個問題,我們一年前曾經嘗試過嵌入原生組件的方式來解決,不過這個方案需要依賴應用端的支持,或許以后瀏覽器會自帶幾個優化后的 Web Components 組件,使用這些組件就能很好解決性能問題。
現階段這三個問題都不好解決,所以有人想干脆不用 HTML/CSS,自己來畫界面,比如 React canvas 直接畫在 Canvas 上,但在我看來這只是現階段解決部分問題的方法,在后面的章節我會詳細介紹自己畫 UI 的各種問題,這里說個歷史吧,6 年前瀏覽器還比較慢的時候,Bespin 就這么干過,后來這個項目被使用 DOM 的 ACE 取代了,目前包括 TextMirror 和 Atom 在內的主流編輯器都是直接使用 DOM,甚至 W3C 有人專門寫了篇文章吐槽用 Canvas 做編輯器的種種缺點,所以使用 Canvas 要謹慎。
另外除了 Canvas,還有人以為 WebGL 快,就嘗試繪制到 WebGL 上,比如 HTML-GL,但它目前的實現太偷懶了,簡單來說就是先用 html2canvas 將 DOM 節點渲染成圖片,然后將這個圖片作為貼圖放在 WebGL 中,這等于將瀏覽器中用 C++ 寫的東東在 Javascript 里實現了一遍,渲染速度肯定反而更慢,但倒是能用 GLSL 做特效來忽悠人。
硬件加速不等同于「快」,如果你以為硬件加速一定比軟件快,那你該抽空學學計算機體系結構了
其實除了性能問題,我認為在 Web 流更嚴重的問題是功能缺失,比如 iOS 8 就新增 4000+ API,而 Web 標準需要漫長的編寫和評審過程,根本趕不上,即便是 Cordova 這樣自己封裝也忙不過來,所以為了更好地使用系統新功能,寫 Native 代碼是必須的。
代碼轉換流
前面提到寫 Native 代碼是必須的,但不同平臺下的官方語言不一樣,這會導致同樣的邏輯要寫兩次以上,于是就有人想到了通過代碼轉換的方式來減少工作量,比如將 Java 轉成 Objective-C。
這種方式雖然聽起來不是很靠譜,但它卻是成本和風險都最小的,因為代碼轉換后就可以用官方提供的各種工具了,和普通開發區別不大,因此不用擔心遇到各種詭異的問題,不過需要注意生成的代碼是否可讀,不可讀的方案就別考慮了。
接下來看看目前存在的幾種代碼轉換方式。
將 Java 轉成 Objective-C
j2objc 能將 Java 代碼轉成 Objective-C,據說 Google 內部就是使用它來降低跨平臺開發成本的,比如 Google Inbox 項目就號稱通過它共用了 70% 的代碼,效果很顯著。
可能有人會覺得奇怪,為何 Google 要專門開發一個幫助大家寫 Objective-C 的工具?還有媒體說 Google 做了件好事,其實吧,我覺得 Google 這算盤打得不錯,因為基本上重要的應用都會同時開發 Android 和 iOS 版本,有了這個工具就意味著,你可以先開發 Android 版本,然后再開發 iOS 版本。。。
既然都有成功案例了,這個方案確實值得嘗試,而且關鍵是會 Java 的人多啊,可以通過它來快速移植代碼到 Objective-C 中。
將 Objective-C 轉成 Java
除了有 Java 轉成 Objective-C,還有 Objective-C 轉成 Java 的方案,那就是 MyAppConverter,比起前面的 j2objc,這個工具更有野心,它還打算將 UI 部分也包含進來,從它已轉換的列表中可以看到還有 UIKit、CoreGraphics 等組件,使得有些應用可以不改代碼就能轉成功,不過這點我并不看好,對于大部分應用來說并不現實。
由于目前是收費項目,我沒有嘗試過,對技術細節也不了解,所以這里不做評價。
將 Java 轉成 C#
Mono 提供了一個將 Java 代碼轉成 C# 的工具 Sharpen,不過似乎用的人不多,Star 才 118,所以看起來不靠譜。
還有 JUniversal 這個工具可以將 Java 轉成 C#,但目前它并沒有發布公開版本,所以具體情況還待了解,它的一個特色是自帶了簡單的跨平臺庫,里面包括文件處理、JSON、HTTP、OAuth 組件,可以基于它來開發可復用的業務邏輯。
比起轉成 Objective-C 和 Java 的工具,轉成 C# 的這兩個工具看起來都非常不成熟,估計是用 Windows Phone 的人少。
將 Haxe 轉成其它語言
說到源碼轉換就不得不提 Haxe 這個奇特的語言,它沒有自己的虛擬機或可執行文件編譯器,所以只能通過轉成其它語言來運行,目前支持轉成 Neko(字節碼)、Javascript、Actionscript 3、PHP、C++、Java、C# 和 Python,盡管有人實現了轉成 Swift 的支持,但還是非官方的,所以要想支持 iOS 開發目前只能通過 Adobe AIR 來運行。
在游戲開發方面做得不錯,有個跨平臺的游戲引擎 OpenFL 的,最終可以使用 HTML5 Canvas、OpenGL 或 Flash 來進行繪制,OpenFL 的開發體驗做得相當不錯,同一行代碼不需要修改就能編譯出不同平臺下的可執行文件,因為是通過轉成 C++ 方式進行編譯的,所以在性能和反編譯方面都有優勢,可惜目前似乎并不夠穩定,不然可以成為 Cocos2d-x 的有利競品。
在 OpenFL 基礎上還有個跨平臺的 UI 組件 HaxeUI,但界面風格我覺得特別丑,也就只能在游戲中用了。
所以目前來看 Haxe 做跨平臺游戲開發或許可行,但 APP 開發就別指望了,而基于它來共用代碼實在就更不靠譜了,因為熟悉它的開發者極少,反而增加成本。
XMLVM
除了前面提到的源碼到源碼的轉換,還有 XMLVM 這種與眾不同的方式,它首先將字節碼轉成一種基于 XML 的中間格式,然后再通過 XSL 來生成不同語言,目前支持生成 C、Objective-C、Javascript、C#、Python 和 Java。
雖然基于一個中間字節碼可以方便支持多語言,然而它也導致生成代碼不可讀,因為很多語言中的語法糖會在字節碼中被抹掉,這是不可逆的,以下是一個簡單示例生成的 Objective-C 代碼,看起來就像匯編:
XMLVM_ENTER_METHOD("org.xmlvm.tutorial.ios.helloworld.portrait.HelloWorld", "didFinishLaunchingWithOptions", "?")XMLVMElem _r0;XMLVMElem _r1;XMLVMElem _r2;XMLVMElem _r3;XMLVMElem _r4;XMLVMElem _r5;XMLVMElem _r6;XMLVMElem _r7;_r5.o = me;_r6.o = n1;_r7.o = n2;_r4.i = 0;_r0.o = org_xmlvm_iphone_UIScreen_mainScreen__();XMLVM_CHECK_NPE(0)_r0.o = org_xmlvm_iphone_UIScreen_getApplicationframe__(_r0.o);_r1.o = __NEW_org_xmlvm_iphone_UIWindow();XMLVM_CHECK_NPE(1)...
在我看來這個方案相當不靠譜,萬一生成的代碼有問題基本沒法修改,也沒法調試代碼,所以不推薦。
小結
雖然代碼轉換這種方式風險小,但我覺得對于很多小 APP 來說共享不了多少代碼,因為這類應用大多數圍繞 UI 來開發的,大部分代碼都和 UI 耦合,所以公共部分不多。
在目前的所有具體方案中,只有 j2objc 可以嘗試,其它都不成熟。
編譯流
編譯流比前面的代碼轉換更進一步,它直接將某個語言編譯為普通平臺下的二進制文件,這種做法有明顯的優缺點:
- 優點
- 可以重用一些實現很復雜的代碼,比如之前用 C++ 實現的游戲引擎,重寫一遍成本太高
- 編譯后的代碼反編譯困難
- 或許性能會好些(具體要看實現)
- 缺點
- 如果這個工具本身有 Bug 或性能問題,定位和修改成本會很高
- 編譯后體積不小,尤其是如果要支持 ARMv8 和 x86 的話
接下來我們通過區分不同語言來介紹這個流派下的各種方案。
C++ 類
C++ 是最常見的選擇,因為目前 Android、iOS 和 Windows Phone 都提供了 C++ 開發的支持,它通常有三種做法:
- 只用 C++ 實現非界面部分,這是官方比較推崇的方案,目前有很多應用是這么做的,比如 Mailbox 和 Microsoft Office。
- 使用 2D 圖形庫來自己繪制界面,這種做法在桌面比較常見,因為很多界面都有個性化需求,但在移動端用得還不多。
- 使用 OpenGL 來繪制界面,常見于游戲中。
使用 C++ 實現非界面部分比較常見,所以這里就不重復介紹了,除了能提升性能和共用代碼,還有人使用這種方式來隱藏一些關鍵代碼(比如密鑰),如果你不知道如何構建這樣的跨平臺項目,可以參考 Dropbox 開源的 libmx3 項目,它還內嵌了 json 和 sqlite 庫,并通過調用系統庫來實現對簡單 HTTP、EventLoop 及創建線程的支持。
而如果要用 C++ 實現界面部分,在 iOS 和 Windows Phone 下可以分別使用 C++ 的超集 Objective-C++ 和 C++/CX,所以還比較容易,但在 Android 下問題就比較麻煩了,主要原因是 Android 的界面絕大部分是 Java 實現的,所以用 C++ 開發界面最大的挑戰是如何支持 Android,這有兩種做法:通過 JNI 調用系統提供的 Java 方法或者自己畫 UI。
第一種做法雖然可行,但代碼太冗余了比如一個簡單的函數調用需要寫那么多代碼:
JNIEnv* env;jclass testClass = (*env)->FindClass(env, "com/your/package/name/Test");
// get ClassjmethodID constructor = (*env)->GetMethodID(env, cls, "", "()V");jobject testObject = (*env)->NewObject(env, testClass, constructor);methodID callFromCpp = (*env)->GetMethodID(env, testClass, "callFromCpp", "()V");
//get methodid(*env)->CallVoidMethod(env, testObject, callFromCpp);
那自己畫 UI 是否會更方便點?比如 JUCE 和 QT 就是自己畫的,我們來看看 QT 的效果:
看起來很不錯是吧?不過在 Android 5 下就悲劇了,很多效果都沒出來,比如按鈕沒有漣漪效果,甚至邊框都沒了,根本原因在于它是通過 Qt Quick Controls 的自定義樣式來模擬的,而不是使用系統 UI 組件,因此它享受不到系統升級自動帶來的界面優化,只能自己再實現一遍,工作量不小。
反而如果最開始用的是 Android 原生組件就什么都不需要做,而且還能用新的 AppCompat 庫來在 Android 5 以下實現 Material Design 效果。
最后一種做法是使用 OpenGL 來繪制界面,因為 EGL+OpenGL 本身就是跨平臺,所以基于它來實現會很方便,目前大多數跨平臺游戲底層都是這么做的。
既然可以基于 OpenGL 來開發跨平臺游戲,是否能用它來實現界面?當然是可行的,而且 Android 4 的界面就是基于 OpenGL 的,不過它并不是只用 OpenGL 的 API,那樣是不現實的,因為 OpenGL API 最初設計并不是為了畫 2D 圖形的,所以連畫個圓形都沒有直接的方法,因此 Android 4 中是通過 Skia 將路徑轉換為位置數組或紋理,然后再交給 OpenGL 渲染的。
然而要完全實現一遍 Android 的 UI 架構工作量不小,以下是其中部分相關代碼的代碼量:
其中光是文字渲染就非常復雜,如果你覺得簡單,那只能說明你沒看過這個世界有多大,或許你知道中文有編碼問題、英語有連字符(hyphen)折行,但你是否知道繁體中文有豎排版、阿拉伯文是從右到左的、日語有平假名注音(ルビ)、印度語有元音附標文字(abugida ????)……?
而相比之下如果每個平臺單獨開發界面,看似工作量不小,但目前在各個平臺下都會有良好的官方支持,相關工具和文檔都很完善,所以其實成本沒那么高,而且可以給用戶和系統風格保持一致的良好體驗,所以我認為對于大多數應用來說自己畫 UI 是很不劃算的。
不過也有特例,對于 UI 比較獨特的應用來說,自己畫也是有好處的,除了更靈活的控制,它還能使得不同平臺下風格統一,這在桌面應用中很常見,比如 Windows 下你會發現幾乎每個必備軟件的 UI 都不太一樣,而且好多都有換膚功能,在這種情況下很適合自己畫 UI。