“一次编写,到处运行” 是为阐明 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引擎)