64 位 Android* 和 Android 运行时






4.78/5 (5投票s)
64 位 Android* 和 Android 运行时
英特尔®开发人员专区提供用于跨平台应用开发的工具和操作指南、平台和技术信息、代码示例以及同行专业知识,帮助开发人员创新和成功。加入我们的Android、物联网、英特尔®实感™技术和Windows社区,下载工具、获取开发套件、与志同道合的开发人员分享想法,并参加编程马拉松、竞赛、路演和本地活动。
引言
移动市场的新热点是Android 64位系统。2013年9月,苹果发布了搭载64位A7处理器的iPhone* 5。移动技术竞赛由此开始。
事实证明,基于Android的GNU/Linux*内核早已支持带64位寄存器的处理器。Ubuntu是“GNU/Linux”,而Android是“Dalvik/Linux”。Dalvik是Google的Android操作系统中的进程虚拟机 (VM),专门执行为Android编写的应用程序。这使得Dalvik成为Android软件堆栈不可或缺的一部分,该堆栈通常用于移动设备,如手机和平板电脑,以及最近的智能电视和可穿戴设备。然而,所有使用NDK的开发人员都必须在最新架构下重新构建其程序,此过程的难易程度取决于Google将提供的工具。此外,Google应提供向后兼容性,即NDK 32位应用程序应能在Android 64位中运行。
首批用于移动设备的英特尔64位处理器于2013年第3季度问世,是用于移动和桌面设备的新型强大多核片上系统 (SoC)。这个新的SoC家族包括用于平板电脑和二合一设备的英特尔®凌动TM处理器,以及用于二合一设备、笔记本电脑、台式PC和一体机PC的英特尔®赛扬®处理器和英特尔®奔腾®处理器。
2014年10月,Google发布了适用于开发人员的64位Android L预览模拟器镜像。这使他们能够在操作系统发布之前测试其程序并在必要时重写代码。在Google+博客中,开发人员指出,完全使用Java*创建的程序不需要移植。他们在支持64位架构的L版本模拟器中“原样”运行它们。那些使用其他语言(特别是C和C++)的开发人员将不得不执行一些步骤以针对新的Android NDK进行构建。市面上有几款较旧的带64位处理器的Android设备。然而,制造商可能不得不相当快地更新它们;否则,用户将面临软件应用匮乏的问题。
Android 64位L模拟器
2014年6月,Google宣布Android将在即将发布的版本中支持64位。这对于希望其设备和应用程序发挥最大性能的用户来说是个好消息。Google在此次更新中强调的优势包括更多寄存器、增加的可寻址内存空间和新的指令集。
Android模拟器支持移动设备上可能找到的许多硬件功能,包括
- 一个ARM* v5 CPU和相应的内存管理单元 (MMU)
- 一个16位LCD显示器
- 一个或多个键盘(一个基于QWERTY的键盘和相关的方向键/电话按钮)
- 一个具有输入和输出功能的声卡芯片
- 闪存分区(通过开发机器上的磁盘镜像文件模拟)
- 一个GSM调制解调器,包括模拟SIM卡
- 一个摄像头,使用连接到开发计算机的网络摄像头。
- 传感器,如加速度计,使用来自USB连接的Android设备的数据
这是构建我们最喜欢的设备和应用程序的巨大进步。不幸的是,我们必须等到Android L发布才能享受这些新的性能提升。Android L发布几周后,原生开发工具包 (NDK) 的修订版10应会发布,支持将能够运行新版本Android的三种64位架构:arm64-v8a、x86_64和mips64。如果您使用Java构建了一个应用程序,您的代码将在新的x86 64位架构上自动获得更好的性能。Google已将NDK更新至修订版10b,并添加了一个模拟器镜像,开发人员可以使用它来准备其应用程序,使其在搭载英特尔64位芯片的设备上运行。
请记住,NDK仅适用于原生应用程序,而不适用于使用常规Android SDK通过Java构建的应用程序。如果您一直期待在64位上运行您的应用程序,或者您需要更新到最新版本的NDK,请访问开发人员门户开始下载。
使用x86_64 Android NDK进行开发
原生开发工具包 (NDK) 是一个工具集,允许您使用C和C++等原生代码语言实现应用程序的某些部分。对于某些类型的应用程序,这可能很有用,因为您可以重用用这些语言编写的现有代码库,但大多数应用程序不需要Android NDK。您需要权衡使用NDK的优点和缺点。值得注意的是,在Android上使用原生代码通常不会带来明显的性能提升,但它总是会增加您的应用程序复杂性。您只应在对您的应用程序至关重要时才使用NDK,而不是仅仅因为您更喜欢用C/C++编程。
您可以从以下网址下载最新版本的Android NDK:https://developer.android.com.cn/tools/sdk/ndk/index.html
本节我将介绍如何使用Android NDK编译一个示例应用程序。
我们将使用位于Android NDK示例目录中的示例应用程序 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位系统上占用的内存是32位系统的三分之一。一个缺点是性能损失。尽管64位程序速度更快,但从内存中提取大量数据可能会抵消所有优势,甚至降低性能。在内存和微处理器(缓存)之间传输数据并不是很便宜。
减少内存消耗的一种方法是优化数据结构。另一种方法是使用节省内存的数据类型。例如,如果我们需要存储大量的整数,并且我们知道它们的值永远不会超过UINT_MAX,我们可以使用“unsigned”类型而不是“size_t”类型,如下一节所述。
在地址算术中使用内存大小类型
在地址算术中使用 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中所示的一小段汇编代码。
表1 - 比较使用 unsigned 和 size_t 类型的64位汇编代码片段
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)。
表2 - FindMinPath32和FindMinPath64函数的执行时间
模式和函数 | 函数执行时间 | |
---|---|---|
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) 应用程序由Google开发,旨在取代Dalvik。这个运行时提供了许多新功能,可以提高Android平台和应用程序的性能和流畅度。ART在Android 4.4 KitKat中引入;在Android 5.0中,它将完全取代Dalvik。与Dalvik不同,ART使用即时 (JIT) 编译器(在运行时),这意味着ART在应用程序安装期间编译应用程序。因此,程序执行速度更快,从而延长了电池寿命。
为了向后兼容,ART 使用与 Dalvik 相同的字节码。
除了潜在的速度提升之外,使用ART还可以带来第二个重要的好处。由于ART直接运行应用程序机器代码(本地执行),因此它不会像Dalvik上的即时代码编译那样对CPU造成太大负担。更少的CPU使用量会导致更少的电池消耗,这对于便携式设备来说是一个很大的优势。
那么为什么ART没有早点实现呢?让我们看看预先 (AOT) 编译的缺点。首先,生成的机器代码比现有字节码需要更多的空间。其次,代码在安装时预编译,因此安装过程需要更长的时间。最后,它在执行时也对应着更大的内存占用。这意味着可以同时运行的应用程序更少。当第一批Android设备上市时,内存和存储容量明显更小,成为性能瓶颈。这就是为什么当时首选JIT方法的原因。如今,内存便宜得多,因此即使在低端设备上也更充足,所以ART是向前迈出的合乎逻辑的一步。
也许最重要的改进是,ART现在在用户设备上安装时将您的应用程序编译为原生机器代码。这被称为预先编译,您可以期望看到巨大的性能提升,因为编译器是针对特定架构(如ARM、x86或MIPS)设置的。这消除了每次运行应用程序时进行即时编译的需要。因此,安装应用程序需要更多时间,但启动时会更快,因为在Dalvik VM上运行时执行的许多任务(如类和方法验证)已经完成。
接着,ART团队致力于优化垃圾收集器(GC)。在Dalvik中,每次GC暂停两次,总计约10毫秒,而ART中只会暂停一次,通常在2毫秒以内。他们还并行化了GC运行的一部分,并优化了收集策略以感知设备状态。例如,只有在手机锁定时且对用户交互的响应不重要时,才会运行完整的GC。这对于对掉帧敏感的应用程序来说是一个巨大的改进。此外,ART的未来版本将包含一个紧凑型收集器,它将把分配的内存块移动到连续的块中,以减少碎片并减少因分配大内存区域而杀死旧应用程序的需要。
最后,ART 使用了一个全新的内存分配器,名为 Rosalloc(槽分配器运行)。大多数现代系统使用基于 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需要您知道在哪里找到设备上的切换选项。Google已将其隐藏在“设置”下。幸运的是,有一种方法可以在基于Android 4.4 KitKat的设备上启用ART运行时。
免责声明:在尝试此操作之前,您应该备份您的数据。如果您的设备变砖(无论您如何尝试都无法开机),英特尔概不负责。风险自负!
- 需要Root权限
- 如果已安装WSM Tools,请勿尝试,因为它们不支持ART。
要启用ART,请仔细遵循以下步骤
- 确保您的设备已root。
- 从Play商店安装“ES文件浏览器”。
- 打开ES文件浏览器,点击左上角的菜单图标并选择工具。在工具中,启用“Root Explorer”选项,并在提示时授予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。
结论
Google已经为即将发布的Android L发布了64位模拟器镜像,但仅适用于Intel x86芯片。新的模拟器将允许开发人员为即将到来的Android L操作系统及其新的64位架构构建或优化旧应用程序。转向64位增加了可寻址内存空间,并允许开发人员使用更多寄存器和新的指令集,但64位应用程序不一定更快。
Java应用程序会自动获得64位的好处,因为它们的字节码将由新的64位ART虚拟机解释。这也意味着纯Java应用程序无需更改。基于Android NDK构建的应用程序需要进行一些优化以包含x86_64构建目标。英特尔提供了有关如何将针对ARM的代码移植到x86/x64的建议。使用新的模拟器,开发人员只能为基于英特尔®凌动™处理器的芯片创建应用程序。
英特尔一直为Android开发人员提供工具和良好的系统支持,特别是其英特尔®硬件加速执行管理器 (Intel® HAXM) 和一系列英特尔凌动OS镜像。许多Android程序员定期在模拟的英特尔架构上进行测试,尽管他们的大部分部署都是针对ARM设备。除了新的模拟器,HAXM加速器还进行了64位升级,这应该会使HAXM更具吸引力。引用英特尔的话:
“这一承诺不仅体现在业界首个用于英特尔架构的64位模拟器镜像,以及Android L开发者预览SDK中提供的64位英特尔HAXM,还体现在过去十年中许多其他创新,例如今年早些时候推出的首个适用于Android KitKat的64位内核,64位Android原生开发套件 (NDK) 以及其他64位进步。”
是否可能英特尔架构的改变会成为从32位移动到64位移动的一部分?
Android SDK包括一个运行在您计算机上的虚拟移动设备模拟器。该模拟器允许您无需使用物理设备即可原型化、开发和测试Android应用程序。Android模拟器模仿了典型移动设备的所有硬件和软件功能,除了它无法进行实际通话。它提供了各种导航和控制键,您可以使用鼠标或键盘“按下”这些键,为您的应用程序生成事件。它还提供了一个屏幕,其中显示您的应用程序以及任何其他活动的Android应用程序。
为了让您更轻松地建模和测试您的应用程序,模拟器利用Android虚拟设备 (AVD) 配置。AVD允许您定义模拟手机的某些硬件方面,并允许您创建许多配置来测试许多Android平台和硬件排列。一旦您的应用程序在模拟器上运行,它就可以使用Android平台的服务来调用其他应用程序、访问网络、播放音频和视频、存储和检索数据、通知用户以及渲染图形转换和主题。