64 位 Android 和 Android 运行时
64 位 Android 和 Android 运行时
英特尔® 开发者专区为跨平台应用开发提供工具和操作指南信息、平台与技术信息、代码示例以及同行专业知识,帮助开发者进行创新并取得成功。加入我们的社区,面向Android、物联网、英特尔® 实感™ 技术和Windows下载工具、访问开发工具包、与志同道合的开发者分享想法,并参与黑客马拉松、竞赛、路演和本地活动。
引言
移动市场的新热点是关于 Android 64 位系统。2013 年 9 月,苹果发布了搭载 64 位 A7 处理器的 iPhone* 5。移动技术竞赛由此拉开序幕。
事实证明,基于 Android 的内核 GNU/Linux* 早已支持具有 64 位寄存器的处理器。Ubuntu 是“GNU/Linux”,而 Android 是“Dalvik/Linux”。Dalvik 是谷歌安卓操作系统中的进程虚拟机 (VM),专门执行为 Android 编写的应用程序。这使得 Dalvik 成为 Android 软件栈不可或缺的一部分,通常用于手机和平板电脑等移动设备,以及最近的智能电视和可穿戴设备等。然而,所有使用 NDK 的开发者都必须在最新的架构下重新构建他们的程序,而这个过程的难易程度取决于谷歌将提供的工具。此外,谷歌应该提供向后兼容性,即 NDK 32 位应用程序应该能在 64 位 Android 上运行。
首批用于移动设备的英特尔 64 位处理器于 2013 年第三季度问世,是面向移动和桌面设备的全新强大片上多核系统 (SoC)。这个新的 SoC 系列包括用于平板电脑和二合一设备的英特尔® 凌动TM 处理器、英特尔® 赛扬® 处理器,以及用于二合一设备、笔记本电脑、台式电脑和一体机的英特尔® 奔腾® 处理器。
2014 年 10 月,谷歌为开发者发布了 64 位 Android L 的预览版模拟器镜像。这使得他们可以在操作系统发布前测试他们的程序并根据需要重写代码。在一个 Google+ 博客中,开发者表示,完全用 Java* 创建的程序不需要移植。他们在支持 64 位架构的 L 版本模拟器中“按原样”运行了这些程序。而那些使用其他语言,特别是 C 和 C++ 的开发者,则需要执行一些步骤来针对新的 Android NDK 进行构建。市场上已经有一些搭载 64 位处理器的旧版 Android 设备。然而,制造商可能需要相当快地更新它们;否则,用户将面临软件应用匮乏的问题。
Android 64 位 L 模拟器
2014 年 6 月,谷歌宣布 Android 将在即将到来的 L 版本中支持 64 位。对于那些希望从设备和应用中获得最高性能的人来说,这是个好消息。谷歌在此次更新中强调的优点包括更大的寄存器数量、增加的可寻址内存空间以及新的指令集。
Android 模拟器支持移动设备上可能具备的许多硬件功能,包括
- 一个 ARM* v5 CPU 及相应的内存管理单元 (MMU)
- 一个 16 位 LCD 显示屏
- 一个或多个键盘(一个基于 Qwerty 的键盘及相关的 Dpad/电话按钮)
- 一个具有输入和输出功能的声卡芯片
- 闪存分区(通过开发机器上的磁盘镜像文件进行模拟)
- 一个 GSM 调制解调器,包括一个模拟的 SIM 卡
- 一个摄像头,使用连接到您开发计算机的网络摄像头。
- 传感器,如加速计,使用来自 USB 连接的 Android 设备的数据
这是构建我们喜爱的设备和应用的一大步。不幸的是,我们必须等到 Android L 发布后才能享受到这些新的性能提升。Android L 发布几周后,原生开发工具包 (NDK) 的第 10 版应该会发布,支持能够运行新版 Android 的三种 64 位架构:arm64-v8a、x86_64 和 mips64。如果您使用 Java 构建了应用,您的代码将在新的 x86 64 位架构上自动获得更好的性能。谷歌已将 NDK 更新到 10b 版本,并增加了一个模拟器镜像,开发者可以用它来准备他们的应用,以便在采用英特尔 64 位芯片的设备上运行。
请记住,NDK 仅适用于原生应用,而不适用于在常规 Android SDK 上用 Java 构建的应用。如果您一直期待让您的应用在 64 位上运行,或者您需要更新到最新版本的 NDK,请访问开发者门户开始下载。
使用 x86_64 Android NDK 进行开发
原生开发工具包 (NDK) 是一个工具集,允许您使用 C 和 C++ 等原生代码语言实现应用程序的部分功能。对于某些类型的应用,这可能很有帮助,因为您可以重用以这些语言编写的现有代码库,但大多数应用并不需要 Android NDK。您需要在其优点和缺点之间进行权衡。值得注意的是,在 Android 上使用原生代码通常不会带来明显的性能提升,但它总是会增加您的应用复杂性。您只应在对您的应用至关重要时才使用 NDK,而不是因为您仅仅偏爱用 C/C++ 编程。
您可以从以下地址下载最新版本的 Android NDK:https://developer.android.com.cn/tools/sdk/ndk/index.html
在本节中,我将回顾如何使用 Android NDK 编译一个示例应用程序。
我们将使用位于 Android NDK samples 目录下的示例应用程序 san-angeles。
$ANDROID_NDK/samples/san-angeles
原生代码位于 jni/
目录中。
$ANDROID_NDK/samples/san-angeles/jni
原生代码是为指定的 CPU 架构编译的。Android 应用程序可以在一个 apk 文件中包含用于多种架构的库。
要设置目标架构,您需要在 jni/
目录内创建 Application.mk
文件。以下这行将为所有支持的架构编译原生库。
APP_ABI := all
有时,指定一个目标架构列表会更好。这行代码为 x86 和 ARM 架构编译库。
APP_ABI := x86 armeabi armeabi-v7a
因为我们正在构建一个 64 位应用,所以需要为 x86_64 架构编译库。
APP_ABI := x86_64
在示例目录中运行以下命令来构建库。
cd $ANDROID_NDK/samples/san-angeles
成功构建后,在 Eclipse* 中以 Android 应用程序的形式打开该示例,然后点击“运行”。选择您希望运行该应用程序的模拟器或已连接的 Android 设备。
为了支持所有可用的设备,您需要为所有架构编译应用程序。如果包含所有架构库的 apk 文件过大,请考虑遵循 Google Play 多 APK 支持 中的说明,为每个平台准备一个单独的 apk 文件。
检查支持的架构
您可以使用此命令来检查 apk 文件中包含了哪些架构。
aapt dump badging file.apk
以下这行代码列出了所有架构。
native-code: 'armeabi', 'armeabi-v7a', 'x86', 'x86_64'
另一种方法是将 apk 文件作为 zip 文件打开,并查看 lib/
目录中的子目录。
64 位程序的优化
减少应用消耗的内存量
当一个程序以 64 位模式编译时,它会比其 32 位版本消耗更多的内存。这种增加通常不易被察觉,但有时内存消耗可能会比 32 位应用高出两倍。内存消耗量由以下因素决定:
- 一些对象,如指针,需要更多的内存
- 数据对齐和数据结构填充
- 增加的栈内存消耗
64 位系统为用户应用程序提供的可用内存量比 32 位系统多。因此,如果一个程序在拥有 2GB 内存的 32 位系统上占用 300MB,但在拥有 8GB 内存的 64 位系统上需要 400MB,那么以相对单位计算,该程序在 64 位系统上占用的内存要少三倍。一个缺点是性能损失。尽管 64 位程序更快,但从内存中提取更大量的数据可能会抵消所有优势,甚至降低性能。在内存和微处理器(缓存)之间传输数据并不是很廉价。
减少内存消耗的一种方法是优化数据结构。另一种方法是使用节省内存的数据类型。例如,如果我们需要存储大量的整数,并且我们知道它们的值永远不会超过 UINT_MAX,我们可以使用“unsigned”类型而不是“size_t”,如下一节所述。
在地址算术中使用 memsize 类型
在地址算术中使用 ptrdiff_t
和 size_t
类型,除了使代码更安全外,还可能带来额外的性能提升。例如,使用大小与指针容量不同的 int
类型作为索引,会导致二进制代码中出现额外的数据转换命令。我们可能有 64 位代码,指针的大小是 64 位,而 int
类型的大小保持不变——32 位。
要给出一个简短的例子来证明 size_t
比 unsigned
更好并不容易。为了保持公正,我们必须使用编译器的优化能力。但两种优化代码的变体通常差异太大,难以简单地展示它们的区别。经过六次尝试,我们设法创建了一个类似简单的例子。但这个例子远非理想,因为它没有展示上面讨论的不必要的数据类型转换代码,而是表明编译器在使用 size_t
时可以构建更高效的代码。考虑这段将数组项按相反顺序排列的代码:
<code1.txt>
unsigned arraySize;
...
for (unsigned i = 0; i < arraySize / 2; i++)
{
float value = array[i];
array[i] = array[arraySize - i - 1];
array[arraySize - i - 1] = value;
}
示例中的变量“arraySize
”和“i
”的类型是 unsigned
。您可以轻松地将其替换为 size_t
,并比较表 1 中显示的一小段汇编代码。
array [arraySize - I - 1] = value; |
|
---|---|
arraySize, i : unsigned |
arraySize, i : size_t |
mov eax, DWORD PTR arraySize$[rsp] sub eax, r11d sub r11d, 1 add eax, -1 movss DWORD PTR [rbp + rax*4], xmm0 … |
mov rax, QWORD PTR arraySize$[rsp] sub rax, r11 add r11, 1
movss DWORD PTR [rdi + rax*4 - 4], xmm0 … |
当使用 64 位寄存器时,编译器能够构建出更简洁的代码。我们并不是说使用 unsigned
类型创建的代码(第 1 列)会比使用 size_t
类型的代码(第 2 列)慢。在当代处理器上比较代码执行速度是很困难的。但您可以在这个例子中看到,当使用 64 位类型时,编译器构建了更简洁、更快的代码。
现在让我们看一个例子,展示 ptrdiff_t
和 size_t
类型在性能方面的优势。为了演示,我们将采用一个计算最小路径长度的简单算法。
函数 FindMinPath32
是用经典的 32 位风格编写的,使用了 unsigned
类型。函数 FindMinPath64
与它的唯一区别在于,其中所有的 unsigned
类型都被替换为了 size_t
类型。没有其他区别!现在让我们比较这两个函数的执行速度(表 2)。
模式和函数 | 函数执行时间 | |
---|---|---|
1 | 32 位编译模式。函数 FindMinPath32 | 1 |
2 | 32 位编译模式。函数 FindMinPath64 | 1.002 |
3 | 64 位编译模式。函数 FindMinPath32 | 0.93 |
4 | 64 位编译模式。函数 FindMinPath64 | 0.85 |
表 2 显示了相对于 32 位系统上函数 FindMinPath32
执行速度的缩减时间。此表是为了清晰起见而制作的。第一行中
FindMinPath32
函数在 32 位系统上的运行时间为 1。这代表了我们作为测量单位的基准。
在第二行中,我们看到 FindMinPath64
函数在 32 位系统上的运行时间也是 1。这并不奇怪,因为在 32 位系统上,unsigned 类型与 size_t 类型是相同的,FindMinPath32
和 FindMinPath64
函数之间没有区别。一个微小的偏差 (1.002) 仅表示测量中的一个小误差。
在第三行中,我们看到了 7% 的性能提升。为 64 位系统重新编译代码后,我们完全可以预料到这个结果。
第四行是我们最感兴趣的。性能提升了 15%。仅仅通过使用 size_t 类型代替 unsigned
,编译器就构建了一个更有效的代码,其运行速度甚至快了 8%!
这个简单明了的例子展示了不等于机器字长的数据如何减慢算法性能。仅仅将 int
和 unsigned
类型替换为 ptrdiff_t
和 size_t
就可能带来显著的性能提升。这个结果首先适用于那些在索引数组、地址算术和安排循环中使用这些数据类型的情况。
内联函数
内联函数是特殊的系统相关函数,它们执行一些在 C/C++ 代码级别无法执行的操作,或者以更有效的方式执行这些函数。实际上,它们让您摆脱了内联汇编代码,因为使用内联汇编通常是不可取或不可能的。
程序可以使用内联函数来创建更快的代码,因为没有调用普通函数的开销。当然,代码大小会稍微大一些。MSDN 提供了一个可以被其内联版本替换的函数列表。这些例子包括 memcpy
、strcmp
等。
除了将普通函数自动替换为其内联版本外,您还可以在代码中显式使用内联函数。这可能很有帮助,原因如下:
- Visual C++ 编译器在 64 位模式下不支持内联汇编,但支持内联代码。
- 内联函数使用起来更简单,因为它们不需要了解寄存器和其他类似的底层结构。
- 内联函数在编译器中会更新,而汇编代码必须手动更新。
- 内置的优化器不处理汇编代码。
- 内联代码比汇编代码更容易移植。
在自动模式下(借助编译器开关)使用内联函数会让您获得一定百分比的性能提升,而使用“手动”开关则帮助更大。这就是为什么使用内联函数是一个好方法。
对齐
数据结构对齐是数据在计算机内存中排列和访问的方式。它包括两个独立但相关的问题:数据对齐和数据结构填充。当现代计算机从内存地址读取或写入时,它会以字大小的块(例如,在32 位系统上为 4 字节块)或更大的块进行操作。数据对齐意味着将数据放置在等于字大小的某个倍数的内存偏移处,这由于CPU处理内存的方式而提高了系统性能。为了对齐数据,可能需要在上一个数据结构的末尾和下一个数据结构的开头之间插入一些无意义的字节,这就是数据结构填充。
例如,当计算机的字长为 4 字节(在大多数机器上是 8 位,但在某些系统上可能不同)时,要读取的数据应该位于 4 的某个倍数的内存偏移处。如果不是这种情况,例如,数据从第 14 字节开始而不是第 16 字节,那么计算机必须读取两个 4 字节的块并进行一些计算,然后才能读取请求的数据,或者它可能会生成一个对齐错误。即使前一个数据结构在第 13 字节结束,下一个数据结构也应该从第 16 字节开始。在两个数据结构之间插入两个填充字节,以将下一个数据结构对齐到第 16 字节。
虽然数据结构对齐是所有现代计算机的一个基本问题,但许多计算机语言和计算机语言实现会自动处理数据对齐。
在某些情况下,通过手动定义对齐来帮助编译器以提高性能是很好的做法。例如,流式 SIMD 扩展 (SSE) 数据必须在 16 字节边界上对齐。您可以通过以下方式实现:
// 16-byte aligned data
__declspec(align(16)) double init_val[2] = {3.14, 3.14};
// SSE2 movapd instruction
_m128d vector_var = __mm_load_pd(init_val);
Android运行时
Android 运行时 (ART) 应用程序由谷歌开发,作为 Dalvik 的替代品。此运行时提供了一系列新功能,可提高 Android 平台和应用的性能和流畅度。ART 在 Android 4.4 KitKat 中引入;在 Android 5.0 中,它将完全取代 Dalvik。与 Dalvik 不同,ART 使用即时 (JIT) 编译器(在运行时),这意味着 ART 在应用程序安装期间对其进行编译。因此,程序执行速度更快,从而延长了电池寿命。
为了向后兼容,ART 使用与 Dalvik 相同的字节码。
除了潜在的速度提升,使用 ART 还可以提供第二个重要好处。由于 ART 直接运行应用的机器码(原生执行),它对 CPU 的冲击不像 Dalvik 上的即时代码编译那么大。CPU 使用率降低意味着电池消耗减少,这对于便携式设备来说是一个很大的优势。
那么为什么 ART 没有更早实现呢?让我们看看预先 (AOT) 编译的缺点。首先,生成的机器码比现有的字节码需要更多空间。其次,代码在安装时进行预编译,因此安装过程需要更长的时间。最后,它在执行时也对应着更大的内存占用。这意味着可以同时运行的应用更少。当第一批 Android 设备上市时,内存和存储容量要小得多,成为性能的瓶颈。这就是为什么当时 JIT 方法是首选。如今,内存便宜得多,因此也更充裕,即使在低端设备上也是如此,所以 ART 是一个合乎逻辑的进步。
也许最重要的一项改进是,ART 现在会在您的应用程序安装到用户设备上时将其编译为原生机器码。这被称为预先编译 (ahead-of-time compilation),您可以期待看到巨大的性能提升,因为编译器是为特定架构(如 ARM、x86 或 MIPS)设置的。这消除了每次运行应用程序时进行即时编译的需要。因此,安装您的应用程序需要更多时间,但启动时会更快,因为许多在 Dalvik VM 上运行时执行的任务,如类和方法验证,已经完成了。
接下来,ART 团队致力于优化垃圾回收器 (GC)。在 Dalvik 中,每次 GC 都会有两次总计约 10 毫秒的暂停,而您在 ART 中只会看到一次,通常在 2 毫秒以下。他们还并行化了部分 GC 运行,并优化了回收策略以感知设备状态。例如,只有在手机锁定时,对用户交互的响应不再重要时,才会运行完整的 GC。对于对掉帧敏感的应用程序来说,这是一个巨大的改进。此外,未来版本的 ART 将包含一个紧凑型回收器,它会将已分配的内存块移动到连续的块中,以减少碎片化和为分配大内存区域而终止旧应用程序的需要。
最后,ART 使用了一种全新的内存分配器,名为 Rosalloc(runs of slots allocator)。大多数现代系统使用的分配器基于 Doug Lea 的设计,该设计具有单个全局内存锁。在多线程、面向对象的环境中,这会干扰垃圾回收器和其他内存操作。在 Rosalloc 中,Java 中常见的小对象在线程局部区域分配,无需锁定,而大对象则有自己的锁。因此,当您的应用程序尝试为新对象分配内存时,它不必等待垃圾回收器释放不相关的内存区域。
目前,Dalvik 是 Android 设备的默认运行时,而 ART 在一些 Android 4.4 设备上作为可选功能提供,例如 Nexus 手机、Google Play 版设备、运行原生 Android 的摩托罗拉手机以及许多其他智能手机。ART 目前正在开发中,并寻求开发者和用户的反馈。一旦 ART 变得完全稳定,它最终将取代 Dalvik 运行时。在此之前,拥有兼容设备的用户如果对尝试这一新功能并体验其性能感兴趣,可以从 Dalvik 切换到 ART。
要切换或启用 ART,您的设备必须运行 Android 4.4 KitKat 并且与 ART 兼容。您可以轻松地从“设置”->“开发者选项”->“运行时选项”中开启 ART 运行时。(提示:如果您在“设置”中看不到“开发者选项”,请转到“关于手机”,向下滚动并点击“版本号”7 次以启用开发者选项。)手机将重新启动并开始为 ART 优化应用,这可能需要大约 15-20 分钟,具体取决于您手机上安装的应用数量。启用 ART 运行时后,您还会注意到已安装应用的大小有所增加。
注意:切换到 ART 后,当您第一次重启设备时,它会再次优化所有应用;这有点烦人。
由于 Dalvik 是 Android 设备上的默认运行时,某些应用可能无法在 ART 上运行,不过,大多数现有应用都与 ART 兼容并且应该可以正常工作。但如果您在使用 ART 时遇到任何错误或应用崩溃,那么明智的做法是切换回去,继续使用 Dalvik。
在设备上切换到 ART 需要您知道在哪里找到切换选项。谷歌已将其隐藏在“设置”下。幸运的是,有一个技巧可以在基于 Android 4.4 KitKat 的设备上启用 ART 运行时。
免责声明:在尝试此操作之前,您应该备份您的数据。如果您的设备变砖(无论您尝试什么都无法开机),英特尔概不负责。请自行承担风险!
- 需要 Root
- 如果您安装了 WSM 工具,请不要尝试,因为它们不支持 ART。
要启用 ART,请仔细遵循以下步骤:
- 确保您的设备已获得 root 权限。
- 从 Play 商店安装“ES 文件浏览器”。
- 打开 ES 文件浏览器,点击左上角的菜单图标并选择“工具”。在工具中,启用“Root 工具箱”选项,并在提示时授予 ES 浏览器完全的 root 访问权限。
- 在 ES 浏览器中,从菜单 -> 本地 -> 设备打开设备 (/) 目录。进入 /data/property 文件夹。以文本方式打开 persist.sys.dalvik.vm.lib 文件,然后选择 ES 文本编辑器。
- 通过选择右上角的编辑选项来编辑文件。将该行从 libdvm.so 重命名为 libart.so
- 返回到 persist.sys.dalvik.vm.lib 文件,并选择“是”以保存文件。然后重启手机。
- 手机现在将重新启动并开始为 ART 优化应用程序。重新启动可能需要一些时间,具体取决于您设备上安装的应用程序数量。
如果您想恢复到 Dalvik 运行时,只需按照上述步骤,并将 persist.sys.dalvik.vm.lib 文件中的文本重命名为 libdvm.so 即可。
结论
谷歌已经为即将到来的 Android L 发布了一个 64 位模拟器镜像——但仅限于英特尔 x86 芯片。新的模拟器将允许开发者为即将到来的 Android L 操作系统及其新的 64 位架构构建或优化旧应用。转向 64 位增加了可寻址内存空间,并为开发者提供了更多的寄存器和新的指令集,但 64 位应用不一定更快。
Java 应用自动获得 64 位的好处,因为它们的字节码将由新的 64 位 ART 虚拟机解释。这也意味着纯 Java 应用无需任何更改。而基于 Android NDK 构建的应用则需要进行一些优化,以包含 x86_64 构建目标。英特尔有关于如何将针对 ARM 的代码移植到 x86/x64 的建议。使用新的模拟器,开发者将只能为基于英特尔® 凌动™ 处理器的芯片创建应用。
英特尔一直在为开发者提供工具和良好的 Android 系统支持,特别是其英特尔® 硬件加速执行管理器 (Intel® HAXM) 和一系列英特尔凌动操作系统镜像。许多 Android 程序员经常在模拟的英特尔架构上进行测试,尽管他们的大部分部署是针对 ARM 设备。除了新的模拟器,HAXM 加速器还有一个 64 位的升级,这应该会使使用 HAXM 更具吸引力。引用英特尔的话:
“这一承诺不仅体现在为 Android L 开发者预览版 SDK 提供业界首个用于英特尔架构的 64 位模拟器镜像和 64 位英特尔 HAXM,还体现在一路走来的许多其他创新中,例如今年早些时候为 Android KitKat 提供的首个 64 位内核、64 位 Android 原生开发工具包 (NDK),以及过去十年中的其他 64 位进步。”
会不会随着从 32 位移动设备向 64 位移动设备的转变,英特尔架构也会发生变化?
Android SDK 包括一个在您计算机上运行的虚拟移动设备模拟器。该模拟器让您无需使用物理设备即可对 Android 应用程序进行原型设计、开发和测试。Android 模拟器模仿了典型移动设备的所有硬件和软件功能,只是它不能拨打实际电话。它提供了各种导航和控制键,您可以使用鼠标或键盘“按下”这些键,为您的应用程序生成事件。它还提供了一个屏幕,您的应用程序以及任何其他活动的 Android 应用程序都在其中显示。
为了让您更轻松地建模和测试您的应用程序,模拟器使用了 Android 虚拟设备 (AVD) 配置。AVD 允许您定义模拟手机的某些硬件方面,并允许您创建多种配置来测试多种 Android 平台和硬件排列。一旦您的应用程序在模拟器上运行,它就可以使用 Android 平台的服务来调用其他应用程序、访问网络、播放音频和视频、存储和检索数据、通知用户以及渲染图形过渡和主题。
相关文章与资源
- 在此处获取有关 Android NDK 修订版 10d 的信息并下载。
- 在此处了解更多关于 Android 5.0 Lollipop 的信息。
- 在此处阅读关于使用 x86 Android* 4.4 (KitKat) 模拟器开发应用的文章。
关于作者
Egor Filimonov 在英特尔公司的软件与服务事业部工作。他是俄罗斯下诺夫哥罗德的罗巴切夫斯基州立大学的一名学生。他的专业是力学和数学。他的专长是应用数学和信息学。他的主要兴趣是 HPC(高性能计算)和移动技术。