英特尔平台上的安卓应用开发教程(四)

原文:Android Application Development for the Intel Platform

协议:CC BY-NC-SA 4.0

十二、NDK 和 C/C++ 优化

Keywords Java Code Android Application Compiler Optimization Head File Executable File

上一章介绍了性能优化的基本原理、优化方法以及 Android 应用开发的相关工具。因为 Java 是 Android 开发者推荐的应用开发语言,所以第十一章中介绍的优化工具主要是针对 Java 的。然而,C/C++ 开发不应该被排除在 Android 应用开发之外。本章介绍了用于 C/C++ 应用开发的 Android NDK 以及相关的优化方法和优化工具。

JNI 简介

Java 应用不直接在硬件上运行,而是在虚拟机上运行。应用的源代码不是为了获得硬件指令而编译的,而是为了允许虚拟机解释和执行代码而编译的。比如 Android 应用运行在 Dalvik 虚拟机(DVM)中;它的编译代码是 DVM 的可执行代码,采用.dex格式。这个特性意味着 Java 运行在虚拟机上,并保证了它的跨平台能力:即它的“编译一次,随处运行”特性。Dalvik 有一个实时(JIT)编译器,并被优化为具有较低的内存需求。

凡事有利有弊。Java 的跨平台能力导致它与本地机器的内部组件的连接较少,并限制了它与本地机器的内部组件的交互,这使得很难使用本地机器指令来利用机器的性能潜力。很难使用基于本地的指令来运行巨大的现有软件库,这限制了它的功能和性能。从 Android 4.4 (KitKat)开始,Google 推出了 Android Runtime (ART),这是一个取代 Dalvik 的应用运行时环境。ART 将应用的字节码转换成本机指令,稍后由设备的运行时环境执行。ART 通过在安装应用时执行提前(AOT)编译来引入它。

有没有办法让 Java 代码和原生代码软件协同工作,共享资源?答案是肯定的,使用 Java 本地接口(JNI),这是一个 Java 本地操作的实现方法。JNI 是由 Java 标准定义的 Java 平台,用于与本地平台上的代码进行交互,通常称为。但这一章讲的是移动平台;为了区别于移动交叉开发主机,我们称之为。Java 代码和本地应用之间的交互包括两个方向:Java 代码调用本地函数(方法),以及本地应用调用 Java 代码。相对来说,前一种方法在 Android 应用开发中使用的更多。所以本章的重点是 Java 代码调用本地函数的方法。

Java 通过 JNI 调用本地函数,将本地方法以库文件的形式存储起来。例如,在 Windows 平台上,文件是.dll文件格式,而在 Unix/Linux 机器上,文件是.so文件格式。调用本地库文件的内部方法使 Java 能够与本地机器建立密切联系:这被称为各种接口的系统级方法。

JNI 通常有两种使用场景:一是能够使用遗留代码(例如,之前使用 C/C++、Delphi 等开发工具);第二,为了更好、更直接地与硬件交互以获得更好的性能。

JNI 的一般工作流程如下:Java 发起调用,让本地函数的侧代码(比如用 C/C++ 写的函数)运行。这一次,对象从 Java 端传递过来并运行一个本地函数,然后结果值被返回给 Java 代码。这里 JNI 是一个适配器,完成 Java 语言和本地编译语言(如 C/C++)之间变量和函数(Java 方法)的映射。Java 和 C/C++ 在函数原型定义和变量类型上有很大的不同。为了使两者匹配,JNI 提供了一个jni.h文件来完成它们之间的映射。该过程如图 12-1 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 12-1。

JNI general workflow

通过 JNI 和 Java 程序(特别是 Android 应用)的 C/C++ 函数调用的一般框架如下:

  1. 一种编译 Java 类中声明的 native 的方法(C/C++ 函数)。
  2. 包含本地方法的.java源代码文件被编译。
  3. javah命令生成一个.h文件,包括基于.class文件实现本地方法的函数原型。
  4. C/C++ 用于实现本地方法。
  5. 这一步推荐的方法是先复制.h文件中的函数原型,然后修改函数原型,添加函数体。在此过程中,应注意以下几点:
    • JNI 函数调用必须使用 C 函数。如果是 C++ 函数,别忘了加上extern C 关键字。
    • 方法名应该使用下面的模板:Java_package_class_method,或者Java_ package name _ class name _ function method name
  6. 将 C/C++ 文件编译成动态库(Windows 下,一个.dll文件;在 Unix/Linux 下,一个.so文件)。

使用 Java 中的System.loadLibrary()System.load()方法加载生成的动态库。这两个功能略有不同:

  • System.loadLibrary()加载本地链接库下的默认目录。
  • System.load()需要一个绝对路径,根据本地目录添加一个交叉链接库。

第一步,Java 调用原生 C/C++ 函数;C 和 C++ 的格式不一样。例如,对于 Java 方法,如不传递参数和返回一个String类,函数的 C 和 C++ 代码在以下方面有所不同:

  • c 代码:

Call function : (*env) -> <jni function> (env, <parameters>)

Return jstring : return (*env)->NewStringUTF(env, "XXX");

  • C++ 代码:

Call function : env -> <jni function> (<parameters>)

Return jstring : return env->NewStringUTF("XXX");

NewStringUTF是用 C/C++ 生成的 Java String对象的函数,由 JNI 提供。

方法和 C 函数原型

前面您已经看到,在 Java 程序调用 C/C++ 函数的代码框架中,您可以使用javah命令,该命令基于.class文件为本地方法生成相应的.h文件。.h文件是按照一定的规则生成的,让正确的 Java 代码找到相应的 C 函数来执行。另一个好的解决方法是使用env - > RegisterNatives函数手动进行映射,避免使用javah

例如,假设您有以下用于 Android 的 Java 代码:

public class HelloJni extends Activity

1.  {

2.      public void onCreate(Bundle savedInstanceState)

3.      {

4.          TextView tv.setText(stringFromJNI() );  // Use C function Code

5.      }

6.      public native String  stringFromJNI();

7.  }

对于第 4 行使用的 C 函数stringFromJNI(),由javah生成的.h文件中的函数原型是

1.  JNIEXPORT jstring JNICALL Java_com_example_hellojni_HelloJni_stringFromJNI

2.    (JNIEnv *, jobject);

定义函数代码的 C 源代码文件大致如下:

1.  /*

2.  ......

3.  Signature: ()Ljava/lang/String;

4.  */

5.  jstring Java_com_example_hellojni_HelloJni_stringFromJNI (JNIEnv* env,  jobject thiz )

6.    {

7.      ......

8.      return (*env)->NewStringUTF(env, "......");

9.  }

从这段代码中,你可以看到函数名相当长但仍然很规则,完全符合命名约定java_package_class_method。即Hello.java中的stringFromJNI()方法对应 C/C++ 中的Java_com_example_hellojni_HelloJni_stringFromJNI()方法。

注意Signature : ()Ljava/lang/String;的注释。这里()Ljava/lang/String;中的()表示功能参数为空,这意味着除了两个参数JNIEnv *jobject之外,没有其他参数。JNIEnv *jobject分别是 JNI 环境和相应 Java 类(或对象)的所有 JNI 函数必须具有的两个参数。Ljava/lang/String;表示函数的返回值是一个 Java String对象。

Java 和 C 数据类型映射

如前所述,Java 和 C/C++ 有非常不同的变量类型。为了使这两者相匹配,JNI 提供了一种机制来完成 Java 和 C/C++ 之间的映射。主要类型的关系如表 12-1 所示。

表 12-1。

The Correspondence between Java Types and Local (C/C++) Types

| Java 类型 | 原生类型 | 描述 | | --- | --- | --- | | `boolean` | `jboolean` | C/C++ 8 位整数 | | `byte` | `jbyte` | C/C++ 无符号 8 位整数 | | `char` | `jchar` | C/C++ 无符号 16 位整数 | | `short` | `jshort` | C/C++ 有符号 16 位整数 | | `int` | `jint` | C/C++ 有符号 32 位整数 | | `long` | `jlong` | C/C++ 无符号 64 位整数 | | `float` | `jfloat` | C/C++ 32 位浮点 | | `double` | `jdouble` | C/C++ 64 位浮点 | | `void` | `void` | 不适用的 | | `Object` | `jobject` | 任何 Java 对象,或者不对应于 Java 类型的对象 | | `Class` | `jclass` | 类对象 | | `String` | `jstring` | 字符串对象 | | `Object[]` | `jobjectArray` | 任何对象的数组 | | `boolean[]` | `jbooleanArray` | 布尔数组 | | `byte[]` | `jbyteArray` | 比特阵列 | | `char[]` | `jcharArray` | 字符数组 | | `short[]` | `jshortArray` | 短整数数组 | | `int[]` | `jintArray` | 整数数组 | | `long[]` | `jlongArray` | 长整数数组 | | `float[]` | `jfloatArray` | 浮点数组 | | `double[]` | `jdoubleArray` | 双浮点数组 |

当传递 Java 参数时,您可以使用 C 代码,如下所示:

  • 基本类型可以直接使用:比如doublejdouble是可以互换的。基本类型为表 12-1 中booleanvoid的类型。在这种类型中,如果用户将一个boolean参数传递给方法,那么就会有一个对应于boolean类型的本地方法jboolean。类似地,如果本地方法返回一个jint,那么 Java 会返回一个int
  • Java 对象用法:一个Object对象有String对象和一个通用对象。这两个对象的处理方式略有不同:
    • String对象:Java 程序传递的String对象是本地方法中对应的jstring类型。C 中的jstring类型和char *不同。所以如果你只是把它当成一个char *,就会出现错误。因此,jstring在使用前必须在 C/C++ 中转换成char *。这里使用JNIEnv方法进行转换。
    • Object object:使用以下代码获取该类的对象处理程序:

jclass objectClass = (env)->FindClass("com/ostrichmyself/jni/Structure");

jfieldID str = (env)->GetFieldID(objectClass,"nameString","Ljava/lang/String;");

jfieldID ival = (env)->GetFieldID(objectClass,"number","I");

(env)->SetObjectField(theObjet,str,(env)->NewStringUTF("my name is D:"));

(env)->SetShortField(theObjet,ival,10);

jobject myNewObjet = env->AllocObject(objectClass);

Note

如果您希望调用对象构造函数,则需要调用。

Java 数组处理

对于数组类型,JNI 提供了一些可操作的函数。例如,GetObjectArrayElement可以接受传入的数组,并使用NewObjectArray创建一个数组结构。

资源释放

资源释放的原理如下:

  • C/C++ new 的对象或 malloc 的对象需要使用 C/C++ 来释放。
  • 如果JNIEnv方法的新对象没有被 Java 使用,就必须释放它。
  • 使用GetStringUTFChars将一个 string 对象从 Java 转换成 UTF,需要打开内存,使用完char *后必须使用ReleaseStringUTFChars方法释放内存。

以上是 Java 与 C/C++ 交换数据时类型映射基本思想的简要描述。有关 Java 和 C/C++ 数据类型的更多信息,请参考相关的 Java 和 JNI 书籍、文档和示例。

NDK 简介

您现在知道 Java 代码可以使用 JNI 访问本地函数(比如 C/C++)。要实现这一点,你需要开发工具。如前所述,基于核心 Android SDK 的一整套开发工具是可用的,您可以使用这些工具将 Java 应用交叉编译为可以在目标 Android 设备上运行的应用。同样,您需要交叉开发工具来将 C/C++ 代码编译成可以在 Android 设备上运行的应用。这个工具就是 Android 原生开发包(NDK),可以从 http://developer.android.com 下载。

在 NDK 之前,Android 平台上的第三方应用是在一个特殊的基于 Java 的 DVM 上开发的。原生 SDK 的发布允许开发者直接访问 Android 系统资源,并使用 C 和 C++ 等原生代码语言实现部分应用。应用包文件(.apk)可以直接嵌入到本地库中。简而言之,有了 NDK,原本在 DVM 上运行的 Android 应用可以使用 C/C++ 等本地语言来执行程序。这带来了以下好处:

  • 通过使用本机代码来开发需要高性能的程序部分,以及通过直接访问 CPU 和硬件来提高性能
  • 重用现有本机代码的能力

当然,与 DVM 相比,使用原生 SDK 编程也有一些缺点,例如增加了程序的复杂性,难以保证兼容性,无法访问框架 API,调试更加困难,灵活性下降等等。此外,访问 JNI 需要额外的性能开销。

简而言之,NDK 应用开发有利有弊。你需要根据自己的判断使用 NDK。最佳策略是使用 NDK 来开发应用中本机代码可以提高性能的部分。

NDK 包括以下主要部分:

  • 从 C/C++ 源代码生成本机代码库所需的工具和构建文件。其中包括一系列 NDK 命令,包括javah(使用.class文件生成相应的.h文件)和gcc(稍后描述)
  • 嵌入在应用包(.apk文件)中的一致的本地库,可以部署在 Android 设备中
  • 支持所有未来 Android 平台的一些原生系统头文件和库
  • 文档、示例和教程

NDK 应用开发的流程框架如图 12-2 所示。Android 应用由三部分组成:Android 应用文件、Java 本地库文件和动态库。这三个部分通过各自的生成路径从不同的源生成。对于一个普通的 Android 应用,Android SDK 生成 Android 应用文件和 Java 原生库文件。Android NDK 使用本地代码(通常是 C 源代码文件)生成动态库文件(扩展名为.so的文件)。最后在目标机上安装 Android 应用文件、Java 原生库文件、动态库,完成协同应用运行。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 12-2。

Flowchart of Android NDK application development

与 NDK 合作开发的应用项目(简称 NDK 应用项目)的组成如图 12-3 所示。与使用 Android SDK 开发的典型应用不同,除了 Dalvik 类代码、清单文件和资源,NDK 应用项目还包括 JNI 和 NDK 生成的共享库。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 12-3。

Application components for an Android NDK application

Android 在其关键 API 版本中增加了 NDK 支持。每个版本都包括一些新的 NDK 特性、简单的 C/C++、兼容的标准模板库(STL)、硬件扩展等等。这些特性使得 Android 更加开放,更加强大。Android API 的映射及其与 NDK 的对应关系如表 12-2 所示。

表 12-2。

Relationship between the Main Android API and NDK Versions

| API 版本 | 支持的 NDK 版本 | | --- | --- | | API 级 | Android 1.5 NDK 1 | | API 级 | Android 1.6 NDK 2 | | API 级 | Android 2.1 NDK 3 | | API 级 | Android 2.2 NDK 4 | | API 级 | Android 2.3 NDK 5 | | API 级 | Android 3.1 NDK 6 | | API 级 | Android 4.0.1 NDK 7 | | API 级 | Android 4.0.3 NDK 8 | | API 级 | Android 4.1 NDK 8b | | API 级 | Android 4.2 NDK 8d | | API 级 | Android 4.2 NDK 9 | | API 级 | Android 4.3 NDK 9d | | API 等级 19 | Android 4.4 NDK 10 |

TIP: THE MEANING OF APPLICATION BINARY INTERFACE (ABI)

使用 Android NDK 生成的每段本机代码都有一个匹配的应用二进制接口(ABI)。ABI 精确地定义了应用及其代码在运行时如何与系统交互。ABI 大致类似于计算机体系结构中的指令集体系结构(ISA)。

典型的 ABI 通常包含以下信息:

  • CPU 指令集应该使用的机器代码
  • 运行时内存访问排名
  • 可执行二进制文件的格式(动态库、程序等)以及允许和支持的内容类型
  • 在应用代码和系统之间传递数据时使用的不同约定(例如,函数调用何时注册和/或如何使用堆栈、对齐限制等)
  • 枚举类型、结构字段和数组的对齐和大小限制
  • 独特的名字;运行时应用机器码的可用函数符号列表通常来自一组非常特定的库

