NDK Android* 应用移植方法

概述

本指南用于帮助开发人员将现有的基于 ARM* 的 NDK 应用移植到 x86。如果您已经拥有一个正常运行的应用,需要知道如何能够快速让 x86 设备在 Android* Market 中找到您的应用,本文将可以为您提供一些入门信息。同时本指南还提供了一些技巧和指南,以帮助您解决在移植过程中可能会遇到的编译器问题。

内容

  1. 概述
  2. NDK 概述
  3. 移植概述
  4. 移植技巧:从 ARM* 到 x86
  5. 结论

 

NDK 概述

原生开发套件 NDK 是一款强大的工具,将原生 x86 代码的强大功能和 Android* 应用的图形界面结合在一起。通过使用该工具,开发人员将能够提升某些应用的性能优势,但同时开发人员也需要十分谨慎,因为在某些情况并不能实现预期的效果.

NDK 致力于支持开发人员完成以下工作:

  • 编译原生 C/C++ 库(由包装程序 Java* 代码调用),以备在 Android* 应用包中使用
  • 重新编译 ARM* 原生库,供在 x86 平台(英特尔® 凌动™ 微架构)上使用,并在必要时进行移植

对于以上提到的第二点,部分情况下可能只需要简单地修改 Build Flag 并重新编译即可,但有时却并不这么简单。 例如,如果原生库涉及到 C 代码内的内嵌汇编,那么代码将无法通过简单的汇编来实现在两种不同的架构自如运行的目标,此时将需要进行重新编写(更多信息请参见比较 ARM* 的 NEON* 与英特尔的 SSE 的部分)。

JNI 的性能影响和开销

Java* 原生接口(JNI)将 Android* Java* 代码与由 NDK 预编译的原生代码结合在一起。如欲了解有关该接口的更多信息,请访问: http://java.sun.com/docs/books/jni/

以上链接对 JNI 规范进行了广泛、深入的剖析。 若只是希望了解其概况,Wiki 页面即可满足您的要求(存在疑问时,请随时参考规范以检查其正确性)。Wiki 页面的网址如下: http://en.wikipedia.org/wiki/Java_Native_Interface

JNI 的开销十分庞大,因此在理想情况下,开发人员应在应用中尽可能减少对 JNI 的调用。具体而言,在 Android* 应用中使用原生代码并不一定能提升性能。通常而言,当原生代码涉及到由 CPU 进行的运算时(如大量使用 SSE 指令),将可以实现一定的性能提升。但在另外一些情况下,如现有的应用仅用于为用户提供复杂的 Web 界面,此时通过 JNI 使用原生代码可能会降低性能。在何时该用和不该用 NDK 上,不存在成文的规则,以上几点只是提供了一些需要注意的通用准则和事项。

获取 NDK

开发人员可通过以下网址获取 NDK 的最新版本: http://developer.android.com/sdk/ndk/index.html。 在最新版本 NDK r6b 中,NDK 可用于构建基于 ARM* 和基于 x86(英特尔® 凌动™ 微架构)的原生库。 这为开发人员在一个应用包内进行原生代码移植提供了方便。

NDK 内部组件简介 Makefiles

开发人员将需要为项目创建一个 Android.mk 文件和一个 Application.mk 文件(可选)。其中,Application.mk 文件用于描述您的应用需要哪些原生模块。Android.mk 文件用于控制如何和从哪里构建一个模块(静态/共享库)。以下是一个简单 Android.mk 文件的片段:


图 1:简单 Android.mk 文件的内容

 

构建系统时将前置一个库,并同时生成一个名称为 libtest.so 的库。按预期,开发者将在 LOCAL_SRC_FILES 中为项目源文件命名。 LOCAL_LDLIBS 和 LOCAL_CFLAGS 分别用于指定 Linking Flag(链接标记)和Compilation Flag(编译标记)。

命令行构建

以下命令行提供了一个有关如何指定构建指向 x86 架构的示例:ndk-build APP_ABI=x86

调用原生库可采用以下两种方法: System.loadLibrary("relative_path_and_name") and System.load("full_path_to_lib_file")。 前者更常用,后者更稳定。 使用前者时,Android.mk 指定的库名称中的“lib”部分可丢弃。 调用示例如下:


图 2:调用原生代码示例

 

