如何開發一個移動跨平臺庫
“一次編寫,到處運行” 是為闡明 Java 的跨平臺功能而編寫的著名標語。程序員都希望他們的代碼能到處運行。盡管如此,要跟上 CPU 體系結構的變化是如此困難,新的程序語言日日涌現,框架來了又去,并且如果你想跟操作系統打交道,代碼的重用性就不必再提。
但是也許,如果我們只把范圍限制在移動設備,可能還有機會!近年來移動化的趨勢日趨明顯,所以開發一個移動跨平臺的一定會對一些開發者有所幫助。
如果我們要進一步縮小,到iOS和Android上,它們目前的市場占有率為93.9%。 這給我們鎖定了7個目標CPU架構/ABI(ARMv7、armv7s、IOS的arm64、armeabi、armeabi-V7A、x86和針對 Android的MIPS),以及兩種編程語言(iOS的Objective-C和Android的Java)。至于框架和操作系統,支持最新的兩個 iOS版本應該就足夠了,因為新的iOS版本有著較高的使用率,但當涉及到Android時,要有一個不錯覆蓋率的話我們需要支持Froyo(或者Gingerbread)以后的所有版本。正如你所看到的,這不是件容易的事,但我們需要這樣做。
我們想要做的總結在下圖中;在有一些適配特定平臺膠水代碼的情況下在兩個平臺間共享一個庫。由于Skyscanner嚴重依賴在于互聯網,一些網絡函數是不可少的。
通常情況下,在iOS中,一個庫可以通過Objective-C源代碼或預編譯的靜態二進制庫 中導入,并要有相應的頭文件。在Android中,除了Java源代碼,一個庫也可以通過.class(字節碼)文件和靜態/共享二進制庫導入。然而,由 于這些選擇是限制性的,這里的研究將更進一步,去探索在Android和iOS中導入代碼的替代方法。
那么,我們該如何開始呢?有什么選項呢?是否有簡化的工具呢?
選項1-移動跨平臺開發工具
如果你是一名移動開發者,你一定聽說過大量的移動跨平臺開發工具,比如PhoneGap,Appcelerator Titanium 和Xamarin。它們中的一些工具允許我們開發類庫,對嗎?
具備這一功能的工具通常存在的主要問題如下:
1輸出到終端產品(.app/ .ipa或者 .apk)而不是類庫中
2嵌入到運行時的環境中運行跨平臺的代碼,這些代碼與在環境中的本地代碼交互非常的困難.
-
網頁視圖工具-這些工具使用視圖做為運行時環境,用JavaScript/HTML5來編寫代碼.如果它們與運行在網頁視圖的代碼交互困難時,它們將會立即失效.(然而,它總是試圖這樣,你隨后將會發現.)這樣的工具包括PhoneGap, RhoMobile,Secha Touch,appMobi, Telerik.
-
Adobe AIR--這的運行時環境中Adobe集成運行時環境.它的失效取決與與AIR中代碼交互的困難程度.
-
Xamarin--它只能輸出到中間的Xamarin 庫中,而不是本地庫中.
-
Appcelerator Titanium--創建的本地類庫并不是官方支持的,但是它可能是可以工作的,如果我們寫Titanium的擴展,這些擴展允許與本地代碼交互.太多的麻煩,存在問題的那些結果,同時不確保在下一個版本Titanium的升級中這些功能是否保留.
-
Corona--Corona的員工聲稱它已支持Android,同時它未來將支持iOS.
-
MoSync--與Corona類似
-
Kony--不支持.
-
Trigger.io--不支持
-
OpenFL--不支持
-
DragonRad--已過時,似乎不支持
因此,失敗了,沒什么真正可行的:(
但是等一下,難道C/C++的代碼不能訪問iOS和Android嗎?
選項2--C++
使用C++來開發類庫是可行的兩個解決方案之一。
在Android平臺,本地開發套件(NDK)和Java本地套件框架(JNI)允許Java與C/C++的代碼運行和交互.NDK的負責為 Android的每個目標對象(armeabi,armeabi-v7a,x86和mips)編譯C++代碼; 而JNI允許這兩種語言溝通交流.使用JNI相當的啰唆;程序員必須遵守命名規則,而且需要用Java和c++兩層包裝.一方面,通過用Java語言暴露 所有的c++類和方法(包括了本地關鍵字),Java封裝提供了一個用于c++類庫的Java的接口.另一方面,c++封裝提供了Java封狀與c++類 庫之間的橋梁,這兩種語言的對象可以相互轉化。
在iOS中,事情就變得簡單多了。在此系統中,沒有命名規則,只需要采用 “Objective-C++”進行額外一層的封裝就可以。“Objective-C++”是一種允許變量在單一的源文件中既使用“Objective- C”代碼,也可以使用“C++”代碼的語言。所以,所有的對象翻譯都只發生在這個單一封裝層中。你可以查看略微修改后的Android/iOS應用的流程 圖如下:
引入第三方庫也是不常規的,因為程序員不能直接訪問JRE/Android以及Cocoa Touch框架。在這種情況下,第三方庫可以通過兩種方式引入,源代碼或預編譯的二進制文件(或者找到它們,或者編譯它們)。其中的一個特例是執行網絡操 作(HTTP請求),在標準模板庫(STL)中它是不被支持的,所以我們整合了libcurl到跨平臺庫中。libcurl不能以源代碼引入,只能作為一 個可執行的配置腳本。幸運的是能夠找到為iOS預編譯的二進制文件。在Android中,我們使用NDK工具鏈/編譯器為每個Android目標系統編譯 libcurl。為7個目標架構(3適用于iOS,4為Android)編譯庫是很費時的,但這個過程的一部分可以用腳本實現自動化。
這個措施相當奏效,C++是種流行的語言,它有一個龐大數量的可用的第三方庫,并且所有使用的 工具(Android的NDK、JNI、Objective-C++)都有官方的解決方案,由谷歌和蘋果的支持。這個措施的唯一的缺點是在Android 上,如果我們想保持Java包裝對象對C++對象的引用,我們必須在Java對象釋放前手動回收C++對象(通常叫刪除C++對象)。然而,如果沒有理由 保留C++對象的話,它們可以在被復制成Java中對應部分后立即銷毀。
選項 3 - 代碼移植
另一個考慮過的選擇是,只維護一個代碼庫,然后用適當的工具把代碼翻譯為平臺對應的語言。這種選擇也有它的缺陷:
-
生成代碼效率不會像原生開發者寫的那樣高。
-
翻譯過程很容易引入Bug,而且必須手動修復。
-
導入的二進制文件很難被翻譯,因為大多數的工具只能翻譯源代碼。
以下是幾種移動平臺代碼移植工具。遺憾的是,沒有一種能滿足需求:
-
J2ObjC - Google 開發的工具,用于翻譯 Java 代碼為 Objective-C 。看起來質量比較高(與以下其他相比)。目前為止,它能把部分 Java 類翻譯為 Objective-C ,但開發還沒有完成。不幸的是,它目前翻譯不了 Java 的 HTTP 請求,但是如果我們為每個平臺單獨實現這部分功能,也有實現的可能。這個項目從2012年9月建立至今。
-
Hyperloop - 將 JavaScript 翻譯為平臺原生代碼的工具。目前為止,它只支持 iOS ,而且并不穩定,但他們的計劃是擴展到所有流行的平臺。這個項目從2013年8月建立至今。
-
ObjC2J - 將 Objective-C 翻譯為 Java 的工具。這本來也是個很好的思路,但不幸的是,它還不夠成熟,含有很多bug,經常輸出不能編譯的代碼。
-
XMLVM - 將 JVM 字節碼交叉編譯為 Objective-C 的工具。這個工具不僅不夠完善,用起來很復雜,并且需要下載/導入很多legacy jar。
-
Apportable - 將 iOS 應用轉化為 Android 應用的工具。不幸的是,它達不到我們的要求,因為它只能翻譯整個應用,無法翻譯庫,而且直接輸出 .apk(Android 應用安裝包)文件。
-
Avian - 輕量級的 Java 虛擬機,可以嵌入 iOS app bundle 并運行 Java 代碼。這個方案滿足不了需求,因為想要讓 iOS 上跑的 UI 代碼與虛擬機中跑的 Java 庫代碼交互非常困難。
-
in the box - 在 iOS 上運行的移植 Dalvik 虛擬機和 Android Gingerbread (2.3) API。這個選項被否決了,因為這個項目已經失效。
選項4 - WebView中的JavaScript
JavaScript是近幾年普及很快的語言,其初衷是作為客戶端的腳本語言,但是現在也用于服務器端應用程序(node.js),并成為了上述的移動跨平臺工具的一部分。它可能成為解決我們問題的跨平臺語言嗎?
所有的移動跨平臺都能在web-browser視圖里執行JacaScript腳本 (WebViews),并且WebView的API通常都呈現在開發者眼前。
我們在JavaScript中需要的最少功能如下:
-
執行函數
-
調用腳本
-
計算全局變量和返回的結果
-
執行回調 (到本地代碼)
我們來單獨地探討各個平臺。
在Android中,WebView能執行腳本串。回調到Java代碼是JavaScript實現的,它注解(用 @JavascriptInterface)可以調用的Java類中的確定方法,并添加這些類的實例到WebView的JavaScript全局作用域的 引用(用addJavascriptInterface()方法)。然而計算變量或者函數調用,并不是這么簡單,因為沒有一種像腳本一樣直接計算的方法。 應對這個問題的唯一措施是向JavaScript傳遞一個回調函數,這樣當結果計算出來之后,回調函數被調用,傳遞結果到Java的方法作為參數。詳見這里
在iOS中,UIWebView能執行腳本串。與Android不同的是,IOS中可以計算全 局變量和函數調用(用stringByEvaluatingJavaScriptFromString:),,但是要作為字符串返回,因此當結果不是字符 串的時候要做一些適當的轉換。然而,回調函數卻不像Android中的那樣簡單,這是因為在UIWebView中沒有這種機制。從JavaScript中 調用Objective-C的唯一應對方案,是試圖在JavaScript中打開一個帶有定制協議的URL(例如skycallback://) ,并在Objective-C中捕捉這一事件,然后解析URL,看協議中是否含有回調協議的名稱,或者解析URL的資源路徑的字符串值,或者計算存放結果 的全局變量。詳見這里answer.
你可以看到,JavaScript和本地代碼之間的交互是十分困難的,并因平臺而已,而且當代碼量的增長,這種交互很容易導致bug,并不可避免地變得難以維護。因此,這個選項被拋棄掉。
選項5 - JS引擎中的Javascript
讓JavaScript運行在一個獨立的JavaScript引擎中也可以工作。
和Web視圖的方式相比,Javascript直接與Js引擎交互更為直接。但不幸的是,純凈的JS引擎缺少網絡功能。JS中處理Http請求的 XMLHttpRequest對象無效,原因是它是web瀏覽器的一部分而并非嚴格的JavaScript規范。因此,通過代理特定平臺(膠水)代碼的網 絡功能,一個與眾不同的架構應運而生。雖然這使得有些事情變得錯綜復雜,但是我們特別感興趣的是可以開發跨平臺的JavaScript庫。下面是它的工作 原理:
在 iOS 中,JavaScriptCore 引擎通過極佳的 JavaScriptCore 框架被使用。這個框架在 iOS 7 中被引入,并且它在幾秒鐘之內就可以很容易的可以集成到應用中,就如你處理任何 Cocoa Touch framework 一樣。它的 API 非常簡單,所需的綁定代碼也很簡潔。
在 Android 中,事情還是有些復雜,因為沒有 JavaScript 引擎,所以我們必須手工嵌入一個。兩個 JavaScript 引擎都可以被嵌入成功,Rhino 和 V8。Rhino 用 Java 編寫,所以它很容易嵌入,并且它僅僅增加了 2.6MB 的應用程序大小。它由 Mozilla 基金會開發,但它的開發現在有一段時間不活躍了。 V8 嵌入難度要大些,它用 C++ 編寫。因此,必須使用 Android NDK 和 JNI 來供 Java 與其交互,又增加了一個轉換層(Java<->C++<->JavaScript,而非 Java<->JavaScript)。此外,應用程序大小增加了 7.1MB,這對于一些應用程序并不是可以忽略的。不管怎樣,它的開發非常活躍。
跨平臺庫以Http請求處理為存根由JavaScript開發。這個存根一開始工作為一個占位符,而在庫載入到JavaScript引擎中后會被重新寫入。它被一個調用實現這個請求的本地方法(特定平臺)的方法替換。
“JavaScript引擎中的JavaScript"解決方案的全部說明將出現在這個系列文章的第三部分,這些文章將在接下來的幾周內發布。
結論
雖然研究了很多工具和技術,但是其中只有兩個可以工作。在一方面,C + +的解決方案是一種廣泛使用的,可靠的,靈活的解決方案,但在Java中手動垃圾收集(提出了一種解決方法的)的一個顯著的缺點。另一方 面,JavaScript的解決方案更容易實現,但在復雜的體系結構缺少功能,并且是依賴于并非積極開發中的Rhino,或在V8這對應用程序大小有顯著 影響。如果您使用這些方法中的一種,請對這些缺點統籌考慮,謹慎行事。
一些項目看上去很有前途,將來值得重復查看:
-
Corona
-
MoSync
-
J2ObjC
-
Appcelerator Hyperloop
-
Nashorn (Oracle用Java重寫的Javascript引擎)