Android 目前支持以下 ABI 类型:

  • ARM eabi:ARM CPU 的 abi 名称,它至少支持 ARMv5TE 指令集。
  • armeabi-v7a:基于 ARM 的 CPU 的另一个 abi 名字;它扩展了 armeabi CPU 指令集扩展,如 Thumb-2 指令集扩展和用于向量浮点硬件的浮点处理单元指令。
  • x86 : ABI 名称,通常用于支持 CPU 的 x86 或 IA-32 指令集。更具体地说,它的目标通常在下面的章节中称为 i686 或奔腾 Pro 指令集。英特尔凌动处理器属于这种 ABI 类型。
  • MIPS:支持 MIPS32r1 指令集的基于 MIPS 的 CPU 的 ABI。ABI 包括以下特性:MIPS32 修订版 1 ISA、little-endian、O32、硬浮点和无 DSP 应用。这些类型具有不同的兼容性。x86 与 armeabi 和 armeabi-v7a 不兼容。armeabi-v7a 机器与 armeabi 兼容,这意味着 armeabi 框架指令集可以在 armeabi-v7a 机器上运行,但不一定相反,因为一些 ARMv5 和 ARMv6 机器不支持 armeabi-v7a 代码。因此,在构建应用时,应该根据用户对应的 ABI 机器类型仔细选择用户。

安装 NDK 和设置环境

NDK 包含在面向 Linux 的英特尔 Beacon Mountain、面向 OS X 的英特尔 Beacon Mountain 和面向 Windows 主机系统的英特尔集成本地开发人员体验(INDE)中,并在您安装这些英特尔工具时安装。安装详见第三章。英特尔 INDE 中还包含一个环境设置程序;您可以下载它并自动运行安装程序。

安装 CDT

CDT 是一个 Eclipse 插件,它将 C 代码编译成.so共享库。安装完 Cygwin 和 NDK 模块后,你已经可以在命令行将 C 代码编译成.so共享库,这意味着 Windows NDK 的核心组件已经安装好了。如果您喜欢使用 Eclipse IDE 而不是命令行编译器来编译本地库,那么您需要安装 CDT 模块。

如果您需要安装它,请遵循以下步骤。

  1. 查看“许可”对话框,然后单击“我接受许可协议的条款”继续。
  2. 安装过程开始。完成后,重启 Eclipse 以完成安装。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 12-4。

Detailed information for the CDT component installation

  1. 访问 Eclipse 官方网站( www.eclipse.org/cdt/downloads.php )并下载最新的 CDT 包。
  2. 启动 Eclipse。选择帮助➤安装新软件➤开始安装 CDT。
  3. 在弹出的安装对话框中,单击添加。
  4. 在弹出的添加存储库对话框中,输入名称。
  5. 对于位置,您可以输入本地地址或互联网地址。如果使用互联网地址,Eclipse 会上网下载并安装软件包;本地地址指示 Eclipse 从本地包安装软件。在这种情况下,输入本地地址;然后在弹出的对话框中点击 Archive 按钮,输入下载的 CDT 文件的目录和文件名。如果你是从网上下载的,地址是 http://download.eclipse.org/tools/cdt/releases/galileo/
  6. 返回安装对话框后,单击选择需要安装的软件组件。在本例中,CDT 主功能是您需要选择的必需组件。显示要安装的 CDT 组件的详细信息列表,如图 12-4 所示。

NDK 的例子

本节提供一个例子来说明 JNI 和 NDK 的用法。如前所述,NDK 既可以从命令行运行,也可以在 Eclipse IDE 中运行。该示例使用两种方法生成相同的 NDK 应用。

使用命令行生成库文件

本例中的 app 名称为jnitest。这是一个演示 JNI 代码框架的简单例子。步骤如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 12-5。

Setting up the jnitest project parameters

  1. 创建一个 Android app 项目,编译代码,生成.apk包。首先在 Eclipse 中创建一个项目,并将项目命名为jnitest。选择 Build SDK 支持 x86 版本的 API,如图 12-5 所示。对于其他选项,请使用默认值。然后生成项目。

项目生成后,文件结构创建如图 12-6 所示。请注意库文件(在本例中为android.jar)所在的目录,因为后面的步骤会用到这个参数。

  1. 修改 Java 文件以使用 C 函数创建代码。在这种情况下,唯一的 Java 文件是MainActivity.java;修改其代码,如下所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 12-6。

File structure of the jnitest project

1.  package com.example.jnitest;

2.  import android.app.Activity;

3.  import android.widget.TextView;

4.  import android.os.Bundle;

5.  public class MainActivity extends Activity

6.  {

7.      @Override

8.      public void onCreate(Bundle savedInstanceState)

9.      {

10.         super.onCreate(savedInstanceState);

11.         TextView tv = new TextView(this);

12.         tv.setText(stringFromJNI() );  // stringFromJNIas a  C function

13.         setContentView(tv);

14.     }

15.     public native String stringFromJNI();

16.

17.     static {

18.         System.loadLibrary("jnitestmysharelib");

19.     }

20.  }

代码非常简单。在第 11–13 行,您使用一个TextView来显示从stringFromJNI()函数返回的字符串。但是与前面讨论的 Android 应用不同,在整个项目中,您找不到该功能的实现代码。那么函数实现发生在哪里呢?第 15 行声明该函数不是用 Java 编写的,而是由本地(本机)库编写的,这意味着该函数在 Java 之外。因为它是在本地库实现的,问题是,什么库?答案在第 17–20 行中描述。System类的静态函数LoadLibrary的参数描述了库的名称:该库是 Linux 中的一个共享库,名为libjnitestmysharelib.so。在静态区声明的应用代码将在Activity.onCreate之前执行。该库将在第一次使用时加载到内存中。

有趣的是,当loadLibrary函数加载库名时,它会自动在参数前添加前缀lib并在末尾添加后缀.so。当然,如果参数指定的库文件名称以lib开头,该函数不会添加lib前缀。

  1. 在 Eclipse 中生成项目。只构建它,不运行它。这会编译项目,但是.apk文件不会部署到目标机器上。

当这一步完成后,在项目目录bin\classes\com\example\jnitest中生成相应的.class文件。这一步必须在下一步之前完成,因为下一步需要使用合适的.class文件。

  1. 在项目根目录下创建一个jni子目录。例如,如果项目根目录是E:\temp\AndroidDev\workspace\jnitest,那么可以使用md命令创建jni子目录:

E:\temp\Android Dev\workspace\jnitest>mkdir jni

测试目录是否已经建立:

E:\temp\Android Dev\workspace\jnitest>dir

......

2013-02-01  00:45    <DIR>          jni

  1. 创建一个 C 接口文件。这是使用本地(外部)函数的 C 函数原型。特定于这种情况的是stringFromJNI函数的 C 函数原型。你用 Java 声明你需要使用外部函数的原型;但是它是 Java 格式的,所以你需要把它改成 C 格式,这意味着要建立一个 C JNI 接口文件。该步骤可通过javah命令完成:

$ javah -classpath <directory of jar and .class documents> -d <directory of .h documents>  <the package + class name of class>

命令参数如下:

  • -classpath:类路径
  • -d:生成的头文件的存放目录
  • <class name>:正在使用的原生函数的完整.class类名,由“包+类的类名”组件组成。

对于此示例,请遵循以下步骤:

  1. 使用命令行输入根目录(在本例中为E:\temp\Android Dev\workspace\jnitest)。
  2. 运行以下命令:

E:> javah -classpath "D:\Android\android-sdk\platforms\android-15\android.jar";bin/classes  com.example.jnitest.MainActivity

在这个例子中,使用的本地函数stringFromJNI的类是MainActivity;并且这个类编译后的结果文件是MainActivity.class,位于项目根目录bin\classes\com\example目录下。其类MainActivity.java的源代码文件的第一行显示了该类的包在哪里:

package com.example.jnitest;

因此,这是命令:“类名=包名。类名”(注意不要使用.class后缀)。