此外,对于原生代码,开发人员需要确保原生代码的输入方法具有正确的 JNIEXPORT 方法签名,而不是典型的 C/C++ 标头。前面提及的 JNI 链接包含有更多相关信息。

处理原生库文件

开发人员可通过两种方式加载原生库:1)在 Android* apk 包中提供该库并在运行时对其进行引用;2)在 Android* 文件系统上提供通往该库的绝对路径。采用以上方式中的哪一种取决于开发人员的偏好。但无论采用哪种方式,均应进行相应的正确处理。

运行时验证

通过使用 adb logcat 命令,开发人员可确保在运行时成功加载目标原生库。以下提供了一个描述原生库已加载的系统日志的示例。注意需提供通往原生库文件的完整路径。


图 2:调用原生代码示例

总结

以上各部分提供了有关如何使用 NDK 的入门知识。如欲了解更加复杂的细节,请阅读 NDK 应用包中包含的相关文档。 这些文档提供有出色的教程和针对各种应用的源代码示例。

移植概述

对于大多数应用而言,将现有的 NDK 应用移植到 x86 非常简单。除非原生代码使用 ARM* 特有的特性,否则移植应用只需进行重新编译、重新打包和重新发布操作即可完成。

以下内容向您介绍了将 NDK 应用移植到 x86 涉及到的步骤。

  1. 获取最新的 NDK 工具。x86 支持最先在 android-ndk-r6 中提供,但当时仍存在一些问题,之后谷歌很快进行了修复。确保您已经从 Android* NDK 网站 下载和安装了最新的(写入时,最新的 NDK 为 android-ndk-r6b)NDK。

     

  2. 如果您已有一个 Application.mk 文件,可编辑 APP_ABI 行加入 x86。示例:

    APP_ABI := armeabi armeabi-v7a x86

    如果您没有 Application.mk 文件,可将 x86 添加到命令行构件中。以下为构建一个 NDK 示例应用时的命令行和输出内容。
    $ ndk-build APP_ABI="armeabi armeabi-v7a x86"

    Install : test-libstl => libs/armeabi/test-libstl
    Install : test-libstl => libs/armeabi-v7a/test-libstl
    Install : test-libstl => libs/x86/test-libstl
  3. 在前一步中,我们可以发现包含每种架构的二进制代码的文件夹在 Libs 目录下创建。下一步,我们将重新打包 APK 以包含新库。由于 Libs 目录位于根项目文件夹之下,用于创建 APK 的构建工具已经知晓该文件夹中的二进制代码。利用 Eclipse,只需简单地重新构建项目 APK 即可包含新的 x86 二进制代码。使用命令行进行构建的操作与此相同。以下列出了重新构建示例演示 hello-jni 时的示例输出内容:

    $ android.bat update project --path C:/Tools/android-ndk-r6b/samples/hello-jni
    Updated local.properties
    Added file C:\Tools\android-ndk-r6b\samples\hello-jni\build.xml
    Added file C:\Tools\android-ndk-r6b\samples\hello-jni\proguard.cfg
    $ ant -f hello-jni/build.xml debug
    Buildfile: C:\Tools\android-ndk-r6b\samples\hello-jni\build.xml

    debug:
    [echo] Running zip align on final apk...
    [echo] Debug Package: android-ndk-r6b\samples\hello-jni\bin\HelloJni-debug.apk
    BUILD SUCCESSFUL
  4. 下一步为在英特尔架构设备或 x86 模拟器上进行运行和测试。验证所有二进制代码均已正确打包的最后一步为使用 zip 存档工具打开 APK 并确保其中包含二进制代码。以下是存在 x86 二进制代码时 APK 结构外观的截屏。

移植技巧:从 ARM* 到 x86

将应用移植到 x86 应当非常简单,尽管很多人可能都有这样的想法,但在实际的代码中,仍需要谨慎注意并解决英特尔® 凌动™ 和 ARM* 架构之间的差异。以下主题介绍了您在移植过程中可能遇到的问题以及如何予以解决。

工具链兼容性

您的构建环境有可能直接使用了工具链,而不是 Android* 构建脚本。在 ARM* 中,所用的路径如下:

