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