首先需要说明整个包的 Java 库路径(本例中,库文件是android.jar;它的位置在D:\Android\android-sdk\ platforms\android-15\android.jar。其次,它需要定义目标类(MainActivity.class)目录。在本例中,它是bin\classes\com\example\MainActivity.class下的bin\classes,两者用分号©隔开。

  1. 现在在当前目录(项目根目录)中生成了.h文件。该文件定义了 C 语言的函数接口。您可以测试输出:

E:\temp\Android Dev\workspace\jnitest>dir

......

2013-01-31  22:00         3,556 com_example_jnitest_MainActivity.h

显然已经生成了一个新的.h文件。该文件内容如下:

1.  /* DO NOT EDIT THIS FILE - it is machine generated */

2.  #include <jni.h>

3.  /* Header for class com_example_jnitest_MainActivity */

4.

5.  #ifndef _Included_com_example_jnitest_MainActivity

6.  #define _Included_com_example_jnitest_MainActivity

7.  #ifdef __cplusplus

8.  extern "C" {

9.  #endif

10\. #undef com_example_jnitest_MainActivity_MODE_PRIVATE

11\. #define com_example_jnitest_MainActivity_MODE_PRIVATE 0L

12\. #undef com_example_jnitest_MainActivity_MODE_WORLD_READABLE

13\. #define com_example_jnitest_MainActivity_MODE_WORLD_READABLE 1L

14\. #undef com_example_jnitest_MainActivity_MODE_WORLD_WRITEABLE

15\. #define com_example_jnitest_MainActivity_MODE_WORLD_WRITEABLE 2L

16\. #undef com_example_jnitest_MainActivity_MODE_APPEND

17\. #define com_example_jnitest_MainActivity_MODE_APPEND 32768L

18\. #undef com_example_jnitest_MainActivity_MODE_MULTI_PROCESS

19\. #define com_example_jnitest_MainActivity_MODE_MULTI_PROCESS 4L

20\. #undef com_example_jnitest_MainActivity_BIND_AUTO_CREATE

21\. #define com_example_jnitest_MainActivity_BIND_AUTO_CREATE 1L

22\. #undef com_example_jnitest_MainActivity_BIND_DEBUG_UNBIND

23\. #define com_example_jnitest_MainActivity_BIND_DEBUG_UNBIND 2L

24\. #undef com_example_jnitest_MainActivity_BIND_NOT_FOREGROUND

25\. #define com_example_jnitest_MainActivity_BIND_NOT_FOREGROUND 4L

26\. #undef com_example_jnitest_MainActivity_BIND_ABOVE_CLIENT

27\. #define com_example_jnitest_MainActivity_BIND_ABOVE_CLIENT 8L

28\. #undef com_example_jnitest_MainActivity_BIND_ALLOW_OOM_MANAGEMENT

29\. #define com_example_jnitest_MainActivity_BIND_ALLOW_OOM_MANAGEMENT 16L

30\. #undef com_example_jnitest_MainActivity_BIND_WAIVE_PRIORITY

31\. #define com_example_jnitest_MainActivity_BIND_WAIVE_PRIORITY 32L

32\. #undef com_example_jnitest_MainActivity_BIND_IMPORTANT

33\. #define com_example_jnitest_MainActivity_BIND_IMPORTANT 64L

34\. #undef com_example_jnitest_MainActivity_BIND_ADJUST_WITH_ACTIVITY

35\. #define com_example_jnitest_MainActivity_BIND_ADJUST_WITH_ACTIVITY 128L

36\. #undef com_example_jnitest_MainActivity_CONTEXT_INCLUDE_CODE

37\. #define com_example_jnitest_MainActivity_CONTEXT_INCLUDE_CODE 1L

38\. #undef com_example_jnitest_MainActivity_CONTEXT_IGNORE_SECURITY

39\. #define com_example_jnitest_MainActivity_CONTEXT_IGNORE_SECURITY 2L

40\. #undef com_example_jnitest_MainActivity_CONTEXT_RESTRICTED

41\. #define com_example_jnitest_MainActivity_CONTEXT_RESTRICTED 4L

42\. #undef com_example_jnitest_MainActivity_RESULT_CANCELED

43\. #define com_example_jnitest_MainActivity_RESULT_CANCELED 0L

44\. #undef com_example_jnitest_MainActivity_RESULT_OK

45\. #define com_example_jnitest_MainActivity_RESULT_OK -1L

46\. #undef com_example_jnitest_MainActivity_RESULT_FIRST_USER

47\. #define com_example_jnitest_MainActivity_RESULT_FIRST_USER 1L

48\. #undef com_example_jnitest_MainActivity_DEFAULT_KEYS_DISABLE

49\. #define com_example_jnitest_MainActivity_DEFAULT_KEYS_DISABLE 0L

50\. #undef com_example_jnitest_MainActivity_DEFAULT_KEYS_DIALER

51\. #define com_example_jnitest_MainActivity_DEFAULT_KEYS_DIALER 1L

52\. #undef com_example_jnitest_MainActivity_DEFAULT_KEYS_SHORTCUT

53\. #define com_example_jnitest_MainActivity_DEFAULT_KEYS_SHORTCUT 2L

54\. #undef com_example_jnitest_MainActivity_DEFAULT_KEYS_SEARCH_LOCAL

55\. #define com_example_jnitest_MainActivity_DEFAULT_KEYS_SEARCH_LOCAL 3L

56\. #undef com_example_jnitest_MainActivity_DEFAULT_KEYS_SEARCH_GLOBAL

57\. #define com_example_jnitest_MainActivity_DEFAULT_KEYS_SEARCH_GLOBAL 4L

58\. /*

59.  * Class:     com_example_jnitest_MainActivity

60.  * Method:    stringFromJNI

61.  * Signature: ()Ljava/lang/String;

62\. */

63\. JNIEXPORT jstring JNICALL Java_com_example_jnitest_MainActivity_stringFromJNI

64\. (JNIEnv *, jobject);

65.

66\. #ifdef __cplusplus

67\. }

68\. #endif

69\. #endif

在这段代码中,请特别注意第 63–64 行,这是一个局部函数stringFromJNI的 C 函数原型。

  1. 编译相应的 C 文件。这是一个局部函数的真正实现(stringFromJNI)。源代码文件是在前面步骤的基础上修改.h文件得到的。

在项目的jni子目录下创建一个新的.c文件。文件名可以是任何名称;在这种情况下,它就是jnitestccode.c。内容如下:

1\. #include <string.h>

2\. #include <jni.h>

3\. jstring Java_com_example_hellojni_HelloJni_stringFromJNI( JNIEnv* env,  jobject thiz )

4\. {

5.     return (*env)->NewStringUTF(env, "Hello from JNI !");  // Newly added code

6\. }

定义函数实现的代码非常简单。第 3 行是函数stringFromJNI的原型定义中使用的 Java 代码;它基本上是从com_example_jnitest_MainActivity.h的第 63–64 行获得的.h文件的相应内容的副本,为了说明这一点,稍微做了一些修改。这个函数的原型格式是固定的;JNIEnv* envjobject thiz是 JNI 的固有参数。因为stringFromJNI函数的参数为空,所以生成的 C 函数只有两个参数。第五行代码的作用是返回字符串“你好,来自 JNI!”作为返回值。

第 2 行的代码是包含 JNI 函数的头文件,任何使用 JNI 的函数都需要这个函数。因为它与字符串函数相关,所以在这种情况下,第 1 行包含相应的头文件。完成这些步骤后,.h文件就没有用了,可以删除。

  1. jni目录下创建 NDK makefile 文件。这些文件主要是Android.mkApplication.mk:需要Android.mk,但是如果使用默认的应用配置,就不需要Application.mk。具体步骤如下:
    1. 在项目的jni目录下创建一个新的Android.mk文本文件。这个文件用来告诉编译器一些要求,比如编译哪些 C 文件,编译的代码用什么文件名等等。输入以下内容:

1\. LOCAL_PATH      := $(call my-dir)

2\. include $(CLEAR_VARS)

3\. LOCAL_MODULE    := jnitestmysharelib

4\. LOCAL_SRC_FILES := jnitestccode.c

5\. include $(BUILD_SHARED_LIBRARY)

第 3 行代表生成的.so文件名(标识您的Android.mk文件中描述的每个模块)。它必须与 Java 代码中的System.loadLibrary函数的参数值一致。该名称必须是唯一的,并且不能包含任何空格。

Note

构建系统自动生成适当的前缀和后缀;换句话说,如果一个是名为jnitestmysharelib的共享库模块,那么就会生成一个libjnitestmysharelib.so文件。如果您将库命名为libhello-jni,编译器不会添加前缀lib,也会生成libhello-jni.so

第 4 行的LOCAL_SRC_FILES变量必须包含要编译并打包成模块的 C 或 C++ 源代码文件。前面的步骤创建了一个 C 文件名。

Note

您不必在这里列出头文件和包含文件,因为编译器会自动为您识别相关文件—只需列出直接传递给编译器的源代码文件。另外,C++ 源文件的默认扩展名是.cpp。只要定义了LOCAL_DEFAULT_CPP_EXTENSION变量,就可以指定不同的扩展名。不要忘记开头的句点字符(.cxx,而不是cxx)。

第 3 行和第 4 行中的代码非常重要,必须根据每个 NDK 应用的配置进行修改。其他行的内容可以从示例中复制。

  1. 在项目的jni目录下创建一个Application.mk文本文件。这个文件用来告诉编译器这个应用的具体设置。输入以下内容:

APP_ABI := x86

这个文件非常简单;应用指令生成的目标代码适用于 86 架构,因此您可以在英特尔凌动处理器上运行应用。对于APP_ABI参数,你可以使用任何你想要支持的架构(x86,armeabi,armeabi-v7a 或者 MIPS)。

  1. .c文件编译成.so共享库文件。转到项目根目录(AndroidManifest.xml所在),运行ndk-build命令:

E:\temp\Android Dev\workspace\jnitest>ndk-build

D:/Android/android-ndk-r8d/build/core/add-application.mk:128: Android NDK: WARNING: APP_PLATFORM android-14 is larger than android:minSdkVersion 8 in ./AndroidM

anifest.xml

"Compile x86  : jnitestmysharelib <= jnitestccode.c

SharedLibrary : libjnitestmysharelib.so

Install       : libjnitestmysharelib.so => libs/x86/libjnitestmysharelib.so

该命令在项目文件夹中增加两个子目录(libsobj),并在obj目录下创建一个.so文件(名为libjnitestmysharelib.so的命令执行信息提示文件)。

如果这些步骤没有在Application.mk文件中定义指定的 ABI,ndk-build命令会为 ARM 架构(armeabi)生成目标代码。如果您想要生成 x86 架构指令,您可以使用ndk-build APP_ABI = x86命令来补救这种情况。该命令生成的目标代码的架构仍然是 x86。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 12-7。

jnitest application interface

  1. 运行项目。图 12-7 显示了在目标设备上运行的应用。

在 IDE 中生成库文件

上一节描述了将 C 文件编译成可以在 Android 目标设备上运行的动态库.so文件的过程。为此,您可以在命令行中运行ndk-build命令。您也可以在 Eclipse IDE 中完成这一步。

Eclipse 支持直接的 NDK 集成。您可以将 CDT 安装到 Eclipse 中,创建一个想要添加 C/C++ 代码的 Android 项目,在项目目录中创建一个jni /目录,将 C/C++ 源文件放在同一个目录中,并将Android.mk文件放入其中——这是一个 makefile,它告诉 Android 构建系统如何构建您的文件。

如果出于某种原因,您需要手动构建代码,您可以使用以下过程在 IDE 中生成库文件。步骤 1–7 中的代码与上一节完全相同,除了在步骤 6 中,您将把.c文件编译成.so共享库文件。稍后将对此进行详细解释:

  1. 在弹出的编辑配置对话框中,对于主选项卡设置,输入以下内容:
    • 位置:通往 Cygwin 的路径bash.exe
    • 工作目录:Cygwin 的bin目录
    • 参数:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 12-8。

Entering parameters settings for the interface to compile C code in Eclipse

  1. .c文件编译成.so共享库文件。右键单击项目名称,选择构建路径➤配置构建路径,在弹出的对话框中,选择构建器分支。单击对话框中的新建按钮,然后;在提示对话框中双击程序。该过程如图 12-8 所示。

--login -c "cd '/cygdrive/E/temp/Android Dev/workspace/jnitest' && $ANDROID_NDK_ROOT/ndk-build"

  • E/temp/Android Dev/workspace/jnitest是项目的驱动器号和路径。设置如图 12-9 所示。
  1. 保存配置。它会自动编译jni目录下的 C 相关代码,并将相应的.so库文件输出到项目的libs目录下。libs目录自动创建。在控制台窗口中,您可以看到构建的输出如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 12-12。

Select source code and directories where related files are located

  1. 单击“指定资源”按钮。在编辑工作集对话框中选择jni目录,如图 12-12 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 12-11。

Edit Configuration dialog box Build Options tab settings

  1. 重新配置“构建选项”选项卡。在自动构建时选择,并指定相关资源的工作集,如图 12-11 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 12-10。

Edit Configuration dialog box Refresh tab settings

  1. 配置刷新选项卡,确保选择整个工作区和递归包含子文件夹项,如图 12-10 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 12-9。

Main tab setting in the Edit Configuration dialog box

/cygdrive/d/Android/android-ndk-r8d/build/core/add-application.mk:128: Android NDK: WARNING: APP_PLATFORM android-14 is larger than android:minSdkVersion 8 in ./AndroidManifest.xml

Cygwin        : Generating dependency file converter script

Compile x86   : jnitestmysharelib <= jnitestccode.c

SharedLibrary : libjnitestmysharelib.so

Install       : libjnitestmysharelib.so => libs/x86/libjnitestmysharelib.so

NDK 应用开发的工作流分析

如上所述生成 NDK 项目的过程自然会实现 C 库与 Java 的集成。您将.c文件编译成.so共享库文件。库的中间版本放在obj目录中,最终版本放在libs目录中。完成后,项目文件结构创建完成,如图 12-13 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 12-13。

The jnitest project structure after NDK library files are generated

当您运行项目时,共享库.so文件位于主机上的项目目录中,并打包在一个生成的.apk文件中。.apk文件本质上是一个压缩文件;可以使用 WinRAR 之类的压缩软件查看其内容。对于这个例子,你可以在项目目录的bin子目录中找到.apk文件;用 WinRAR 打开,显示文件结构。.apklib子目录的内容是项目的lib子目录内容的克隆。

.apk被部署到目标机器时,它被解包。.so文件放在/data/dat/XXX/lib目录中,其中XXX是应用包名称。对于这个例子,目录是/data/data/com.example.jnitest/lib。您可以在 Eclipse DDMS 下查看目标机器的文件结构;该示例的文件结构如图 12-14 所示。有兴趣的可以在命令行上试试,用adb shell 命令查看目标文件目录中相应的内容。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 12-14。

jnitest application deployment target file structure

此外,如果您在模拟器中运行jnitest应用(在本例中,目标机器是一个虚拟机),您可以在 Eclipse Logcat 窗口中看到以下输出:

1\. 07-10 05:43:08.579: E/Trace(6263): error opening trace file: No such file or directory (2)

2\. 07-10 05:43:08.729: D/dalvikvm(6263): Trying to load lib /data/data/com.example.jnitest/lib/libjnitestmysharelib.so 0x411e8b30

3\. 07-10 05:43:08.838: D/dalvikvm(6263): Added shared lib /data/data/com.example.jnitest/lib/libjnitestmysharelib.so 0x411e8b30

4\. 07-10 05:43:08.838: D/dalvikvm(6263): No JNI_OnLoad found in /data/data/com.example.jnitest/lib/libjnitestmysharelib.so 0x411e8b30, skipping init

5\. 07-10 05:43:11.773: I/Choreographer(6263): Skipped 143 frames!  The application may be doing too much work on its main thread.

6\. 07-10 05:43:12.097: D/gralloc_goldfish(6263): Emulator without GPU emulation detected.

第 2–3 行是应用中加载的.so共享库的提示。

NDK 编译器优化

从例子中可以看出,NDK 工具的核心作用是将源代码编译成可以在 Android 机器上运行的.so库文件。将.so库文件放入项目目录的lib子目录中,这样当您使用 Eclipse 部署应用时,您可以将库文件部署到目标设备上的适当位置,并且应用可以使用库函数。

Note

NDK 应用的本质是建立一个符合 JNI 标准的代码框架,让 Java 应用使用虚拟机范围之外的本地功能。

将源代码编译成.so库文件的关键 NDK 命令是ndk-build。这个命令实际上不是一个单独的命令,而是一个可执行的脚本。它调用 GNU 交叉开发工具中的make命令来编译一个项目;而make调用例如gcc编译器编译源代码来完成这个过程,如图 12-15 所示。当然,你也可以直接使用已经在安卓应用中的第三方开发的.so共享库,这样就不用自己写库(函数代码)了。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 12-15。

The working mechanism of NDK tools

如图 12-15 所示,GNU 编译器gcc是 NDK 中完成 C/C++ 源代码编译的核心工具。gcc是标准的 Linux 编译器,可以在本地机器上编译链接 C、C++、Object-C、FORTRAN 等源代码。gcc编译器不仅可以进行本地编译,还可以进行交叉编译。Android NDK 和其他嵌入式开发工具已经使用了这个特性。在编译器用法上,gcc交叉编译兼容原生编译;也就是说,本地编译代码的命令参数和开关本质上可以被移植,而无需修改交叉编译代码。因此,下面描述的gcc编译方法对于本地编译和交叉编译都是通用的。

第十一章提到一些优化可以由编译器自动完成,这被称为编译器优化。对于基于 Intel x86 架构处理器的系统,除了 GNU gcc编译器,Intel C/C++ 编译器也不错。相对而言,由于英特尔 C/C ++ 编译器充分利用了英特尔处理器的特性,因此代码优化结果更好。对于 Android NDK,Intel C/C++ 编译器和gcc都可以完成 C/C++ 代码编译。目前,英特尔 C/C ++ 编译器提供了适当的使用机制。普通用户需要专业许可,而gcc是开源的免费软件,更容易获得。因此,本节使用gcc作为实验工具,来解释如何为 Android 应用执行 C/C++ 模块编译器优化。

gcc优化由编译器开关中的选项控制。这些选项有些是独立于机器的,有些是与机器相关联的。本节讨论一些重要的选项。仅当与英特尔处理器相关时,才会介绍与机器相关的选项。

独立于机器的编译器开关选项

gcc编译器开关的独立于机器的选项是-Ox选项,它们对应不同的优化级别。以下是详细内容。

-O 或-O1

一级优化,默认级别,使用-O选项;编译器试图减少代码大小和执行时间。对于大型函数,需要花费更多的编译时间,使用大量的内存资源进行优化编译。

当不使用-O选项时,编译器的目标是减少编译的开销,以便可以调试结果。在这种编译模式下,语句是独立的。通过在两个语句之间插入一个断点来中断程序运行,可以重新分配变量或修改程序计数器以跳转到其他当前正在执行的语句,这样就可以精确地控制运行过程,用户可以在需要调试时得到结果。此外,如果不使用-O选项,只有声明的寄存器变量可以有寄存器分配。

如果指定了-O选项,则-fthread-jumps-fdefer-pop选项打开。在带有延迟槽的机器上,打开-fdelayed-branch选项。即使对于支持无帧指针调试的机器,-fomit-frame-pointer选项也是打开的。有些机器可能还会打开其他选项。

-氧气

这个选项可以优化更多。gcc执行几乎所有不涉及空间速度权衡的支持优化。与-O相比,这个选项增加了编译时间和生成代码的性能。

-臭氧

这个选项还可以进一步优化。它打开由-O2指定的所有优化,并打开-finline-functions-funswitch-loops-fpredictive-commoning-fgcse-after-reload-ftree-vectorize-fvect-cost-model-ftree-partial-pre-fipa-cp-clone选项。

-O0

此选项减少了编译时间,并使调试产生预期的结果。这是默认设置。

自动inline功能通常被用作功能优化措施。c99(1999 年开发的 C 语言 ISO 标准)和 C++ 都支持inline关键字。inline函数使用内联空间来换取时间。编译器不会将内联描述的函数编译成函数,而是直接扩展函数体的代码,从而消除函数调用。例如,考虑下面的函数:

inline long factorial (int i)

{

return factorial_table[i];

}

这里,factorial()调用中出现的所有代码都被替换为factorial_table []数组引用。

在优化状态下,一些编译器将该函数视为内联函数,即使该函数不使用内联指令,如果在适当的情况下(例如,如果函数代码体相对较短并且定义在头文件中),以换取执行时间。

循环展开是一种经典的速度优化方法,被许多编译器用作自动优化策略。例如,以下代码需要循环 100 次:

for (i = 0; i < 100; i++)

{

do_stuff(i);

}

在每个周期结束时,必须检查周期条件,以进行比较判断。通过使用循环展开策略,代码可以转换如下:

for (i = 0; i < 100; )

{

do_stuff(i); i++;

do_stuff(i); i++;

do_stuff(i); i++;

do_stuff(i); i++;

do_stuff(i); i++;

do_stuff(i); i++;

do_stuff(i); i++;

do_stuff(i); i++;

do_stuff(i); i++;

do_stuff(i); i++;

}

新代码将比较指令从 100 次减少到 10 次,用于比较条件的时间可以减少 90%。

这里描述的两种方法都提高了代码效率,并实现了目标代码的优化。这是优化目标代码的一种典型方式,可以提高时间效率

英特尔处理器相关的编译器开关选项

gccm选项是为英特尔 i386 和 x86-64 处理器家族定义的。主要命令选项及其效果如表 12-3 所示。

表 12-3。

Intel Processor-Related gcc Switch Options

| 开关选项 | 笔记 | 描述 | | --- | --- | --- | | `-march=cpu-type` `-mtune=cpu-type` |   | 为指定类型的 CPU 生成代码。`cpu-type`可以是 i386、i486、i586、奔腾、i686、奔腾 4 等等。 | | `-msse` |   | 编译器自动向量化:使用或不使用 MMX、SSE 和 SSE2 指令。例如,`-msse`表示编程入指令,`-mno-sse`表示未编程入 SSE 指令。 | | `-msse2` |   | | `-msse3` |   | | `-mssse3` | `gcc` -4.3 新增内容 | | `-msse4.1` | `gcc` -4.3 新增内容 | | `-msse4.2` | `gcc` -4.3 新增内容 | | `-msse4` | 包括 4.1 和. 2,`gcc` -4.3 新增内容 | | `-mmmx` |   | | `-mno-sse` |   | | `-mno-sse2` |   | | `-mno-mmx` |   | | `-m32` `-m64` |   | 生成 32/64 机器码。 |

在表 12-3 中,-march是机器的 CPU 类型,-mtune是编译器想要优化的 CPU 类型;默认情况下,它与-march相同。-march选项是一个紧约束,-mtune是一个松约束。-mtune选项可以提供向后兼容性。

例如,带有选项-march = i686-mtune = pentium4的编译器针对奔腾 4 处理器进行了优化,但也可以在任何 i686 上运行。而对于-mtune = pentium-mmx编译的程序来说,奔腾 4 处理器是可以运行的。

以下选项生成指定机器类型的cpu-type指令:

-march=cpu-type

只有在优化为cpu-type生成的代码时,-mtune = cpu-type选项才可用。相比之下,-march = cpu-type为指定类型的处理器生成不能在非gcc上运行的代码,这意味着-march = cpu-type隐含了-mtune = cpu-type选项。

与英特尔处理器相关的cpu-type选项值在表 12-4 中列出。

表 12-4。

The Main Option Values of gcc -march Parameters for cpu-type

| cpu 类型值 | 描述 | | --- | --- | | 当地的 | 通过确定编译机器的处理器类型,选择在编译时生成代码的 CPU。使用`-march=native`启用本地机器支持的所有指令子集(因此结果可能不会在不同的机器上运行)。使用`-mtune=native`在所选指令集的约束下产生针对本地机器优化的代码。 | | i386 | 原装英特尔 i386 CPU。 | | i486 | 英特尔 i486 CPU。(该芯片未实施任何调度。) | | i586 | 不支持 MMX 的英特尔奔腾 CPU。 | | 美国英特尔公司生产的微处理器ˌ中文译名为“奔腾” | | 奔腾 mmx 处理器 | 英特尔奔腾 MMX CPU,基于支持 MMX 指令集的奔腾内核。 | | 奔腾 pro | 英特尔奔腾 Pro CPU。 | | i686 | 与`-march`一起使用时,使用的是奔腾 Pro 指令集,所以代码运行在所有 i686 系列芯片上。与`-mtune`连用时,与 generic 含义相同。 | | 奔腾 2 | 英特尔奔腾 II CPU,基于支持 MMX 指令集的奔腾 Pro 内核。 | | 奔腾 3 | 英特尔奔腾 III CPU,基于支持 MMX 和 SSE 指令集的奔腾 Pro 内核。 | | 奔腾 m 处理器 | | pentium(奔腾) | 英特尔奔腾 M;支持 MMX、SSE 和 SSE2 指令集的低功耗版本英特尔奔腾 III CPU。由基于英特尔迅驰的笔记本电脑使用。 | | 奔腾 4 | 支持 MMX、SSE 和 SSE2 指令集的英特尔奔腾 4 CPU。 | | 奔腾 4m 处理器 | | 普雷斯科特 | 英特尔奔腾 4 CPU 的改进版本,支持 MMX、SSE、SSE2 和 SSE3 指令集。 | | 诺科纳 | 英特尔奔腾 4 CPU 的改进版本,具有 64 位扩展,支持 MMX、SSE、SSE2 和 SSE3 指令集。 | | 核心 2 | 具有 64 位扩展和 MMX、SSE、SSE2、SSE3 和 SSSE3 指令集支持的英特尔酷睿 2 CPU。 | | corei7 号 | 具有 64 位扩展和 MMX、SSE、SSE2、SSE3、SSSE3、SSE4.1 和 SSE4.2 指令集支持的英特尔酷睿 i7 CPU。 | | corei 7 avx 系列 | 具有 64 位扩展和 MMX、SSE、SSE2、SSE3、SSSE3、SSE4.1、SSE4.2、AVX、AES 和 PCLMUL 指令集支持的英特尔酷睿 i7 CPU。 | | 核心 avx-i | 具有 64 位扩展和 MMX、SSE、SSE2、SSE3、SSSE3、SSE4.1、SSE4.2、AVX、AES、PCLMUL、FSGSBASE、RDRND 和 F16C 指令集支持的英特尔酷睿 CPU。 | | 原子 | 64 位扩展的英特尔凌动 CPU,支持 MMX、SSE、SSE2、SSE3 和 SSSE3 指令集以及凌动 Silvermont (SLM)架构。 |

Traditional gcc是一个本地编译器。这些命令选项可以添加到gcc来控制gcc编译器选项。例如,假设您有一个int_sin.c文件:

$ gcc int_sin.c

该命令使用O1优化级别(默认级别)并将int_sin.c编译成一个默认命名为a.out的可执行文件。

该命令使用O1优化(默认级别)将int_sin.c编译成可执行文件;可执行文件名称指定为sinnorm:

$ gcc int_sin.c -o sinnorm

这个命令使用O1优化(默认级别)将int_cos.c编译成一个共享库文件coslib.so。与编译成可执行程序的源代码文件不同,该命令要求源代码文件int_cos.c不包含 main 函数:

$ gcc int_cos.c -fPIC -shared -o coslib.so

该命令将int_sin.c编译成默认文件名的可执行文件。编译器不执行任何优化:

$ gcc -O0 int_sin.c

该命令使用最高优化级别O3int_sin.c文件编译成具有默认文件名的可执行文件:

$ gcc -O3 int_sin.c

该命令使用 SSE 指令将int_sin.c编译成可执行文件:

$ gcc -msse int_sin.c

该命令将int_sin.c编译成一个不含任何 SSE 指令的可执行文件:

$ gcc -mno-sse int_sin.c

该命令将int_sin.c编译成一个可执行文件,该文件可以使用英特尔凌动处理器指令:

$ gcc -mtune=atom int_sin.c

从由gcc本地编译的例子中,您已经有了一些使用编译器开关选项进行gcc编译器优化的经验。对于gcc原生编译器,可以在开关选项中直接使用gcc命令来实现编译器优化。然而,从前面的例子中,你知道 NDK 并不直接使用gcc命令。那么如何设置gcc编译器开关选项来实现 NDK 优化呢?

回想一下,在 NDK 的例子中,您使用了ndk-build命令来编译 C/C++ 源代码;该命令首先需要读取 makefile 文件Android.Mk。这个文件包含了gcc命令选项。Android.mk使用LOCAL_CFLAGS控制并完成gcc命令选项。ndk-build命令将LOCAL_CFLAGS运行时间值传递给gcc作为其命令选项来运行gcc命令。LOCAL_CFLAGS将数值传递给gcc并将其作为命令选项来运行gcc命令:

例如,在第三部分中,您将Android.mk修改如下:

1\. LOCAL_PATH      := $(call my-dir)

2\. include $(CLEAR_VARS)

3\. LOCAL_MODULE    := jnitestmysharelib

4\. LOCAL_SRC_FILES := jnitestccode.c

5\. LOCAL_CFLAGS    := -O3

6\. include $(BUILD_SHARED_LIBRARY)

第 5 行是新的:它设置了LOCAL_CFLAGS变量脚本。

当你执行ndk-build命令时,相当于增加了一个gcc -O3命令选项,它指示gcc在最高优化级别O3编译 C 源代码。类似地,如果您将第 5 行编辑为

LOCAL_CFLAGS      := -msse3

您指示gcc使用英特尔凌动支持的 SSE3 指令将 C 源代码编译成目标代码。

您可以将LOCAL_CFLAGS设置为不同的值,并比较目标库文件的大小和内容差异。注意,这个示例jnitest C 代码非常简单,不涉及复杂的任务。因此,当从不同的LOCAL_CFLAGS值编译时,库文件的大小和内容不会有很大的不同。

有没有库文件的大小或内容有显著差异的例子?是的,您将在接下来的章节中看到。

利用英特尔集成高性能多媒体函数库(IPP)进行优化

图 12-15 显示,Android 应用可以绕过 NDK 开发工具,直接使用现有的第三方开发的.so共享库,包括英特尔集成性能基元(英特尔 IPP)提供的第三方共享库。IPP 是面向英特尔处理器和芯片组的强大函数库,涵盖数学、信号处理、多媒体、图像和图形处理、矢量计算以及其他领域。IPP 的一个突出特点是,它的代码已经基于英特尔处理器的特性,使用多种方法进行了广泛的优化。这是一个高度优化的高性能服务库。英特尔 IPP 具有跨平台特性;它提供了一套跨平台和 OS 的通用 API,可用于 Windows、Linux 和其他操作系统;它支持嵌入式、台式机、服务器和其他处理器规模的系统。

IPP 实际上是一组函数库,每个函数库在相应的库中有不同的功能区域,并且根据不同处理器架构支持的功能数量略有不同。例如,英特尔 IPP 5。x 图像处理功能在英特尔架构中可支持 2570 个功能,而在 IXP 处理器架构中仅支持 1574 个功能。

包括英特尔 IPP 在内的各种高性能图书馆提供的服务是多方面和多层次的。应用可以直接或间接使用 IPP。它不仅可以为应用提供支持,还可以为其他组件和库提供支持。

使用 IPP 的应用可以直接使用其函数接口,也可以使用示例代码间接使用 IPP。此外,使用 OpenCV 库(一个跨平台开源计算机视觉库)相当于间接使用英特尔 IPP 库。英特尔 IPP 和英特尔 MKL 函数库都运行在各种架构的高性能英特尔处理器上。

考虑到英特尔 IPP 的强大功能,并根据英特尔处理器优化特性的特点,您可以使用英特尔 IPP 库来替换一些运行频率更高且耗时的关键源代码。这样,您可以获得比一般代码更高的性能加速。这简直就是一种“站在巨人肩膀上”的实用优化方法:不需要在关键区域手动编写代码就可以实现优化。

英特尔最近发布了英特尔集成本地开发体验(INDE ),为 Android 应用开发人员提供了英特尔 IPP 和英特尔线程构建模块(英特尔 TBB)。您可以轻松使用英特尔 IPP、英特尔 TBB、英特尔 GPA 和其他工具进行 Android 应用开发。

NDK 集成优化示例

本节使用一个案例研究来演示通过将 NDK 与 C/C++ 相集成的综合优化技术。本案分为两步。第一步是从 C/C++ 代码中编译一个本地函数,以加速传统的基于 Java 的程序中的计算任务;第二步演示了使用 NDK 编译器优化来实现 C/C++ 优化。每一步都在它自己的章节中介绍;这两个部分紧密相连。

C/C++:加速原始应用

前一章介绍了一个计算π的 Java 代码示例(SerialPi)。在本节中,您将计算任务从 Java 代码转换为 C 代码,使用 NDK 将其转换为本地库。然后将它与原始的 Java 代码任务进行比较,并获得一些使用 C/C++ 本地库函数实现传统的基于 Java 的任务加速的第一手经验。

用于本案例研究的应用名为NdkExp;参见图 12-16 。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 12-16。

Original version of NdkExp

图 12-16(a) 显示了应用的主界面,包括三个按钮:启动 Java 任务、启动 C 任务、退出应用。单击 Start Java Task 按钮启动一个计算π的传统 Java 任务。当任务完成后,按钮下方会显示计算的结果以及花费的时间,如图 12-16(b) 所示。单击 Start C Task 按钮启动用 C 编写的计算任务,使用相同的数学公式计算π。当任务完成后,按钮下方会显示计算的结果以及花费的时间,如图 12-16© 所示。

同样的任务,用传统 Java 编写的应用需要 12.565 秒才能完成;用 C 语言编写并由 NDK 开发工具编译的应用只需 6.378 秒即可完成。这个例子向您展示了使用 NDK 实现性能优化的强大功能。

该示例实现如下:

  1. 修改类源代码文件MainActivity.java的主布局,如下所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 12-17。

Layout of the original NdkExp

  1. 在 Eclipse 中生成项目,命名为NdkExp,选择 Build SDK 选项,支持 x86 版本的 API。对其他选项使用默认值。然后生成项目。
  2. 修改主布局文件。在布局中放置三个TextView微件和三个Button微件,设置TextID属性,调整大小和位置,如图 12-17 所示。

1.  package com.example.ndkexp;

2.  import android.os.Bundle;

3.  import android.app.Activity;

4.  import android.view.Menu;

5.  import android.widget.Button;

6.  import android.view.View;

7.  import android.view.View.OnClickListener;

8.  import android.os.Process;

9.  import android.widget.TextView;

10\. import android.os.Handler;

11\. import android.os.Message;

12.

13\. public class MainActivity extends Activity {

14.     private JavaTaskThread javaTaskThread = null;

15.     private CCodeTaskThread cCodeTaskThread = null;

16.     private TextView tv_JavaTaskOuputInfo;

17.     private TextView tv_CCodeTaskOuputInfo;

18.     private Handler mHandler;;

19.     private long end_time;

20.     private long time;

21.     private long start_time;

22.     @Override

23.     public void onCreate(Bundle savedInstanceState) {

24.         super.onCreate(savedInstanceState);

25.         setContentView(R.layout.activity_main);

26.         tv_JavaTaskOuputInfo = (TextView)findViewById(R.id.javaTaskOuputInfo);

27.         tv_JavaTaskOuputInfo.setText("Java the task is not started ");

28.         tv_CCodeTaskOuputInfo = (TextView)findViewById(R.id.cCodeTaskOuputInfo);

29.         tv_CCodeTaskOuputInfo.setText("C  code task is not start ");

30.         final Button btn_ExitApp = (Button) findViewById(R.id.exitApp);

31.         btn_ExitApp.setOnClickListener(new /*View.*/OnClickListener(){

32.             public void onClick(View v) {

33.                 exitApp();

34.             }

35.         });

36.         final Button btn_StartJavaTask = (Button) findViewById(R.id.startJavaTask);

37.         final Button btn_StartCCodeTask = (Button) findViewById(R.id.startCCodeTask);

38.         btn_StartJavaTask.setOnClickListener(new /*View.*/OnClickListener(){

39.             public void onClick(View v) {

40.                 btn_StartJavaTask.setEnabled(false);

41.                 btn_StartCCodeTask.setEnabled(false);

42.                 btn_ExitApp.setEnabled(false);

43.                 startJavaTask();

44.             }

45.         });

46.         btn_StartCCodeTask.setOnClickListener(new /*View.*/OnClickListener(){

47.             public void onClick(View v) {

48.                 btn_StartJavaTask.setEnabled(false);

49.                 btn_StartCCodeTask.setEnabled(false);

50.                 btn_ExitApp.setEnabled(false);

51.                 startCCodeTask();

52.             }

53.         });

54.         mHandler = new Handler() {

55.             public void handleMessage(Message msg) {

56.             String s;

57.                switch (msg.what)

58.                {

59.                case JavaTaskThread.MSG_FINISHED:

60.                     end_time = System.currentTimeMillis();

61.                     time = end_time - start_time;

62.                     s = " The return value of the Java task "+ (Double)(msg.obj) +"  Time consumed:"

63.                          + JavaTaskThread.msTimeToDatetime(time);

64.                     tv_JavaTaskOuputInfo.setText(s);

65.                     btn_StartCCodeTask.setEnabled(true);

66.                     btn_ExitApp.setEnabled(true);

67.                   break;

68.                 case CCodeTaskThread.MSG_FINISHED:

69.                     end_time = System.currentTimeMillis();

70.                     time = end_time - start_time;

71.                     s = " The return value of the C code task"+ (Double)(msg.obj) +"  time consumed:"

72.                          + JavaTaskThread.msTimeToDatetime(time);

73.                     tv_CCodeTaskOuputInfo.setText(s);

74.                     btn_StartJavaTask.setEnabled(true);

75.                     btn_ExitApp.setEnabled(true);

76.                   break;

77.                 default:

78.                   break;

79.                 }

80.             }

81.         };

82.     }

83.

84.     @Override

85.     public boolean onCreateOptionsMenu(Menu menu) {

86.        getMenuInflater().inflate(R.menu.activity_main, menu);

87.        return true;

88.     }

89.

90.     private void startJavaTask() {

91.         if (javaTaskThread == null)

92.             javaTaskThread = new JavaTaskThread(mHandler);

93.         if (! javaTaskThread.isAlive())

94.         {

95.                start_time = System.currentTimeMillis();

96.                javaTaskThread.start();

97.                tv_JavaTaskOuputInfo.setText("The Java task is running...");

98.         }

99.     }

100.

101.     private void startCCodeTask() {

102.         if (cCodeTaskThread == null)

103.             cCodeTaskThread = new CCodeTaskThread(mHandler);

104.         if (! cCodeTaskThread.isAlive())

105.         {

106.                start_time = System.currentTimeMillis();

107.                cCodeTaskThread.start();

108.                tv_CCodeTaskOuputInfo.setText("C code task is running...");

109.         }

110.     }

111.     private void exitApp() {

112.         try {

113.             if (javaTaskThread !=null)

114.             {

115.                 javaTaskThread.join();

116.                 javaTaskThread = null;

117.             }

118.         } catch (InterruptedException e) {

119.         }

120.         try {

121.             if (cCodeTaskThread  !=null)

122.             {

123.                 cCodeTaskThread.join();

124.                 cCodeTaskThread = null;

125.             }

126.         } catch (InterruptedException e) {

127.         }

128.         finish();

129.         Process.killProcess(Process.myPid());

130.     }

131.

132.     static {

133.         System.loadLibrary("ndkexp_extern_lib");

134.     }

135\. }

这段代码与SerialPi的示例代码基本相同。只有第 123–134 行中的代码是 ew。这段代码要求在应用运行之前加载libndkexp_extern_lib.so共享库文件。应用需要使用这个库中的本地函数。

  1. 项目中新的线程任务类JavaTaskThread用于计算π。代码类似于SerialPi示例中的MyTaskThread类代码,此处省略。
  2. 新项目中的线程任务类CCodeTaskThread调用本地函数计算π;其源代码文件CCodeTaskThread.java如下所示:

1.  package com.example.ndkexp;

2.  import android.os.Handler;

3.  import android.os.Message;

4.  public class CCodeTaskThread extends Thread {

5.     private Handler mainHandler;

6.     public static final int MSG_FINISHED = 2;  // The message after the end of the task

7.     private native double cCodeTask();   // Calling external C functions to accomplish computing tasks

8.     static String msTimeToDatetime(long msnum){

9.         long hh,mm,ss,ms, tt= msnum;

10.        ms = tt % 1000; tt = tt / 1000;

11.        ss = tt % 60; tt = tt / 60;

12.        mm = tt % 60; tt = tt / 60;

13.        hh = tt % 60;

14.        String s = "" + hh +" Hour "+mm+" Minute "+ss + " Second " + ms +" Millisecond ";

15.        return s;

16.    }

17.    @Override

18.    public void run()

19.    {

20.        double pi = cCodeTask();   // Calling external C function to complete the calculation

21.        Message msg = new Message();

22.        msg.what = MSG_FINISHED;

23.        Double dPi = Double.valueOf(pi);

24.        msg.obj = dPi;

25.        mainHandler.sendMessage(msg);

26.    }

27.    public CCodeTaskThread(Handler mh)

28.    {

29.        super();

30.        mainHandler = mh;

31.    }

32\. }

这段代码类似于SerialPi示例的MyTaskThread类的代码框架。主要区别在第 20 行。原来计算π的 Java 代码被替换为调用一个本地函数cCodeTask来完成任务。为了表明cCodeTask是一个局部函数,您在第 7 行添加了local声明。

  1. 在 Eclipse 中构建项目。还是那句话,只是建造,而不是运行。
  2. 在项目根目录下创建jni子目录。
  3. 编写cCodeTask函数的 C 实现代码。
  4. 将文件编译成一个.so库文件。主要步骤如下。
    1. 创建一个 C 接口文件。因为是使用局部函数的cCodeTaskThread类,所以需要根据这个类的类文件生成类头文件。在命令行中,转到项目目录并运行以下命令:

E:\temp\Android Dev\workspace\NdkExp> javah -classpath "D:\Android\android-sdk\platforms\android-15\android.jar";bin/classes com.example.ndkexp.CCodeTaskThread

该命令在项目目录中生成一个名为com_example_ndkexp_CCodeTaskThread.h的文件。文件的主要内容如下:

......

23\. JNIEXPORT jdouble JNICALL Java_com_example_ndkexp_CCodeTaskThread_cCodeTask

24\. (JNIEnv *, jobject);

......

在第 23–24 行,定义了本地函数cCodeTask的原型。

  1. 基于这些头文件,在项目的jni目录下创建一个对应的 C 代码文件。在这种情况下,将其命名为mycomputetask.c,如下所示:

1.  #include <jni.h>

2.  jdouble Java_com_example_ndkexp_CCodeTaskThread_cCodeTask (JNIEnv* env, jobject thiz )

3.  {

4.      const long num_steps = 100000000;    // The total step length

5.      const double step = 1.0 / num_steps;

6.      double x, sum = 0.0;

7.      long i;

8.      double pi = 0;

9.

10.     for (i=0; i< num_steps; i++){

11.         x = (i+0.5)*step;

11.         sum = sum + 4.0/(1.0 + x*x);

12.     }

13.     pi = step * sum;

14.

15.     return (pi);

16\. }

第 4–16 行是函数的主体——计算π的代码,它是对应于SerialPi中的MyTaskThread类的代码。不难理解。注意,在第 4 行,变量num_steps(总步长)的值必须与JavaTaskThread类表示的步长值相同。否则,在这里比较性能是没有意义的。

每个 Jni 文件的第一行必须包含头。第 2 行是cCodeTask函数原型,基于上一步中获得的稍加修改的头文件。

第 16 行返回结果。对于对应于 C jdouble类型的 Java double类型,C 可以有一个直接返回给它的double类型的pi变量。这将在本章的导言中讨论。

  1. 在项目jni目录下,按照本章第 12 页的方法部分:使用命令行方法生成库文件,创建Android.mkApplication.mk文件。Android.mk的内容如下:

1.  LOCAL_PATH := $(call my-dir)

2.  include $(CLEAR_VARS)

3.  LOCAL_MODULE        := ndkexp_extern_lib

4.  LOCAL_SRC_FILES     := mycomputetask.c

5.  include $(BUILD_SHARED_LIBRARY)

第 4 行指定了案例文件中的 C 代码。第 3 行表示生成的库的文件名;其名称必须与项目文件MainActivity.java第 133 行System.loadLibrary函数的参数一致。

  1. 根据本章第 12 页“使用命令行方法生成库文件”一节所述的方法,将 C 代码编译到项目的lib目录下的.so库文件中。
  2. 运行项目。

应用的运行界面如下图 12-18 所示。

扩展编译器优化

该示例展示了 NDK 在应用加速方面的能力。但是,应用只实现了一个局部函数,并且不能提供信息来比较编译器优化的效果。为此,在本节中,您将重新构建应用,并使用它来试验编译器优化的效果;参见图 12-18 。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 12-18。

Extended version of NdkExp

该应用有四个按钮。当您单击“启动 Java 任务”按钮时,响应代码不会改变。当您单击“启动 C 任务”或“启动另一个 C 任务”按钮时,应用会启动一个正在运行的本地函数。

两个函数的代码(函数体)是一样的。它计算π的值,但是使用不同的名称。第一个按钮调用cCodeTask函数,第二个按钮调用函数。这些函数分别位于mycomputetask.canothertask.c文件中,编译后对应库文件libndkexp_extern_lib.solibndkexp_another_lib.so。在这种情况下,使用-O0选项编译libndkexp_extern_lib.so,使用-O3选项编译libndkexp_another_lib.so,因此一个编译为非优化,另一个编译为优化。

点击开始 C 任务运行未优化版本的 C 函数,如图 12-20(b);点击启动另一个 C 任务运行优化后的版本,如图 12-20© 所示。任务执行后,系统显示消耗时间的计算结果。

如图 12-18 所示,无论是否使用编译器优化,本地函数的运行时间总是比 Java 函数的运行时间(12.522 秒)短。-O3优化函数的执行时间(5.632 秒)小于未优化(-O0编译器选项)函数的执行时间(7.321 秒)。从这个结果比较中,您可以看到使用编译器优化实际上减少了应用的执行时间。不仅如此,它甚至比 C/C++:原应用加速中的原应用运行时间(6.378 秒)还要短。这是因为没有编译器选项的原始应用默认为-O1优化级别,而-O3优化级别甚至比原始应用更高,因此它的运行时间最短也就不足为奇了。

该应用是原始应用NdkExp的修改和扩展版本。步骤如下:

  1. 修改主布局的类源代码文件MainActivity.java。主要变化如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 12-19。

Extended NdkExp layout

  1. 修改主布局文件。在布局中添加TextView小部件和Button小部件。设置TextID属性,调整其大小和位置,如图 12-19 所示。

...

13.  public class MainActivity extends Activity {

14.      private JavaTaskThread javaTaskThread = null;

15.      private CCodeTaskThread cCodeTaskThread = null;

16.      private AnotherCCodeTaskThread anotherCCodeTaskThread = null;

17.      private TextView tv_JavaTaskOuputInfo;

18.      private TextView tv_CCodeTaskOuputInfo;

19.      private TextView tv_AnotherCCodeTaskOuputInfo;

......

182.     static {

183.         System.loadLibrary("ndkexp_extern_lib");

184.         System.loadLibrary("ndkexp_another_lib");

185.     }

186\. }

分别在第 16 行和第 19 行,为新的 Start Other C Task 按钮添加所需的变量。

关键的变化在第 184 行;这里,除了加载原始的共享库文件之外,还添加了另一个库文件。

  1. 在项目中,添加一个调用本地函数计算π的线程任务类AnotherCCodeTaskThread。其源代码文件AnotherCCodeTaskThread.java如下所示:

1.  package com.example.ndkexp;

2.  import android.os.Handler;

3.  import android.os.Message;

4.  public class AnotherCCodeTaskThread extends Thread {

5.      private Handler mainHandler;

6.      public static final int MSG_FINISHED = 3;

// The message after the end of the task

7.      private native double anotherCCodeTask();

// Calling external C functions to complete computing tasks

8.      static String msTimeToDatetime(long msnum){

9.          long hh,mm,ss,ms, tt= msnum;

10.         ms = tt % 1000; tt = tt / 1000;

11.         ss = tt % 60; tt = tt / 60;

12.         mm = tt % 60; tt = tt / 60;

13.         hh = tt % 60;

14.         String s = "" + hh +"Hour "+mm+"Minute "+ss + "Second " + ms +"Millisecond";

15.         return s;

16.     }

17.     @Override

18.     public void run()

19.     {

20.         double pi = anotherCCodeTask();  // Calling external C function to complete the calculation

21.         Message msg = new Message();

22.         msg.what = MSG_FINISHED;

23.         Double dPi = Double.valueOf(pi);

24.         msg.obj = dPi;

25.         mainHandler.sendMessage(msg);

26.     }

27.     public CCodeTaskThread(Handler mh)

28.     {

29.         super();

30.         mainHandler = mh;

31.     }

32\. }

这段代码和CCodeTaskThread类的代码几乎一模一样。它通过调用另一个外部 C 函数anotherCCodeTask来完成第 20 行的计算任务,做了一点处理。为此,在第 7 行中,它为本地函数提供了适当的指令,并在第 6 行中更改了消息类型的值。这样就用一个消息把自己和之前的 C 区分开了。第 4 行显示了从Thread类继承的任务类。

  1. 在 Eclipse 中构建项目:只是构建,而不是运行。
  2. 修改mycomputetask.c的 makefile 文件,重建库文件。为此,首先修改项目的jni目录下的Android.mk文件,如下所示:

1.  LOCAL_PATH      := $(call my-dir)

2.  include $(CLEAR_VARS)

3.  LOCAL_MODULE    := ndkexp_extern_lib

4.  LOCAL_SRC_FILES := mycomputetask.c

5.  LOCAL_CFLAGS    := -O0

6.  include $(BUILD_SHARED_LIBRARY)

与原来的应用不同,在第 5 行中,您为传递给gcc的命令LOCAL_CFLAGS添加了参数。值-O0表示没有优化。

  1. 将 C 代码文件编译成项目的lib目录下的.so库文件。
  2. 将项目的lib目录中的.so库文件(在本例中,该文件为libndkexp_extern_lib.so)保存到其他目录中,因为下面的操作将删除这个.so库文件。
  3. 编写anotherCCodeTask函数的 C 实现代码。复制上一节中cCodeTask功能的处理步骤。使用“NDK 示例”一节中的方法,将文件编译成.so库文件。主要步骤如下:E:\temp\Android Dev\workspace\NdkExp> javah -classpath "D:\Android\android-sdk\platforms\android-15\android.jar";bin/classes com.example.ndkexp.AnotherCCodeTaskThread该命令生成一个com_example_ndkexp_AnotherCCodeTaskThread.h文件。文件主要内容如下:...... 23\. JNIEXPORT jdouble JNICALL Java_com_example_ndkexp_AnotherCCodeTaskThread_anotherCCodeTask 24.   (JNIEnv *, jobject); ......第 23–24 行定义局部函数,是原型。1.  #include <jni.h>``2.  jdouble Java_com_example_ndkexp_AnotherCCodeTaskThread_anotherCCodeTask (JNIEnv* env,  jobject thiz )``3.  {``......``17\. }``mycomputetask.c的第二行被替换为anotherCCodeTask函数的原型。这是从上一步创建的.h文件中复制的同一个函数原型,有微小的修改。最终形式在第 2 行。1.  LOCAL_PATH      := $(call my-dir) 2.  include $(CLEAR_VARS) 3.  LOCAL_MODULE    := ndkexp_another_lib 4.  LOCAL_SRC_FILES := anothertask.c 5.  LOCAL_CFLAGS    := -O3 6.  include $(BUILD_SHARED_LIBRARY)第 4 行,该值被替换为新的 C 代码文件anothertask.c。在第 3 行,该值被替换为一个与System.loadLibrary函数的参数一致的新库文件名,该文件名在MainActivity.java文件的第 184 行。在第 5 行,传递的gcc命令的LOCAL_CFLAGS参数值被替换为-O3,这代表了最高级别的优化。
    1. 创建一个 C 接口文件。在命令行中,转到项目目录,然后运行以下命令:
    2. 基于前面提到的项目Jni目录下的头文件,建立相应的 C 代码文件,这里是anothertask.c。内容是对mycomputetask.c的修改:
    3. 修改jni目录中的Android.mk文件,如下所示:
    4. 按照 3.1 节描述的方法,将 C 代码文件编译成项目的lib目录下的.so库文件。lib目录下的libndkexp_extern_lib.so文件消失,取而代之的是新生成的libndkexp_another_lib.so文件。所以,保存库文件是非常重要的。
  4. 将之前保存的libndkexp_extern_lib.so库文件放回libs目录。现在目录中有两个文件。您可以使用dir命令来验证:

E:\temp\Android Dev\workspace\NdkExp>dir libs\x86

2013-02-28  00:31     5,208 libndkexp_another_lib.so

2013-02-28  00:23     5,208 libndkexp_extern_lib.so

  1. 运行项目。

比较编译器优化

通过这个案例研究,您已经了解了编译器优化的效果。任务执行时间从优化前的 7.321 秒缩短到优化后的 5.632 秒。但是您只比较了示例中的gcc -O3-O0命令选项之间的区别。在编译mycomputetask.canothertask.c两个文件时,可以通过修改Android.mk文件内容来扩展这种配置,比较使用不同编译器命令选项时优化效果的差异。修改Android.mk文件,只需要修改LOCAL_CFLAGS项的值即可;您可以选择许多gcc命令选项进行比较。让我们看一个例子。

例 1。使用 SSE 指令比较优化结果

编译mycomputetask.cAndroid.mk文件对应的启动 C 任务按钮:

LOCAL_CFLAGS := -mno-sse

并编译anothertask.cAndroid.mk文件对应的启动其他 C 任务按钮:

LOCAL_CFLAGS := -msse3

前者告诉编译器不要编译 SSE 指令;后者允许编译器编程为 SSE3 指令。选择 SSE3 指令的原因是 SSE3 是英特尔凌动处理器支持的最高级别的指令。

运行应用的结果如图 12-20 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 12-20。

Optimization comparison of compiler SSE instructions for NdkExp

使用 SSE 指令的相同任务比不使用 SSE 指令的执行时间更短。执行时间从原来的 6.759 秒缩短到 5.703 秒。

注意,在这个例子中,我们完成了修改Android.mk并重新运行ndk-build来生成.so库文件。我们立即部署并运行了NdkExp项目,但是发现我们无法达到预期的效果,因为只有.so库文件被更新。Eclipse 项目管理器没有检测到项目需要重新构建。结果,.apk没有更新,目标机器上的NdkExp不会运行更新或原始代码。考虑到这种情况,您可以使用以下方法来避免此问题:

  1. 从手机上卸载应用。
  2. 删除宿主项目目录的bin子目录中的classes.dexjarlist.cacheNdkExp.apk三个文件。
  3. 在 Eclipse 中删除项目。
  4. 在 Eclipse 中,重新导入项目。
  5. 重新部署并运行项目。

这里你只比较了 SSE 指令的效果。你可以尝试其他的gcc编译器选项,比较它们的运行结果。

此外,前面的例子只涉及 NDK 效应,所以 C 函数仍然使用单线程代码。你可以把本章的 NDK 优化知识和上一章的多线程优化结合起来,把 C 函数改成多线程,和编译器优化一起实现。各种应用中的这一组编写的优化技术将允许应用运行得更快。

摘要

本章介绍了用于 C/C++ 应用开发的 Android NDK,以及相关的优化方法和优化工具。英特尔移动硬件和软件为低功耗设计奠定了基础。英特尔凌动处理器为低功耗提供了硬件支持,这是 Android 操作系统的一大特色。

下一章概述低功耗设计。还讨论了 Android 电源控制机制以及如何实现低功耗应用设计的目标。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Open Access This chapter is licensed under the terms of the Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License ( http://​creativecommons.​org/​licenses/​by-nc-nd/​4.​0/​ ), which permits any noncommercial use, sharing, distribution and reproduction in any medium or format, as long as you give appropriate credit to the original author(s) and the source, provide a link to the Creative Commons licence and indicate if you modified the licensed material. You do not have permission under this licence to share adapted material derived from this chapter or parts of it. The images or other third party material in this chapter are included in the chapter’s Creative Commons licence, unless indicated otherwise in a credit line to the material. If material is not included in the chapter’s Creative Commons licence and your intended use is not permitted by statutory regulation or exceeds the permitted use, you will need to obtain permission directly from the copyright holder.

十三、Android 应用和英特尔图形性能分析器(GPA)的低功耗设计:辅助功耗优化

Keywords Power Consumption Polling Code Idle State Reduce Power Consumption Standby Mode

与使用交流电源的通用计算机不同,并非所有移动设备都可以直接连接到交流电源;不能假定电力供应是用之不竭的。此外,移动设备必须考虑散热问题。如果功耗太高,就需要系统增加散热,在某些时候可能会达到不允许的程度。由于对系统总功耗的严格限制,通常称为省电的低功耗设计是移动设备应用的重要元素;在许多情况下,这是一个硬性要求或生存的基础。例如,很难想象市场会接受只能支持几个小时的手机。

另一方面,英特尔移动硬件和软件为低功耗设计提供了基础。英特尔凌动处理器提供低功耗的硬件支持,低功耗支持是 Android 的一大特色。两者都为移动应用的低功耗设计提供了良好的平台。

本章组织如下:首先是低功耗设计的概述和介绍,然后讨论 Android 电源控制机制,最后讨论如何实现低功耗应用设计的目标。

低功耗设计概述

让我们看看移动系统的功耗特性。对于移动设备来说,处理器、无线电通信和屏幕是其功耗(功率)的三个主要组成部分。处理器及其辅助设备负责大部分电池功耗。因此,本章重点讨论处理器及其辅助设备的功耗(这里简称为处理器功耗)。

消费的基础

电池寿命主要是指笔记本电脑、MP3 播放器或手机等移动设备在没有外部电源适配器的情况下,仅配备自己的电池可以维持的运行时间。一般来说,影响机器电池寿命的因素包括电池本身以及机器功耗(瓦特/小时)。

对于半导体产品来说,数字电路的功耗由两部分组成。第一部分是,从集成电路工艺的角度来看,是漏电流(漏电流)引起的功耗,是电子电路(如 CMOS)的一部分。控制这种功耗的能力主要取决于生产工艺和所用的材料。数字电路功耗的第二部分是。影响这部分的因素很多,比如电路设计、电路复杂度、工作时钟频率等。

处理器(或 CPU)的动态功率,也称为开关功率,称为功耗,由以下经验公式确定:

在该公式中,P 是处理器功耗,a 是与电路相关的调整参数,C 是单个时钟周期的总栅极电容(对于处理器是固定的),F 是处理器工作频率,V 是工作电压。如您所见,处理器的功耗与工作电压的平方成正比,与工作频率成正比。

Tip

关于处理器功耗,有一个相关的概念叫做。TDP 容易和 CPU 功耗混淆。虽然两者都是用来衡量处理器功耗的指标,都使用瓦特(W)作为单位,但 TDP 与处理器功耗的含义不同。

TDP 是处理器散热指示器的反映。根据定义,它是处理器达到最大负载时释放的热量。处理器 TDP 功耗不是处理器的真实功耗。处理器功耗(power)是一个物理参数,它等于流过处理器内核的电流值与处理器内核的电压值的乘积,它反映了单位时间内能量的实际功耗。TDP 是处理器的电流热效应和其他形式的散热产生的热量。显然,处理器 TDP 小于处理器的功耗。

TDP 冷却系统要求是硬件设计者和制造商要考虑的一个重要因素。但本章讨论的是实际的电能消耗——处理器功耗——而不是 TDP。

根据处理器功耗公式,调整参数 a 和总栅极电容 C 由处理器设计和材料决定。对于处理器,参数 a 和 C 是固定的;想要降低功耗,必须从工作频率(F)和工作电压(V)入手,这是很多低功耗技术的出发点。

一般来说,实现 CMOS 处理器更高能效的方法如下:

  1. 降低电压或处理器时钟频率。
  2. 在内部禁用一些不需要功能部件的当前正在执行的功能。
  3. 允许部分处理器与主电源完全断开,以消除漏电。
  4. 改进处理器电路设计和制造工艺;运用物理学原理获得能源效率。

管理处理器功率(功耗)有两种策略。一是使用静态电源管理机制。这种机制由用户调用,并且不依赖于处理器活动。静态机制的一个例子是省电模式以节省功率。省电模式可以通过一条指令进入,也可以通过接收中断或其他事件退出。

管理处理能力的另一个策略是使用动态电源管理机制。这种机制基于处理器功耗控制的动态活动。例如,当运行命令时,如果处理器逻辑的某些部分不需要运行,则处理器可以关闭这些特定部分。

功耗控制技术

为了帮助您了解半导体(包括处理器)的功耗基础知识,我们来看看如何在硬件中实现功耗控制技术。这些途径将在以下章节中讨论。

动态电压/频率调节技术

是一种控制功耗的方法,通过调整(降低)处理器的工作频率,使其运行频率低于峰值频率,从而降低处理器功耗。这项技术最早用于笔记本电脑,现在越来越广泛地用于移动设备。

除了在处理器上节能,DFS 技术还有其他用途。它可用于机器上安静的计算环境或轻负载条件下,以降低冷却成本和总体能源需求。当系统冷却不足且温度接近临界值时,该技术有助于减少热量积聚,从而防止机器出现严重的温度问题。许多超频系统也使用这种技术来实现临时补充冷却。

Tip

与 DFS 技术相反但相关的是超频。该技术升级处理器(动态)能力,使其超过制造商规定的设计限制,并提高处理器性能。DFS 和超频之间有一个重要的区别:在现代计算机系统中,超频是在前端总线中(主要是因为倍数通常被锁定),而 DFS 是在乘法器中完成的。而且超频往往是静态的;DFS 通常是动态的。

实际上,高级配置和电源接口(ACPI)规定现代处理器的 C0 工作状态可以分为命名的性能状态(P 状态)和节流状态(T 状态)。P 状态允许您降低时钟频率,T 状态通过插入 STPCLK(停止时钟)信号来暂时关闭时钟信号,并进一步抑制处理器功耗(但不是实际的时钟频率)。英特尔还与谷歌合作改善 Android 的电源管理,并为三种 CPU 待机状态创建了驱动程序:活动待机(S0i1)、永远在线永远连接(AOAC)待机(S0i2)和深度睡眠待机(S0i3)。

如上所述,功耗主要是由于静态功率的存在而由漏电流引起的;动态功耗只是芯片总功耗的一部分。当芯片尺寸变小时,CMOS 阈值水平降低,漏电流的影响显得更加明显。特别是对于当前的芯片制造工艺,其处于微米级以下,动态功率仅为芯片总功率的大约三分之二,这限制了频率缩放的效果。

(DVS)是控制处理器功耗的另一种方法。这是通过调整(降低)处理器的工作电压来降低处理器功率来实现的。

DFS 仅仅作为一种节省动态功率的方法并没有太大的价值。考虑到 V 2 在动态功耗公式中的重要作用,以及在 DFS 中已经对现代处理器的低功耗空闲状态进行了深入优化,以节省大量功耗,您需要考虑 DVS。降低处理器时钟频率还提供了降压空间(因为在一定范围内,处理器所能支持的最大工作频率随着处理器电源电压的增加而增加)。电压调整和频率调整可以结合使用,形成一种全面的功率控制方法:动态电压/频率放电减少,或。这项技术也被称为英特尔处理器 CPU 节流。

动态电压/频率调整技术会影响性能。这种技术减少了处理器在给定时间发出的指令数量,从而导致处理性能(速度)下降。因此,它通常用在处理器负载较低的情况下(比如当系统运行在空闲状态时)。

时钟门控

是实现节能的另一种方式,在这种情况下,通过关闭和打开模块时钟和电源控制。这项技术应用于第一个应用系列,如类似 OMAP3 的传统电话芯片;英特尔奔腾 4 处理器也使用了它。

对于 CMOS 处理器部件,改变电平状态所消耗的功率远大于维持电平状态所消耗的功率,因为改变电平状态时时钟信号极其频繁。如果在当前时钟周期中使用时钟门控技术,如果系统不使用某些逻辑模块,则模块时钟信号被切断,在模块中创建闭合电路,因此逻辑开关不会改变状态。您只需要在开关功耗接近于零时保留漏电流,以降低功耗。当有工作要做时,模块时钟被重新激活。这个过程也被称为修剪时钟树。在某种意义上,时钟门控是变频时钟的一个极端情况,但这两个值是零和最大频率。

这种技术要求每个模块(称为功能单元块(FUB))都包含时钟门逻辑电路。也就是说,限幅时钟树的技术必须由附加的逻辑元件来保证。

时钟门控有几种形式。通过软件手动时钟门控方法,驱动器控制何时开启或关闭指定空闲控制器使用的各种时钟。另一种方法是自动时钟门控:硬件可以被通知或检测是否有工作要做,然后在您指定不再需要时钟时关闭门控。例如,内部桥或总线可以使用自动时钟门控方法,以便它总是被关闭,直到处理器或 DMA 引擎需要使用它。如果软件不使用总线上的外围设备,驱动程序可以在门控代码中关闭它们。

节能电路设计和制造流程

芯片电路设计选择和制造工艺可以在物理层面上提高节能。其中一个设计选择是使用超低电压(ULV)处理器。ULV 系列处理器降低了处理器内核电压,减少了处理器内核数量甚至尺寸,实现了从硬件(在物理层)上的功耗控制。

此外,与 ULV 处理器类似,45 纳米制造工艺在硬件层面降低了处理器功耗。该芯片功耗更低,电池寿命更长,晶体管更多,体积更小。英特尔凌动 Bay Trail 处理器采用 22 纳米制造工艺实现节能技术(下一代处理器将采用 14 纳米技术)。随着制造工艺和制造精度的进一步提高,芯片越来越小,同时物理功耗也越来越低。

了解了硬件电源控制之后,您可以看看系统电源控制技术。这些技术有些是硬件级的,有些是操作系统级的,有些是系统级的,包括软件和硬件。

英特尔 SpeedStep 和增强型英特尔 SpeedStep 技术

英特尔 SpeedStep 技术旨在为英特尔 CPU 提供电源控制;该技术现在通常被称为增强型英特尔 SpeedStep 技术(EIST)。它首先用于英特尔奔腾 M、奔腾 4 6xx 系列和奔腾 D 处理器。英特尔酷睿、英特尔凌动和其他处理器系列也采用了它。EIST 主要利用动态电压和频率缩放;基本原理是调整处理器电压和频率,以降低功耗和热量。当然,随着电压和频率的降低,处理速度也随之降低。这项技术已经经历了几代的发展,如下所述。

第一代英特尔 SpeedStep 技术

独创的英特尔 SpeedStep 技术允许处理器在两种操作模式之间自由切换:交流状态,提供最高性能模式(最大性能模式);和电池状态(电池优化模式)。这两种模式是根据电脑的电源自动选择的:外接电源或电池。最高性能模式是计算机连接到交流电源(即始终由外部电源供电)时的近似性能。当计算机使用最小的电池电量来实现最佳性能时,使用电池优化模式。通常,当使用英特尔 SpeedStep 技术切换模式时,处理器的功耗会降低 40%,同时仍能保持 80%的峰值性能。

模式切换的转换速度非常快——只有 1/2000 秒,所以用户感觉不到转换。即使一个程序的性能要求很敏感(比如播放 DVD 电影),这个转换过程也不会影响程序运行。此外,用户可以设置自己的模式,在最高性能模式下使用电池,或在电池优化模式下使用外部电源。为此,用户在屏幕上选择一种模式,而不必重启计算机。

第二代英特尔 SpeedStep 技术(EIST)

EIST 根据处理器负载实时在电压和频率两种性能模式之间进行动态切换。使用这种技术,电池供电的处理器负载会自动切换到最大工作频率和电压。它还可以根据外部电源中的处理器负载,自动切换到最低的工作频率和电压。换句话说,工作频率和电压变化的技术处理不再由电源的类型决定。

第三代英特尔 SpeedStep 技术(改进的 EIST)

除了两种基本的操作模式之外,改进的 EIST 还根据处理器当前负载的强度,提供了多种中间模式,并支持多种频率、速度和电压设置(由处理器电压调节机制控制)。它会自动切换操作模式。

EIST 包括许多软件和硬件技术,以确保其顺利运行,包括系统 BIOS、用户终端软件、ASIC 控制和芯片组支持。软件程序本身不需要做任何改动;它可以很容易地使用这种技术。同时,EIST 还需要操作系统来应对,比如它的处理器负载检测,这是通过操作系统来完成的。

APM 和 ACPI 标准

为了使移动计算系统的低功耗成为可能,硬件和操作系统需要协同工作。协调操作系统和硬件的功耗和电源管理需要一套统一的接口规范。最早的规范是高级电源管理(APM),由英特尔和微软发布;它是一组 API,运行在 IBM 兼容的 PC 操作系统和 BIOS synergy 上,用于管理功耗。目前的规范是高级配置和电源接口(ACPI),它来自于 APM 的发展。

ACPI 是电源管理服务的开放行业标准。它兼容多种操作系统;最初的目标是在个人电脑上使用。ACPI 有电源管理工具和硬件抽象层。操作系统有自己的电源管理模式。它通过 ACPI 向硬件发送需求控制,然后观察硬件状态作为输入,以控制计算机和外围设备的电源。ACPI 在整个计算机系统中的结构如图 13-1 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 13-1。

ACPI structure

ACPI 支持以下五个基本的全球强国:

  • G3:机械关闭状态;该系统不耗电。
  • G2:软关状态;整个操作系统重新启动,将机器恢复到工作状态。该状态有四个子状态:
  • S1:没有系统上下文;缺失低唤醒延迟状态。
  • S2:丢失低 CPU 和系统缓存状态唤醒延迟状态。
  • S3:除了主内存,其他所有系统状态都丢失了;低唤醒延迟状态。
  • S4:低功率睡眠模式;所有设备都已关闭。
  • G1:睡眠状态;系统似乎已关闭;低功率状态。返回正常工作状态所需的时间与低功率状态的功耗成反比。
  • G0:工作状态;该系统完全可用。
  • 保留状态:系统不符合 ACPI。

典型的电源管理程序包括一个查看器,用于查看 ACPI 收到的描述系统行为的消息。还包括一个基于观察的决策模型,用于确定电源管理行为。

流行的操作系统和软件平台,如 Windows 和 Android,都支持 ACPI。

低功耗操作系统状态

当任务空闲时(或处于非活动状态),计算机系统通过进入各种低功率操作模式来实现节能。这些低功率模式有时统称为睡眠模式。它们介于系统完全启动和完全关闭的状态之间,形式多样;每种形式都有自己的特点,以满足用户的各种需求。这些模式将在以下章节中介绍。

备用的

当系统处于待机模式时,它会切断硬件组件的电源,从而降低计算机功耗。待机会切断外围设备、显示器甚至硬盘的电源,但它会保留计算机内存的电源,以确保工作数据不会丢失。

待机模式的主要优点是恢复时间短,系统只需几秒钟就可以恢复到以前的状态。缺点是待机模式需要内存供电,所以内存内容不会保存到文件夹中,因此不会影响内存重载的运行速度。但是,如果在这种模式下发生电源故障,所有未保存的存储内容都将丢失。因此,待机也被称为挂起到 RAM (STR)。

当系统处于待机模式时,硬盘和其他设备处于电源等待状态,直到收到唤醒呼叫。电源、处理器、显卡和其他风扇工作正常,键盘指示灯亮起。您可以按任意键盘键或移动鼠标来唤醒电脑。硬盘重新通电,允许内存、处理器和其他设备交换数据并返回到原始操作模式。

冬眠

当系统处于休眠模式时,操作模式的图像保存到外部存储器,然后关闭计算机。当您打开电源并重新启动时,操作恢复到以前的样子:文件和文档按照您在桌面上留下的样子排列。

休眠模式比待机模式更深入,因此有助于节省更多的电力,但计算机需要更长的时间来重新启动。此外,休眠模式包括更高的安全性。这是因为这种模式不仅会关闭外围设备和硬盘的电源,还会切断 RAM 存储芯片的电源。这种模式也称为磁盘挂起(STD)。

当计算机进入休眠模式时,在关闭电源之前,所有数据都存储(写入)到外部存储器(通常是硬盘)的参考文件中。退出休眠模式后,系统从参考文件中恢复(读取),数据重新加载到内存中。这样,系统恢复到先前的操作模式。因为休眠模式需要保存存储器数据,所以恢复(唤醒)时间比待机模式长。

这种模式的优点是不消耗电力,因此您不必担心睡眠期间的电力异常。它还可以保存和恢复用户状态,但这需要与物理内存大小相同的硬盘空间。

计算机系统的休眠几乎和正常关机一样安静;可以完全断电,内存数据(运行中)不会因为断电而丢失。相比待机,休眠一般很难用外接设备唤醒;它需要通过正常启动来启动系统。然而,休眠模式在不触发常规启动过程的情况下引导系统:它只需要将硬盘存储器镜像读取到存储器中,因此它比标准引导快得多。

睡眠

睡眠模式结合了待机和休眠的所有优点。系统切换到睡眠状态;系统内存中的所有数据都转储到硬盘上的休眠文件中,然后关闭除内存之外的所有设备电源,以便保留内存中的数据。因此,在睡眠期间恢复电力也不例外;您可以直接从内存中的数据快速恢复。如果有电源异常,睡眠时内存中的数据丢失,也可以从硬盘中恢复数据,只是速度稍慢。在任何情况下,这种模式都不会导致数据丢失。

睡眠模式并不总是持续保持。如果系统进入睡眠模式一段时间而未被唤醒,它可能会自动切换到休眠模式,并关闭存储器的电源,以进一步降低能耗。

实现这些低功耗节能功能需要操作系统支持和硬件支持,例如对 ACPI 的支持。只有结合这些特性,您才能实现所述的节能。当空闲时间(也称为非活动时间)达到指定的长度或电池电量低时,操作系统可以自动将您的计算机系统置于低功耗状态,从而为整个系统节省能源。

Linux 电源控制机制

Android 是基于 Linux 的。Linux 有很多分析和降低功耗的实用工具,其中一些已经被 Android 采用。以下部分描述了几种类型的 Linux 电源控制和管理,包括该技术及其组件的许多方面。

轻松空闲

,有时称为非固定频率或无空循环,是 Android Linux 内核中用于提高其省电能力的技术。

传统的 Linux 内核处理器使用周期性的定时器来记录系统的状态,负载平衡,调度和维护各种处理器定时器事件。早期的计时器频率通常为 100 赫兹。新内核使用 250 赫兹或高达 1000 赫兹。然而,当处理器空闲时,这些周期性定时事件消耗大量功率。Tickless idle 消除了处理器中的这种周期性定时器事件,也与其他定时器的优化有关。

使用 tickless idle 后,Linux 内核是一个空的无周期内核。内核仍然记录时间,但是使用不同的方法。不再经常检查是否有工作要做。当内核知道有工作要做时,它调度硬件发出中断请求。Tickless idle 技术在能效方面还有另一个间接好处:您可以更好地利用虚拟技术,这意味着虚拟化软件不会被不必要地或过于频繁地中断。

Tickless idle 为出色的节能提供了必要的内核基础。然而,它也需要与应用的协作。如果应用没有遵循低功耗设计的原则,编写得很糟糕,或者使用了错误的行为,它可能会很容易地消耗或浪费由 tickless idle 带来的电能节省。

PowerTOP(超级用户)

PowerTOP 帮助用户找到在计算机空闲时消耗额外功率的应用。它对于高级软件有着更加突出的作用。以下是 PowerTOP 的功能:

  • 提供建议,帮助用户更好地利用系统的各种硬件节能功能
  • 识别妨碍硬件节能实现最佳性能的罪魁祸首软件模块
  • 帮助开发人员测试他们的应用并实现最佳行为
  • 提供访问低功率的调整建议

图 13-2 为 PowerTOP 运行截图。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 13-2。

PowerTOP interface example

英特尔凌动平台上的许多 Linux 系统,如 Ubuntu,都支持 PowerTOP 工具。图 13-2 显示了在 Ubuntu 中运行的 PowerTOP。Android 目前还不支持这个工具(以后 Android 会不会支持还不知道)。然而,英特尔最近在 Android 上提供了功能类似于 PowerTOP 的工具,如下文所述。

英特尔电源优化辅助工具

英特尔推出了一些辅助工具来帮助 Android 应用的低功耗设计。这些辅助工具在性能优化、VTune 等方面的作用类似于评测器。借助这些工具,您可以对应用的功耗进行工具辅助优化。换句话说,艾滋病提供指导或咨询。为了实现真正的优化,您必须根据低功耗设计原则重写代码(在下面的章节中描述)。

英特尔开发了面向 Android 的英特尔移动开发套件,供系统或中间件开发人员创建 Android 系统或中间件软件,充分利用英特尔平台提供的最新创新。该套件提供基于 x86(英特尔架构)的平板电脑、旨在为该设备无缝创建软件的开发工具,以及有关操作系统、工具、系统软件、中间件和硬件的技术资料。可以在 https://software.intel.com/en-us/intel-mobile-development-kit-for-android 购买套装。

您还可以使用英特尔图形性能分析器(GPA):由英特尔提供的免费低功耗辅助工具,帮助 Android 应用节省功耗。英特尔 GPA 辅助的速度和性能优化特性已在前一章中介绍。本节强调其在电源优化方面的辅助功能。

与机器功耗相关的指标包括 CPU 频率、充电电流、放电电流等。CPU 频率反映了 CPU 列中处理器的工作频率。正如在“功耗基础”一节中提到的,工作频率直接反映了处理器的动态功耗:频率越高,处理器功耗越高。因此,通过观察 CPU 频率,可以分析应用运行时处理器的(动态)功耗。

分析 CPU 频率时,可以将 CPU 栏 XX 频率指示器项中的 CPU 拖放到显示窗口进行观察。图 13-3 显示了一个示例应用 MoveCircle 分析期间的 CPU 频率。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 13-3。

Intel GPA CPU frequency analysis

图 13-3 中的纵轴是 CPU 的工作频率;单位是兆赫(MHz)。在本例中,目标机器是一台联想 K800 智能手机,配有一个英特尔凌动处理器、两个逻辑 CPU 和两个显示窗口。如你所见,当应用有计算任务时,CPU 提高频率以迎合计算的需要;当计算任务较轻时,CPU 会降低工作频率以节省电能。

电流充电和电流放电指示器反映了充电和放电条件。与 CPU 频率不同,这些频率反映了机器的整体功耗。当前放电指示放电状态;这是机器耗电的直接反映,是你要观察的直接目标。但是在 Intel GPA 分析过程中,目标机是通过 USB 线连接到主机的,所以主机变成了电源,正在给目标机(手机)充电。因此,在分析整体机器功耗时,您不应该忽略当前的充电指示灯。

在分析整体机器功耗时,您可以将相应的电流充电(上图)和电流放电(下图)索引条目下的功率条拖放到显示窗口中进行观察。图 13-4 显示了使用样本 MoveCircle 应用对机器充电和放电的分析。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 13-4。

Intel GPA machine overall power analysis

图 13-4 中的纵轴是以毫安(mA)为单位的电流。电压不变的情况下,是功耗的直接反映。当没有应用运行时,充电(电流充电)保持状态的自然波动,放电(电流放电)在低状态下几乎为 0,如图 13-4(b) 所示。应用运行时,由于 CPU 动态功耗的增加,放电不再维持 0 状态。同时放电降低了电荷的价值;这在图 13-4(a) 中可见。当用户锁屏时,屏幕可能会黑屏,正在运行的应用也可能会暂停,快速降低 CPU 的动态功耗;这使得放电几乎回到 0 状态,并且电荷上升。这个过程如图 13-4© 所示。

正如您在前面的图中所看到的,英特尔凌动处理器和 Android 具有负载感应电源管理功能,它们在动态电源管理方面协同工作。当应用没有运行或正在完成低功耗计算任务时,系统会感知到这种变化,硬件(处理器)控制技术会介入并降低功耗。这通常是通过使用 EIST 降低工作频率/电压来实现的。

应用设计中的低功耗考虑

硬件和操作系统为系统的低功耗提供了良好的技术支持,这也可以通过使用适当的管理机制和控制手段来实现。然而,最终的低功耗目标需要应用的密切配合。如果开发的应用没有遵循低功耗设计原则,最终的程序可能不会利用系统的低功耗潜力或浪费功耗,从而抵消硬件和操作系统提供的低功耗技术带来的节能效果。因此,本章强调低功耗要求和原则在应用设计中的重要性。

应用开发中的低功耗设计要求和原则涉及许多技术和方法。让我们检查一下主要的原则和建议。

低功耗优化的最基本原则

低功耗优化最基本的原则是尽量减少处理器和各种外设的工作时间。当不需要外设且不需要处理器操作时,降低功耗的最佳方法是将其关闭。

因为处理器在系统总功耗中使用的比例较大,所以处理器的工作时间需要尽可能的短;它应该在空闲模式或省电模式下工作更长时间。这是降低移动系统功耗的软件设计关键。

一般建议:高性能=低功耗

在固定电压的大多数情况下,短时间内以峰值速度(高频率)运行并长时间处于深度怠速状态比以中等工作频率长时间运行并处于轻度怠速状态更节能。因此,对于相同的任务,如果您的应用在尽可能短的时间内运行完成,然后进入空闲状态,则比在进入短暂空闲状态之前运行更长时间来完成要消耗更少的电力。

快速算法也可以降低功耗,这符合高性能等于低功耗的建议。

尽可能使用低功耗硬件来完成任务

相同的任务可以用不同类型的硬件完成,不同的硬件有不同的功耗开销。当您的应用可以选择不同的硬件来运行相同的任务时,您应该选择低功耗硬件。

一般来说,寄存器访问的能耗是最低的;并且缓存访问的能耗低于主存储器访问的能耗。因此,程序设计应该尽量遵循这些建议:

  • 尽可能有效地使用登记册。
  • 分析缓存的行为以发现主要的缓存冲突。
  • 在存储系统中尽可能使用页面模式访问。

轮询是低功耗优化的敌人

等待状态改变或访问外围设备的程序可以使用轮询;这种方法有时被称为快速旋转或旋转代码。轮询允许处理器重复执行一些指令。功耗大致等于繁重的计算任务,它的作用只是等待一个状态的改变;但是等待期不能让处理器进入空闲状态,导致大量的电能浪费。因此,在低功耗设计中,您应该尽量避免使用轮询,而是使用替代方法。例如,你应该使用中断而不是轮询访问外设。在客户机/服务器协作模型中,您应该更改客户机查询服务,让服务器主动向客户机推送服务。对于线程同步,如果需要查询状态变化,应该使用操作系统事件或信号量。

例如,假设线程 2 想要访问一个资源。访问能力由访问控制变量canGo决定。线程 1 负责变量canGo的开或关访问控制。如果这是通过轮询语句实现的,线程代码可能如下所示:

volatile boolean canGo = false;            // Shared variables

// The code of thread 1                    // The code of thread 2

void run()                                 void run()

{                                          {

canGo = true;                               while (!canGo);

// Allow thread 2 to access a resource      // Wait canGo Change to true// Access to the resource code

}                                           }

在前面的代码中,Thread 2 while语句是典型的轮询;防止进入空闲睡眠状态会消耗大量处理器时间。您可以更改为 Java 等待通知机制来实现相同的功能:

volatile boolean canGo = false;

Object sema;                      // The synchronization lock canGo variable

// The code of thread 1           // The code of thread 2

void run()                         void run()

{                                  {

synchronized(sema){                synchronized(sema){

canGo = true; // Allow thread 2 to access a resource       while (!canGo)

sema.notifyAll()                    sema.wait();

}                                  }

// Access to the resource code

}                                  }

被 wait-notify 代码替换后,线程 2 没有轮询语句的快速旋转:每次循环检查canGo变量,如果不满足条件,就进入挂起状态,释放 CPU。因此,CPU 负载不会浪费在线程上。当 CPU 没有其他任务时,负载很快下降到低状态。当检测到处理器的低负载时,系统会采取措施降低功耗。在优化前快速轮换轮询模式无法做到这一点。

事件驱动编程

除了实现软件设计方法之外,如果可能,低功率程序应该总是遵循程序设计的事件驱动模型。事件驱动编程意味着程序被设计成响应事件:当事件到来时,应用运行来处理事件;当没有事件到达或事件结束时,程序放弃处理器并进入睡眠状态。这里的事件被称为广义事件,包括用户输入、网络通信事件和进程/线程同步事件。

当使用事件驱动的设计过程时,处理器利用率特别高:程序只在有真正的事情要处理时才运行,在无事可做时才释放处理器。当处理器处于空闲状态时,操作系统和硬件可以及时检测到空闲,并启动操作以降低功耗。

减少应用中类似轮询的周期性操作

前面您已经看到轮询操作消耗了不必要的能量。周期性触发或运行操作的不必要编程可能具有类似于轮询的效果,并且不必要地消耗功率。

如前所述,Tickless idle 是遵循这一原则的操作系统内核改进;它从内核中移除周期性定时操作。此外,Linux 应用有许多不必要的周期性触发器或运行操作,例如:

  • 鼠标移动,每秒一次。这是屏保中常用的。
  • 音量变化,每秒 10 次。这是调音台程序中常用的。
  • 下一分钟,每秒一次。这个时钟程序是常用的。
  • USB 读卡器,每秒 10 次。这个守护进程是常用的。
  • 其他变化的应用数据和条件:
  • 每秒超过 10 次(网络浏览器)
  • 每秒 30 次以上(GPS 信号采集应用)
  • 每秒超过 200 次(Flash 插件)

这些不必要的触发和操作导致系统从空闲状态唤醒。移植到 Android 上,这样的操作要注意,小心避免或改进;否则,它们很容易抵消节省的功耗。

针对数据采集和通信的低功耗建议

在设计通信模块时,尽量提高通信速率。当通信速率提高时,意味着通信时间缩短,高功率通信减少,从而降低总功耗。

同样,在使用 Wi-Fi 通信时,应该使用突发模式传输数据,这样可以缩短通信时间(尤其是发送数据时)。Wi-Fi 设备很容易尽快进入空闲状态。

建立节能计划

Android 设备的电源通常在连接外部电源和使用电池电源之间切换。两种功率状态下软件的功率要求完全不同:前者对功率不敏感,但要求大多数时间优先考虑性能;后者对功耗敏感,因此需要在性能和功耗之间取得平衡。因此,应用应该检测电源的类型,并做出调整以适应与电源相关的变化。

此外,一些电源管理因素可能会影响软件行为,例如当设备的电池低于某个阈值时,设备会进入关闭状态和自动睡眠,等等。应用设计应考虑这些电源管理事件带来的环境变化,关注这些因素可能产生的影响,并做出适当的响应。例如,正在进行的耗时操作处理(如冗长的浮点操作、查询循环系统和复杂的图形再现)可能会被电源管理事件中断或挂起。对策之一是保存场景并确保环境有时间从中断状态中恢复。

此外,你还可以开发一种针对权力的防御性编程,比如提前考虑或预测用户将启动什么样的任务或应用(比如播放电影);或者预先确定是否有足够的电池电量来完成任务,如果没有,则在任务开始时警告用户。

案例研究 1:面向 Android 应用的英特尔 GPA 辅助电源优化

下面是一个案例研究,展示了一种全面的方法,该方法使用英特尔 GPA 功耗分析工具根据低功耗设计原则重写和优化高功耗应用。

原始应用和英特尔 GPA 功耗分析

示例应用运行一段指定的时间(20 秒)。这个没有低功耗优化代码的应用叫做PollLast。应用设计要求它通过 Java System类的静态函数currentTimeMillis获取当前时间;当前时间加上程序中指定的运行时间等于程序运行的结束时间。然后,程序在循环函数currentTimeMillis中获取当前时间,并将其与程序结束时间进行比较。如果当前时间超过程序结束时间,程序结束循环并结束程序。因为整个任务需要很长时间来处理,所以您将程序作为工作线程运行,这样它就不会影响主界面的响应。主界面控制任务的开始。

该应用的操作界面如图 13-5 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 13-5。

Separate PollLast applications running in the interface

创建应用的主要步骤如下:

  1. 创建一个名为PollLast的新项目。将建议的项目属性设置为使用默认值,并选择支持 x86 API 的 Build SDK 版本。

  2. 编辑主布局文件,在布局上放置两个Button和两个TextView;一个用于显示任务线程的运行状态,如图 13-6 所示。

  3. 创建一个新的任务线程类MyTaskThread,它将在指定的时间运行。编辑源代码文件MyTaskThread.java,如下所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 13-6。

The main layout of the  application

1\. package com.example.polllast;

2\. import android.os.Handler;

3\. import android.os.Message;

4\. public class MyTaskThread extends Thread {

5.  private Handler mainHandler;

6.  public static final int MSG_FINISHED = 1;

7.  public static final int lasthour = 0;    // The number of hours the program is running

8.  public static final int lastmin = 0;     // The number of minutes of the program to run

9.  public static final int lastsec = 20;    // The number of seconds the program is running

10\. @Override

11\. public void run()

12\. {

13.     long start_time = System.currentTimeMillis();

14.     long millisecduration = ((lasthour * 60 + lastmin) * 60 + lastsec)*1000;

15.         long endtime = start_time + millisecduration;

16.         long now;

17.         do {

18.             now = System.currentTimeMillis();      // Polling

19.         }    while (now < endtime);

20.         Message msg = new Message();

21.         msg.what = MSG_FINISHED;

22.         mainHandler.sendMessage(msg);

23.     }

24.     public MyTaskThread(Handler mh)

25.     {

26.         super();

27.         mainHandler = mh;

28.     }

29\. }

灰色背景标记进行更改的主要代码段。在第 7–9 行,您分别指定三个常量lasthourlastminlastsec,作为任务的运行时间,以小时、分钟和秒为单位。第 13 行和第 14 行的代码是该任务的核心部分。在第 13–15 行,您设置了任务的开始时间、持续时间和结束时间,以毫秒为单位。第 16 行定义了当前时间变量now。在第 17–19 行,您使用一个循环来轮询和比较时间。每个周期首先获取当前时间,然后与结束时间进行比较;如果当前时间大于结束时间,则循环结束。

这是典型的轮询代码。循环体只是一个查询当前时间的语句,所以循环非常快,消耗大量处理器计算资源。

  1. 编辑主活动类源代码文件MainActivity.java,让它控制运行任务线程。代码部分几乎与MainActivity示例的SerialPi类代码相同(参见第八章)。
  2. 修改项目的AndroidManifest.xml文件,以满足英特尔 GPA 监控要求。

现在,您可以将应用部署到目标机器上。这个例子使用联想 K800 手机作为测试目标。

图 13-7 和图 13-8 显示了使用英特尔 GPA 的分析。这个例子分析了主监视器的 CPU 频率(CPU XX 频率指示器)和充电或放电(当前充电和当前放电指示器)。单击开始运行按钮,开始运行任务并记录英特尔 GPA 监控信息。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 13-8。

PollLast Intel GPA charge/discharge analysis

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 13-7。

PollLast Intel GPA CPU frequency analysis

从图 13-7 中的 CPU 频率图可以看到,启动任务后 CPU 频率从 600 MHz 跃升至 1.6 GHz,运行任务后又回落至 600 MHz。当然,当任务运行时,两个逻辑 CPU 频率都不会跳到 1.6 GHz:它们有互补关系。当任务运行时,只有一个 CPU 频率跳到最高值。这种互补效果的主要原因是这个示例任务只有一个工作线程。

机器的电荷如图 13-8 所示,为放电条件的地图视图。任务开始前,放电维持在 400 mA 以下,如图 13-8(a) 所示。开始这项任务后,放电跃至 550 毫安以上。运行任务后,放电水平回到 400 mA 或更低。手机在运行之前是充满电的,所以整个示例过程总是在大约 0 的低状态下充电。放电反映了机器在相同总功耗下的充电水平。运行该任务导致功耗急剧增加。

优化的应用和英特尔 GPA 功耗分析

通过对PollLast应用的代码分析,您知道使用轮询语句会导致机器功耗增加,尤其是在MyTaskThread.java的第 17–19 行。您需要通过应用前面描述的低功耗应用设计原则来重写这个数据段,并更改轮询代码。您可以创建一个优化的解决方案,让线程在指定的时间休眠,而不是轮询。该应用是基于PollLast的改进版本,有以下变化:

  1. 创建一个新项目SleepLast。将建议的项目属性设置为使用默认值,并选择支持 x86 API 的构建 SDK。
  2. PollLast主布局文件复制到项目中,并替换项目文件的原始布局。
  3. 将原始应用MyTaskThread.java复制到该项目,并对其内容进行如下修改:

1\. package com.example.sleeplast;

2\. import android.os.Handler;

3\. import android.os.Message;

4\. public class MyTaskThread extends Thread {

5.     private Handler mainHandler;

6.     public static final int MSG_FINISHED = 1;

7.     public static final int lasthour = 0;        // The number of hours run

8.     public static final int lastmin = 0;         // The number of minutes run

9.     public static final int lastsec = 20;        // The number of seconds to run

10.     @Override

11.     public void run()

12.     {

13.         long millisecduration = ((lasthour * 60 + lastmin) * 60 + lastsec)*1000;

14.         try {

15.             Thread.sleep(millisecduration);

16.         } catch (InterruptedException e) {

17.             e.printStackTrace();

18.         }

19.         Message msg = new Message();

20.         msg.what = MSG_FINISHED;

21.         mainHandler.sendMessage(msg);

22.     }

23.     public MyTaskThread(Handler mh)

24.     {

25.         super();

26.         mainHandler = mh;

27.     }

28\. }

第一行代码是应用包的声明。

主要变化来自第 13–18 行。这里您使用Thread类的静态函数sleep来指定线程应该休眠多长时间。应用在第 13 行以毫秒为单位计算睡眠时间。因为sleep可能抛出一个InterruptedException异常,所以你把函数放到一个语句块中。

  1. 从原始申请中复制MainActivity.java以覆盖相同的文件。将其包装声明行更改为

package com.example.sleeplast;

  1. 修改项目的AndroidManifest.xml文件,以符合英特尔 GPA 监控要求。

现在,您可以将应用部署到目标机器上。同样,这个例子使用了联想 K800。

在现实世界中,您只需要修改原始应用的源代码就可以实现低功耗优化,而不需要创建单独的应用。例如,在这种情况下,您只需要执行步骤 3。这个例子创建了一个应用的优化版本来突出不同之处。

按照与原始应用相同的步骤,您可以使用英特尔 GPA 来分析优化的应用。结果如图 13-9 和图 13-10 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 13-10。

Intel GPA charge/discharge analysis of

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 13-9。

Intel GPA CPU frequency analysis of the  application

在图 13-9 中,对比任务尚未开始时的图形(图 13-9(a) 和任务完成时的图形(图 13-9© ):任务运行时处理器频率不变(图 13-9(b) )。本质上,所有三种状态具有相同的频率,并保持在 600 MHz 的低水平。这反映了处理器在处理之前、期间和之后的动态功耗没有显著变化,并且保持了低负载水平。

图 13-10 反映了整机功耗,也是一致的。在开始任务之前(图 13-10(a) ),在应用运行期间(图 13-10(b) ),以及在结束时(图 13-10© ),放电维持在大约 0 的低水平。代表充电的图形在应用运行之前、期间或之后没有显著变化。与导致显著整体功耗的PollLast相比,优化的SleepLast应用实现了优化的省电结果。

案例研究 2:定时器优化和英特尔 GPA 功耗分析

本节介绍另一种功耗优化解决方案:定时器方法。你使用 Java 的TimerTimerTask来实现一个定时器。计时器测量指定的时间,并在指定的时间过去时通知任务应该结束。

按照以下步骤创建应用:

  1. 创建一个名为TimerLast的新项目。将建议的项目属性设置为使用默认值,并选择支持 x86 API 的 Build SDK 版本。
  2. 将主布局文件从PollLast复制到该项目,并替换布局文件。
  3. PollLast中的MainActivity.java复制到该项目中,并对其内容进行如下修改:

1.  package com.example.timerlast;

2.  import android.os.Bundle;

3.  import android.app.Activity;

4.  import android.view.Menu;

5.  import android.widget.Button;

6.  import android.view.View;

7.  import android.view.View.OnClickListener;

8.  import android.os.Process;

9.  import android.widget.TextView;

10\. import android.os.Handler;

11\. import android.os.Message;

12\. import java.util.Timer;

13.

14\. public class MainActivity extends Activity {

15.     private TextView tv_TaskStatus;

16.     private Button btn_ExitApp;

17.     private Handler mHandler;

18.     private Timer timer =null;            // Timer

19.

20.     @Override

21.     public void onCreate(Bundle savedInstanceState) {

......

35.         final Button btn_StartTask = (Button) findViewById(R.id.startTask);

36.         btn_StartTask.setOnClickListener(new /*View.*/OnClickListener(){

37.             public void onClick(View v) {

38.                 btn_StartTask.setEnabled(false);

39.                 btn_ExitApp.setEnabled(false);

40.                 tv_TaskStatus.setText("Task operation...");

41.                  startTask();

42.              }

43.         });

......

58.     }

......

66.     private void startTask() {

67.            long millisecduration =

68.             ((MyTaskTimer.lasthour * 60 + MyTaskTimer.lastmin) * 60 + MyTaskTimer.lastsec)*1000;

69.         timer = new Timer();              // Creating Timer

70.         timer.schedule(new MyTaskTimer(mHandler), millisecduration);                // Set the timer

71.     }

......

79\. }

第 35–43 行是单击开始运行按钮时的响应代码。关键代码行是第 41 行,它调用自定义函数startTask。第 66–71 行实现了这个功能代码。程序首先计算计时的总毫秒数。在第 69 行,创建了计时器。第 70 行设置计时器,并在计时结束时回调MyTaskTimer对象。

  1. 创建一个新类,让它从TimerTask类继承。它负责通知活动接口任务已经完成。编辑源代码文件MyTaskTimer.java,如下所示:

1\. package com.example.timerlast;

2\. import java.util.TimerTask;      // TimerTask classes using Java

3\. import android.os.Handler;

4\. import android.os.Message;

5.

6\. public class MyTaskTimer extends TimerTask {

7.     private Handler mainHandler;

8.     public static final int MSG_FINISHED = 1;

9.     public static final int lasthour = 0;   // The task of operating hours

10.     public static final int lastmin = 0;   // The task of operating minutes

11.     public static final int lastsec = 20;  // The task of operating seconds

12.

13.     public MyTaskTimer(Handler mh)

14.     {

15.         super();

16.         mainHandler = mh;

17.     }

18.

19.     @Override

20.     public void run(){

21.         Message msg = new Message();

22.         msg.what = MSG_FINISHED;         // Defined message types

23.         mainHandler.sendMessage(msg);    // Send a message

24.     }

25\. }

根据 Java 定时器框架,当定时器到期时,运行TimerTask的程序回调函数。前面的代码让MyTaskTimer类从TimerTask继承,并允许自定时计时器的代码在run函数中终止。在这种情况下,第 19–24 行包含回调代码,表示计时完成,并向主界面发送“完成”消息。主界面在其自己的处理程序中响应此消息,并显示任务结束的消息。

现在,您可以将应用部署到目标机器上。与之前一样,本例使用了一款搭载英特尔凌动处理器的联想 K800 智能手机。

按照与前面相同的步骤,您可以使用英特尔 GPA 来分析优化的应用,记录 GPA 监控信息,并分析结果,如图 13-11 和图 13-12 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 13-12。

Intel GPA charge/discharge analysis of

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 13-11。

Intel GPA CPU frequency analysis of

图 13-11 中所示的频率曲线图与图 13-9 中的SleepLast曲线图相似。运行任务后,处理器频率在图 13-11(b) 中没有变化,并且与任务开始前(图 13-11(a) )和任务结束后(图 13-11© )的频率基本相同。它停留在低 600 兆赫的范围。唯一的区别是在任务结束时偶然故障的增加(图 13-11© )。在处理之前、之中和之后,处理器的动态功耗没有显著变化:它保持低负载水平。

图 13-12 显示整机功耗与图 13-11 一致。当然,TimerLast图没有图 13-10 中显示的漂亮,图中显示了SleepLast的性能——放电图总是有一些小故障。然而,该指标在任务之前、任务运行期间以及任务完成之后没有显著变化。这证明运行该任务不会导致额外的功耗。与导致显著整体功耗的PollLast相比,优化后的TimerLast应用实现了优化的省电结果。

书籍摘要

在本书中,您学习了如何在英特尔凌动平台上开发和优化 Android 应用,以及如何开发节能应用。以下是关键概念的总结:

  • 大多数用 Java 编写的 Android 应用可以直接在英特尔凌动平台上执行。NDK 应用需要重新编译本机代码。如果应用中包含汇编代码,这部分代码必须重写。
  • 充分利用英特尔架构特性来提高您的 Android 应用性能。
  • 添加特定于平台的编译开关,使 GCC 构建代码更加有效。
  • 英特尔提供了各种有用的工具来帮助 Android 开发者。其中许多都专注于提高性能,可以帮助您优化应用。

创建 Android 应用的常用方法如下:

  • 用 Java 编译的 Android SDK APIs,运行在 Dalvik VM 上。谷歌将在 2014 年底为新的 Android L OS 发布新的 Android 运行时(ART)。
  • 使用最新的 SDK,如果您使用英特尔 HAXM 加速您的 Android 仿真软件,测试速度会更快。
  • 在 NDK 制造或者移植到那里。如果您有 C++ 代码,这是首选方法。原生 C++ 代码在执行前被编译成二进制,不需要翻译成机器语言。

如果您没有 Android 开发环境(IDE),新的工具套件英特尔集成本地开发人员体验(INDE)将加载选定的 Android IDE,并下载和安装多个英特尔工具,以帮助您制作、编译、故障排除和发布 Android 应用。前往 https://software.intel.com/en-us/android 下载并使用那些工具。你也可以访问这本书的网页来了解更新和任何发布的勘误表: www.apress.com/9781484201015

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Open Access This chapter is licensed under the terms of the Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License ( http://​creativecommons.​org/​licenses/​by-nc-nd/​4.​0/​ ), which permits any noncommercial use, sharing, distribution and reproduction in any medium or format, as long as you give appropriate credit to the original author(s) and the source, provide a link to the Creative Commons licence and indicate if you modified the licensed material. You do not have permission under this licence to share adapted material derived from this chapter or parts of it. The images or other third party material in this chapter are included in the chapter’s Creative Commons licence, unless indicated otherwise in a credit line to the material. If material is not included in the chapter’s Creative Commons licence and your intended use is not permitted by statutory regulation or exceeds the permitted use, you will need to obtain permission directly from the copyright holder.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值