android-ndk\toolchains\arm-linux-androideabi-4.4.3
对于 x86,使用路径:
android-ndk\toolchains\x86-4.4.3
有关详细信息,请参阅位于 android-ndk/docs/STANDALONE-TOOLCHAIN.html 的 NDK 文档。

内存对齐影响:比较 ARM* 与英特尔® 凌动™

在 ARM* 和英特尔® 凌动™ 微架构之间移植 C/C++ 代码时可能出现内存对齐不匹配的情况。以下文章针对这一点提供了一个典型的示例: https://software.intel.com/zh-cn/blogs/2011/08/18/understanding-x86-vs-arm-memory-alignment-on-android。主要的一点是:开发者应当在设计代码时,在必要的地方考虑对数据进行明确的强制对齐。否则,开发人员将无法保证数据在不同的平台能够得到正确的处理。

浮点预算:比较 ARM 与英特尔® 凌动™

目前,在构建 NDK 库时,可以使用三种支持的应用二进制接口(ABI):

  1. ‘armeabi’ – 默认选项,将创建以基于 ARM* v5TE 的设备为目标的库。具有这种目标的浮点运算使用软件浮点运算。使用此 ABI 创建的二进制代码将可以在所有 ARM* 设备上运行。
  2. ‘armeabi-v7a’ – 创建支持基于 ARM* v7 的设备的库,并将使用硬件 FPU 指令。
  3. ‘x86’ – 生成的二进制代码可支持包含基于硬件的浮点运算的 IA-32 指令集。

所有这些 ABI 选项均支持浮点运算。除非使用的是特定于 ARM* 的汇编指令,否则在将代码移植到 x86 时不会发生问题。其优势在于,如果碰巧您的应用仅针对“armeabi”进行编译,而现在需要支持 x86,则您在进行大多数浮点运算时均能感觉到性能提升。

将 ARM* NEON* 指令移植至英特尔® 凌动™ 的英特尔® SSE

尽管这篇短小的文章不可能包罗万象,但是以下提供的信息将能够让您大致了解在英特尔架构和 ARM* 中,SIMD 扩展的实施有何不同。借助此简介,开发者还将获得一些工具,以便于开始进行一些简单的编码练习。

什么是 NEON?

NEON* 是一种 ARM* 技术,主要用于多媒体(智能手机和高清电视等)应用。ARM* 表示其基于 128 位 SIMD 引擎的技术 – ARM* Cortex*(一种串行扩展)—可提供比 ARM* v5 架构至少高 3 倍的性能,以及比 ARM* v6 至少高 2 倍的性能。如欲了解有关此技术的详细信息,以深入了解 NEON 及其它性能考虑,请访问以下网址: http://www.arm.com/products/processors/technologies/neon.php

此处的关键理念为,各寄存器被“堆积”成一个矢量,其中每一个寄存器均为一个元素,并与其它元素的数据类型相匹配。在此基础之上,运算在管道内执行,因而这一方法被称作 Packed SIMD。

SSE:英特尔推出的类似工具

SSE 指面向英特尔架构(IA)的SIMD 流指令扩展。目前,英特尔® 凌动™ 最高支持 SSSE3(补充 SIMD 流指令扩展 3)。 凌动™ 暂不支持 SSE4.x。后者也是一个 128 位引擎,用于打包浮点数据。这一执行模式开始于 MMX 技术。SSx 是较新的技术,取代了 MMX。如欲了解详细信息,请参阅英特尔《IA-32 和 IA-64 软件开发人员手册》中的“第一卷: 基础架构”部分。网址为: http://www.intel.com/content/www/cn/zh/processors/architectures-software-developer-manuals.html。目前,SSE 概述部分在 5.5 节。它提供 SSE、SSE2、SSE3 和 SSSE3 的操作码。注意,数据运算通常会涉及到处理基于精度的打包浮点数值;并且需要在 XMM 寄存器之间,或在这些寄存器与内存之间批量传输数据。XMM 寄存器主要用于取代 MMX 寄存器。

NEON 与 SSE 在汇编层面的比较

在推荐使用前述《英特尔架构软件开发人员手册》来了解所有单个 SSE(x) 助记符的同时,我们也鼓励开发人员通过以下链接了解各种 SSE 汇编级指令,网址为: http://neilkemp.us/src/sse_tutorial/sse_tutorial.html

在该链接中,您可通过“目录”部分直接跳到代码示例或首先详细了解某些背景信息。同样,以下直接来自 ARM* 的手册提供了一些信息和 NEON* 汇编小片段:/sites/default/files/m/b/4/c/DHT0002A_introducing_neon.pdf。请参阅 ARM* 文档中的第 1.4 节。

以下是在一般层面上比较 NEON 和 SSE 汇编代码时的几个要点(注意:随着技术的发展,信息随时会过时;根据具体的 SIMD 技术和当下的应用编码问题,可能还存在其它差异):

字节存储次序。英特尔仅支持低位优先汇编,而 ARM* 则同时支持高位或低位优先顺序(ARM* 支持两种顺序)。在提供的代码示例中,同英特尔一样,ARM* 代码采用的也是低位优先顺序。注意尽管如此,在 ARM 中可能会存在一些编译器影响*。例如,使用 GCC* 为 ARM* 进行编译时具有 -mlittle-endian 和 -mbig-endian 标记。有关详细信息请访问:http://gcc.gnu.org/onlinedocs/gcc/ARM-Options.html

Granularity. 在引用简单汇编代码示例的情况中(请再次注意这并不包含开发人员可能发现的 NEON 和 SSE 之间的所有差异),将 SSE 的 ADDPS 指令与 NEON 的 VADD.ix(即:x = 8 或 16)进行比较。 请注意,后者在作为引用助记符的一部分而待处理的数据上有一定粒度降低。

NEON 与 SSE 在C/C++ 层面的比较

在将 C/C++ 代码 NEON 代码移植到 SSE 时,可能会出现很多 API 问题。此处请注意本文的一个假设,即这里未使用内嵌汇编,而使用的是真正的 C/C++ 代码。

对于更高层级的编程,NEON 与 SSE 之间的差异涉及到大尺寸数据(128 位)的处理。本文针对这种移植练习提供了一个简短示例: http://stackoverflow.com/questions/7203231/neon-vs-intel-sse-equivalence-of-certain-operations

结论

我们希望这一指南能够为您提供一些实用信息,帮助您成功将基于 NDK 的应用移植到 x86。移植到 x86 后,您的应用将可以供一种全新类型的 Android* 设备进行下载、购买和使用。如果您在移植过程中遇到问题,请随时在本文中发表评论。我们将非常乐意回答您的问题,为您提供帮助。

相关文章与资源:

声明

* 其他的名称和品牌可能是其他所有者的资产

英特尔公司 © 2011 年版权所有。 所有权保留。

英特尔、Atom 和凌动是英特尔在美国和/或其他国家的商标。

本文件中包含关于英特尔产品的信息。 本文件不构成对任何知识产权的授权,包括明示的、暗示的,也无论是基于禁止反言的原则或其他。 英特尔不承担任何其他责任。英特尔在此作出免责声明:本文件不构成英特尔关于其产品的使用和/或销售的任何明示或暗示的保证,包括不就其产品的(i)对某一特定用途的适用性、(ii)适销性以及(iii)对任何专利、版权或其他知识产权的侵害的承担任何责任或作出任何担保。

除非经过英特尔的书面同意认可,英特尔的产品无意被设计用于或被用于以下应用:即在这样的应用中可因英特尔产品的故障而导致人身伤亡。

英特尔有权随时更改产品的规格和描述,恕不另行通知。设计者不应信赖任何英特产品所不具有的特性,设计者亦不应信赖任何标有“保留权利”或“未定义”说明或特性描述。对此,英特尔保留将来对其进行定义的权利,同时,英特尔不应为因其日后更改该等说明或特性描述而产生的冲突和不相容承担任何责任。此处提供的信息可随时更改,恕不另行通知。请勿根据本文件提供的信息完成一项产品设计。

本文件所描述的产品可能包含使其与宣称的规格不符的设计缺陷或失误。这些缺陷或失误已收录于勘误表中,可索取获得。

在发出订单之前,请联系当地的英特尔营业部或分销商以获取最新的产品规格。

如欲获得本文涉及的带编号文档的副本或其他英特尔文献,可致电 1-800-548-4725,或访问: http://www.intel.com/design/literature.htm

Optimization Notice in English

Para obter informações mais completas sobre otimizações do compilador, consulte nosso aviso de otimização.