安卓-NDK-秘籍-全-

安卓 NDK 秘籍(全)

原文:zh.annas-archive.org/md5/7FB9DA0CE2811D0AA0DFB1A6AD308582

译者:飞龙

协议:CC BY-NC-SA 4.0

序言

自 2008 年首次发布以来,安卓已成为世界上最大的移动平台。预计到 2013 年中,Google Play 中的应用总数将达到 100 万。大多数安卓应用都是使用安卓软件开发工具包(SDK)的 Java 编写的。许多开发者即使有 C/C++经验,也只编写 Java 代码,没有意识到他们放弃了一个多么强大的工具。

安卓原生开发工具包NDK)于 2009 年发布,旨在帮助开发者编写和移植原生代码。它提供了一套交叉编译工具和一些库。使用 NDK 编程有两个主要优点。首先,你可以用原生代码优化你的应用程序并提升性能。其次,你可以复用大量的现有 C/C++代码。《Android Native Development Kit》是一本实用的指南,帮助你使用 NDK 编写安卓原生代码。我们将从基础内容开始,例如Java 原生接口JNI),并构建和调试一个原生应用程序(第 1 至 3 章)。然后,我们将探讨 NDK 提供的各种库,包括 OpenGL ES、原生应用程序 API、OpenSL ES、OpenMAX AL 等(第 4 至 7 章)。之后,我们将讨论如何使用 NDK 将现有应用程序和库移植到安卓(第 8 和 9 章)。最后,我们将展示如何使用 NDK 编写多媒体应用程序和游戏(附加章节 1 和 2)。

本书涵盖内容

第一章,Hello NDK,介绍了如何在 Windows、Linux 和 MacOS 中设置安卓 NDK 开发环境。我们将在本章末尾编写一个“Hello NDK”应用程序。

第二章,Java Native Interface,详细描述了 JNI 的使用方法。我们将从 Java 代码中调用原生方法,反之亦然。

第三章,Build and Debug NDK Applications,展示了如何从命令行和 Eclipse IDE 构建原生代码。我们还将探讨使用gdbcgdb、eclipse 等调试原生代码的方法。

第四章,Android NDK OpenGL ES API,阐述了 OpenGL ES 1.x 和 2.0 API。我们将涵盖 2D 绘图、3D 图形、纹理映射、EGL 等内容。

第五章,Android Native Application API,讨论了安卓原生应用程序 API,包括管理原生窗口、访问传感器、处理输入事件、管理资源等。在本章中,我们将看到如何编写一个纯粹的原生应用程序。

第六章,Android NDK Multithreading,描述了安卓多线程 API。我们将涵盖创建和终止原生线程、各种线程同步技术(互斥量、条件变量、信号量以及读写锁)、线程调度和线程数据管理。

第七章,其他 Android NDK API,讨论了一些额外的 Android 库,包括jnigraphics图形库,动态链接库,zlib压缩库,OpenSL ES 音频库,以及 OpenMAX AL 媒体库。

第八章,使用 Android NDK 移植和使用现有库,描述了使用 NDK 移植和使用现有 C/C++库的各种技术。在章节的最后,我们将移植boost库。

第九章,使用 NDK 将现有应用移植到 Android,提供了分步指南,用于使用 NDK 将现有应用移植到 Android。我们以一个开源的图像调整大小程序为例。

第一章附录使用 NDK 开发多媒体应用,演示了如何使用ffmpeg库编写多媒体应用。我们将移植ffmpeg库,并使用库 API 编写一个帧抓取器应用。

第二章附录使用 NDK 开发游戏,讨论了使用 NDK 编写游戏。我们将移植《德军总部 3D》游戏,以展示如何为游戏设置显示、添加游戏控制以及启用音频效果。

你可以从以下链接下载附录章节:www.packtpub.com/sites/default/files/downloads/Developing_Multimedia_Applications_with_NDK.pdfwww.packtpub.com/sites/default/files/downloads/Developing_Games_with_NDK.pdf

阅读本书所需的知识

需要一台安装了 Windows、Ubuntu Linux 或 MacOS 的计算机(推荐使用 Linux 或 MacOS)。虽然我们可以使用模拟器运行 Android 应用,但对于 Android 开发来说既慢又低效。因此,建议使用 Android 设备。

本书假设读者具备 C 和 C++编程语言的基础知识。你也应该熟悉 Java 和 Android SDK。

请注意,除非另有说明,本书的示例代码基于 Android ndk r8,因为这是本书撰写时的最新版本 NDK。到本书出版时,应该会有更新的版本。代码也应该在任何新版本上运行。因此,我们可以安装 NDK r8 或更高版本。

本书适合的读者

本书面向任何对为 Android 编写原生代码感兴趣的人。章节从基础到中级再到高级排列,相对独立。对于 NDK 的新手,建议从头到尾阅读,而熟悉 NDK 的读者可以选择任何特定的章节,甚至是特定的食谱。

编写约定

在这本书中,你会发现多种文本样式,用于区分不同类型的信息。以下是一些样式示例,以及它们含义的解释。

文本中的代码字如下所示:"Windows NDK 带有一个新的 ndk-build.cmd 构建脚本。"

代码块设置如下:

#include <string.h>
#include <jni.h>

jstring 
Java_cookbook_chapter1_HelloNDKActivity_naGetHelloNDKStr(JNIEnv* pEnv,  jobject pObj)
{
    return (*pEnv)->NewStringUTF(pEnv, "Hello NDK!");
}

当我们希望引起你注意代码块的某个部分时,相关的行或项目将以粗体显示:

LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE  := framegrabber
LOCAL_SRC_FILES := framegrabber.c
#LOCAL_CFLAGS := -DANDROID_BUILD
LOCAL_LDLIBS := -llog -ljnigraphics -lz  
LOCAL_STATIC_LIBRARIES := libavformat_static libavcodec_static libswscale_static libavutil_static
include $(BUILD_SHARED_LIBRARY)
$(call import-module,ffmpeg-1.0.1/android/armv5te)

命令行输入或输出将如下书写:

$sudo update-java-alternatives -s <java name>

新术语重要词汇以粗体显示。你在屏幕上看到的词,例如菜单或对话框中的,会在文本中以这样的形式出现:"转到控制面板 | 系统和安全 | 系统 | 高级系统设置。"

注意

警告或重要说明将如下框所示。

提示

提示和技巧如下所示。

读者反馈

我们始终欢迎读者的反馈。告诉我们你对这本书的看法——你喜欢或可能不喜欢的内容。读者的反馈对我们开发能让你们充分利用的标题非常重要。

要给我们发送一般反馈,只需发送电子邮件至<feedback@packtpub.com>,并在邮件的主题中提及书名。

如果你对某个主题有专业知识,并且有兴趣撰写或参与书籍编写,请查看我们在www.packtpub.com/authors的作者指南。

客户支持

既然你现在拥有了 Packt 的一本书,我们有很多方法可以帮助你充分利用你的购买。

下载示例代码

你可以从你在www.packtpub.com的账户下载你所购买的所有 Packt 图书的示例代码文件。如果你在其他地方购买了这本书,可以访问www.packtpub.com/support注册,我们会直接将文件通过电子邮件发送给你。

错误更正

尽管我们已经尽力确保内容的准确性,但错误仍然会发生。如果你在我们的书中发现了一个错误——可能是文本或代码中的错误——如果你能报告给我们,我们将不胜感激。这样做,你可以让其他读者免受挫折,并帮助我们改进本书的后续版本。如果你发现任何错误更正,请通过访问www.packtpub.com/submit-errata,选择你的书,点击错误更正 提交 表单链接,并输入你的错误更正详情。一旦你的错误更正得到验证,你的提交将被接受,并且错误更正将在我们网站的相应位置上传,或添加到现有错误更正列表中。任何现有的错误更正可以通过从www.packtpub.com/support选择你的标题来查看。

盗版

网络上的版权材料盗版问题在所有媒体中持续存在。在 Packt,我们非常重视对我们版权和许可的保护。如果您在网上以任何形式遇到我们作品的非法副本,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。

如果您发现疑似盗版材料,请通过<copyright@packtpub.com>联系我们,并提供链接。

我们感谢您在保护我们的作者以及我们为您提供有价值内容的能力方面的帮助。

问题咨询

如果您在阅读本书的任何方面遇到问题,可以通过<questions@packtpub.com>联系我们,我们将尽力解决。

第一章:NDK 你好

在本章中,我们将涵盖以下内容:

  • 在 Windows 上搭建 Android NDK 开发环境

  • 在 Ubuntu Linux 上搭建 Android NDK 开发环境

  • 在 Mac OS 上搭建 Android NDK 开发环境

  • 更新 Android NDK

  • 编写一个 Hello NDK 程序

引言

Android NDK 是一个工具集,允许开发者在原生语言(如 C、C++和汇编)中实现部分或整个 Android 应用程序。在我们开始 NDK 之旅之前,理解 NDK 的优势很重要。

首先,NDK 可能提高应用程序性能。这对于许多处理器受限的应用程序通常是正确的。许多多媒体应用程序和视频游戏使用原生代码处理处理器密集型任务。

性能提升可以来自三个源头。首先,原生代码被编译成二进制代码,直接在操作系统上运行,而 Java 代码则被翻译成 Java 字节码,由 Dalvik 虚拟机VM)解释执行。在 Android 2.2 或更高版本中,Dalvik VM 加入了一个即时编译器JIT)来分析优化 Java 字节码(例如,JIT 可以在执行前将部分字节码编译成二进制代码)。但在许多情况下,原生代码的运行速度仍然快于 Java 代码。

提示

Java 代码在 Android 上的 Dalvik VM 中运行。Dalvik VM 是专门为硬件资源受限的系统(如内存空间、处理器速度等)设计的。

NDK 性能提升的第二个来源是原生代码允许开发者使用 Android SDK 无法访问的一些处理器特性,例如 NEON,这是一种单指令多数据SIMD)技术,可以同时处理多个数据元素。一个特定的编码任务示例是对视频帧或照片的颜色转换。假设我们要将一个 1920x1280 像素的照片从 RGB 颜色空间转换为 YCbCr 颜色空间。最简单的方法是对每个像素应用转换公式(即超过两百万个像素)。使用 NEON,我们可以一次处理多个像素,以减少处理时间。

第三点是,我们可以从汇编层面优化关键代码,这在桌面软件开发中是一种常见做法。

提示

使用原生代码的优势并非没有代价。调用 JNI 方法为 Dalvik VM 引入了额外的工作,并且由于代码是编译过的,无法应用运行时优化。实际上,使用 NDK 开发并不保证性能提升,有时甚至可能损害性能。因此,我们只是说它可能提高应用程序的性能。

NDK 的第二个优势是它允许将现有的 C 和 C++代码移植到 Android。这不仅显著加快了开发速度,还允许我们在 Android 和非 Android 项目之间共享代码。

在我们决定为 Android 应用使用 NDK 之前,需要了解的是 NDK 并不会让大多数 Android 应用受益。仅仅因为个人偏好 C 或 C++编程而非 Java,并不建议使用 NDK。NDK 不能直接访问 Android SDK 中提供的许多 API,而且使用 NDK 进行开发总会为你的应用引入额外的复杂性。

了解 NDK 的优缺点后,我们可以开始 Android NDK 的旅程。本章将介绍如何在 Windows、Ubuntu Linux 和 Mac OS 中设置 Android NDK 开发。对于之前设置过 Android NDK 开发环境的开发者,提供了一个详细步骤的食谱,介绍如何更新 NDK 开发环境。在章节的最后,我们将使用设置好的环境编写一个 Hello NDK 程序。

在 Windows 中设置 Android NDK 开发环境

在本节中,我们将探讨如何在 Windows 中设置 Android NDK 开发环境。

准备就绪

检查 Windows 版本和系统类型。Android 开发环境可以在 Windows XP 32 位、Windows Vista 32 位或 64 位以及 Windows 7 32 位或 64 位上设置。

Android 开发需要安装 Java JDK 6 或更高版本。按照以下步骤安装和配置 Java JDK:

  1. 访问 Oracle Java JDK 网页 www.oracle.com/technetwork/java/javase/downloads/index.html,选择适合你平台的 JDK6 或更高版本进行下载。

  2. 双击下载的可执行文件,按照安装向导点击以完成安装。

  3. 前往控制面板 | 系统和安全 | 系统 | 高级系统设置。将会弹出一个系统属性窗口。

  4. 高级选项卡中点击环境变量按钮;另一个环境变量窗口将会弹出。

  5. 系统变量下,点击新建,添加一个名为JAVA_HOME的变量,其值为 JDK 安装根目录的路径。如下所示:准备就绪

  6. 系统变量下,滚动到PATH(或Path)环境变量。在值的开头插入%JAVA_HOME%\bin;。如果不存在PATHPath变量,创建一个新变量,其值为%JAVA_HOME%\bin。一路点击确定,关闭所有窗口。

  7. 要验证 JDK 是否已正确安装和配置,请启动一个新的命令行控制台,并输入javac -version。如果 JDK 配置正确,你将在输出中得到 Java 版本。准备就绪

Cygwin是 Windows 上的一个类 Linux 环境,用于运行 Linux 上可用的软件。Android NDK 开发需要安装 Cygwin 1.7 或更高版本,以便执行某些 Linux 程序,例如 GNU make。

自 NDK r7 起,Windows NDK 附带了一个新的ndk-build.cmd构建脚本,该脚本使用 NDK 预构建的 GNU make、awk 和其他工具的二进制文件。因此,使用ndk-build.cmd构建 NDK 程序时不需要 Cygwin。但是,建议您仍然安装 Cygwin,因为ndk-build.cmd是一项实验性功能,调试脚本ndk-gdb仍然需要 Cygwin。

按照以下步骤安装 Cygwin:

  1. 前往cygwin.com/install.html下载 Cygwin 的setup.exe。下载完成后双击它以开始安装。

  2. 点击下一步,然后选择从互联网安装。继续点击下一步,直到你看到可用的下载站点列表。选择离你位置最近的站点,然后点击下一步准备就绪

  3. 开发下查找 GNU make,确保它是 3.81 或更高版本,以及在基础下的gawk。或者,您也可以使用搜索框搜索 make 和 gawk。确保 GNU make 和 gawk 都被选中安装,然后点击下一步。安装可能需要一段时间才能完成:准备就绪

Eclipse 是一个功能强大的软件集成开发环境IDE),具有可扩展的插件系统。它是开发 Android 应用的推荐 IDE。前往www.eclipse.org/downloads/,下载 Eclipse Classic 或适用于 Java 开发者的 Eclipse IDE。解压压缩文件后即可使用。请注意,Android 开发需要 Eclipse 3.6.2(Helios)或更高版本。

提示

Android 开发者网站在developer.android.com/sdk/index.html提供了一个 Android Developer Tools 捆绑包。它包括带有 ADT 插件的 Eclipse IDE 和 Android SDK。我们可以下载这个捆绑包,并跳过以下如何操作…部分 1 到 10 步的 SDK 安装。

如何操作…

以下步骤将展示如何在 Windows 中设置 Android NDK 开发环境。我们首先将设置一个 SDK 开发环境。如果已经设置好了 SDK,可以跳过 1 到 10 步。

  1. 启动 Eclipse。选择帮助 | 安装新软件,会弹出一个名为安装的窗口。

  2. 点击位于右上角的添加…按钮,会弹出一个名为添加仓库的窗口。

  3. 添加仓库窗口中,为名称输入ADT,为位置输入dl-ssl.google.com/android/eclipse/。然后点击确定

  4. Eclipse 可能需要几秒钟从 ADT 网站加载软件项。加载后,选择开发者工具NDK 插件,然后点击下一步继续操作:如何操作…

  5. 在下一个窗口中,将显示要安装的工具列表。只需点击 下一步。阅读并接受所有许可协议,然后点击 完成

  6. 安装完成后,按照提示重启 Eclipse

  7. developer.android.com/sdk/index.html 下载 Android SDK。

  8. 双击安装程序以开始安装。按照向导完成安装。

  9. 在 Eclipse 中,选择 窗口 | 首选项 打开 首选项 窗口。从左侧面板中选择 Android,然后点击 浏览 以定位 Android SDK 根目录。点击 应用,然后点击 确定如何操作…

  10. 在 Android SDK 安装根目录下启动 Android SDK 管理器。选择 Android SDK 工具Android SDK Platform-tools,至少一个 Android 平台(最新版本为佳),系统映像SDK 示例Android 支持。然后点击 安装。在下一个窗口中,阅读并接受所有许可协议,然后点击 安装如何操作…

  11. 访问 developer.android.com/tools/sdk/ndk/index.html 下载最新版本的 Android NDK。解压下载的文件。

    提示

    下载示例代码

    您可以从您在 www.packtpub.com 的账户中下载您购买的所有 Packt 图书的示例代码文件。如果您在别处购买了这本书,可以访问 www.packtpub.com/support 注册,我们会直接将文件通过电子邮件发送给您。

  12. cygwin根目录下打开Cygwin.bat文件。默认情况下,它包含以下内容:

    @echo off
    C:
    chdir C:\cygwin\bin
    bash --login -i
    
  13. @echo off 之后,C 之前添加以下内容:

    set IS_UNIX=
    set JAVA_HOME=<JDK path>
    set PATH=<SDK path>\tools;<NDK path>
    set ANDROID_NDK_ROOT=/cygdrive/<NDK path>
    

    作为一个例子,我的机器上的文件内容如下;注意 Progra~1Program Files 文件夹的短名称:

    set IS_UNIX=set JAVA_HOME=c:/Progra~1/Java/jdk1.7.0_05
    set PATH=C:/Users/Administrator/AppData/Local/Android/android-sdk/tools;C:/Users/Administrator/Downloads/android-ndk-r8-windows/android-ndk-r8
    set ANDROID_NDK_ROOT=/cygdrive/c/Users/Administrator/Downloads/android-ndk-r8-windows/android-ndk-r8
    
  14. 通过双击 cygwin.bat 启动 Cygwin,然后进入 NDK 中的 samples/hello-jni 目录。输入命令 ndk-build。如果构建成功,这证明 NDK 环境已正确设置:如何操作…

  15. 在 Eclipse 中,选择 窗口 | 首选项 打开 首选项 窗口。从左侧面板中选择 Android,然后从下拉列表中选择 NDK。点击 浏览 以定位 Android NDK 根目录。点击 确定 关闭弹窗。这样我们就可以使用 Eclipse NDK 插件构建和调试 Android NDK 应用程序了:如何操作…

工作原理…

在本教程中,我们首先设置 Android SDK 开发环境,然后是 NDK 开发环境。

Android NDK 不需要安装。我们下载了 NDK,并配置了路径以便更方便地使用。

Android SDK 开发不需要 Cygwin,但对于 NDK 开发来说却是必不可少的,因为 NDK 使用了一些依赖 Cygwin 的 Linux 工具。

ADT 中的 NDK 插件:Eclipse 的 NDK 插件在Android Development Tools (ADT) 中可用,它使我们能够轻松构建和调试 Android NDK 应用程序。

提示

NDK 插件仅在 ADT 20.0.0 或更高版本中可用,该版本于 2012 年 6 月发布。您可能需要更新 Eclipse ADT 以使用 NDK 插件。

还有更多…

我们将 Eclipse IDE 作为开发环境的一部分安装。Eclipse 是开发 Android 应用程序推荐的 IDE,它附带了许多有用的工具和实用程序以帮助我们的开发。然而,它并非开发环境的必需要素。

在 Ubuntu Linux 中设置 Android NDK 开发环境

本文档描述如何在 Ubuntu Linux 中设置 Android NDK 开发环境。

准备工作

检查您的 Ubuntu 版本,确保它是 8.04 或更高版本。

需要 GNU C 库 (glibc) 2.7 或更高版本。它通常随 Linux 默认安装。有两种简单的方法可以检查 glibc 的版本:

  1. 启动一个终端,并输入 ldd --version。这将打印出 lddglibc 的版本:准备工作

  2. 我们可以将库作为应用程序执行。启动一个终端,找到库的位置,然后输入以下命令:

    <glibc library location>/<glibc library>. 
    
    

    将显示以下输出:

    准备工作

  3. 如果我们使用的是 64 位机器,需要启用 32 位应用程序执行。启动一个终端,并输入以下命令:

    sudo apt-get install ia32-libs
    
    
  4. 安装 JDK 6 或更高版本。在终端中,输入命令 sudo apt-get install openjdk-6-jdk,或者也可以输入 sudo apt-get install sun-java6-jdk。安装完成后,我们需要将 JDK 路径添加到 PATH 环境变量中,通过向 ~/.bashrc 添加以下行来实现:

    export JDK_PATH=/usr/local/jdk1.7.0/bin
    export PATH=$PATH:$JDK_PATH
    

我们将使用 Eclipse 作为我们的 IDE。请参考在 Windows 中设置 Android NDK 开发环境的食谱获取指导。

如何操作…

以下步骤说明了在 Ubuntu Linux 上设置 Android NDK 开发环境的程序:

  1. 按照在 Windows 中设置 Android NDK 开发环境的食谱中的步骤 1 到 6 来为 Eclipse 安装 ADT 插件。

  2. developer.android.com/sdk/index.html下载 Android SDK,然后解压下载的包。

  3. 将以下行追加到 ~/.bashrc

    export ANDROID_SDK=<path to Android SDK directory>
    export PATH=$PATH:$ ANDROID_SDK/tools:$ANDROID_SDK/platform-tools
    
  4. 按照在 Windows 中设置 Android NDK 开发环境的食谱中的步骤 9 和 10 来配置 Eclipse 中的 SDK 路径,并下载额外的包。

  5. developer.android.com/tools/sdk/ndk/index.html下载最新的 Android NDK 版本,然后解压下载的文件。

  6. 更改在第 3 步中添加到 ~/.bashrc 的行:

    export ANDROID_SDK=<path to Android SDK directory>
    export ANDROID_NDK=<path to Android NDK directory> 
    export PATH=$PATH:$ANDROID_SDK/tools:$ANDROID_SDK/platform-tools:$ANDROID_NDK
    
  7. 启动一个新的终端,然后进入 NDK 中的samples/hello-jni目录。输入命令ndk-build。如果构建成功,这证明 NDK 环境设置正确:如何操作...

工作原理...

我们首先设置安卓 SDK,然后是安卓 NDK。确保路径设置正确,这样就可以在不引用 SDK 和 NDK 目录的情况下访问工具。

.bashrc文件是 bash shell 在启动新终端时读取的启动文件。export 命令将安卓 SDK 和 NDK 目录位置追加到环境变量PATH中。因此,每次新的 bash shell 启动时,都会正确设置 SDK 和 NDK 工具的PATH

还有更多内容...

以下是一些关于设置 NDK 开发环境的额外技巧:

  • 在启动文件中配置路径:我们在~/.bashrc文件中向PATH环境变量追加 SDK 和 NDK 的路径。这假设我们的 Linux 系统使用 bash shell。然而,如果你的系统使用其他 shell,所使用的启动文件可能会有所不同。一些常用 shell 的启动文件如下所示:

    • 对于 C shell (csh),要使用的启动文件是~/.cshrc

    • 对于ksh,要使用的启动文件可以通过命令echo $ENV获得。

    • 对于sh,要使用的启动文件是~/.profile。用户需要退出当前会话并重新登录,才能使其生效。

  • 切换 JDK:在安卓开发中,我们可以使用 Oracle Java JDK 或 OpenJDK。如果我们遇到任何一个 JDK 的问题,如果我们安装了这两个 JDK,我们可以切换到另一个 Java JDK。

    • 要检查系统当前使用的 JDK,请使用以下命令:

       $update-java-alternatives -l
      
      
    • 要在两个 JDK 之间切换,请使用以下命令:

       $sudo update-java-alternatives -s <java name>
      
      

    下面是一个切换到 Oracle JDK 1.6.0 的例子:

    $sudo update-java-alternatives -s java-1.6.0-sun 
    
    

在 Mac OS 上设置安卓 NDK 开发环境

本文档介绍了如何在 Mac OS 上设置安卓 NDK 开发环境。

准备工作

安卓开发需要 Mac OS X 10.5.8 或更高版本,并且只在 x86 架构上运行。在开始之前,请确保你的机器满足这些要求。

注册一个 Apple 开发者账户,然后访问developer.apple.com/xcode/下载 Xcode,其中包含许多开发工具,包括安卓 NDK 开发所需的make工具。下载完成后,运行安装包并确保选择安装UNIX Development选项。

与往常一样,需要 Java JDK 6 或更高版本。Mac OS X 通常会附带完整的 JDK。我们可以使用以下命令来验证你的机器是否拥有所需的版本:

$javac -version

如何操作...

在 Mac OS X 上设置安卓 NDK 开发环境与在 Ubuntu Linux 上设置类似。以下步骤说明我们如何进行操作:

  1. 按照在 Windows 中设置 Android NDK 开发环境的第 1 至 6 步的食谱来安装 Eclipse 的 ADT 插件。

  2. developer.android.com/sdk/index.html下载 Android SDK,然后解压下载的包。

  3. 将以下行追加到~/.profile。如果文件不存在,请创建一个新的。保存更改并退出当前会话:

    export ANDROID_SDK=<path to Android SDK directory>
    export PATH=$PATH:$ ANDROID_SDK/tools:$ANDROID_SDK/platform-tools
    
  4. 在 Eclipse 中,选择Eclipse | Preferences打开Preferences窗口。从左侧面板中选择Android,然后点击Browse定位到 Android SDK 的根目录。点击Apply,然后点击OK

  5. 在终端中,通过在tools目录下输入命令android启动 Android SDK Manager。选择Android SDK ToolsAndroid SDK Platform-tools、至少一个 Android 平台(最好是最新版本)、System ImageSDK SamplesAndroid Support。然后点击Install。在下一个窗口中,阅读并接受所有许可协议,然后点击Install

  6. developer.android.com/sdk/index.html下载 Android SDK,然后解压下载的包。

  7. 更改你在第 3 步添加到~/.profile的行:

    export ANDROID_SDK=<path to Android SDK directory>
    export ANDROID_NDK=<path to Android NDK directory> 
    export PATH=$PATH:$ANDROID_SDK/tools:$ANDROID_SDK/platform-tools:$ANDROID_NDK
    
  8. 启动一个新的终端,然后进入 NDK 中的samples/hello-jni目录。输入命令ndk-build。如果构建成功,这证明 NDK 环境设置正确。

工作原理…

在 Mac OS X 上设置 Android NDK 开发环境的步骤与 Ubuntu Linux 类似,因为它们都是类 Unix 操作系统。我们首先安装了 Android SDK,然后安装了 Android NDK。

更新 Android NDK

当 NDK 有新版本发布时,我们可能想要更新 NDK,以便利用新版本的新功能或错误修复。本食谱讲述如何在 Windows、Ubuntu Linux 和 Mac OS 中更新 Android NDK。

准备工作

根据你选择的平台,请阅读本章之前的食谱。

如何操作…

在 Windows 中,按照以下说明更新 Android NDK:

  1. 访问developer.android.com/tools/sdk/ndk/index.html下载最新版本的 Android NDK。解压下载的文件。

  2. 打开cygwin根目录下的Cygwin.bat。如果之前在系统上配置过 NDK,内容应该与以下代码片段类似:

    @echo off
    set IS_UNIX=
    set JAVA_HOME=<JDK path>
    set PATH=<SDK path>\tools;<NDK path>
    set ANDROID_NDK_ROOT=/cygdrive/<NDK path>
    C:
    chdir C:\cygwin\bin
    bash --login -i
    
  3. <NDK path>从旧的 NDK 路径更新到新下载和解压的位置。

在 Ubuntu Linux 中,按照以下说明更新 Android NDK:

  1. developer.android.com/tools/sdk/ndk/index.html下载最新版本的 Android NDK,然后解压下载的文件。

  2. 如果我们遵循了在 Ubuntu Linux 中设置 Android NDK 开发环境的食谱,以下内容应该出现在~/.bashrc的末尾:

    export ANDROID_SDK=<path to Android SDK directory>
    export ANDROID_NDK=<path to Android NDK directory>
    export PATH=$PATH:$ANDROID_SDK/tools:$ANDROID_SDK/platform-tools:$ANDROID_NDK
    
  3. 更新 ANDROID_NDK 路径到新下载并解压的 Android NDK 文件夹。

在 Mac OS 中,步骤几乎与 Ubuntu Linux 完全相同,除了我们需要将路径追加到 ~/.profile 而不是 ~/.bashrc

工作原理…

通过简单下载并解压 NDK 文件,并正确配置路径,完成 NDK 安装。因此,更新 NDK 就像将配置的路径更新到新的 NDK 文件夹一样简单。

还有更多…

有时,更新 NDK 需要先更新 SDK。由于本书专注于 Android NDK,因此解释如何更新 SDK 超出了本书的范围。你可以访问 developer.android.com/sdk/index.html 的 Android 开发者网站,了解如何操作的详细信息。

有时,由于兼容性问题,我们可能需要使用旧版本的 NDK 来构建某些应用程序。因此,保留多个版本的 Android NDK 并通过更改路径或在引用特定版本的 NDK 时使用完整路径之间切换可能很有用。

编写一个 Hello NDK 程序

环境设置好后,让我们开始编写 NDK 中的代码。本食谱将带你完成一个 Hello NDK 程序的编写。

准备就绪

在开始编写 Hello NDK 程序之前,需要正确设置 NDK 开发环境。请根据你选择的平台参考本章前面的内容。

如何操作…

按照以下步骤编写、编译并运行 Hello NDK 程序:

  1. 启动 Eclipse,选择 文件 | 新建 | Android 项目。将 HelloNDK 作为 项目名称 的值。选择 在 workspace 中创建新项目。然后点击 下一步如何操作…

  2. 在下一个窗口中,选择你想要定位的 Android 版本。通常推荐使用最新版本。然后点击 下一步

  3. 在下一个窗口中,将你的包名指定为 cookbook.chapter1。勾选 创建 Activity 的复选框,并将名称指定为 HelloNDKActivity。将 最低 SDK 的值设置为 5 (Android 2.0)。点击 完成如何操作…

  4. 在 Eclipse 包资源管理器中,右键点击 HelloNDK 项目,选择 新建 | 文件夹。在弹出的窗口中输入名称 jni,然后点击 完成如何操作…

  5. HelloNDK 项目下新创建的 jni 文件夹上右键点击。选择 新建 | 文件,输入 hello.c 作为 文件名 的值,然后点击 完成。在 hello.c 文件中输入以下代码:

    #include <string.h>
    #include <jni.h>
    
    jstring 
    Java_cookbook_chapter1_HelloNDKActivity_naGetHelloNDKStr(JNIEnv* pEnv,  jobject pObj)
    {
        return (*pEnv)->NewStringUTF(pEnv, "Hello NDK!");
    }
    
  6. jni 文件夹上右键点击。选择 新建 | 文件,输入 Android.mk 作为 文件名 的值,然后点击 完成。在 Android.mk 文件中输入以下代码:

    LOCAL_PATH := $(call my-dir)
    include $(CLEAR_VARS)
    LOCAL_MODULE    := hello
    LOCAL_SRC_FILES := hello.c
    include $(BUILD_SHARED_LIBRARY)
    
  7. 启动终端,进入 jni 文件夹,并输入 ndk-build 以构建 hello.c 程序为本地库。

  8. 编辑 HelloNDKActivity.java 文件。该文件应包含以下内容:

    public class HelloNDKActivity extends Activity {
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            TextView tv = new TextView(this);
            tv.setTextSize(30);
            tv.setText(naGetHelloNDKStr());
            this.setContentView(tv);
        }
        public native String naGetHelloNDKStr();
        static {
            System.loadLibrary("hello");
        }
    }
    
  9. 在 Eclipse 中右键点击HelloNDK项目。选择运行方式 | Android 应用程序。你的 Android 手机或模拟器将显示类似于以下截图的内容:如何操作…

工作原理…

本文档展示了如何在 Android 上编写一个 Hello NDK 程序。

  • 原生代码:Hello NDK 程序由原生 C 代码和 Java 代码组成。原生函数naGetHelloNDKStrHello NDK字符串返回给调用者,这一点在原生代码函数定义和 Java 代码方法声明中都有体现。原生函数名称必须遵循特定的模式,包括包名、类名和方法名。包和类名必须与调用原生方法的 Java 类的包和类名一致,而方法名必须与该 Java 类中声明的方法名相同。

    这有助于 Dalvik VM 在运行时定位原生函数。如果不遵循该规则,将在运行时导致UnsatisfiedLinkError

    原生函数有两个参数,这是所有原生函数的标准。可以根据需要定义额外的参数。第一个参数是指向JNIEnv的指针,这是访问各种 JNI 函数的门户。第二个参数的含义取决于原生方法是静态方法还是实例方法。如果是静态方法,第二个参数是对定义方法的类的引用。如果是实例方法,第二个参数是对调用原生方法的对象的引用。我们将在第二章,Java Native Interface中详细讨论 JNI。

  • 原生代码编译:Android NDK 构建系统让开发者无需编写makefile。该构建系统接受一个Android.mk文件,该文件简单描述了源代码。它会解析该文件以生成makefile,并为我们完成所有繁重的工作。

    我们将在第三章,构建和调试 NDK 应用程序中详细介绍如何编写Android.mk文件,甚至编写我们自己的makefile

    一旦我们编译了原生代码,项目下将会创建一个名为libs的文件夹,并在armeabi子目录下生成一个libhello.so库。

  • Java 代码:调用原生方法遵循三个步骤:

    1. 加载原生库:这是通过调用System.loadLibrary("hello")完成的。请注意,我们应该使用hello而不是libhello。如果指定了libhello,Dalvik VM 将无法定位库。

    2. 声明方法:我们使用 native 关键字声明方法,以指示它是一个原生方法。

    3. 调用方法:我们像调用任何普通的 Java 方法一样调用该方法。

还有更多内容...

原生方法的名称较长,手动编写容易出错。幸运的是,JDK 中的javah程序可以帮助我们生成包含方法名称的头文件。要使用javah,应遵循以下步骤:

  1. 编写 Java 代码,包括原生方法定义。

  2. 编译 Java 代码,并确保类文件出现在我们项目的bin/classes/文件夹下。

  3. 打开一个终端,进入jni文件夹,并输入以下命令:

    $ javah -classpath ../bin/classes –o <output file name> <java package name>.<java class anme>
    
    

    在我们的HelloNDK示例中,命令应如下所示:

    $ javah -classpath ../bin/classes –o hello.h cookbook.chapter1.HelloNDKActivity
    
    

    这将生成一个名为hello.h的文件,其函数定义如下:

    JNIEXPORT jstring JNICALL Java_cookbook_chapter1_HelloNDKActivity_naGetHelloNDKStr
      (JNIEnv *, jobject);
    

第二章:Java Native Interface

在本章中,我们将介绍以下食谱:

  • 加载原生库和注册原生方法

  • 使用基本类型传递参数和接收返回值

  • 在 JNI 中操作字符串

  • 在 JNI 中管理引用

  • 在 JNI 中操作类

  • 在 JNI 中操作对象

  • 在 JNI 中操作数组

  • 在原生代码中访问 Java 的静态字段和实例字段

  • 在原生代码中调用静态方法和实例方法

  • 缓存 jfieldID、jmethodID 和引用数据以提高性能

  • 在 JNI 中检查错误和处理异常

  • 在 JNI 中集成汇编代码

介绍

使用 Android NDK 编程本质上是同时用 Java 和 C、C++、汇编等原生语言编写代码。Java 代码在 Dalvik 虚拟机 (VM) 上运行,而原生代码编译为直接在操作系统上运行的二进制文件。Java Native Interface (JNI) 像一座桥梁,将这两个世界连接起来。Java 代码、Dalvik VM、原生代码和 Android 系统之间的关系可以用以下图表来说明:

介绍

图表中的箭头表示哪个方面发起交互。Dalvik VM原生代码 都在 Android 系统 之上运行(Android 是基于 Linux 的操作系统)。它们需要系统提供执行环境。JNIDalvik VM 的一部分,它允许 原生代码 访问 Java 代码的字段和方法。JNI 还允许 Java 代码 调用 原生代码 中实现的原生方法。因此,JNI 促进了 原生代码Java 代码 之间的双向通信。

如果你熟悉 Java 编程以及 C 或 C++或汇编编程,那么学习使用 Android NDK 编程主要是学习 JNI。JNI 包含基本类型和引用类型。这些数据类型在 Java 中有相应的映射数据类型。操作基本类型通常可以直接进行,因为一个数据类型通常等同于一个原生的 C/C++数据类型。然而,引用数据操作通常需要借助预定义的 JNI 函数。

在本章中,我们首先介绍 JNI 中的各种数据类型,并演示如何从 Java 调用原生方法。然后描述如何从原生代码访问 Java 字段和调用 Java 方法。最后,我们将讨论如何缓存数据以实现更好的性能,如何处理错误和异常,以及如何在原生方法实现中使用汇编。

本章节的每个食谱都附带一个示例 Android 项目,展示了主题及相关 JNI 函数。由于篇幅限制,书中无法列出所有源代码。代码是本章非常重要的部分,强烈建议你在阅读食谱时下载源代码并参考。

提示

JNI 是一个复杂的话题,我们尝试在 Android NDK 编程的背景下覆盖它最基本的部分。然而,一个章节并不足以提供所有的细节。读者可能需要参考 Java JNI 规范,在docs.oracle.com/javase/6/docs/technotes/guides/jni/或者《Java Native Interface: Programmer's Guide and Specification》一书,在java.sun.com/docs/books/jni/中查找更多信息。对于 Android 特定的信息,你可以参考 JNI 小贴士,在developer.android.com/guide/practices/jni.html

加载本地库和注册本地方法

本地代码通常被编译成共享库,并在本地方法被调用之前加载。这个食谱涵盖了如何加载本地库和注册本地方法。

准备就绪

如果还没有这样做,请阅读 第一章,Hello NDK 的食谱,以设置 Android NDK 开发环境。

如何操作…

以下步骤将向你展示如何构建一个演示加载本地库和注册本地方法的 Android 应用程序:

  1. 启动 Eclipse,选择 文件 | 新建 | Android 项目。将 项目名称 的值设置为 NativeMethodsRegister。选择 在工作区中创建新项目。然后点击 下一步

  2. 在下一个窗口中,选择最新的 Android SDK 版本,然后点击 下一步 进入下一个窗口。

  3. 将包名指定为 cookbook.chapter2。选中 创建活动 复选框,并将名称指定为 NativeMethodsRegisterActivity。将 最低 SDK 的值设置为 5 (Android 2.0)。然后点击 完成

  4. Eclipse 包浏览器 中,右键点击 NativeMethodsRegister 项目,然后选择 新建 | 文件夹。在弹出的窗口中输入名称 jni,然后点击 完成

  5. NativeMethodsRegister 项目下新创建的 jni 文件夹上右键点击,然后选择 新建 | 文件。将 文件名 的值设置为 nativetest.c,然后点击 完成

  6. 将以下代码添加到 nativetest.c 文件中:

    #include <android/log.h>
    #include <stdio.h>
    
    jint NativeAddition(JNIEnv *pEnv, jobject pObj, jint pa, jint pb) {
      return pa+pb;
    }
    
    jint NativeMultiplication(JNIEnv *pEnv, jobject pObj, jint pa, jint pb) {
      return pa*pb;
    }
    
    JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* pVm, void* reserved)
    {
        JNIEnv* env;
        if ((*pVm)->GetEnv(pVm, (void **)&env, JNI_VERSION_1_6)) {
         return -1;
      }
        JNINativeMethod nm[2];
        nm[0].name = "NativeAddition";
        nm[0].signature = "(II)I";
        nm[0].fnPtr = NativeAddition;
        nm[1].name = "NativeMultiplication";
        nm[1].signature = "(II)I";
        nm[1].fnPtr = NativeMultiplication;
        jclass cls = (*env)->FindClass(env, "cookbook/chapter2/NativeMethodRegisterActivity");
        // Register methods with env->RegisterNatives.
        (*env)->RegisterNatives(env, cls, nm, 2);
        return JNI_VERSION_1_6;
    }
    
  7. NativeMethodRegisterActivity.java 添加以下代码以加载本地共享库并定义本地方法:

    public class NativeMethodRegisterActivity extends Activity {
        … …
          private void callNativeMethods() {
            int a = 10, b = 100;
              int c = NativeAddition(a, b);
              tv.setText(a + "+" + b + "=" + c);
              c = NativeMultiplication(a, b);
              tv.append("\n" + a + "x" + b + "=" + c);
          }
          private native int NativeAddition(int a, int b);
          private native int NativeMultiplication(int a, int b);
          static {
            //use either of the two methods below
    //System.loadLibrary("NativeRegister");
              System.load("/data/data/cookbook.chapter2/lib/libNativeRegister.so");
          }
    }
    
  8. 修改 res/layout/activity_native_method_register.xml 文件中的 TextView,如下所示:

    <TextView
            android:id="@+id/display_res"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerHorizontal="true"
            android:padding="@dimen/padding_medium"
            android:text="@string/hello_world"
            tools:context=".NativeMethodRegisterActivity" />
    
  9. jni 文件夹下创建一个名为 Android.mk 的文件,内容如下:

    LOCAL_PATH := $(call my-dir)
    include $(CLEAR_VARS)
    LOCAL_MODULE    := NativeRegister
    LOCAL_SRC_FILES := nativetest.c
    LOCAL_LDLIBS := -llog
    include $(BUILD_SHARED_LIBRARY)
    
  10. 启动终端,进入项目的 jni 文件夹,并输入 ndk-build 以构建本地库。

  11. 在 Android 设备或模拟器上运行项目。你应该会看到类似于以下截图的内容:如何操作…

工作原理…

这个食谱描述了如何加载本地库和注册本地方法:

  • 加载本地库java.lang.System类提供了两种加载本地库的方法,即loadLibraryloadloadLibrary接受不带前缀和文件扩展名的库名。例如,如果我们想要加载在示例项目中编译为libNativeRegister.so的 Android 本地库,我们使用System.loadLibrary("NativeRegister")System.load方法不同,它需要本地库的完整路径。在我们的示例项目中,我们可以使用System.load("/data/data/cookbook.chapter2/lib/libNativeRegister.so")来加载本地库。当我们想要在不同版本的本地库之间切换时,System.load方法很有用,因为它允许我们指定完整的库路径。

    我们在NativeMethodRegisterActivity.java类的静态初始化器中演示了这两种方法的用法。请注意,在构建和运行示例应用程序时,只应启用一种方法。

  • JNIEnv 接口指针:在 JNI 中,本地代码中定义的每个本地方法都必须接受两个输入参数,第一个是指向JNIEnv的指针。JNIEnv接口指针指向线程局部数据,进而指向所有线程共享的 JNI 函数表。以下图可以说明这一点:工作原理

    JNIEnv接口指针是访问所有预定义 JNI 函数的网关,包括使本地代码能够处理 Java 对象、访问 Java 字段、调用 Java 方法的函数等。我们接下来将要讨论的RegisterNatives本地函数也是其中之一。

    提示

    JNIEnv接口指针指向线程局部数据,因此不能在多个线程之间共享。此外,JNIEnv仅可由 Java 线程访问。本地线程必须调用 JNI 函数AttachCurrentThread将自己附加到虚拟机,以获取JNIEnv接口指针。我们将在本章的在 JNI 中操作类的菜谱中看到一个例子。

  • 注册本地方法:如果本地方法实现的函数名遵循第一章中提到的特定命名约定,JNI 可以自动发现本地方法实现。这不是唯一的方法。在我们的示例项目中,我们显式调用了RegisterNatives JNI 函数来注册本地方法。RegisterNatives函数具有以下原型:

    jint RegisterNatives(JNIEnv *env, jclass clazz, const JNINativeMethod *methods, jint nMethods);
    

    clazz参数是对注册本地方法的类的引用。methods参数是JNINativeMethod数据结构的数组。JNINativeMethod定义如下:

    typedef struct {
      char *name;
      char *signature;
      void *fnPtr;
    } JNINativeMethod;
    

    name表示本地方法名称,signature是方法的输入参数数据类型和返回值数据类型的描述符,fnPtr是指向本地方法的函数指针。RegisterNatives的最后一个参数nMethods表示要注册的方法数量。函数返回零表示成功,否则返回负值。

    RegisterNatives方便注册不同类的本地方法实现。此外,它还可以简化本地方法名称,以避免粗心大意。

    使用RegisterNatives的典型方式是在JNI_OnLoad方法中,如下面的模板所示。当加载本地库时,会调用JNI_OnLoad,因此我们可以确保在调用本地方法之前注册它们:

    JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* pVm, void* reserved)
    {
        JNIEnv* env;
        if ((*pVm)->GetEnv(pVm, (void **)&env, JNI_VERSION_1_6)) {
        return -1;
      }
    
      // Get jclass with env->FindClass.
      // Register methods with env->RegisterNatives.
    
      return JNI_VERSION_1_6;
    }
    

我们在示例代码的JNI_OnLoad方法中演示了前面模板的使用,在那里我们注册了两个本地方法,分别用于对两个输入整数进行加法和乘法。前面显示的执行结果证明 Java 代码可以成功调用这两个注册的本地方法。

请注意,此示例使用了一些 JNI 功能,我们将在后面的菜谱中介绍,包括FindClass函数和字段描述符。如果目前你不完全理解代码,这是正常的。在学习了这些主题后,你可以随时回来复习。

以原始类型传递参数和接收返回值

Java 代码可以将参数传递给本地方法,并接收返回的处理结果。这个菜谱将介绍如何以原始类型传递参数和接收返回值。

准备工作

在阅读这个菜谱之前,你应该至少构建过一个带有本地代码的 Android 应用程序。如果你还没有这样做,请先阅读第一章中的编写 Hello NDK 程序菜谱,Hello NDK

如何操作…

以下步骤将创建一个示例 Android 应用程序,其中本地方法接收来自 Java 代码的输入参数,并返回处理结果:

  1. 创建一个名为PassingPrimitive的项目。将包名设置为cookbook.chapter2。创建一个名为PassingPrimitiveActivity的活动。在此项目下,创建一个名为jni的文件夹。如果你需要更详细的说明,请参考本章中的加载本地库和注册本地方法菜谱。

  2. jni文件夹下添加一个名为primitive.c的文件,并实现本地方法。在我们的示例项目中,我们为八种原始数据类型中的每一种都实现了一个本地方法。以下是jbooleanjintjdouble的代码。请参考下载的代码以获取完整的方法列表:

    #include <jni.h>
    #include <android/log.h>
    
    JNIEXPORT jboolean JNICALL Java_cookbook_chapter2_PassingPrimitiveActivity_passBooleanReturnBoolean(JNIEnv *pEnv, jobject pObj, jboolean pBooleanP){
      __android_log_print(ANDROID_LOG_INFO, "native", "%d in %d bytes", pBooleanP, sizeof(jboolean));
      return (!pBooleanP);
    }
    
    JNIEXPORT jint JNICALL Java_cookbook_chapter2_PassingPrimitiveActivity_passIntReturnInt(JNIEnv *pEnv, jobject pObj, jint pIntP) {
      __android_log_print(ANDROID_LOG_INFO, "native", "%d in %d bytes", pIntP, sizeof(jint));
      return pIntP + 1;
    }
    
    JNIEXPORT jdouble JNICALL Java_cookbook_chapter2_PassingPrimitiveActivity_passDoubleReturnDouble(JNIEnv *pEnv, jobject pObj, jdouble pDoubleP) {
      __android_log_print(ANDROID_LOG_INFO, "native", "%f in %d bytes", pDoubleP, sizeof(jdouble));
      return pDoubleP + 0.5;
    }
    
  3. PassingPrimitiveActivity.java Java 代码中,我们添加了加载本地库、声明本地方法并调用本地方法的代码。以下是代码的部分内容。""表示未显示的部分。请参考从网站下载的源文件以获取完整代码:

    @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_passing_primitive);
            StringBuilder strBuilder = new StringBuilder();
            strBuilder.append("boolean: ").append(passBooleanReturnBoolean(false)).append(System.getProperty("line.separator"))
             ......
    
              .append("double: ").append(passDoubleReturnDouble(11.11)).append(System.getProperty("line.separator"));
            TextView tv = (TextView) findViewById(R.id.display_res);
            tv.setText(strBuilder.toString());
        }
        private native boolean passBooleanReturnBoolean(boolean p);
        private native byte passByteReturnByte(byte p);
        private native char passCharReturnChar(char p);
        private native short passShortReturnShort(short p);
        ......
        static {
            System.loadLibrary("PassingPrimitive");
        }
    
  4. 根据本章的“加载本地库和注册本地方法”的步骤 8 或下载的项目代码,修改res/layout/activity_passing_primitive.xml文件。

  5. jni文件夹下创建一个名为Android.mk的文件,并向其中添加以下内容:

    LOCAL_PATH := $(call my-dir)
    include $(CLEAR_VARS)
    LOCAL_MODULE    := PassingPrimitive
    LOCAL_SRC_FILES := primitive.c
    LOCAL_LDLIBS := -llog
    include $(BUILD_SHARED_LIBRARY)
    
  6. 启动终端,进入jni文件夹,并输入ndk-build以构建本地库PassingPrimitive

  7. 在 Eclipse 中,选择窗口 | 显示视图 | LogCat以显示 logcat 控制台。或者,启动终端并在终端中输入以下命令以在终端上显示logcat输出:

    $adb logcat -v time
    
  8. 在 Android 设备或模拟器上运行项目。你应该会看到类似以下截图的内容:如何操作…

    logcat 输出如下:

    如何操作…

工作原理…

代码说明了如何从本地方法中以基本类型传递参数和接收返回值。我们为每种基本类型创建了一个方法。在本地代码中,我们将接收到的值打印到logcat,修改了值,并将其返回。

  • JNI 基本类型与 Java 基本类型映射:JNI 和 Java 中的基本类型有以下映射:

    Java 类型 JNI 类型 字节数 符号
    boolean jboolean 1 无符号
    byte jbyte 1 有符号
    char jchar 2 无符号
    short jshort 2 有符号
    int jint 4 有符号
    long jlong 8 有符号
    float jfloat 4 -
    double jdouble 8 -

    请注意,Java 中的char和 JNI 中的jchar都是两个字节,而 C/C++中的char类型只有一个字节长。实际上,在 JNI 编程中,C/C++的char可以与jbyte互换,而不是jchar

  • Android 日志库:我们通过以下代码在本地方法中使用 Android 日志系统输出接收到的值:

    __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__);
    

    ANDROID_LOG_INFO是在android/log.h中定义的enum值,表示我们正在使用信息级别的日志。LOG_TAG可以是任何字符串,__VA_ARGS__被传递给 API 的参数替换,格式类似于 C 中的printf方法。

    我们必须在本地代码中包含android/log.h头文件以使用日志功能:

    #include <android/log.h>
    

    此外,为了使用 API,我们还需要在Android.mk文件中包含 NDK 日志库:

    LOCAL_LDLIBS := -llog
    

我们将在第三章中详细介绍 Android 日志 API 的更多细节,同时利用日志 API 进行调试。

在 JNI 中操作字符串

在 JNI 中,字符串有点复杂,主要是因为 Java 字符串和 C 字符串在内部表示上是不同的。本指南将涵盖最常使用的 JNI 字符串特性。

准备就绪

了解编码的基础知识对于理解 Java 字符串和 C 字符串之间的区别至关重要。我们将简要介绍 Unicode。

根据统一码联盟的定义,统一码标准如下所述:

统一码标准是一个字符编码系统,旨在支持现代世界各种语言和技术学科书面文本的全球交换、处理和显示。此外,它还支持许多书面语言的古典和历史文本。

Unicode 为其定义的每个字符分配了一个唯一的数字,称为码点。主要有两类编码方法支持整个 Unicode 字符集或其子集。

第一种是统一码转换格式UTF),它将统一码码点编码为不同数量的代码值。UTF-8、UTF-16、UTF-32 及其他几种格式都属于这一类。数字 8、16 和 32 指的是一个代码值的位数。第二种是通用字符集UCS)编码,它将统一码码点编码为一个单一的代码值。UCS2 和 UCS4 属于这一类。数字 2 和 4 指的是一个代码值的字节数。

注意

Unicode 定义的字符比两个字节能表示的要多,因此 UCS2 只能表示 Unicode 字符的一个子集。由于 Unicode 定义的字符比四个字节能表示的要少,UTF-32 的多个代码值从未被需要。因此,UTF-32 和 UCS4 在功能上是相同的。

Java 编程语言使用 UTF-16 来表示字符串。如果一个字符无法适应 16 位的代码值,就会使用一对名为代理对的代码值。C 字符串只是一个以空字符终止的字节数组。实际的编码/解码几乎完全由开发人员和底层系统来处理。JNI 使用修改后的 UTF-8 版本来表示字符串,包括本地代码中的类、字段和方法名称。修改后的 UTF-8 与标准 UTF-8 有两个区别。首先,空字符使用两个字节进行编码。其次,JNI 只支持标准 UTF-8 的一字节、两字节和三字节的格式,而较长的格式无法被正确识别。JNI 使用自己的格式来表示无法适应三个字节的 Unicode。

如何操作

以下步骤将指导您如何创建一个示例 Android 项目,该项目展示了 JNI 中的字符串操作:

  1. 创建一个名为StringManipulation的项目。将包名设置为cookbook.chapter2。创建一个名为StringManipulationActivity的活动。在项目下,创建一个名为jni的文件夹。如果你需要更详细的说明,请参考本章中的加载本地库和注册本地方法的菜谱。

  2. jni文件夹下创建一个名为stringtest.c的文件,然后按照以下方式实现passStringReturnString方法:

    JNIEXPORT jstring JNICALL Java_cookbook_chapter2_StringManipulationActivity_passStringReturnString(JNIEnv *pEnv, jobject pObj, jstring pStringP){
    
        __android_log_print(ANDROID_LOG_INFO, "native", "print jstring: %s", pStringP);
      const jbyte *str;
      jboolean *isCopy;
      str = (*pEnv)->GetStringUTFChars(pEnv, pStringP, isCopy);
      __android_log_print(ANDROID_LOG_INFO, "native", "print UTF-8 string: %s, %d", str, isCopy);
    
        jsize length = (*pEnv)->GetStringUTFLength(pEnv, pStringP);
      __android_log_print(ANDROID_LOG_INFO, "native", "UTF-8 string length (number of bytes): %d == %d", length, strlen(str));
      __android_log_print(ANDROID_LOG_INFO, "native", "UTF-8 string ends with: %d %d", str[length], str[length+1]);
      (*pEnv)->ReleaseStringUTFChars(pEnv, pStringP, str);
    
      char nativeStr[100];
      (*pEnv)->GetStringUTFRegion(pEnv, pStringP, 0, length, nativeStr);
      __android_log_print(ANDROID_LOG_INFO, "native", "jstring converted to UTF-8 string and copied to native buffer: %s", nativeStr);
    
      const char* newStr = "hello 安卓";
      jstring ret = (*pEnv)->NewStringUTF(pEnv, newStr);
      jsize newStrLen = (*pEnv)->GetStringUTFLength(pEnv, ret);
      __android_log_print(ANDROID_LOG_INFO, "native", "UTF-8 string with Chinese characters: %s, string length (number of bytes) %d=%d", newStr, newStrLen, strlen(newStr));
      return ret;
    }
    
  3. StringManipulationActivity.java的 Java 代码中,添加加载本地库、声明本地方法并调用本地方法的代码。源代码详情请参考下载的代码。

  4. 根据本章中加载本地库和注册本地方法的步骤 8 或下载的项目代码,修改res/layout/activity_passing_primitive.xml文件。

  5. jni文件夹下创建一个名为Android.mk的文件。具体细节请参考本章中加载本地库和注册本地方法的步骤 9 或下载的代码。

  6. 启动终端,进入jni文件夹,并输入ndk-build以构建本地库。

  7. 在 Android 设备或模拟器上运行项目。我们应该看到类似于以下截图的内容:如何操作

    在 logcat 输出中应该看到以下内容:

    如何操作

工作原理…

本菜谱讨论了 JNI 中的字符串操作。

  • 字符编码:Android 使用 UTF-8 作为其默认字符集,通过执行Charset.defaultCharset().name()方法在我们的程序中显示。这意味着在本地代码中的默认编码是 UTF-8。如前所述,Java 使用 UTF-16 字符集。这意味着当我们从 Java 传递字符串到本地代码,反之亦然时,需要进行编码转换。如果不这样做,会导致不想要的结果。在我们的例子中,我们尝试在本地代码中直接打印jstring,但结果是一些无法识别的字符。

    幸运的是,JNI 附带了一些预定义的函数来进行转换。

  • Java 字符串到本地字符串:当本地方法被调用并带有字符串类型的输入参数时,首先需要将接收到的字符串转换为本地字符串。对于不同的情况可以使用两个 JNI 函数。

    第一个函数是GetStringUTFChars,其原型如下:

    const jbyte * GetStringUTFChars(JNIEnv *env, jstring string, jboolean *isCopy);
    

    这个函数将 Java 字符串转换为 UTF-8 字符数组。如果创建了 Java 字符串内容的新副本,当函数返回时isCopy被设置为true;否则isCopy被设置为false,返回的指针指向与原始 Java 字符串相同的字符。

    提示

    我们无法预测虚拟机是否会返回 Java 字符串的新副本。因此,在转换大字符串时我们必须小心,因为可能的内存分配和复制可能会影响性能,甚至可能导致“内存不足”的问题。还要注意,如果将isCopy设置为false,我们不能修改返回的 UTF-8 本地字符串,因为这会修改 Java 字符串内容,破坏 Java 字符串的不可变性属性。

    当我们完成了所有转换后的本地字符串的操作后,应该调用ReleaseStringUTFChars来通知虚拟机我们不再需要访问 UTF-8 本地字符串了。该函数的原型如下,第二个参数是 Java 字符串,第三个参数是 UTF-8 本地字符串:

    void ReleaseStringUTFChars(JNIEnv *env, jstring string, const char *utf);
    

    第二个转换函数是GetStringUTFRegion,其原型如下:

    void GetStringUTFRegion(JNIEnv *env, jstring str, jsize start, jsize len, char *buf);
    

    startlen参数表示 Java UTF-16 字符串的起始位置和需要转换的 UTF-16 字符数量。buf参数指向存储转换后的本地 UTF-8 字符数组的位置。

    让我们比较一下这两种方法。第一种方法可能需要也可能不需要为转换后的 UTF-8 字符串分配新内存,这取决于虚拟机是否决定创建新副本;而第二种方法使用了预分配的缓冲区来存储转换后的内容。此外,第二种方法允许我们指定转换源的位置和长度。因此,可以遵循以下规则:

    • 要修改转换后的 UTF-8 本地字符串,应该使用 JNI 方法GetStringUTFRegion

    • 如果我们只需要原始 Java 字符串的一个子串,并且这个子串不大,应该使用GetStringUTFRegion

    • 如果我们处理的是一个大字符串,并且我们不打算修改转换后的 UTF-8 本地字符串,应该使用GetStringUTFChars

      提示

      在我们的示例中,调用GetStringUTFRegion函数时使用了固定长度的缓冲区。我们应该确保它足以容纳字符串,否则应该使用动态分配的数组。

  • 字符串长度:可以使用 JNI 函数GetStringUTFLength来获取 UTF-8 编码的jstring的字符串长度。注意,它返回的是字节数量,而不是 UTF-8 字符的数量,正如我们的示例所示。

  • 本地字符串到 Java 字符串:有时我们也需要从本地代码向 Java 代码返回字符串数据。返回的字符串应该是 UTF-16 编码的。JNI 函数NewStringUTF从 UTF-8 本地字符串构造一个jstring。它具有以下原型:

    jstring NewStringUTF(JNIEnv *env, const char *bytes);
    
  • 转换失败GetStringUTFCharsNewStringUTF需要分配内存空间来存储转换后的字符串。如果内存不足,这些方法将抛出OutOfMemoryError异常并返回NULL。我们将在JNI 中的检查错误和处理异常的菜谱中详细介绍异常处理。

还有更多…

关于 JNI 字符编码的更多内容:JNI 字符编码比我们这里介绍的更为复杂。除了 UTF-8,它还支持 UTF-16 转换函数。也可以在本地代码中调用 Java 字符串方法以编码/解码其他格式的字符。由于 Android 使用 UTF-8 作为其平台字符集,我们这里只介绍如何处理 Java UTF-16 和 UTF-8 本地字符串之间的转换。

在 JNI 中管理引用

JNI 将字符串、类、实例对象和数组作为引用类型暴露。上一个菜谱介绍了字符串类型。这个菜谱将涵盖引用管理,接下来的三个菜谱将分别讨论类、对象和数组。

如何操作…

以下步骤创建了一个示例 Android 项目,说明了 JNI 中的引用管理:

  1. 创建一个名为 ManagingReference 的项目。将包名设置为 cookbook.chapter2。创建一个名为 ManagingReferenceActivity 的活动。在项目下,创建一个名为 jni 的文件夹。如果你需要更详细的说明,请参考本章中的加载本地库和注册本地方法的菜谱。

  2. jni 文件夹下创建一个名为 referencetest.c 的文件,然后实现 localReferenceglobalReferenceweakReferencereferenceAssignmentAndNew 方法。以下代码片段展示了这一点:

    JNIEXPORT void JNICALL Java_cookbook_chapter2_ManagingReferenceActivity_localReference(JNIEnv *pEnv, jobject pObj, jstring pStringP, jboolean pDelete){
        jstring stStr;
      int i;
      for (i = 0; i < 10000; ++i) {
        stStr = (*pEnv)->NewLocalRef(pEnv, pStringP);
        if (pDelete) {
          (*pEnv)->DeleteLocalRef(pEnv, stStr);
        }
      }
    }
    
    JNIEXPORT void JNICALL Java_cookbook_chapter2_ManagingReferenceActivity_globalReference(JNIEnv *pEnv, jobject pObj, jstring pStringP, jboolean pDelete){
      static jstring stStr;
      const jbyte *str;
      jboolean *isCopy;
      if (NULL == stStr) {
        stStr = (*pEnv)->NewGlobalRef(pEnv, pStringP);
      }
      str = (*pEnv)->GetStringUTFChars(pEnv, stStr, isCopy);
      if (pDelete) {
        (*pEnv)->DeleteGlobalRef(pEnv, stStr);
        stStr = NULL;
      }
    }
    
    JNIEXPORT void JNICALL Java_cookbook_chapter2_ManagingReferenceActivity_weakReference(JNIEnv *pEnv, jobject pObj, jstring pStringP, jboolean pDelete){
      static jstring stStr;
      const jbyte *str;
      jboolean *isCopy;
      if (NULL == stStr) {
        stStr = (*pEnv)->NewWeakGlobalRef(pEnv, pStringP);
      }
      str = (*pEnv)->GetStringUTFChars(pEnv, stStr, isCopy);
      if (pDelete) {
        (*pEnv)->DeleteWeakGlobalRef(pEnv, stStr);
        stStr = NULL;
      }
    }
    
  3. 修改 ManagingReferenceActivity.java 文件,添加加载本地库的代码,然后声明并调用本地方法。

  4. 根据本章中加载本地库和注册本地方法的步骤 8,修改 res/layout/activity_managing_reference.xml 文件,或者下载的项目代码。

  5. jni 文件夹下创建一个名为 Android.mk 的文件。参考本章中加载本地库和注册本地方法的步骤 9,或下载的代码以获取详细信息。

  6. 启动终端,进入 jni 文件夹,并输入 ndk-build 以构建本地库。

  7. 在 Android 设备或模拟器上运行项目,并使用 eclipse 或终端中的 adb logcat -v time 命令监控 logcat 输出。在下一节详细介绍时,我们将展示每个本地方法的样本结果。

工作原理…

这个菜谱涵盖了 JNI 中的引用管理:

  • JNI 引用:JNI 将字符串、类、实例对象和数组作为引用暴露。引用的基本思想可以用以下图表说明:工作原理…

    引用为对象(可以是类、实例对象、字符串或数组)访问增加了一层间接寻址。对象由对象指针指向,引用用于定位对象指针。尽管这种间接寻址为对象操作引入了开销,但它允许虚拟机(VM)将对象指针从开发者面前隐藏起来。因此,VM 可以在运行时内存管理中移动底层对象,并相应地更新对象指针值,而不会影响引用。

    请注意,虚拟机中的垃圾收集器移动对象以实现廉价的内存分配、批量释放、减少堆碎片、提高局部性等。

    提示

    引用不一定是指针。引用如何用于定位对象指针的具体细节对开发者是隐藏的。

  • 局部引用、全局引用与弱引用:为了指向同一数据,可以创建三种不同类型的引用,即局部引用、全局引用和弱引用。除非我们明确创建全局引用或弱引用,否则 JNI 默认使用局部引用。下表总结了这三种不同类型引用之间的区别:

创建 生命周期 可见性 对被引用对象的垃圾收集器(GC)行为 释放
局部引用 DefaultNewLocalRef 本地方法调用期间有效。本地方法返回后无效 在创建它的线程内有效 GC 不会回收被引用对象 自动释放或调用 DeleteLocalRef
全局引用 NewGlobalRef 明确释放前有效 多个线程间有效 GC 不会回收被引用对象 DeleteGlobalRef
弱引用 NewGlobalWeakRef 明确释放前有效 多个线程间有效 GC 可以回收被引用对象 DeleteWeakGlobalRef

现在,我们将逐一查看引用类型,同时参考示例源代码:

  • 局部引用:本地方法 localReference 展示了两个基本的 JNI 函数,即 NewLocalRefDeleteLocalRef。第一个函数创建局部引用,而第二个释放它。请注意,通常我们不需要显式释放局部引用,因为它会在本地方法返回后自动释放。然而,有两种例外情况。首先,如果在本地方法调用中创建大量局部引用,我们可能会引起溢出。当我们将 false 传递给 pDelete 输入参数时,我们的示例方法展示了这种情况。以下截图是此类场景的一个示例:如何工作…

    第一次执行在用完后立即删除了局部引用,所以它顺利完成,而第二次没有删除局部引用,最终导致 ReferenceTable 溢出。

    其次,当我们实现一个由其他本地函数调用的实用函数时,我们不应该泄露除返回值以外的任何引用。否则,如果该实用函数被本地方法多次调用,它也将导致溢出问题。

    提示

    在安卓 4.0 之前,局部引用是通过直接指向对象的指针实现的。此外,即使调用了DeleteLocalRef,这些直接指针也从未失效。因此,程序员可以在引用声称被删除后,仍然使用局部引用作为直接指针。由于这种设计,很多不符合 JNI 规范的代码也能工作。然而,从安卓 4.0 开始,局部引用已经改为使用间接机制。因此,在安卓 4.0 及以后版本中,使用局部引用作为直接指针的 buggy 代码将会出错。强烈建议您始终遵循 JNI 规范。

  • 全局引用:本地方法globalReference展示了全局引用的一个典型用法。当向pDelete输入参数传递false时,会保留全局引用,因为这是一个静态变量。下次调用该方法时,静态全局引用仍然会引用同一个对象。因此,我们不需要再次调用NewGlobalRef。这种技术可以让我们避免在每次调用全局引用时执行相同的操作。

    我们在 Java 代码中三次调用globalReference,如下所示:

    globalReference("hello global ref", false); 
    globalReference("hello global ref 2", true);
    globalReference("hello global ref 3", true);
    

    结果应该类似于以下内容:

    它是如何工作的…

    伴随第一次方法调用的字符串被保留,因此前两次调用显示相同的字符串。在我们第二次调用结束时删除全局引用后,第三次调用显示的是伴随其调用的字符串。

    请注意,尽管DeleteGlobalRef释放了全局引用,但它并没有将其设置为NULL。我们在删除操作之后明确地将全局引用设置为NULL

  • 弱引用:弱引用与全局引用类似,不同之处在于它不会阻止垃圾收集器GC)收集它所引用的底层对象。弱引用不如局部引用和全局引用常用。一个典型的用例是,当我们引用大量非关键对象,并且我们不希望当 GC 认为有必要时,阻止 GC 收集其中一些对象。

    提示

    安卓对弱引用的支持取决于版本。在安卓 2.2 之前,弱引用根本没有实现。在安卓 4.0 之前,它只能传递给NewLocalRefNewGlobalRefDeleteWeakGlobalRef。从安卓 4.0 开始,安卓完全支持弱引用。

  • 赋值与 NewRef 的区别:在referencetest.c源代码中,我们实现了本地ReferenceAssignmentAndNew方法。这个方法展示了赋值与分配新引用之间的区别。

    我们将输入的 jstring pStringP 两次传递给 JNI 函数 NewGlobalRef,以创建两个全局引用(globalRefNewglobalRefNew2),并将其中一个全局引用赋值给变量 globalRefAssignment。然后我们测试它们是否都引用了同一个对象。

    由于jobjectjstring实际上是 void 数据类型的指针,我们可以将它们的值作为整数打印出来。最后,我们调用了三次DeleteGlobalRef。以下是 Android logcat 输出的截图:

    它是如何工作的…

    前三行表明,输入的 jstring pStringP,两个全局引用 globalRefNewglobalRefNew2,以及赋值的 jstring globalRefAssignment 都引用了同一个对象。输出的第五到八行显示了相同的值,这意味着所有引用本身都是等价的。最后,前两次DeleteGlobalRef调用成功,而最后一次失败。

    New<ReferenceType>Ref JNI函数实际上会找到底层对象,然后为该对象添加一个引用。它允许为同一个对象添加多个引用。请注意,尽管我们的示例执行显示由New<ReferenceType>Ref创建的引用值相同,但这并不保证。两个指向同一对象的对象指针和引用同一对象的引用与两个不同的指针相关联是有可能的。

    建议您永远不要依赖引用的值;你应该使用 JNI 函数。例如,使用IsSameObject,永远不要使用"=="来测试两个引用是否指向同一个底层对象,除非是与NULL进行比较。

    Delete<ReferenceType>Ref的调用次数必须与New<ReferenceType>Ref的调用次数相匹配。较少的调用可能会潜在地导致内存泄漏,而更多的调用则会失败,正如前面的结果所示。

    赋值操作不会通过虚拟机,因此它不会导致虚拟机添加新的引用。

    请注意,虽然我们使用了全局引用来示例,但这些原则同样适用于局部引用和弱引用。

还有更多内容...

另外一种管理局部引用的方法是使用 JNI 函数 PushLocalFramePopLocalFrame。感兴趣的读者可以参考 JNI 规范以获取更多信息。

在使用AttachCurrentThread将本地线程附加到原生线程后,线程中运行的代码在未分离线程之前不会释放局部引用。局部引用应该明确释放。通常,只要我们不再需要它,明确释放局部引用是一个好习惯。

在 JNI 中操作类

之前的食谱讨论了 Android JNI 支持三种不同的引用。这些引用用于访问引用数据类型,包括字符串、类、实例对象和数组。这个食谱专注于 Android JNI 中的类操作。

准备工作

在阅读这个食谱之前,应该先阅读在 NDK 中管理引用的食谱。

如何操作…

以下步骤描述了如何构建一个示例 Android 应用程序,演示 JNI 中的类操作:

  1. 创建一个名为ClassManipulation的项目。将包名设置为cookbook.chapter2。创建一个名为ClassManipulationActivity的活动。在项目下,创建一个名为jni的文件夹。如果你需要更详细的说明,请参考本章中加载本地库和注册本地方法的食谱。

  2. jni文件夹下创建一个名为classtest.c的文件,然后实现findClassDemofindClassDemo2GetSuperclassDemoIsAssignableFromDemo方法。我们可以参考下载的ClassManipulation项目源代码。

  3. 修改ClassManipulationActivity.java文件,添加代码以加载本地库,声明本地方法,并调用本地方法。

  4. 创建一个Dummy类和一个继承Dummy类的DummySubClass子类。创建一个DummyInterface接口和一个继承DummyInterfaceDummySubInterface子接口。

  5. 修改layout XML 文件,添加Android.mk构建文件,并构建本地库。具体细节请参考本章中加载本地库和注册本地方法的步骤 8 至 10。

  6. 我们现在准备运行项目。在下一节中讨论每个本地方法时,我们将展示输出结果。

它是如何工作的…

这个食谱演示了 JNI 中的类操作。我们突出以下几点:

  • 类描述符:类描述符指的是类或接口的名称。它可以通过在 JNI 编程中将 Java 中的"."字符替换为"/"来得到。例如,类java.lang.String的描述符是java/lang/String

  • FindClass 和类加载器:JNI 函数FindClass具有以下原型:

    jclass FindClass(JNIEnv *env, const char *name);
    

    它接受一个JNIEnv指针和一个类描述符,然后定位到一个类加载器来加载相应的类。它返回一个初始化后的类的局部引用,如果失败则返回NULLFindClass使用调用堆栈最顶层方法关联的类加载器。如果找不到,它会使用"系统"类加载器。一个典型的例子是,在我们创建一个线程并将其附加到虚拟机之后,调用堆栈的最顶层方法将是如下所示:

    dalvik.system.NativeStart.run(Native method)
    

    这个方法不是我们应用程序代码的一部分。因此,使用的是"系统"类加载器。

    提示

    线程可以在 Java 中创建(称为托管线程或 Java 线程),也可以在本地代码中创建(称为本地线程或非虚拟机线程)。通过调用 JNI 函数AttachCurrentThread,本地线程可以附加到虚拟机。一旦附加,本地线程就像 Java 线程一样运行,在本地方法内部工作。它将保持连接状态,直到调用 JNI 函数DetachCurrentThread

    在我们的ClassManipulation项目中,我们用本地方法findClassDemofindClassDemo2说明了FindClassfindClassDemo方法在虚拟机创建的线程中运行。FindClass调用将正确找到类加载器。findClassDemo2方法创建了一个非虚拟机线程并将该线程附加到虚拟机。它说明了我们在上一节中描述的情况。调用这两个本地方法的 logcat 输出如下:

    工作原理…

    如输出所示,非虚拟机线程成功加载了String类,但未能加载我们定义的Dummy类。解决此问题的方法是在JNI_OnLoad方法中缓存对Dummy类的引用。我们将在缓存 jfieldID、jmethodID 和引用数据以提高性能的菜谱中提供一个详细的例子。

  • GetSuperclass:JNI 函数GetSuperclass具有以下原型:

    jclass GetSuperclass(JNIEnv *env, jclass clazz);
    

    它可以帮助我们查找给定类的超类。如果clazzjava.lang.Object,这个函数返回NULL;如果是接口,它返回对java.lang.Object的本地引用;如果是其他任何类,它返回对其超类的本地引用。

    在我们的ClassManipulation项目中,我们用本地方法GetSuperclassDemo说明了GetSuperclass。我们在 Java 代码中创建了一个Dummy类和一个DummyInterface接口,其中DummySubClass扩展了Dummy,而DummySubInterface扩展了DummyInterface。在本地方法中,我们分别对java.lang.ObjectDummySubClassDummySubInterface调用GetSuperclass。以下是 logcat 输出的截图:

    工作原理…

    如截图所示,GetSuperclass可以成功找到DummySubClass的超类。在这个本地方法中,我们使用了实用函数nativeGetClassName,在那里我们调用了toString方法。我们将在在 JNI 中调用实例和静态方法的菜谱中介绍如何进行此类方法调用。

  • IsAssignableFrom:JNI 函数IsAssignableFrom具有以下原型:

    jboolean IsAssignableFrom(JNIEnv *env, jclass cls1, jclass cls2);
    

    如果cls1可以安全地转换为cls2,此函数返回JNI_TRUE,否则返回JNI_FALSE。我们在本地方法IsAssignableFromDemo中演示了其用法。我们获得了对DummySubClass的本地引用,并调用GetSuperclass获取对Dummy的本地引用。然后,我们调用IsAssignableFrom来测试是否可以将DummySubClass转换为Dummy以及反之。以下是 logcat 输出的截图:

    工作原理…

    如预期的那样,子类可以安全地转换为超类,但反之则不行。

提示

Android 上不支持 JNI 函数DefineClass。这是因为该函数需要原始类数据作为输入,而 Android 上的 Dalvik VM 不使用 Java 字节码或类文件。

在 JNI 中操作对象

上一个菜谱展示了如何在 Android JNI 中操作类。这个菜谱描述了如何在 Android NDK 编程中操作实例对象。

准备就绪

在阅读这个菜谱之前,应该先阅读以下菜谱:

  • 在 JNI 中管理引用

  • 在 JNI 中操作类

如何操作…

现在,我们将创建一个带有本地方法的 Android 项目,演示与实例对象相关的 JNI 函数的使用。执行以下步骤:

  1. 创建一个名为ObjectManipulation的项目。将包名设置为cookbook.chapter2。创建一个名为ObjectManipulationActivity的活动。在项目下,创建一个名为jni的文件夹。如果你需要更详细的说明,请参考本章中的加载本地库和注册本地方法的菜谱。

  2. jni文件夹下创建一个名为objecttest.c的文件,然后实现AllocObjectDemoNewObjectDemoNewObjectADemoNewObjectVDemoGetObjectClassDemoIsInstanceOfDemo方法。你可以参考下载的ObjectManipulation项目源代码。

  3. 修改ObjectManipulationActivity.java,添加加载本地库、声明本地方法并调用它们的代码。

  4. 创建一个Dummy类,以及一个继承自DummyDummySub类。创建一个具有两个字段nameage、一个构造函数以及一个getContactStr方法的Contact类。

  5. 修改layout XML 文件,添加Android.mk构建文件,并构建本地库。更多详细信息,请参考本章中加载本地库和注册本地方法的步骤 8 至 10。

  6. 我们现在准备运行项目。在下一节讨论每个本地方法时,我们将展示输出结果。

工作原理…

这个菜谱介绍了在 JNI 中操作对象的多种方法:

  • 在本地代码中创建实例对象:可以使用四个 JNI 函数在本地代码中创建 Java 类的实例对象,它们分别是AllocObjectNewObjectNewObjectANewObjectVAllocObject函数创建一个未初始化的对象,而其他三种方法则将构造函数作为输入参数来创建对象。这四个函数的原型如下:

    jobject AllocObject(JNIEnv *env, jclass clazz);
    
    jobject NewObject(JNIEnv *env, jclass clazz,jmethodID methodID, ...);
    
    jobject NewObjectA(JNIEnv *env, jclass clazz,jmethodID methodID, jvalue *args);
    jobject NewObjectV(JNIEnv *env, jclass clazz,jmethodID methodID, va_list args);
    

    clazz参数是我们想要创建实例对象的 Java 类的引用。它不能是一个数组类,数组类有其自己的 JNI 函数集。methodID是构造函数方法 ID,可以通过使用GetMethodID JNI 函数获得。

    对于NewObject,在methodID之后可以传递可变数量的参数,函数会将它们传递给构造函数以创建实例对象。NewObjectA接受类型为jvalue的数组,并将其传递给构造函数。jvalue是一个联合类型,定义如下:

    typedef union jvalue {
       jboolean z;
       jbyte    b;
       jchar    c;
       jshort   s;
       jint     i;
       jlong    j;
       jfloat   f;
       jdouble  d;
       jobject  l;
    } jvalue;
    

    NewObjectV将存储在va_list中的参数传递给构造函数。va_listva_startva_endva_arg一起,使我们能够访问函数的可变数量的输入参数。具体的细节超出了本书的范围。但是,你可以从提供的示例代码中了解到它的工作原理。

    在 Java 代码中,我们调用了所有四个本地方法,这些方法分别使用不同的 JNI 函数来创建我们定义的Contact类的实例对象。然后我们将显示所有四个Contact对象的名称和年龄字段的值。以下是样本运行的截图:

    工作原理…

    如所示,由AllocObject创建的实例对象未初始化,因此所有字段都包含 Java 赋予的默认值,而其他三种方法则创建了我们传递初始值的对象。

  • GetObjectClass:这个 JNI 函数具有以下原型:

    jclass GetObjectClass(JNIEnv *env, jobject obj);
    

    它返回对实例对象obj的类的本地引用。obj参数不能为NULL,否则会导致虚拟机崩溃。

    在我们的GetObjectClassDemo本地方法实现中,我们获得了对Contact类的引用,然后调用AllocObject创建未初始化的对象实例。在 Java 代码中,我们以下列方式显示创建的对象实例的字段:

    工作原理…

    正如预期的那样,未初始化的Contact对象实例的字段值是由 Java 赋予的默认值。

  • IsInstanceOf:这个 JNI 函数调用的原型如下:

    jboolean IsInstanceOf(JNIEnv *env, jobject obj, jclass clazz);
    

    它判断实例对象obj是否是类clazz的实例。我们在IsInstanceOfDemo本地方法中说明了这个函数的使用。该方法创建了对Dummy类的本地引用和对DummySub类的本地引用,DummySubDummy的子类。然后它创建了两个对象,每个类一个。然后代码针对每个对象引用和每个类引用调用IsInstanceOf,总共进行了四次检查。我们将输出发送到 logcat。此方法的样本执行给出了以下结果:

    工作原理…

    结果显示,Dummy实例对象是Dummy类的实例但不是DummySub类的实例,而DummySub实例对象既是Dummy类的实例也是DummySub类的实例。

在 JNI 中操作数组

JNI 将字符串、类、实例对象和数组作为引用类型暴露出来。本节将讨论 JNI 中的数组。

准备工作

在阅读本节之前,你应当确保已经阅读了以下内容:

  • 在 JNI 中管理引用

  • 在 JNI 中操作类

如何操作…

在这一节中,我们将创建一个示例 Android 项目,演示如何在 JNI 中操作数组。

  1. 创建一个名为ArrayManipulation的项目。将包名设置为cookbook.chapter2。创建一个名为ArrayManipulationActivity的活动。在项目下,创建一个名为jni的文件夹。更多详细说明请参考本章中关于加载本地库和注册本地方法的菜谱。

  2. jni文件夹下创建一个名为arraytest.c的文件,然后实现GetArrayLengthDemoNewObjectArrayDemoNewIntArrayDemoGetSetObjectArrayDemoGetReleaseIntArrayDemoGetSetIntArrayRegionDemoGetReleasePrimitiveArrayCriticalDemo本地方法。

  3. 修改ArrayManipulationActivity.java,添加加载本地库、声明本地方法并调用它们的代码。

  4. 创建一个名为Dummy的类,它有一个名为value的整数字段。

  5. 修改布局 XML 文件,添加Android.mk构建文件,并构建本地库。更多细节请参考本章中关于加载本地库和注册本地方法的步骤 8 至 10。

  6. 我们现在准备运行这个项目。在下一节中,我们将展示输出结果,同时讨论每个本地方法。

它是如何工作的…

数组由jarray或其子类型如jobjectArrayjbooleanArray表示。与jstring类似,它们不能像 C 数组那样直接被本地代码访问。JNI 提供了各种访问数组的函数:

  • 创建新数组:JNI 提供了NewObjectArrayNew<Type>Array函数来创建对象和基本类型的数组。它们的函数原型如下:

    jarray NewObjectArray(JNIEnv *env, jsize length, jclass elementType, jobject initialElement);
    <ArrayType> New<Type>Array(JNIEnv *env, jsize length);
    

    我们在本地方法NewObjectArrayDemo中展示了NewObjectArray的使用,在这里我们创建了 10 个Dummy类的实例。该函数的length参数表示要创建的对象数量,elementType是对类的引用,initialElement是将为所有创建的对象实例在数组中设置的初始化值。在 Java 代码中,我们实现了callNewObjectArrayDemo方法,该方法调用NewObjectArrayDemo本地方法来创建一个包含 10 个Dummy对象的数组,所有对象的value字段都设置为5。执行结果应类似于以下截图:

    它是如何工作的…

    如预期的那样,由NewObjectArray创建的所有对象的value字段都是5

    New<Type>Array的用法在原生方法NewIntArrayDemo中展示,我们使用 JNI 函数NewIntArray创建一个包含 10 个整数的数组,然后为每个整数分配一个值。JNI 的所有八种基本类型(jbooleanjbytejcharjshortjintjlongjfloatjdouble)都有一个对应的New<Type>Array函数来创建其类型的数组。请注意,NewIntArrayDemo调用了GetIntArrayElementsReleaseIntArrayElements JNI 函数,我们将在本食谱后面的内容中讨论。在 Java 代码中,我们实现了一个callNewIntArrayDemo方法来调用NewIntArrayDemo并在屏幕上显示整数数组元素。callNewIntArrayDemo的执行给出了以下结果:

    工作原理…

    如截图所示,整数数组被分配了从09的值。

  • GetArrayLength:这个原生函数具有以下原型:

    jsize GetArrayLength(JNIEnv *env, jarray array);
    

    它接受对jarray的引用并返回其长度。我们在原生方法GetArrayLengthDemo中演示了其用法。在 Java 代码中,我们实现了callGetArrayLengthDemo方法,该方法创建了三个数组,包括一个double数组、一个Dummy对象数组和一个二维整数数组。该方法调用GetArrayLengthDemo原生方法来获取这三个数组的长度。我们在原生方法中将数组长度输出到 logcat。示例执行输出应与以下截图类似:

    工作原理…

  • 访问对象数组:JNI 提供了两个访问对象数组的函数,分别是GetObjectArrayElementSetObjectArrayElement。顾名思义,第一个函数获取数组中对象元素的引用,而第二个函数设置对象数组的元素。这两个函数具有以下原型:

    jobject GetObjectArrayElement(JNIEnv *env,jobjectArray array, jsize index);
    void SetObjectArrayElement(JNIEnv *env, jobjectArray array, jsize index, jobject value);
    

    在这两个函数中,参数array指的是对象数组,而index是元素的位置。get函数返回对对象元素的引用,而set函数根据value参数设置元素。

    我们在原生方法GetSetObjectArrayDemo中展示了这两个函数的用法。该方法接受一个对象数组和另一个对象。它将索引为 1 的对象替换为接收到的对象,然后返回索引为 1 的原始对象。在 Java 代码中,我们调用了callGetSetObjectArrayDemo方法,传递一个包含三个值为012Dummy对象数组,以及另一个值为100Dummy对象给原生方法。执行结果应与以下截图类似:

    工作原理…

    如所示,索引为1的对象被值为100的对象替换,而值为1的原始对象被返回。

  • 访问基本类型数组:JNI 提供了三组函数来访问基本类型数组。我们分别用三种不同的本地方法演示它们,都以jintarray为例。其他基本类型数组的操作与整数类似。

    首先,如果我们想在本地缓冲区创建jintarray的独立副本,或者只访问大型数组的一小部分,GetIntArrayRegion/ SetIntArrayRegion函数是合适的选择。这两个函数具有以下原型:

    void GetIntArrayRegion(JNIEnv *env, jintArray array, jsize start, jsize len, jint* buf);
    void SetIntArrayRegion(JNIEnv *env, jintArray array, jsize start, jsize len, jint* buf);
    

    这两个函数接受相同的输入参数集。参数array指的是我们操作的jintArraystart是起始元素位置,len表示要获取或设置的元素数量,buf是本地整数缓冲区。我们在名为GetSetIntArrayRegionDemo的本地方法中展示了这两个函数的用法。该方法接受一个输入jintArray,将数组中索引 1 到 3 的三个元素复制到本地缓冲区,在本地缓冲区将它们的值乘以2,然后将值复制回索引02

    在 Java 代码中,我们实现了callGetSetIntArrayRegionDemo方法来初始化整数数组,将数组传递给本地方法GetSetIntArrayRegionDemo,并显示所有元素调用前后的值。你应该会看到类似于以下截图的输出:

    工作原理…

    这五个元素的初始值是01234。我们从索引一复制三个元素(123)到本地缓冲区buf。然后我们在本地缓冲区将值乘以2,使得本地缓冲区的前三个元素变成了246。我们将这三个值从本地缓冲区复制回整数数组,从索引0开始。因此,这三个元素的最终值是246,最后两个元素保持不变,为34

    其次,如果我们想要访问大型数组,那么GetIntArrayElementsReleaseIntArrayElements就是为我们准备的 JNI 函数。它们具有以下原型:

    jint *GetIntArrayElements(JNIEnv *env, jintArray array, jboolean *isCopy);
    void ReleaseIntArrayElements(JNIEnv *env, jintArray array, jint *elems, jint mode);
    

    GetIntArrayElements返回指向数组元素的指针,如果失败则返回NULL。数组输入参数指的是我们想要访问的数组,isCopy在函数调用结束后如果创建了新副本,则设置为true。返回的指针在调用ReleaseIntArrayElements之前都是有效的。

    ReleaseIntArrayElements通知虚拟机我们不再需要访问数组元素。输入参数array指的是我们操作的数组,elemsGetIntArrayElements返回的指针,mode指示释放模式。当GetIntArrayElements中的isCopy设置为JNI_TRUE时,我们通过返回的指针所做的更改将反映在jintArray上,因为我们操作的是同一份副本。当isCopy设置为JNI_FALSE时,mode参数决定数据释放的方式。根据我们是否需要从原生缓冲区将值复制回原数组,以及是否需要释放elems原生缓冲区,mode参数可以是0JNI_COMMITJNI_ABORT,如下所示:

    复制值回原数组
    自由原生缓冲区
    ---
    0 JNI_ABORT
    COMMIT -

    我们通过本地方法GetReleaseIntArrayDemo说明这两个 JNI 函数。该方法接受一个输入整数数组,通过GetIntArrayElements获取原生指针,将每个元素乘以2,最后通过将mode设置为0ReleaseIntArrayElements提交更改。在 Java 代码中,我们实现了callGetReleaseIntArrayDemo方法来初始化输入数组并调用GetReleaseIntArrayDemo本地方法。以下是执行callGetReleaseIntArrayDemo方法后手机屏幕显示的截图:

    工作原理…

    如预期的那样,原始数组中的所有整数元素都乘以了2

    第三组 JNI 函数是GetPrimitiveArrayCriticalReleasePrimitiveArrayCritical。这两个函数的使用与Get<Type>ArrayElementsRelease<Type>ArrayElements类似,但有一个重要的区别——GetRelease方法之间的代码块是关键区域。在同一个虚拟机中,当前线程等待其他线程的任何其他 JNI 函数或函数调用都不应该发生。这两个方法本质上是增加了获取原始原始数组的未复制版本的可能性,从而提高了性能。我们在本地方法GetReleasePrimitiveArrayCriticalDemo中演示了这些函数的使用,以及 Java 方法callGetReleasePrimitiveArrayCriticalDemo。实现与第二组函数调用相似,显示结果相同。

在原生代码中访问 Java 的静态和实例字段

我们已经演示了如何将不同类型的参数传递给本地方法并将数据返回给 Java。这不是原生代码和 Java 代码之间共享数据的唯一方式。这个方法涵盖了另一种方式——从原生代码访问 Java 字段。

准备就绪

我们将介绍如何访问不同类型的 Java 字段,包括基本类型、字符串、实例对象和数组。在阅读这个食谱之前,应先阅读以下食谱:

  • 在基本类型中传递参数和接收返回值

  • 在 JNI 中操作字符串

  • 在 JNI 中操作类

  • 在 JNI 中操作对象

  • 在 JNI 中操作数组

读者还应该熟悉 Java 反射 API。

如何操作…

按照以下步骤创建一个示例 Android 项目,演示如何从本地代码访问 Java 的静态和实例字段:

  1. 创建一个名为AccessingFields的项目。将包名设置为cookbook.chapter2。创建一个名为AccessingFieldsActivity的活动。在项目下,创建一个名为jni的文件夹。更多详细说明请参考本章的加载本地库和注册本地方法食谱。

  2. jni文件夹下创建一个名为accessfield.c的文件,然后实现AccessStaticFieldDemoAccessInstanceFieldDemoFieldReflectionDemo本地方法。

  3. 修改AccessingFieldsActivity.java,添加加载本地库、声明本地方法并调用它们的代码。此外,添加四个实例字段和四个静态字段。

  4. 创建一个Dummy类,包含一个名为value的整数实例字段和一个名为value2的整数静态字段。

  5. 修改布局 XML 文件,添加Android.mk构建文件,并构建本地库。更多细节请参考本章的加载本地库和注册本地方法食谱中的步骤 8 至 10。

  6. 我们现在准备运行项目。在下一节中,我们将展示每个本地方法的输出。

工作原理…

本食谱讨论了从本地代码访问 Java 中的字段(包括静态和实例字段):

  • jfieldID数据类型jfieldID是一个常规的 C 指针,指向一个对开发者隐藏详细信息的结构体。我们不应将其与jobject或其子类型混淆。jobject是对应于 Java 中Object的引用类型,而jfieldID在 Java 中没有这样的对应类型。然而,JNI 提供了将java.lang.reflect.Field实例转换为jfieldID以及反之的函数。

  • 字段描述符:它指的是用来表示字段数据类型的修改后的 UTF-8 字符串。下表总结了 Java 字段类型及其对应的字段描述符:

    Java 字段类型 字段描述符
    boolean Z
    byte B
    char C
    short S
    int I
    long J
    float F
    double D
    String Ljava/lang/String;
    Object Ljava/lang/Object;
    int[] [I
    Dummy[] [Lcookbook/chapter2/Dummy;
    Dummy[][] [Lcookbook/chapter2/Dummy;

    如表所示,八种原始类型每种都有一个字符字符串作为其字段描述符。对于对象,字段描述符以"L"开头,后跟类描述符(详细内容请参阅在 JNI 中操作类的菜谱),并以";"结束。对于数组,字段描述符以"["开头,后跟元素类型的描述符。

  • 访问静态字段:JNI 提供了三个函数来访问 Java 类的静态字段。它们具有以下原型:

    jfieldID GetStaticFieldID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
    <NativeType> GetStatic<Type>Field(JNIEnv *env,jclass clazz, jfieldID fieldID);
    void SetStatic<Type>Field(JNIEnv *env, jclass clazz, jfieldID fieldID,<NativeType> value);
    

    要访问静态字段,第一步是获取字段 ID,这是这里列出的第一个功能完成的。在方法原型中,clazz参数指的是定义静态字段的 Java 类,name表示字段名称,sig是字段描述符。

    获取到方法 ID 后,我们可以通过调用第二个或第三个函数来获取或设置字段值。在函数原型中,<Type>可以指代八种 Java 原始类型中的任意一种或ObjectfieldID是由第一个方法返回的jfieldID。对于set函数,value是我们想要分配给字段的新值。

    前述三个 JNI 函数的使用在本地方法AccessStaticFieldDemo中进行了演示,我们为整数字段、字符串字段、数组字段和一个Dummy对象字段设置和获取值。这四个字段在 Java 类AccessingFieldsActivity中定义。在本地代码中,我们将获取的值输出到 Android logcat,而在 Java 代码中,我们将本地代码设置的值显示在手机屏幕上。以下截图显示了 logcat 输出:

    ![工作原理… 手机显示将与以下截图类似: 工作原理…

    如所示,我们在 Java 代码中为字段设置的值可以通过本地代码获取;而本地方法设置的值也反映在 Java 代码中。

  • 访问实例字段:访问实例字段与访问静态字段类似。JNI 也为我们提供了以下三个函数:

    jfieldID GetFieldID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
    <NativeType> Get<Type>Field(JNIEnv *env,jobject obj, jfieldID fieldID);
    void Set<Type>Field(JNIEnv *env, jobject obj, jfieldID fieldID, <NativeType> value);
    

    同样,我们需要首先获取字段 ID,然后才能为字段获取和设置值。在调用getset函数时,我们应该传递对象引用,而不是传递类引用。

    使用方法在本地方法AccessInstanceFieldDemo中展示。同样,我们在本地代码中将get的值打印到 logcat,并在手机屏幕上显示修改后的字段值。以下截图显示了 logcat 输出:

    工作原理…

    手机显示将与以下截图类似:

    工作原理…

    对于访问静态字段的解释也可以类似地应用于结果。

  • 字段反射支持:JNI 提供了两个函数以支持与 Java Reflection API 中的Field进行互操作。它们具有以下原型:

    jfieldID FromReflectedField(JNIEnv *env, jobject field);
    jobject ToReflectedField(JNIEnv *env, jclass cls, jfieldID fieldID, jboolean isStatic);
    

    第一个函数将 java.lang.reflect.Field 转换为 jfieldID,然后我们可以使用前面描述的 setget JNI 函数。参数字段是 java.lang.reflect.Field 的一个实例。

    第二个函数则相反。它接受一个类引用,一个 jfieldID,以及一个表示是静态字段还是实例字段的 jboolean 变量。函数返回一个指向 java.lang.reflect.Field 对象的引用。

    这两个函数的用法在本地方法 FieldReflectionDemo 中得到了演示。我们使用调用者传递的 Field 实例来访问字段值,然后为另一个字段返回一个 Field 实例。在 Java 方法 callFieldReflectionDemo 中,我们将 Field 实例传递给本地代码,并使用返回的 Field 实例获取 field 值。本地代码将字段值输出到 logcat,如下所示:

    它是如何工作的…

    Java 代码如下所示,在手机屏幕上显示另一个字段的值:

    它是如何工作的…

从本地代码调用静态和实例方法

上一个食谱涵盖了如何在 NDK 中访问 Java 字段。除了字段,Java 类还有方法。这个食谱重点介绍如何从 JNI 调用静态和实例方法。

准备就绪

代码示例需要了解 JNI 基本类型、字符串、类和实例对象的基础知识。在阅读这个食谱之前,最好确保你已经阅读了以下食谱:

  • 在基本类型中传递参数和接收返回值

  • 在 JNI 中操作字符串

  • 在 JNI 中操作类

  • 在 JNI 中操作对象

  • 在本地代码中访问 Java 的静态和实例字段

期望读者也熟悉 Java 反射 API。

如何操作…

可以按照以下步骤创建一个示例 Android 项目,说明如何从本地代码调用静态和实例方法:

  1. 创建一个名为 CallingMethods 的项目。将包名设置为 cookbook.chapter2。创建一个名为 CallingMethodsActivity 的活动。在项目下,创建一个名为 jni 的文件夹。更多详细说明请参考本章的加载本地库和注册本地方法食谱。

  2. jni 文件夹下创建一个名为 callmethod.c 的文件,然后实现本地方法 AccessStaticMethodDemoAccessInstanceMethodDemoMethodReflectionDemo

  3. 修改 CallingMethodsActivity.java,添加加载本地库、声明本地方法并调用它们的代码。

  4. 创建一个名为 Dummy 的类,其中有一个名为 value 的整数实例字段和一个名为 value2 的整数静态字段。此外,创建一个名为 DummySubDummy 子类,并添加一个名为 name 的 String 字段。

  5. 修改布局 XML 文件,添加Android.mk构建文件,并构建本地库。更多详细信息请参考本章中加载本地库和注册本地方法的步骤 8 至 10。

  6. 现在我们准备运行项目。在下一节讨论每个本地方法时,我们将展示输出。

工作原理…

本节将说明如何从本地代码调用 Java 的静态和实例方法:

  • jmethodID数据类型:与jfieldID类似,jmethodID是一个常规的 C 指针,指向一个从开发者那里隐藏详细信息的结构体。JNI 提供了将java.lang.reflect.Method实例转换为jmethodID以及反向转换的函数。

  • 方法描述符:这是一个修改后的 UTF-8 字符串,用于表示方法的输入(输入参数)数据类型和输出(返回类型)数据类型。方法描述符通过将所有输入参数的字段描述符组合在"()"内,并在后面追加返回类型的字段描述符来形成。如果返回类型是void,我们应该使用"V"。如果没有输入参数,我们只需使用"()",然后是返回类型的字段描述符。对于构造函数,应使用"V"来表示返回类型。下表列出了一些 Java 方法及其对应的方法描述符:

    Java 方法 方法描述符
    Dummy(int pValue) (I)V
    String getName() ()Ljava/lang/String;
    void setName(String pName) (Ljava/lang/String;)V
    long f(byte[] bytes, Dummy dummy) (BLcookbook/chapter2/Dummy;)J
  • 调用静态方法:JNI 为本地代码调用 Java 方法提供了四组函数。它们的原型如下:

    jmethodID GetStaticMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
    
    <NativeType> CallStatic<Type>Method(JNIEnv *env, jclass clazz, jmethodID methodID, ...);
    
    <NativeType> CallStatic<Type>MethodA(JNIEnv *env, jclass clazz, jmethodID methodID, jvalue *args);
    
    <NativeType> CallStatic<Type>MethodV(JNIEnv *env, jclass clazz,jmethodID methodID, va_list args);
    

    第一个函数获取方法 ID。它接受指向 Java 类的引用clazz,以修改后的 UTF-8 字符串格式的方法名和方法描述符sig。其他三组函数用于调用静态方法。<Type>可以是八种原始类型中的任意一种,VoidObject。它表示被调用方法的返回类型。methodID参数是GetStaticMethodID函数返回的jmethodID。Java 方法的参数在CallStatic<Type>Method中逐个传递,或者放入jvalue数组作为CallStatic<Type>MethodA,或者放入va_list结构作为CallStatic<Type>MethodV

    我们在本地方法 AccessStaticMethodDemo 中展示了所有四组 JNI 函数的用法。该方法获取 Dummy 类的 getValue2setValue2 静态方法的方法 ID,并使用三种不同的方式传递参数来调用这两个方法。在 CallingMethodsActivity.java 中,我们实现了 callAccessStaticMethodDemo,它将 value2 静态字段初始化为 100,调用本地方法 AccessStaticMethodDemo,并在手机屏幕上打印最终的 value2 值。以下截图展示了 logcat 输出:

    ![工作原理… 屏幕输出与以下截图相似: 工作原理…

    如所示,本地方法首先获取 value2100,然后使用三种不同的 JNI 函数调用 set 方法来修改值。最终,手机屏幕显示最终修改的值反映在 Java 代码中。

  • 调用实例方法:从本地代码调用实例方法与调用静态方法类似。JNI 也提供了以下四组函数:

    jmethodID GetMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
    
    <NativeType> Call<Type>Method(JNIEnv *env, jobject obj, jmethodID methodID, ...);
    
    <NativeType> Call<Type>MethodA(JNIEnv *env,jobject obj, jmethodID methodID, jvalue *args);
    
    <NativeType> Call<Type>MethodV(JNIEnv *env, jobject obj, jmethodID methodID, va_list args);
    

    这四组函数的用法与调用静态方法的 JNI 函数类似,不同之处在于我们需要传递实例对象的引用而不是类。此外,JNI 还提供了另外三组用于调用实例方法的函数,如下所示:

    <NativeType> CallNonvirtual<Type>Method(JNIEnv *env, jobject obj, jclass clazz, jmethodID methodID, ...);
    
    <NativeType> CallNonvirtual<Type>MethodA(JNIEnv *env, jobject obj, jclass clazz, jmethodID methodID, jvalue *args);
    
    <NativeType> CallNonvirtual<Type>MethodV(JNIEnv *env, jobject obj, jclass clazz, jmethodID methodID, va_list args);
    

    与之前的三组函数相比,这三组方法接受一个额外的参数 clazzclazz 参数可以是 obj 实例化自的类的引用,或者是 obj 的超类。一个典型的用例是在类上调用 GetMethodID 以获取 jmethodID。我们有一个该类子类的对象的引用,然后我们可以使用前面的函数通过对象引用调用与 jmethodID 相关联的 Java 方法。

    在本地方法 AccessInstanceMethodDemo 中展示了所有七组函数的用法。我们使用前四组函数通过 DummySub 类的对象调用了该类的 getNamesetName 方法。然后,我们使用 CallNonvirtual<Type>Method 来调用在 Dummy 超类中定义的 getValuesetValue 方法。在 CallingMethodsActivity.java 中,我们实现了 callAccessInstanceMethodDemo 方法来调用 AccessInstanceMethodDemo 本地方法。以下截图展示了 logcat 输出:

    工作原理…

    结果显示,getNamesetNamegetValuesetValue 方法已成功执行。

  • 方法反射支持:与字段类似,JNI 也提供了以下两个函数来支持反射:

    jmethodID FromReflectedMethod(JNIEnv *env, jobject method);
    
    jobject ToReflectedMethod(JNIEnv *env, jclass cls, jmethodID methodID, jboolean isStatic);
    

    第一个函数接受对java.lang.reflect.Method实例的引用,并返回其对应的jmethodID。返回的jmethodID值随后可用于调用相关的 Java 方法。第二个函数则相反。它接受对 Java 类、jmethodID以及指示是否为静态方法的jboolean的引用,并返回对java.lang.reflect.Method的引用。返回值可以在 Java 代码中使用,以访问相应的方法。

    我们在本地方法MethodReflectionDemo中说明了这两个 JNI 函数。在CallingMethodsActivity.java中,我们实现了callMethodReflectionDemo方法,以将getValuejava.lang.reflect.Method对象传递给本地代码,获取返回的setValue java.lang.reflect.Method对象,并用返回的对象调用setValue方法。

    本地方法将getValue方法的返回值输出到 logcat,如下所示:

    它是如何工作的…

    Java 代码在手机屏幕上显示调用setValue前后的getValue方法返回值,如下所示:

    它是如何工作的…

    如预期的那样,本地代码可以通过从 Java 代码传递来的Method对象访问getValue方法,而 Java 代码也可以通过从本地方法返回的Method对象调用setValue方法。

缓存jfieldIDjmethodID以及引用数据以提高性能

本教程涵盖了 Android JNI 中的缓存,这可以提高我们的本地代码性能。

准备就绪

在进行本教程之前,您应该确保已经阅读了以下教程:

  • 在本地代码中访问 Java 的静态字段和实例字段

  • 从本地代码调用静态方法和实例方法

如何操作…

以下步骤详细介绍了如何构建一个示例 Android 应用程序,演示 JNI 中的缓存:

  1. 创建一个名为Caching的项目。将包名设置为cookbook.chapter2。创建一个名为CachingActivity的活动。在项目下,创建一个名为jni的文件夹。更多详细说明请参考本章中的加载本地库和注册本地方法

  2. jni文件夹下创建一个名为cachingtest.c的文件,然后实现InitIDsCachingFieldMethodIDDemo1CachingFieldMethodIDDemo2CachingReferencesDemo方法。

  3. 修改CachingActivity.java文件,添加加载本地库的代码,然后声明并调用本地方法。

  4. 修改布局 XML 文件,添加Android.mk构建文件,并构建本地库。具体细节请参考本章中加载本地库和注册本地方法的步骤 8 至 10。

  5. 在 Android 设备或模拟器上运行项目,并使用 eclipse 或终端中的adb logcat -v time命令监控 logcat 输出。

  6. CachingActivity.javaonCreate方法中,启用callCachingFieldMethodIDDemo1方法,并禁用其他演示方法。启动 Android 应用程序,你应该能在 logcat 中看到以下内容:如何操作…

  7. CachingActivity.java中启用callCachingFieldMethodIDDemo2,同时禁用其他演示方法以及InitIDs方法(在静态初始化器中)。启动 Android 应用程序,你应该能在 logcat 中看到以下内容:如何操作…

  8. CachingActivity.java中启用callCachingReferencesDemo,同时注释掉其他演示方法。启动 Android 应用程序,你应该能在 logcat 中看到以下内容:如何操作…

工作原理…

本食谱讨论了在 JNI 中使用缓存的方法:

  • 缓存字段和方法 ID:字段和方法 ID 是内部指针。它们用于访问 Java 字段或进行本地到 Java 方法调用。获取字段或方法 ID 需要调用预定义的 JNI 函数,根据名称和描述符进行符号查找。查找过程通常需要多次字符串比较,相对耗时。

    一旦获得了字段或方法 ID,访问字段或进行本地到 Java 的调用相对较快。因此,一个好的实践是只执行一次查找并缓存字段或方法 ID。

    缓存字段和方法 ID 有两种方法。第一种方法在类初始化器中缓存。在 Java 中,我们可以有类似于以下的内容:

    private native static void InitIDs();
    static {
        System.loadLibrary(<native lib>);
        InitIDs();
    }
    

    静态初始化器在类的方法之前执行是有保障的。因此,我们可以确保在调用本地方法时所需的 ID 是有效的。这种方法的使用在InitIDsCachingFieldMethodIDDemo1本地方法以及CachingActivity.java中有所展示。

    第二种方法在使用的时刻缓存 ID。我们将字段或方法 ID 存储在静态变量中,这样下次调用本地方法时 ID 仍然有效。这种方法的使用在本地方法CachingFieldMethodIDDemo2CachingActivity.java中有所展示。

    对比这两种方法,第一种更为推荐。首先,第一种方法在使用 ID 之前不需要进行有效性检查,因为静态初始化器总是首先被调用,因此在调用本地方法之前 ID 始终有效。其次,如果类被卸载,缓存的 ID 将无效。如果使用第二种方法,我们需要确保类不会被卸载并重新加载。如果使用第一种方法,当类重新加载时静态初始化器会自动被调用,因此我们永远不需要担心类被卸载和重新加载。

  • 缓存引用:JNI 将类、实例对象、字符串和数组作为引用暴露出来。我们在在 JNI 中管理引用的菜谱中介绍了如何管理引用。有时,缓存引用也可以提高性能。与直接指针的字段和方法 ID 不同,引用是通过开发者不可见的间接机制实现的。因此,我们需要依赖 JNI 函数来缓存它们。

    为了缓存引用数据,我们需要将其设置为全局引用或弱全局引用。全局引用保证在显式删除之前引用始终有效。而弱全局引用允许底层的对象被垃圾回收。因此,在使用它之前我们需要进行有效性检查。

    原生方法CachingReferencesDemo演示了如何缓存字符串引用。注意,虽然DeleteGlobalRef使全局引用无效,但它不会将引用赋值为NULL。我们需要手动进行这一操作。

检查错误和在 JNI 中处理异常

JNI 函数可能会因为系统限制(例如,内存不足)或无效的参数(例如,函数期望得到 UTF-16 字符串时却传递了原生 UTF-8 字符串)而失败。这个菜谱讨论了如何在 JNI 编程中处理错误和异常。

准备工作

在继续本菜谱之前,应先阅读以下菜谱:

  • 在 JNI 中操作字符串

  • 在 JNI 中管理引用

  • 在原生代码中访问 Java 的静态和实例字段

  • 从原生代码中调用静态方法和实例方法

如何操作…

按照以下步骤创建一个示例 Android 项目,说明在 JNI 中的错误和异常处理:

  1. 创建一个名为ExceptionHandling的项目。将包名设置为cookbook.chapter2。创建一个名为ExceptionHandlingActivity的活动。在项目下,创建一个名为jni的文件夹。更多详细说明请参考本章中的加载原生库和注册原生方法的菜谱。

  2. jni文件夹下创建一个名为exceptiontest.c的文件,然后实现ExceptionDemoFatalErrorDemo方法。

  3. 修改ExceptionHandlingActivity.java文件,添加加载原生库的代码,然后声明并调用原生方法。

  4. 修改布局 XML 文件,添加Android.mk构建文件,并构建原生库。更多详细信息请参考本章中加载原生库和注册原生方法菜谱的步骤 8 至 10。

  5. 我们现在准备运行项目。在下一节中,我们将展示每个原生方法的输出。

工作原理…

本菜谱讨论了在 JNI 中的错误检查和异常处理:

  • 检查错误和异常:许多 JNI 函数返回一个特殊值来表示失败。例如,FindClass函数返回NULL表示未能加载类。许多其他函数不使用返回值来表示失败;而是抛出异常。

    提示

    除了 JNI 函数之外,本地代码调用的 Java 代码也可能抛出异常。我们应该确保检查这些情况,以编写健壮的本地代码。

    对于第一组函数,我们可以简单地检查返回值以查看是否发生错误。对于第二组函数,JNI 定义了两个函数来检查异常,如下所示:

    jboolean ExceptionCheck(JNIEnv *env);
    jthrowable ExceptionOccurred(JNIEnv *env);
    

    第一个函数返回JNI_TRUE表示发生异常,否则返回JNI_FALSE。第二个函数返回异常的本地引用。当使用第二个函数时,可以调用附加的 JNI 函数来检查异常的详细信息:

    void ExceptionDescribe(JNIEnv *env);
    

    该函数将异常和堆栈的回溯打印到 logcat 中。

    在本地方法ExceptionDemo中,我们使用了两种方法来检查异常的发生,并使用ExceptionDescribe打印异常详情。

  • 处理错误和异常:JNI 的异常与 Java 异常不同。在 Java 中,当发生错误时,会创建一个异常对象并将其交给运行时。然后运行时搜索调用堆栈以找到一个可以处理异常的异常处理器。搜索从发生异常的方法开始,按照方法被调用的相反顺序进行。当找到这样的代码块时,运行时将控制权交给异常处理器。因此,正常的控制流程被打断。相比之下,JNI 异常不会改变控制流程,我们需要显式检查异常并正确处理。

    通常有两种处理异常的方法。第一种方法是释放 JNI 中分配的资源并返回。这将把处理异常的责任留给本地方法的调用者。

    第二种实践是清除异常并继续执行。这是通过以下 JNI 函数调用来完成的:

    void ExceptionClear(JNIEnv *env);
    

    在本地方法ExceptionDemo中,我们使用了第二种方法来清除java.lang.NullPointerException,并使用第一种方法将java.lang.RuntimeException返回给调用者,即ExceptionHandlingActivity.java中的 Java 方法callExceptionDemo

    当有异常待处理时,并不是所有的 JNI 函数都可以安全调用。以下函数在有挂起异常时是可以被允许的:

    • DeleteGlobalRef

    • DeleteLocalRef

    • DeleteWeakGlobalRef

    • ExceptionCheck

    • ExceptionClear

    • ExceptionDescribe

    • ExceptionOccurred

    • MonitorExit

    • PopLocalFrame

    • PushLocalFrame

    • Release<PrimitiveType>ArrayElements

    • ReleasePrimitiveArrayCritical

    • ReleaseStringChars

    • ReleaseStringCritical

    • ReleaseStringUTFChars

    它们基本上是异常检查和处理函数,或者是用于在本地代码中清除已分配资源的函数。

    注意

    当有异常悬而未决时,调用这里未列出的 JNI 函数可能会导致意外的结果。我们应该正确处理待处理的异常,然后继续。

  • 在本地代码中抛出异常:JNI 提供了两个函数从本地代码抛出异常。它们的原型如下:

    jint Throw(JNIEnv *env, jthrowable obj);
    jint ThrowNew(JNIEnv *env, jclass clazz, const char *message);
    

    第一个函数接受对jthrowable对象的引用并抛出异常,而第二个函数接受对异常类的引用。它将创建一个clazz类的异常对象,带有消息参数,并抛出它。

    ExceptionDemo本地方法中,我们使用了ThrowNew函数来抛出java.lang.NullPointerException,以及一个Throw函数来抛出java.lang.RuntimeException

    下面的 logcat 输出显示了如何检查、清除和抛出异常:

    它是如何工作的…

    最后一个异常在本地方法中没有清除。在 Java 代码中,我们捕获了异常并在手机屏幕上显示消息:

    它是如何工作的…

  • 致命错误:一种特殊的、不可恢复的错误是致命错误。JNI 定义了一个函数FatalError,如下所示,用于引发致命错误:

    void FatalError(JNIEnv *env, const char *msg);
    

    这个函数接受一条消息并将其打印到 logcat。之后,应用程序的虚拟机实例将被终止。我们在本地方法FatalErrorDemo和 Java 方法callFatalErrorDemo中演示了此函数的用法。以下是在 logcat 捕获的输出:

    它是如何工作的…

    请注意,FatalError函数之后的代码永远不会执行,无论是在本地代码还是 Java 代码中,因为FatalError永远不会返回,虚拟机实例会被终止。在我的 Android 设备上,这不会导致 Android 应用程序崩溃,但会导致应用程序冻结。

还有更多内容...

当前在 Android JNI 编程中不支持 C++异常。换句话说,本地 C++异常不会通过 JNI 传播到 Java 世界。因此,我们应在 C++代码内处理 C++异常。或者,我们可以编写一个 C 包装器来抛出异常或返回错误代码给 Java。

在 JNI 中集成汇编代码

Android NDK 允许你在 JNI 编程中编写汇编代码。汇编代码有时用于优化代码的关键部分以获得最佳性能。本指南无意讨论如何在汇编中编程。它描述了如何在 JNI 编程中集成汇编代码。

准备就绪

在继续之前,请阅读在基本类型中传递参数和接收返回值的指南。

如何操作…

以下步骤创建了一个集成汇编代码的示例 Android 项目:

  1. 创建一个名为AssemblyInJNI的项目。设置包名为cookbook.chapter2。创建一个名为AssemblyInJNIActivity的活动。在项目下,创建一个名为jni的文件夹。有关更详细的说明,请参考本章中的加载本地库和注册本地方法的教程。

  2. jni文件夹下创建一个名为assemblyinjni.c的文件,然后实现InlineAssemblyAddDemo方法。

  3. jni文件夹下创建一个名为tmp.c的文件,并实现本地方法AssemblyMultiplyDemo。使用以下命令将tmp.c代码编译成名为AssemblyMultiplyDemo.s的汇编源文件:

    $ $ANDROID_NDK/toolchains/arm-linux-androideabi-4.4.3/prebuilt/linux-x86/bin/arm-linux-androideabi-gcc -S tmp.c -o AssemblyMultiplyDemo.s --sysroot=$ANDROID_NDK/platforms/android-14/arch-arm/
    
  4. 修改AssemblyInJNIActivity.java文件,添加加载本地库的代码,然后声明并调用本地方法。

  5. 修改布局 XML 文件,添加Android.mk构建文件,并构建本地库。具体步骤请参考本章中加载本地库和注册本地方法教程的第 8 至第 10 步。

  6. AssemblyInJNIActivity.java文件中,启用callInlineAssemblyAddDemo本地方法,禁用callAssemblyMultiplyDemo方法。在 Android 设备或模拟器上运行项目。手机显示应与以下截图相似:如何操作…

  7. AssemblyInJNIActivity.java中,启用callAssemblyMultiplyDemo本地方法,禁用callInlineAssemblyAddDemo方法。在 Android 设备或模拟器上运行项目。手机显示应与以下截图相似:如何操作…

工作原理…

本教程演示了如何使用汇编代码实现本地方法:

  • C 代码中的内联汇编:我们可以为 Android NDK 开发编写内联汇编代码。这可以在本地方法InlineAssemblyAddDemo中看到。

  • 生成单独的汇编代码:编写汇编代码的一种方法是先用 C 或 C++编写代码,并使用编译器将代码编译成汇编代码。然后,我们根据自动生成的汇编代码进行优化。由于本教程不是关于用汇编语言编写代码,我们使用 Android NDK 交叉编译器生成本地方法AssemblyMultiplyDemo,并从 Java 方法callAssemblyMultiplyDemo中调用它。

    我们首先在AssemblyMultiplyDemo.c中编写本地方法AssemblyMultiplyDemo,然后使用 Android NDK 的编译器进行交叉编译,使用以下命令:

    $ $ANDROID_NDK/toolchains/arm-linux-androideabi-4.4.3/prebuilt/linux-x86/bin/arm-linux-androideabi-gcc -S <c_file_name>.c -o <output_file_name>.s --sysroot=$ANDROID_NDK/platforms/android-<level>/arch-<arch>/
    

    在前面的命令中,$ANDROID_NDK 是一个指向 Android NDK 安装位置的环境变量。如果你按照第一章中的步骤操作过,Hello NDK,那么它应该已经被正确配置。否则,你可以将其替换为你的 Android NDK 完整路径(例如,在我的电脑上,路径是/home/roman10/Desktop/android/android-ndk-r8)。<level>表示目标 Android 版本。在我们的例子中,我们使用了14<arch>表示架构;我们使用了arm。如果我们为其他架构(比如 x86)构建应用,那么这里应该是x86-S选项告诉交叉编译器将<c_file_name>.c文件编译成汇编代码,但不要进行汇编或链接。-o选项告诉编译器将汇编代码输出到文件<output_file_name>.s中。如果没有指定这个选项,编译器会输出到名为<c_file_name>.s的文件中。

  • 编译汇编代码:编译汇编代码与编译 C/C++ 源代码类似。正如在Android.mk文件中所示,我们只需像下面这样将汇编文件列为源文件:

    LOCAL_SRC_FILES := AssemblyMultiplyDemo.s assemblyinjni.c
    

第三章:构建和调试 NDK 应用程序

在本章中,我们将介绍以下食谱:

  • 在命令行构建 Android NDK 应用程序

  • 在 Eclipse 中构建 Android NDK 应用程序

  • 为不同的 ABI 构建 Android NDK 应用程序

  • 为不同的 CPU 特性构建 Android NDK 应用程序

  • 使用日志消息调试 Android NDK 应用程序

  • 使用 CheckJNI 调试 Android NDK 应用程序

  • 使用 NDK GDB 调试 Android NDK 应用程序

  • 使用 CGDB 调试 Android NDK 应用程序

  • 在 Eclipse 中调试 Android NDK 应用程序

引言

我们在第一章 Hello NDK中介绍了环境设置,以及第二章 Java Native Interface中的 JNI 编程。为了构建 Android NDK 应用程序,我们还需要使用 Android NDK 的构建调试工具。

Android NDK 附带了 ndk-build 脚本,以方便构建任何 Android NDK 应用程序。这个脚本隐藏了调用交叉编译器、交叉链接器等的复杂性,让开发者无需处理。我们将从介绍 ndk-build 命令的用法开始。

Android Development Tools (ADT) 插件的最近一次发布支持从 Eclipse 构建 Android NDK 应用程序。我们将演示如何使用它。

我们将探讨为不同的应用程序二进制接口 (ABIs) 构建 NDK 应用程序,并利用可选的 CPU 特性。这对于在不同 Android 设备上实现最佳性能至关重要。

除了构建,我们还将介绍各种用于 Android NDK 应用程序的调试工具和技术。从简单但强大的日志技术开始,我们将展示如何从命令行和 Eclipse IDE 中调试 NDK 应用程序。还将介绍 CheckJNI 模式,它可以帮助我们捕获 JNI 错误。

在命令行构建 Android NDK 应用程序

尽管 Eclipse 是推荐用于 Android 开发的 IDE,但有时我们希望在命令行中构建 Android 应用程序,以便可以轻松地自动化该过程并成为持续集成过程的一部分。本食谱重点介绍如何在命令行中构建 Android NDK 应用程序。

准备工作

Apache Ant 主要是一个用于构建 Java 应用程序的工具。它接受一个 XML 文件来描述构建、部署和测试过程,管理这些过程,并自动跟踪依赖关系。

我们将使用 Apache Ant 来构建和部署我们的示例项目。如果你还没有安装它,可以按照以下命令进行安装:

  • 如果你使用的是 Ubuntu Linux,请使用以下命令:

    $ sudo apt-get install ant1.8
    
    
  • 如果你使用的是 Mac,请使用以下命令:

    $ sudo port install apache-ant
    
    
  • 如果你使用的是 Windows,可以从code.google.com/p/winant/downloads/list下载 winant 安装程序,并进行安装。

读者在阅读本节之前,应该已经设置好了 NDK 开发环境,并阅读了第一章中的编写 Hello NDK 程序部分,Hello NDK

如何操作…

以下步骤创建并构建一个示例HelloNDK应用:

  1. 创建项目。启动命令行控制台并输入以下命令:

    $ android create project \
    --target android-15 \
    --name HelloNDK \
    --path ~/Desktop/book-code/chapter3/HelloNDK \
    --activity HelloNDKActivity \
    --package cookbook.chapter3
    
    

    提示

    android工具可以在 Android SDK 文件夹的tools/目录下找到。如果你按照第一章设置了 SDK 和 NDK 开发环境,并正确配置了PATH,那么可以直接从命令行执行android命令。否则,你需要输入到android程序的相关路径或完整路径。这也适用于本书中使用的其他 SDK 和 NDK 工具。

    以下是命令输出的截图:

    如何操作…

  2. 转到HelloNDK项目文件夹,并使用以下命令创建一个名为jni的文件夹:

    $ cd ~/Desktop/book-code/chapter3/HelloNDK
    $ mkdir jni
    
    
  3. jni文件夹下创建一个名为hello.c的文件,并添加以下内容:

    #include <string.h>
    #include <jni.h>
    
    jstring Java_cookbook_chapter3_HelloNDKActivity_naGetHelloNDKStr(JNIEnv* pEnv, jobject pObj)
    {
       return (*pEnv)->NewStringUTF(pEnv, "Hello NDK!");
    }
    
  4. jni文件夹下创建一个名为Android.mk的文件,内容如下:

    LOCAL_PATH := $(call my-dir)
    include $(CLEAR_VARS)
    LOCAL_MODULE    := hello
    LOCAL_SRC_FILES := hello.c
    include $(BUILD_SHARED_LIBRARY)
    
  5. 使用以下命令构建本地库:

    $ ndk-build
    
    
  6. 修改HelloNDKActivity.java文件为以下内容:

    package cookbook.chapter3;
    import android.app.Activity;
    import android.os.Bundle;
    import android.widget.TextView;
    public class HelloNDKActivity extends Activity {
       @Override
       public void onCreate(Bundle savedInstanceState) {
           super.onCreate(savedInstanceState);
           TextView tv = new TextView(this);
           tv.setTextSize(30);
           tv.setText(naGetHelloNDKStr());
           this.setContentView(tv);
       }
       public native String naGetHelloNDKStr();
       static {
           System.loadLibrary("hello");
       }
    }
    
  7. 更新项目。我们添加了一个本地库,因此需要使用以下命令更新项目。注意,除非我们更改项目设置,否则此命令只需执行一次,而之前的ndk-build命令每次更新本地代码都需要执行:

    $ android update project --target android-15 --name HelloNDK \
    --path ~/Desktop/book-code/chapter3/HelloNDK
    
    

    以下是命令输出的截图:

    如何操作…

  8. 转到项目根文件夹,并使用以下命令以调试模式构建我们的项目:

    $ ant debug
    
    

    在以下截图中,我们展示了输出的最后几行,这表示构建成功的是:

    如何操作…

    输出的apk文件将生成在bin/HelloNDK-debug.apk

  9. 使用以下命令创建一个模拟器:

    $ android --verbose create avd --name android_4_0_3 \
    --target android-15 --sdcard 32M
    
    

    以下是命令输出的截图:

    如何操作…

  10. 使用以下命令启动模拟器:

    $ emulator -wipe-data -avd android_4_0_3
    
    

    或者,我们可以使用"android avd"命令打开Android 虚拟设备管理器窗口,然后选择一个模拟器启动,如下所示:

    如何操作…

  11. 在模拟器上安装应用。我们首先通过以下命令检查设备序列号:

    $ adb devices
    
    

    以下是命令输出的截图:

    如何操作…

  12. 然后,我们使用以下命令将debug.apk文件安装到模拟器上:

    $ adb -s emulator-5554 install bin/HelloNDK-debug.apk
    
    

    如何操作…

    提示

    如果只有一个设备连接到电脑,那么无需指定设备序列号。在上述命令中,我们可以移除"-s emulator-5554"。

  13. 使用以下格式的命令在模拟器上启动HelloNDK应用:

    $ adb shell am start -n com.package.name/com.package.name.ActivityName
    
    

    在我们的示例中,我们使用以下命令:

    $ adb -s emulator-5554 shell am start -n cookbook.chapter3/cookbook.chapter3.HelloNDKActivity
    
    

    如何操作…

  14. 在设备上运行应用。

    假设设备序列号为 HT21HTD09025,那么我们可以使用以下命令在 Android 设备上安装应用。

    $ adb -s HT21HTD09025 install bin/HelloNDK-debug.apk
    
    

    在我们的示例中,我们使用以下命令来启动应用:

    $ adb -s HT21HTD09025 shell am start -n cookbook.chapter3/cookbook.chapter3.HelloNDKActivity
    
    
  15. 创建一个发布包。

一旦我们确认应用程序可以成功运行,我们可能想要创建一个发布包以便上传到 Android 市场。你可以执行以下步骤来实现这一点:

  1. 创建一个密钥库。Android 应用必须使用密钥库中的密钥进行签名。一个 密钥库 是私钥的集合。我们可以使用以下命令创建带有私钥的密钥库:

    $ keytool -genkey -v -keystore release_key.keystore \
    -alias androidkey \
    -keyalg RSA -keysize 2048 -validity 10000 \
    -dname "CN=MyCompany, OU=MyAndroidDev, O=MyOrg, L=Singapore, S=Singapore, C=65" \
    -storepass testkspw -keypass testkpw
    
    

    以下是命令输出的截图:

    如何操作…

    如所示,创建了一个带有密码为 testkwpw 的密钥库,并在其中添加了一个带有密码为 testkpw 的 RSA 密钥对。

  2. 输入命令 "ant release" 为应用构建一个 apk。输出可以在 bin 文件夹中找到,文件名为 HelloNDK-release-unsigned.apk

  3. 使用以下命令对 apk 进行签名:

    $ jarsigner -verbose -keystore <keystore name> -storepass <store password> -keypass <key password> -signedjar <name of the signed output> <unsigned input file name> <alias>
    
    

    对于我们的示例应用程序,命令和输出如下:

    如何操作…

  4. apk 文件进行 zip 对齐。zipalign 工具对 apk 文件内的数据进行对齐,以优化性能。以下命令可用于对齐已签名的 apk

    $ zipalign -v 4 <app apk file name>  <aligned apk file name>
    
    

    对于我们的示例应用程序,命令和输出如下:

    如何操作…

工作原理…

本教程介绍如何从命令行构建 Android NDK 应用程序。

Android NDK 提供了一个具有以下目标的构建系统:

  • 简单性:它为开发者处理了大部分繁重的工作,我们只需要编写简短的构建文件(Android.mkApplication.mk)来描述需要编译的源代码。

  • 兼容性:未来的版本可能会向 NDK 添加更多构建工具、平台等,但构建文件不需要更改。

Android NDK 提供了一套交叉工具链,包括交叉编译器、交叉链接器、交叉汇编器等。这些工具可以在 NDK root 目录下的 toolchains 文件夹中找到。它们可用于在 Linux、Mac OS 或 Windows 上为不同的 Android 平台(ARM、x86 或 MIPS)生成二进制文件。尽管可以直接使用工具链来为 Android 构建本地代码,但除非我们正在移植带有自己的构建脚本的项目,否则不推荐这样做。在这种情况下,我们可能只需要将原始编译器更改为 NDK 交叉编译器,以构建适用于 Android 的版本。

在大多数情况下,我们将在 Android.mk 中描述源代码,并在 Application.mk 上指定 ABIs。Android NDK 的 ndk-build 脚本将在内部调用交叉工具链为我们构建本地代码。以下是一些常用的 ndk-build 选项列表:

  • ndk-build:它用于构建二进制文件。

  • ndk-build clean:它清理生成的二进制文件。

  • ndk-build V=1:构建二进制文件并显示构建命令。当我们想要了解构建过程或检查构建错误时,这很方便。

  • ndk-build -B:此命令强制重新构建。

  • ndk-build NDK_DEBUG=1:生成可调试的构建。

  • ndk-build NDK_DEBUG=0:生成发布版本。

还有更多内容...

本教程使用了许多 Android SDK 的命令行工具。这允许我们提供如何创建、构建和部署 Android NDK 项目的完整说明。然而,由于本书专注于 Android NDK,因此不会详细介绍这些工具。你可以访问developer.android.com/tools/help/index.html了解更多关于这些工具的信息。

从命令行截取屏幕截图

从命令行截取屏幕截图对于记录自动化测试的显示结果很有帮助。然而,目前 Android 没有提供用于截屏的命令行工具。

可以使用位于 Android 源代码\development\tools\screenshot\src\com\android\screenshot\的 Java 程序来截取屏幕截图。该代码使用了与 Eclipse DDMS 插件类似的方法从命令行截取屏幕截图。我们将前面的代码整合到一个名为screenshot的 Eclipse Java 项目中,可以从网站下载。

用户可以导入项目并导出一个可执行的 JAR 文件来使用该工具。假设导出的 JAR 文件名为screenshot.jar,那么以下示例命令使用它从模拟器中截取屏幕:

从命令行截取屏幕截图

在 Eclipse 中构建 Android NDK 应用程序

上一教程讨论了如何在命令行中构建 Android NDK 应用程序。本教程演示如何在 Eclipse IDE 中完成此操作。

准备就绪

添加 NDK 首选项。启动 Eclipse,然后点击窗口 | 首选项。在首选项窗口中,选择Android下的NDK。点击浏览并选择 NDK 的文件夹。点击确定

准备就绪

如何操作…

以下步骤使用 Eclipse 创建一个 NDK 项目:

  1. 创建一个名为HelloNDKEclipse的 Android 应用程序。将包名设置为cookbook.chapter3。创建一个名为HelloNDKEclipseActivity的活动。如果你需要更详细的说明,请参考第二章,Java Native Interface中的加载本地库和注册本地方法教程。

  2. 右键点击项目HelloNDKEclipse,选择Android Tools | 添加本地支持。会出现一个类似以下截图的窗口。点击完成以关闭它:如何操作…

    这将在内部添加一个包含两个文件(HelloNDKEclipse.cppAndroid.mk)的jni文件夹,并将 Eclipse 切换到 C/C++透视图。

  3. HelloNDKEclipse.cpp中添加以下内容:

    #include <jni.h>
    
    jstring getString(JNIEnv* env) {
      return env->NewStringUTF("Hello NDK");
    }
    
    extern "C" {
      JNIEXPORT jstring JNICALL Java_cookbook_chapter3_HelloNDKEclipseActivity_getString(JNIEnv* env, jobject o){
        return getString(env);
      }
    }
    
  4. 将 HelloNDKEclipseActivity.java 的内容更改为以下内容。

    package cookbook.chapter3;
    
    import android.os.Bundle;
    import android.app.Activity;
    import android.widget.TextView;
    
    public class HelloNDKEclipseActivity extends Activity {
      @Override
       public void onCreate(Bundle savedInstanceState) {
           super.onCreate(savedInstanceState);
           TextView tv = new TextView(this);
           tv.setTextSize(30);
           tv.setText(getString());
           this.setContentView(tv);
       }
       public native String getString();
       static {
           System.loadLibrary("HelloNDKEclipse");
       }
    }
    
  5. 右键点击 HelloNDKEclipse 项目,选择 构建项目。这将为我们构建本地库。

  6. 右键点击项目,选择 运行方式,然后选择 Android 应用程序。手机屏幕将显示类似于以下截图的内容:如何操作…

它是如何工作的...

本食谱讨论在 Eclipse 中构建 Android NDK 应用程序。

在所有之前的食谱中我们一直在使用 C。从本食谱开始,我们将用 C++ 编写代码。

默认情况下,Android 提供了最小的 C++ 支持。没有 运行时类型信息 (RTTI) 和 C++ 异常支持,甚至 C++ 标准库支持也是部分的。以下是 Android NDK 默认支持的 C++ 头文件列表:

cassert, cctype, cerrno, cfloat, climits, cmath, csetjmp, csignal, cstddef, cstdint, cstdio, cstdlib, cstring, ctime, cwchar, new, stl_pair.h, typeinfo, utility

通过使用不同的 C++ 库,有可能增加对 C++ 的支持。NDK 除了系统默认库之外,还提供了 gabi++stlportgnustl C++ 库。

在我们的示例代码中,我们使用了外部 "C" 来包装 C++ 方法。这样做是为了避免 JNI 函数名被 C++ 糟蹋。C++ 名称糟蹋可能会改变函数名以包含关于参数的类型信息,函数是否为虚函数等。虽然这使得 C++ 能够链接重载函数,但它破坏了 JNI 函数发现机制。

我们还可以使用 第二章 Java Native Interface加载本地库和注册本地方法 食谱中涵盖的显式函数注册方法来摆脱包装。

为不同的 ABI 构建一个 Android NDK 应用程序

本地代码被编译成二进制文件。因此,一组二进制文件只能在一个特定的架构上运行。Android NDK 提供了技术和工具,使开发者能够轻松地为多个架构编译相同的源代码。

准备就绪

一个 应用程序二进制接口 (ABI) 定义了 Android 应用程序的机器代码如何在运行时与系统交互,包括 CPU 指令集、字节序、内存对齐等。ABI 基本上定义了一种架构类型。

下表简要总结了 Android 支持的四个 ABI:

ABI 名称 支持 不支持 可选
armeabi
  • ARMv5TE 指令集

  • Thumb(也称为 Thumb-1)指令

硬件辅助浮点计算
armeabi-v7a
  • armeabi 支持的所有内容

  • VFP 硬件 FPU 指令

  • Thumb-2 指令集

  • VFPv3-D16 被使用。

  • 高级 SIMD(也称为 NEON)

  • VFPv3-D32

  • ThumbEE

|

x86
  • 通常称为 "x86" 或 "IA-32" 的指令集。

  • MMX、SSE、SSE2 和 SSE3 指令集扩展

  • MOVBE 指令

  • SSSE3 "补充 SSE3" 扩展

  • 任何 "SSE4" 的变体

|

mips
  • MIPS32r1 指令集

  • 硬浮点

  • O32

|

  • DSP 应用特定扩展

  • MIPS16

  • micromips

armeabi 和 armeabi-v7a 是 Android 设备最常用的两种 ABI。ABI armeabi-v7a 与 armeabi 兼容,这意味着为 armeabi 编译的应用程序也可以在 armeabi-v7a 上运行。但反之则不成立,因为 armeabi-v7a 包含额外的功能。在以下部分中,我们将简要介绍在 armeabi 和 armeabi-v7a 中经常提到的一些技术术语。

  • Thumb:这个指令集由 16 位指令组成,是标准 ARM 32 位指令集的一个子集。某些 32 位指令集中的指令在 Thumb 中不可用,但可以用几个 Thumb 指令来模拟。更窄的 16 位指令集可以提供内存优势。

    Thumb-2 通过添加一些 32 位指令扩展了 Thumb-1,从而形成了一种可变长度指令集。Thumb-2 旨在像 Thumb-1 一样实现代码密度,并在 32 位内存上实现与标准 ARM 指令集相似的性能。

    Android NDK 默认生成 thumb 代码,除非在 Android.mk 文件中定义了 LOCAL_ARM_MODE

  • 向量浮点(VFP):它是 ARM 处理器的扩展,提供了低成本的浮点计算功能。

  • VFPv3-D16 和 VFPv3-D32:VFPv3-D16 指的是 16 个专用的 64 位浮点寄存器。同样,VFPv3-D32 意味着有 32 个 64 位浮点寄存器。这些寄存器加速了浮点计算。

  • NEON:NEON 是 ARM 高级单指令多数据(SIMD) 指令集扩展的昵称。它需要 VFPv3-D32 支持,这意味着将使用 32 个硬件浮点单元 64 位寄存器。它提供了一系列标量/向量指令和寄存器,这些在 x86 世界中与 MMX/SSE/SDNow!相当。并非所有 Android 设备都支持 NEON,但许多新设备已经具备 NEON 支持。NEON 可以通过同时执行多达 16 个操作,显著加速媒体和信号处理应用程序。

有关更详细信息,可以参考 ARM 文档网站 infocenter.arm.com/help/index.jsp。这里我们不讨论 x86 和 mips ABI,因为很少有 Android 设备运行在这些架构上。

在进行这一步之前,请阅读 在 Eclipse 中构建 Android NDK 应用程序 的菜谱。

如何进行操作...

以下步骤为不同的 ABI 构建 Android 项目:

  1. 创建一个名为 HelloNDKMultipleABI 的 Android 应用程序。将包名设置为 cookbook.chapter3。创建一个名为 HelloNDKMultipleABIActivity 的活动。

  2. 右键点击 HelloNDKMultipleABI 项目,选择 Android Tools | Add Native Support。出现一个窗口,点击 Finish 关闭它。这将添加一个包含两个文件(HelloNDKMultipleABI.cppAndroid.mk)的 jni 文件夹,并将 Eclipse 切换到 C/C++视角。

  3. HelloNDKMultipleABI.cpp 文件中添加以下内容:

    #include <jni.h>
    
    jstring getString(JNIEnv* env) {
      return env->NewStringUTF("Hello NDK");
    }
    
    extern "C" {
      JNIEXPORT jstring JNICALL Java_cookbook_chapter3_HelloNDKMultipleABIActivity_getString(JNIEnv* env, jobject o){
        return getString(env);
      }
    }
    
  4. HelloNDKMultipleABIActivity.java 文件更改为以下内容:

    package cookbook.chapter3;
    
    import android.os.Bundle;
    import android.app.Activity;
    import android.widget.TextView;
    
    public class HelloNDKMultipleABIActivity extends Activity {
    
       @Override
       public void onCreate(Bundle savedInstanceState) {
           super.onCreate(savedInstanceState);
           TextView tv = new TextView(this);
           tv.setTextSize(30);
           tv.setText(getString());
           this.setContentView(tv);
       }
       public native String getString();
       static {
           System.loadLibrary("HelloNDKMultipleABI");
       }
    }
    
  5. 在项目的jni文件夹下添加一个名为Application.mk的新文件,内容如下:

    APP_ABI := armeabi armeabi-v7a
    
  6. 右键点击HelloNDKMultipleABIActivity项目,选择构建项目。这将为我们构建原生库。

  7. 创建两个模拟器,分别将 ABI 设置为armeabiarmeabi-v7a。以下截图展示了如何创建一个 ABI 为armeabi的模拟器:如何操作...

  8. 在两个模拟器上运行示例 Android 应用程序。在它们上面显示的结果相同:如何操作...

  9. Application.mk的内容更改为以下代码片段,并在两个模拟器上运行示例应用程序。应用程序仍然可以在两个模拟器上运行:

    #APP_ABI := armeabi armeabi-v7a
    APP_ABI := armeabi
    
  10. Application.mk的内容更改如下:

    #APP_ABI := armeabi armeabi-v7a
    #APP_ABI := armeabi
    APP_ABI := armeabi-v7a
    
  11. 在两个模拟器上运行示例应用程序。应用程序在armeabi-v7a模拟器上运行,但在armeabi模拟器上会崩溃,如下面的截图所示:如何操作...

工作原理…

一个 Android 设备可以定义一个或两个 ABI。对于基于 x86、MIPS、ARMv5 和 ARMv6 的典型设备,只有一个首要 ABI。根据平台,它可以是 x86、mips 或 armeabi。对于基于典型 ARMv7 的设备,首要 ABI 通常是 armeabi-v7a,它还有一个次要 ABI 为 armeabi。这使得编译为 armeabi 或 armeabi-v7a 的二进制文件可以在 ARMv7 设备上运行。在我们的示例中,我们证明了当只针对 armeabi 构建时,应用程序可以在 armeabi 和 armeabi-v7a 模拟器上运行。

在安装时,Android 包管理器会搜索为首要 ABI 构建的原生库,并将其复制到应用程序的数据目录中。如果没有找到,它会搜索为次要 ABI 构建的原生库。这确保只有正确的原生库被安装。

在我们的示例中,当我们只针对 armeabi-v7a 编译二进制文件时,原生库将不会安装在 armeabi 模拟器上,因此无法加载原生库,并且会显示崩溃。

为不同的 CPU 特性构建 Android NDK 应用程序

许多项目使用原生代码以提高性能。与 SDK 开发相比,在 NDK 中开发的一个优点是我们可以为不同的 CPU 构建不同的包,这正是本食谱的主题。

准备就绪

在继续本食谱之前,请阅读《为不同 ABI 构建 Android NDK 应用程序》的食谱。

如何操作…

以下步骤为不同的 CPU 特性构建 Android NDK 应用程序。

  1. 在 Eclipse 中,点击文件 | 新建 | 其他。在Android下选择现有代码中的Android 项目,如下面的截图所示。然后点击下一步如何操作…

  2. 浏览到 Android NDK 文件夹中的samples/hello-neon文件夹。然后点击完成

  3. 启动终端,然后进入samples/hello-neon/jni文件夹。输入命令"ndk-build"以构建二进制文件。

  4. 在不同的设备和模拟器上运行安卓项目。根据你的设备/模拟器 ABI 和 NEON 特性的可用性,你应该能够看到如下结果:

    • 对于具有 armeabi ABI 的安卓设备,结果如下:如何操作…

    • 对于具有 armeabi-v7a ABI 和 NEON 的安卓设备,结果如下:

    如何操作…

工作原理…

安卓设备大致可以通过 ABIs 来划分。然而,具有相同 ABI 的不同设备可能有不同的 CPU 扩展和特性。这些扩展和特性是可选的,因此我们在运行时之前无法知道用户的设备是否具备这些特性。在某些设备上,检测并利用这些特性有时可以显著提高应用性能。

安卓 NDK 包含一个名为cpufeatures的库,可以在运行时用来检测 CPU 家族和可选特性。正如示例代码所示,以下步骤指示如何使用这个库:

  1. Android.mk的静态库列表中添加,如下所示:

    LOCAL_STATIC_LIBRARIES := cpufeatures
    
  2. Android.mk文件的末尾,导入cpufeatures模块:

    $(call import-module,cpufeatures)
    
  3. 在代码中,包含头文件<cpu-features.h>

  4. 调用检测函数;目前cpufeatures只提供三个函数:

  5. 获取 CPU 家族。函数原型如下:

    AndroidCpuFamily   android_getCpuFamily(); 
    

    它返回一个枚举。支持的 CPU 系列在下面的章节中列出。

    ANDROID_CPU_FAMILY_MIPS 
    ANDROID_CPU_FAMILY_MIPS 
    ANDROID_CPU_FAMILY_ARM 
    
  6. 获取可选的 CPU 特性。每个 CPU 特性由一个位标志表示,如果特性可用,该位设置为1。函数原型如下:

    uint64_t   android_getCpuFeatures();
    

对于 ARM CPU 家族,支持的 CPU 特性检测如下:

  • ANDROID_CPU_ARM_FEATURE_ARMv7:这意味着支持 ARMv7-a 指令。

  • ANDROID_CPU_ARM_FEATURE_VFPv3:这意味着支持 VFPv3 硬件 FPU 指令集扩展。请注意,这里指的是 VFPv3-D16,它提供 16 个硬件浮点寄存器。

  • ANDROID_CPU_ARM_FEATURE_NEON:这意味着支持 ARM 高级 SIMD(也称为 NEON)向量指令集扩展。请注意,这样的 CPU 也支持 VFPv3-D32,它提供 32 个硬件浮点寄存器。

对于 x86 CPU 家族,支持的 CPU 特性检测如下:

  • ANDROID_CPU_X86_FEATURE_SSSE3:这意味着支持SSSE3指令扩展集。

  • ANDROID_CPU_X86_FEATURE_POPCNT:这意味着支持POPCNT指令。

  • ANDROID_CPU_X86_FEATURE_MOVBE:这意味着支持MOVBE指令。

我们可以进行"&"操作来检测一个特性是否可用,如下所示:

uint64_t features = android_getCpuFeatures();
if ((features & ANDROID_CPU_ARM_FEATURE_NEON) == 0) {
  //NEON is not available
} else {
  //NEON is available
}

获取设备上的 CPU 核心数:

int         android_getCpuCount(void);

提示

自从 NDK r8c 以来,更多的 CPU 特性检测可用。更多详情请参考sources/android/cpufeatures/cpu-features.c

还有更多…

关于安卓上的 CPU 特性还有几个值得注意的点。

关于 CPU 特性检测的更多信息

cpufeatures库只能检测有限的 CPU 特性集。我们可以实现自己的 CPU 检测机制。通过查看 NDK 源代码在/sources/android/cpufeatures/,可以发现cpufeatures库本质上查看的是/proc/cpuinfo文件。我们可以读取这个文件,并在我们的应用程序中解析内容。以下是文件内容的截图:

关于 CPU 特性检测的更多信息

请参考本书网站上的 Android 项目cpuinfo,了解如何通过编程方式实现这一点。

为不同的 CPU 特性构建的不同方法

为不同的 CPU 特性构建本地代码有几种方法:

  • 单一库,构建时不同的二进制文件:这也在示例项目中演示。helloneon-intrinsics.c文件仅针对 armeabi-v7a ABI 编译。

  • 单一库,运行时不同的执行路径:这也在示例项目中展示。代码在运行时检测 NEON 特性是否可用,并执行不同的代码块。

  • 不同库,运行时加载适当的库:有时,我们可能希望将源代码编译成不同的库,并通过名称区分它们。例如,我们可能有libmylib-neon.solibmylib-vfpv3.so。我们在运行时检测 CPU 特性并加载适当的库。

  • 不同包,运行时加载适当的库:如果库很大,最好为不同的 CPU 部署不同的二进制文件作为单独的包。这是 Google Play 上许多视频播放器(例如 MX Player)的做法。

使用日志消息调试 Android NDK 应用程序

Android 日志系统提供了一种从各种应用程序收集日志到一系列循环缓冲区的方法。使用logcat命令查看日志。日志消息是调试程序最简单的方法之一,也是最强大的方法之一。本食谱重点关注 NDK 中的消息日志记录。

如何实现…

以下步骤创建我们的示例 Android 项目:

  1. 创建一个名为NDKLoggingDemo的 Android 应用程序。将包名设置为cookbook.chapter3。创建一个名为NDKLoggingDemoActivity的活动。如果你需要更详细的说明,请参考第二章,Java Native Interface中的加载本地库和注册本地方法食谱。

  2. 右键点击项目NDKLoggingDemo,选择Android Tools | Add Native Support。出现一个窗口,点击Finish关闭它。

  3. jni文件夹下添加一个名为mylog.h的新文件,并向其中添加以下内容:

    #ifndef COOKBOOK_LOG_H
    #define COOKBOOK_LOG_H
    
    #include <android/log.h>
    
    #define LOG_LEVEL 9
    #define LOG_TAG "NDKLoggingDemo"
    
    #define LOGU(level, ...) if (level <= LOG_LEVEL) {__android_log_print(ANDROID_LOG_UNKNOWN, LOG_TAG, __VA_ARGS__);}
    #define LOGD(level, ...) if (level <= LOG_LEVEL) {__android_log_print(ANDROID_LOG_DEFAULT, LOG_TAG, __VA_ARGS__);}
    #define LOGV(level, ...) if (level <= LOG_LEVEL) {__android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__);}
    #define LOGDE(level, ...) if (level <= LOG_LEVEL) {__android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__);}
    #define LOGI(level, ...) if (level <= LOG_LEVEL) {__android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__);}
    #define LOGW(level, ...) if (level <= LOG_LEVEL) {__android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__);}
    #define LOGE(level, ...) if (level <= LOG_LEVEL) {__android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__);}
    #define LOGF(level, ...) if (level <= LOG_LEVEL) {__android_log_print(ANDROID_LOG_FATAL, LOG_TAG, __VA_ARGS__);}
    #define LOGS(level, ...) if (level <= LOG_LEVEL) {__android_log_print(ANDROID_LOG_SILENT, LOG_TAG, __VA_ARGS__);}
    
    #endif
    
  4. NDKLoggingDemo.cpp添加以下内容:

    #include <jni.h>
    #include "mylog.h"
    
    void outputLogs() {
      LOGU(9, "unknown log message");
      LOGD(8, "default log message");
      LOGV(7, "verbose log message");
      LOGDE(6, "debug log message");
      LOGI(5, "information log message");
      LOGW(4, "warning log message");
      LOGE(3, "error log message");
      LOGF(2, "fatal error log message");
      LOGS(1, "silent log message");
    }
    
    extern "C" {
      JNIEXPORT void JNICALL Java_cookbook_chapter3_NDKLoggingDemoActivity_LoggingDemo(JNIEnv* env, jobject o){
        outputLogs();
      }
    }
    
  5. 更改NDKLoggingDemoActivity.java的内容为以下:

    package cookbook.chapter3;
    
    import android.os.Bundle;
    import android.app.Activity;
    
    public class NDKLoggingDemoActivity extends Activity {
       @Override
       public void onCreate(Bundle savedInstanceState) {
           super.onCreate(savedInstanceState);
           LoggingDemo();
       }
       public native void LoggingDemo();
       static {
           System.loadLibrary("NDKLoggingDemo");
       }
    }
    
  6. 更改Android.mk文件,如下包含 Android 日志库:

    LOCAL_PATH := $(call my-dir)
    
    include $(CLEAR_VARS)
    
    LOCAL_MODULE    := NDKLoggingDemo
    LOCAL_SRC_FILES := NDKLoggingDemo.cpp
    LOCAL_LDLIBS := -llog
    include $(BUILD_SHARED_LIBRARY)
    
  7. 右键点击NDKLoggingDemo项目,并选择Build Project

  8. 输入以下命令开始监控logcat输出。然后,在 Android 设备上启动示例 Android 应用:

    $ adb logcat -c
    $ adb logcat NDKLoggingDemo:I *:S -v time
    
    

    以下是logcat输出的屏幕截图:

    如何操作…

  9. 启动另一个命令行终端,并在其中输入以下命令:

    $ adb logcat NDKLoggingDemo:V *:S -v time
    
    

    这将导致以下输出:

    如何操作…

  10. mylog.h中的行从#define LOG_LEVEL 9更改为#define LOG_LEVEL 4。重新构建应用程序,然后重新启动应用程序。

  11. 我们之前启动的两个终端的输出是相同的。如何操作…

它是如何工作的...

本食谱展示了如何使用 Android 日志消息。Android 中的每个日志消息由以下三部分组成:

  • 优先级:通常用于过滤日志消息。在我们的项目中,我们可以通过更改以下代码来控制日志:

    #define LOG_LEVEL 4 
    

    另外,我们可以使用logcat有选择性地显示日志输出。

  • 日志标签:通常用于标识日志来源。

  • 日志信息:它提供了详细的日志信息。

提示

在 Android 上发送日志消息会消耗 CPU 资源,频繁的日志消息可能会影响应用程序性能。此外,日志存储在一个循环缓冲区中。过多的日志会覆盖一些早期的日志,这可能是我们不希望看到的。由于这些原因,建议我们在发布版本中只记录错误和异常。

logcat是查看 Android 日志的命令行工具。它可以根据日志标签和优先级过滤日志,并能够以不同的格式显示日志。

例如,在前面如何操作…部分的步骤 8 中,我们使用了以下logcat命令。

adb logcat NDKLoggingDemo:I *:S -v time

该命令过滤除了具有NDKLoggingDemo标签和优先级I(信息)或更高优先级的日志。过滤器以tag:priority格式给出。NDKLoggingDemo:I表示将显示具有NDKLoggingDemo标签和优先级信息或更高的日志。*:S将所有其他标签的优先级设置为“静默”。

关于logcat过滤和格式的更多详细信息可以在developer.android.com/tools/help/logcat.htmldeveloper.android.com/tools/debugging/debugging-log.html#outputFormat找到。

使用CheckJNI调试 Android NDK 应用程序

JNI 为了更好的性能,错误检查很少。因此,错误通常会导致崩溃。Android 提供了一个名为CheckJNI的模式。在这个模式下,将调用具有扩展检查的 JNI 函数集,而不是正常的 JNI 函数。本食谱讨论如何启用CheckJNI模式以调试 Android NDK 应用程序。

如何操作...

以下步骤创建一个示例 Android 项目并启用CheckJNI模式:

  1. 创建一个名为CheckJNIDemo的 Android 应用程序。将包名设置为cookbook.chapter3。创建一个名为CheckJNIDemoActivity的活动。如果你想获得更详细的说明,请参考第二章中的加载本地库和注册本地方法菜谱。

  2. 右键点击项目CheckJNIDemo,选择Android Tools | 添加本地支持。会出现一个窗口;点击完成以关闭它。

  3. CheckJNIDemo.cpp添加以下内容。

  4. CheckJNIDemoActivity.java更改为以下内容:

    package cookbook.chapter3;
    import android.os.Bundle;
    import android.app.Activity;
    
    public class CheckJNIDemoActivity extends Activity {
       @Override
       public void onCreate(Bundle savedInstanceState) {
           super.onCreate(savedInstanceState);
           setContentView(R.layout.activity_check_jnidemo);
           CheckJNIDemo();
       }
       public native int[] CheckJNIDemo();
       static {
           System.loadLibrary("CheckJNIDemo");
       }
    }
    
  5. 右键点击CheckJNIDemo项目,并选择构建项目

  6. 在命令行控制台输入"adb logcat -v time"启动 monitor logcat 输出。然后在 Android 设备上启动示例 Android 应用。应用程序将崩溃,logcat 输出将如下显示:如何操作...

  7. 启用 CheckJNI。

    • 当你使用模拟器时,CheckJNI 默认是开启的。

    • 如果你使用的是已获得 root 权限的设备,可以使用以下命令序列重新启动启用了 CheckJNI 的运行时。这些命令停止正在运行的 Android 实例,更改系统属性以启用 CheckJNI,然后重新启动 Android。

      $ adb shell stop
      $ adb shell setprop dalvik.vm.checkjni true
      $ adb shell start
      
      
    • 如果你有一个常规设备,你可以使用以下命令:

      $ adb shell setprop debug.checkjni 1
      
      
  8. 再次运行 Android 应用程序。logcat 输出将如下显示:如何操作...

工作原理...

CheckJNI 模式使用一组 JNI 函数,这些函数比默认的具有更多的错误检查。这使得查找 JNI 编程错误变得更加容易。目前,CheckJNI 模式检查以下错误:

  • 负尺寸数组:它尝试分配一个负尺寸的数组。

  • 错误引用:它向 JNI 函数传递了错误的引用jarray/jclass/jobject/jstring。向期望非NULL参数的 JNI 函数传递NULL

  • 类名:它向 JNI 函数传递了无效样式的类名。有效的类名由"/"分隔,例如"java/lang/String"。

  • 关键调用:它在“关键”get 函数及其相应的释放之间调用一个 JNI 函数。

  • 异常:它在有挂起异常时调用 JNI 函数。

  • jfieldIDs:它会无效化jfieldIDs或将jfieldIDs从一个类型赋值给另一个类型。

  • jmethodIDs:它与 jfieldIDs 类似。

  • 引用:它对错误类型的引用使用DeleteGlobalRef/DeleteLocalRef

  • 释放模式:它向释放调用传递了除了0JNI_ABORTJNI_COMMIT之外的释放模式。

  • 类型安全:它从一个本地方法返回了不兼容的类型。

  • UTF-8:它向 JNI 函数传递了无效的修改后的 UTF-8 字符串。

随着 Android 的发展,可能会向 CheckJNI 中添加更多的错误检查。目前,以下检查还不受支持:

  • 本地引用的误用

使用 NDK GDB 调试 Android NDK 应用程序

Android NDK 引入了一个名为ndk-gdb的 shell 脚本,帮助启动一个调试会话来调试本地代码。

准备工作

要使用ndk-gdb调试项目,项目必须满足以下要求:

  • 应用程序是通过ndk-build命令构建的。

  • AndroidManifest.xml中的<application>元素的android:debuggable属性设置为true。这表示即使应用程序在用户模式下运行在设备上,应用程序也是可调试的。

  • 应用程序应该在 Android 2.2 或更高版本上运行。

在进行这一步之前,请阅读在 Eclipse 中构建 Android NDK 应用程序的菜谱。

如何操作...

以下步骤创建一个示例 Android 项目,并使用 NDK GDB 进行调试。

  1. 创建一个名为HelloNDKGDB的 Android 应用程序。将包名设置为cookbook.chapter3。创建一个名为HelloNDKGDBActivity的活动。如果你需要更详细的说明,请参考第二章,Java Native Interface中的加载本地库和注册本地方法的菜谱。

  2. 右键点击项目HelloNDKGDB,选择Android Tools | 添加本地支持。会出现一个窗口;点击完成关闭它。

  3. HelloNDKGDB.cpp文件中添加以下代码:

    #include <jni.h>
    #include <unistd.h>
    
    int multiply(int i, int j) {
      int x = i * j;
      return x;
    }
    
    extern "C" {
      JNIEXPORT jint JNICALL Java_cookbook_chapter3_HelloNDKGDBActivity_multiply(JNIEnv* env, jobject o, jint pi, jint pj){
        int i = 1, j = 0;
        while (i) {
          j=(++j)/100; 
    
        }
        return multiply(pi, pj);
      }
    }
    
  4. HelloNDKGDBActivity.java的内容更改为以下内容:

    package cookbook.chapter3;
    
    import android.os.Bundle;
    import android.widget.TextView;
    import android.app.Activity;
    
    public class HelloNDKGDBActivity extends Activity {
    
       @Overridepublic void onCreate(Bundle savedInstanceState) {
           super.onCreate(savedInstanceState);
           TextView tv = new TextView(this);
           tv.setTextSize(30);
           tv.setText("10 x 20 = " + multiply(10, 20));
           this.setContentView(tv);
       }
       public native int multiply(int a, int b);
       static {
           System.loadLibrary("HelloNDKGDB");
       }
    }
    
  5. 确保在AndroidManifest.xml中的debuggable属性设置为true。以下代码段是从我们示例项目的AndroidManifest.xml中的应用程序元素中提取的一部分:

    <application
           android:icon="@drawable/ic_launcher"
           android:label="@string/app_name"
           android:theme="@style/AppTheme"
           android:debuggable="true"
           >
    
  6. 使用命令"ndk-build NDK_DEBUG=1"构建本地库。或者,我们可以在 Eclipse 中配置项目的属性下的C/C++ Build中的build命令。这在在 Eclipse 中调试 Android NDK 应用程序的菜谱中有演示。

  7. 在 Android 设备上运行应用程序。然后,启动一个终端并输入以下命令:

    $ ndk-gdb
    
    
  8. 一旦调试器连接到远程进程,我们就可以发出 GDB 命令开始调试应用程序。如下所示:如何操作...

工作原理...

随 Android NDK 附带的名为ndk-gdb的 shell 脚本可以启动本地调试会话与本地代码。为了使用ndk-gdb,我们必须以调试模式构建本地代码。这将生成一个gdbserver二进制文件和一个gdb.setup文件以及本地库。在安装时,gdbserver将被安装并在 Android 设备上启动gdbserver

默认情况下,ndk-gdb会搜索正在运行的应用程序,并将gdbserver附加到它上面。也有选项可以在开始调试之前自动启动应用程序。因为应用程序在gdbserver附加之前首先启动,所以在调试之前会执行一些代码。如果我们想调试在应用程序启动时执行的代码,可以插入一个while(true)块。调试会话开始后,我们改变标志值以跳出while(true)块。这在我们示例项目中得到了演示。

调试会话开始后,我们可以使用gdb命令来调试我们的代码。

使用 CGDB 调试 Android NDK 应用程序

CGDB 是基于终端的轻量级 GNU 调试器gdb的界面。它提供了一个分割屏幕视图,同时显示源代码和调试信息。本教程将讨论如何使用cgdb调试 Android 应用程序。

准备工作

以下是在不同操作系统上安装cgdb的说明:

  • 如果您使用的是 Ubuntu,可以使用以下命令安装cgdb

    $ sudo apt-get install cgdb
    
    

    或者,您可以从cgdb.github.com/下载源代码,并按照以下说明安装cgdb

    $ ./configure --prefix=/usr/local
    $ make
    $ sudo make install
    
    

    注意,cgdb需要libreadlinencurses开发库。

  • 如果您使用的是 Windows 系统,可以在cgdb.sourceforge.net/download.php找到 Windows 二进制文件。

  • 如果您使用的是 MacOS,可以使用以下 MacPorts 安装命令:

    $ sudo port install cgdb
    
    

在阅读本篇内容之前,请先阅读《使用 NDK GDB 调试 Android NDK 应用程序》的教程。

如何操作...

以下步骤为 Android NDK 应用程序调试启用cgdb

  1. 在 Android NDK 的目录下复制ndk-gdb脚本。这可以通过以下命令完成:

    $ cp $ANDROID_NDK/ndk-gdb $ANDROID_NDK/ndk-cgdb
    
    

    这里,$ANDROID_NDK指的是 Android NDK 的目录。

  2. ndk-cgdb脚本中的以下行更改为:

    GDBCLIENT=${TOOLCHAIN_PREFIX}gdb
    
    

    更改为以下内容:

    GDBCLIENT="cgdb -d ${TOOLCHAIN_PREFIX}gdb --"
    
    
  3. 我们将使用在《使用 NDK GDB 调试 Android NDK 应用程序》教程中创建的项目。如果您在 Eclipse IDE 中没有打开项目,点击文件 | 导入。在常规下选择现有项目到工作空间,然后点击下一步。在导入窗口中,勾选选择根目录,并浏览到HelloNDKGDB项目。点击完成以导入项目:如何操作...

  4. 在 Android 设备上运行应用程序。然后,启动一个终端,输入以下命令:

    ndk-cgdb
    
    

    下面是cgdb界面的截图:

    如何操作...

  5. 我们可以输入gdb命令。注意,窗口上半部分会用箭头标记当前执行行,并用红色标记所有断点。

工作原理...

如前一个屏幕截图所示,cgdb为在 Android 中调试本地代码提供了一个更直观的界面。我们可以输入gdb命令时查看源代码。这个食谱演示了使用cgdb调试本地代码的基本设置。有关如何使用cgdb的详细信息,请参阅其文档,地址为cgdb.github.com/docs/cgdb.html

在 Eclipse 中调试 Android NDK 应用程序

对于习惯于图形化开发工具的开发者来说,在终端中使用 GDB 或 CGDB 进行调试是很麻烦的。使用Android 开发工具ADT)20.0.0 或更高版本,在 Eclipse 中调试 NDK 应用程序相当简单。

准备就绪

确保您已安装 ADT 20.0.0 或更高版本。如果没有,请参考第一章中的食谱,你好 NDK,了解如何设置您的环境。

确保您已在 Eclipse 中配置了 NDK 路径。此外,在阅读这个食谱之前,您应该至少构建和运行过一个 Android NDK 应用程序。如果没有,请阅读在 Eclipse 中构建 Android NDK 应用程序的食谱。

如何操作...

以下步骤将创建一个示例 Android 项目,并使用 Eclipse 进行调试:

  1. 我们将使用在在 Eclipse 中构建 Android NDK 应用程序的食谱中创建的项目。如果您在 Eclipse IDE 中没有打开项目,请点击文件 | 导入。在常规下选择现有项目到工作空间,然后点击下一步。在导入窗口中,勾选选择根目录,并浏览到HelloNDKEclipse项目。点击完成以导入项目:如何操作...

  2. HelloNDKEclipse项目上右键点击,选择属性。在属性窗口中,选择C/C++ 构建器。取消勾选使用默认构建命令,并将构建命令更改为ndk-build NDK_DEBUG=1

  3. 点击确定关闭窗口:如何操作...

  4. HelloNDKEclipseActivity.java中调用本地方法之前添加以下代码。

    HelloNDKEclipse.cpp中设置两个断点:

    如何操作...

  5. 在您的项目上右键点击,然后选择调试为 | Android 原生应用程序。我们将看看是否触发了断点。如何操作...

它的工作原理...

由于应用程序启动和调试会话启动之间存在几秒钟的延迟,设置断点的源代码可能在调试开始之前就已经执行了。在这种情况下,断点永远不会被触发。在使用 NDK GDB 调试 Android NDK 应用程序的食谱中,我们演示了使用while(true)循环来解决这个问题。这里我们展示了另一种方法,在应用程序启动时让代码休眠几秒钟。这为调试器提供了足够的时间来启动。一旦开始调试,我们可以使用正常的 Eclipse 调试界面来调试我们的代码。

还有更多...

还有其他一些调试器可用于调试 Android NDK 应用程序。

数据显示调试器DDD)是 GDB 的图形前端。可以设置 DDD 来调试 Android 应用程序。详细的操作指南可以在omappedia.org/wiki/Android_Debugging#Debugging_with_GDB_and_DDD找到。

NVIDIA 调试管理器是一个 Eclipse 插件,用于协助在基于 NVIDIA Tegra 平台的设备上调试 Android NDK 应用程序。关于此工具的更多信息可以在developer.nvidia.com/nvidia-debug-manager-android-ndk找到。

第四章.Android NDK OpenGL ES API

在本章中,我们将涵盖以下内容:

  • 使用 OpenGL ES 1.x API 绘制 2D 图形并应用变换

  • 使用 OpenGL ES 1.x API 绘制 3D 图形并照亮场景

  • 使用 OpenGL ES 1.x API 将纹理映射到 3D 对象

  • 使用 OpenGL ES 2.0 API 绘制 3D 图形

  • 使用 EGL 显示图形

引言

开放图形库OpenGL)是一个跨平台的工业标准 API,用于生成 2D 和 3D 图形。它定义了一个与语言无关的软件接口,用于图形硬件或软件图形引擎。OpenGL ES是针对嵌入式设备的 OpenGL 版本。它由 OpenGL 规范的一个子集和一些特定于 OpenGL ES 的附加扩展组成。

OpenGL ES 不需要专用的图形硬件来工作。不同的设备可以配备具有不同处理能力的图形硬件。OpenGL ES 的调用工作负载在 CPU 和图形硬件之间分配。完全从 CPU 支持 OpenGL ES 是可能的。然而,根据其处理能力,图形硬件可以在不同级别上提高性能。

在深入探讨 Android NDK OpenGL ES 之前,有必要简要介绍一下 OpenGL 上下文中的图形渲染管线GRP)。GRP 指的是一系列处理阶段,图形硬件通过这些阶段来生成图形。它以图元(图元指的是简单的几何形状,如点、线和三角形)的顶点形式接受对象描述,并为显示上的像素输出颜色值。它可以大致分为以下四个主要阶段:

  1. 顶点处理:它接受图形模型描述,处理并转换各个顶点以将它们投影到屏幕上,并将它们的信息组合起来进行图元的进一步处理。

  2. 光栅化:它将图元转换为片段。一个片段包含生成帧缓冲区中像素数据所必需的数据。请注意,只有受到一个或多个图元影响的像素才会有片段。一个片段包含信息,如光栅位置、深度、插值颜色和纹理坐标。

  3. 片段处理:它处理每个片段。一系列操作被应用于每个片段,包括 alpha 测试、纹理映射等。

  4. 输出合并:它将所有片段结合起来,为 2D 显示产生颜色值(包括 alpha)。

在现代计算机图形硬件中,顶点处理和片段处理是可编程的。我们可以编写程序来执行自定义的顶点和片段转换和处理。相比之下,光栅化和输出合并是可配置的,但不可编程。

前述每个阶段可以包含一个或多个步骤。OpenGL ES 1.x 和 OpenGL ES 2.0 提供了不同的 GRP。具体来说,OpenGL ES 1.x 提供了一个固定功能管线,我们输入原始数据和纹理数据,设置光照,剩下的由 OpenGL ES 处理。相比之下,OpenGL ES 2.0 提供了一个可编程管线,允许我们用OpenGL ES 着色语言GLSL)编写顶点和片段着色器来处理具体细节。

下图指示了 OpenGL ES 1.x 的固定功能管线:

介绍

下图是另一个说明 OpenGL ES 2.0 可编程管线的图:

介绍

如前图所示,OpenGL ES 1.x 中的固定管线已经被 OpenGL ES 2.0 中的可编程着色器所取代。

通过这篇计算机图形学的介绍,我们现在准备开始学习 Android NDK OpenGL ES 编程的旅程。Android NDK 提供了 OpenGL ES 1.x(版本 1.0 和版本 1.1)和 OpenGL ES 2.0 库,它们之间有显著差异。以下表格概述了在选择 Android 应用程序中使用的 OpenGL ES 版本时需要考虑的因素:

OpenGL 1.x OpenGL 2.0
性能 快速的 2D 和 3D 图形。 根据 Android 设备而定,但通常提供更快的 2D 和 3D 图形。
设备兼容性 几乎所有的 Android 设备。 大多数 Android 设备,且在增加中。
编码便利性 固定管线,方便的功能。对于简单的 3D 应用来说容易使用。 没有内置的基本功能,对于简单的 3-D 应用可能需要更多努力。
图形控制 固定管线。创建某些效果(例如,卡通着色)困难或不可能。 可编程管线。更直接地控制图形处理管线以创建特定效果。

提示

所有 Android 设备都支持 OpenGL ES 1.0,因为 Android 附带了一个 1.0 能力的软件图形引擎,可以在没有相应图形硬件的设备上使用。只有配备相应图形处理单元GPU)的设备支持 OpenGL ES 1.1 和 OpenGL ES 2.0。

本章将介绍 Android NDK 中的 OpenGL 1.x 和 OpenGL ES 2.0 API。我们首先展示了如何使用 OpenGL 1.x API 绘制 2D 和 3D 图形。涵盖了变换、光照和纹理映射。然后我们介绍 NDK 中的 OpenGL 2.0 API。最后,我们描述如何使用 EGL 显示图形。本章介绍了一些计算机图形学的基础知识和 OpenGL 的原则。已经熟悉 OpenGL ES 的读者可以跳过这些部分,专注于如何从 Android NDK 调用 OpenGL ES API。

我们将为本章介绍的每个教程提供一个示例 Android 应用程序。由于篇幅限制,书中无法展示所有源代码。强烈建议读者下载代码并在阅读本章时参考。

使用 OpenGL ES 1.x API 绘制 2D 图形并应用变换

本教程通过示例介绍了 OpenGL ES 1.x 中的 2D 绘图。为了绘制 2D 对象,我们还将描述通过GLSurfaceView的 OpenGL 渲染显示,为它们添加颜色以及变换。

准备就绪

推荐读者阅读本章的介绍,这对于理解本教程中的一些内容至关重要。

如何操作...

以下步骤将创建我们的示例 Android NDK 项目:

  1. 创建一个名为TwoDG1的 Android 应用程序。将包名设置为cookbook.chapter4.gl1x。如果你需要更详细的说明,请参考第二章中的加载本地库和注册本地方法教程,Java 本地接口

  2. 在 Eclipse 中右键点击TwoDG1项目,选择Android Tools | Add Native Support

  3. cookbook.chapter4.gl1x包下添加以下三个 Java 文件:

    • MyActivity.java:它创建了此项目的活动:

      import android.opengl.GLSurfaceView;
      ……
      public class MyActivity extends Activity {
        private GLSurfaceView mGLView;
        @Override
        public void onCreate(Bundle savedInstanceState) {
          super.onCreate(savedInstanceState);
          mGLView = new MySurfaceView(this);
                 setContentView(mGLView);
        }
      }
      
    • MySurfaceView.java:它扩展了GLSurfaceView,后者提供了一个专用的表面来显示 OpenGL 渲染:

      public class MySurfaceView extends GLSurfaceView {
        private MyRenderer mRenderer;
        public MySurfaceView(Context context) {
          super(context);
          mRenderer = new MyRenderer();
          this.setRenderer(mRenderer);
          this.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
        }
      }
      
    • MyRenderer.java:它实现了Renderer并调用本地方法:

      public class MyRenderer implements GLSurfaceView.Renderer{
        @Override
        public void onSurfaceCreated(GL10 gl, EGLConfig config) {
          naInitGL1x();
        }
        @Override
        public void onDrawFrame(GL10 gl) {
          naDrawGraphics();
        }
        @Override
        public void onSurfaceChanged(GL10 gl, int width, int height) {
          naSurfaceChanged(width, height);
        }
        ......
      }
      
  4. jni文件夹下添加TwoDG1.cppTriangle.cppSquare.cppTriangle.hSquare.h文件。请参考下载的项目以获取完整的代码内容。这里,我们只列出代码中的一些重要部分:

    TwoDG1.cpp:它包含了设置 OpenGL ES 1.x 环境并执行变换的代码:

    void naInitGL1x(JNIEnv* env, jclass clazz) {
      glDisable(GL_DITHER);  
      glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_FASTEST);
      glClearColor(0.0f, 0.0f, 0.0f, 1.0f);    glShadeModel(GL_SMOOTH);    }
    
    void naSurfaceChanged(JNIEnv* env, jclass clazz, int width, int height) {
      glViewport(0, 0, width, height);
      float ratio = (float) width / (float)height;
      glMatrixMode(GL_PROJECTION);
      glLoadIdentity();
      glOrthof(-ratio, ratio, -1, 1, 0, 1);  }
    
    void naDrawGraphics(JNIEnv* env, jclass clazz) {
      glClear(GL_COLOR_BUFFER_BIT);
      glMatrixMode(GL_MODELVIEW);
      glLoadIdentity();
      glTranslatef(0.3f, 0.0f, 0.0f);    //move to the right
      glScalef(0.2f, 0.2f, 0.2f);        // Scale down
      mTriangle.draw();
      glLoadIdentity();
      glTranslatef(-0.3f, 0.0f, 0.0f);    //move to the left
      glScalef(0.2f, 0.2f, 0.2f);      // Scale down
    glRotatef(45.0, 0.0, 0.0, 1.0);  //rotate
      mSquare.draw();
    }
    

    Triangle.cpp:它绘制一个 2D 三角形:

    void Triangle::draw() {
      glEnableClientState(GL_VERTEX_ARRAY);
      glVertexPointer(3, GL_FLOAT, 0, vertices);
      glColor4f(0.5f, 0.5f, 0.5f, 0.5f);      //set the current color
      glDrawArrays(GL_TRIANGLES, 0, 9/3);
      glDisableClientState(GL_VERTEX_ARRAY);
    }
    

    Square.cpp:它绘制一个 2D 正方形:

    void Square::draw() {
      glEnableClientState(GL_VERTEX_ARRAY);
      glEnableClientState(GL_COLOR_ARRAY);
      glVertexPointer(3, GL_FLOAT, 0, vertices);
      glColorPointer(4, GL_FLOAT, 0, colors);
      glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_BYTE, indices);
      glDisableClientState(GL_VERTEX_ARRAY);
      glDisableClientState(GL_COLOR_ARRAY);
    }
    
  5. jni文件夹下添加Android.mk文件,内容如下:

    LOCAL_PATH := $(call my-dir)
    include $(CLEAR_VARS)
    LOCAL_MODULE    := TwoDG1
    LOCAL_SRC_FILES := Triangle.cpp Square.cpp TwoDG1.cpp
    LOCAL_LDLIBS := -lGLESv1_CM -llog
    include $(BUILD_SHARED_LIBRARY)
    
  6. 构建 Android NDK 应用程序并在 Android 设备上运行。以下是显示的截图:如何操作...

工作原理...

本教程演示了使用 OpenGL ES 的基本 2D 绘图。

通过 GLSurfaceView 显示 OpenGL ES 渲染

GLSurfaceViewGLSurfaceView.Renderer是 Android SDK 提供的两个基础类,用于显示 OpenGL ES 图形。

GLSurfaceView接受一个用户定义的Renderer对象,该对象实际执行渲染。它通常被扩展以处理触摸事件,这将在下一个教程中说明。它支持按需和连续渲染。在我们的示例代码中,我们只需设置Renderer对象并将渲染模式配置为按需。

GLSurfaceView.Renderer是渲染器的接口。需要实现它的三个方法:

  • onSurfaceCreated:在设置 OpenGL ES 环境时被调用一次。

  • onSurfaceChanged:如果视图的几何形状发生变化,它会被调用;最常见的例子是设备屏幕方向的变化。

  • onDrawFrame:每次重绘视图时都会调用它。

在我们的示例项目中,MyRenderer.java是一个简单的包装器,实际工作是在本地 C++代码中完成的。

在 OpenGL ES 中绘制物体

在 OpenGL ES 中绘制物体通常使用两种方法,包括glDrawArraysglDrawElements。我们分别在Triangle.cppSquare.cpp中演示了这两种方法的用法。请注意,这两种方法都需要启用GL_VERTEX_ARRAY

第一个参数是绘制模式,指明了要使用的图元。在我们的示例代码中,我们使用了GL_TRIANGLES,这意味着我们实际上绘制了两个三角形来形成正方形。在 Android NDK OpenGL ES 中还有其他有效值,包括GL_POINTSGL_LINESGL_LINE_LOOPGL_LINE_STRIPGL_TRIANGLE_STRIPGL_TRIANGLE_FAN

在 OpenGL ES 中的颜色

我们还展示了两种给物体添加颜色的方法。在Triangle.cpp中,我们通过glColor4f API 调用设置当前颜色。在Square.cpp中,我们启用了GL_COLOR_ARRAY,并使用glColorPointer定义了一个颜色数组。该颜色数组将由glDrawElements(使用glDrawArrays也行)API 调用使用。

OpenGL ES 转换

下图展示了 OpenGL ES 1.0 中的不同转换阶段:

OpenGL ES 转换

如图中所示,顶点数据在光栅化之前进行转换。这些转换类似于用相机拍照:

  • 模型视图转换:安排场景并放置相机

  • 投影转换:选择一个相机镜头并调整缩放因子

  • 视点转换:确定最终照片的大小

模型视图转换实际上指的是两种不同的转换,即模型转换和视图转换。模型转换是指将所有物体从其对象空间(也称为局部空间或模型空间)转换到世界空间的过程,该空间被所有物体共享。这个转换通过一系列缩放(glScalef)、旋转(glRotatef)和平移(glTranslatef)来完成。

  • glScalef:它拉伸、缩小或反射物体。x、y 和 z 轴的值分别乘以相应的 x、y 和 z 缩放因子。在我们的示例代码中,我们调用了glScalef(0.2f, 0.2f, 0.2f),以缩小三角形和正方形,使它们能够适应屏幕。

  • glRotatef:它以从原点通过指定点(x, y, z)的方向逆时针旋转物体。旋转角度以度为单位测量。在我们的示例代码中,我们调用了glRotatef(45.0, 0.0, 0.0, 1.0),使正方形绕 z 轴旋转 45 度。

  • glTranslatef:该函数根据给定的值沿着每个轴移动对象。在我们的示例代码中,我们调用了glTranslatef(0.3f, 0.0f, 0.0f)将三角形向右移动,以及glTranslatef(-0.3f, 0.0f, 0.0f)将正方形向左移动,以防止它们重叠。

模型变换在场景中安排对象,而视图变换改变观察相机的位置。为了产生特定的图像,我们可以移动对象或改变相机位置。因此,OpenGL ES 内部使用单一的矩阵——GL_MODELVIEW矩阵执行这两种变换。

提示

OpenGL ES 定义了相机默认位于眼睛坐标空间的原点(0, 0, 0),并指向负 z 轴。可以通过 Android SDK 中的GLU.gluLookAt改变位置。然而,在 Android NDK 中不提供相应的 API。

投影变换决定了可以看到什么(类似于选择相机镜头和缩放因子)以及顶点数据如何投影到屏幕上。OpenGL ES 支持两种投影模式,分别是透视投影(glFrustum)和正交投影(glOrtho)。透视投影使得远离的物体显得更小,这与普通相机相匹配。另一方面,正交投影类似于望远镜,直接映射物体而不影响其大小。OpenGL ES 通过GL_PROJECTION矩阵操纵变换。在投影变换后,位于裁剪体积外的物体将被裁剪掉,在最终场景中不绘制。在我们的示例项目中,我们调用了glOrthof(-ratio, ratio, -1, 1, 0, 10)来指定视景体,其中ratio指的是屏幕的宽高比。

投影变换后,通过将裁剪坐标除以输入顶点的变换后的w值来进行透视除法。x 轴、y 轴和 z 轴的值将被归一化到-1.01.0的范围内。

OpenGL ES 变换管道的最终阶段是视口变换,它将归一化设备坐标映射到窗口坐标(以像素为单位,原点在左上角)。请注意,视点还包括一个 z 分量,这在例如两个重叠的 OpenGL 场景的排序等情况下是需要的,可以通过glDepthRange API 调用设置。当显示尺寸发生变化时,应用程序通常需要通过glViewport API 调用设置视口。在我们的示例中,我们通过调用glViewport(0, 0, width, height)将视口设置为整个屏幕。这个设置与glOrthof调用一起,将保持投影变换后的对象比例,如下图所示:

OpenGL ES 变换

如图表所示,裁剪体积设置为(-width/height, width/height, -1, 1, 0, 1)。在透视除法中,顶点被w除。在视点变换中,x 和 y 坐标范围都被w*height/2放大。因此,对象将如本食谱的如何操作...部分所示成比例显示。以下屏幕截图的左侧显示了如果我们通过调用glOrthof(-1, 1, -1, 1, 0, 1)设置裁剪体积的输出,右侧表示如果通过调用glViewport(0, 0, width/2, height/5)设置视口,图形将呈现什么样子:

OpenGL ES 变换

使用 OpenGL ES 1.x API 绘制 3D 图形并点亮场景

本食谱涵盖了如何在 OpenGL ES 中绘制 3D 对象、处理触摸事件以及点亮对象。

准备就绪

建议读者在阅读本食谱之前,先阅读引言和下面的使用 OpenGL ES 1.x API 绘制 2D 图形和应用变换的食谱。

如何操作...

以下步骤展示了如何开发我们的示例 Android 项目:

  1. 创建一个名为CubeG1的 Android 应用程序。将包名设置为cookbook.chapter4.gl1x。如果你需要更详细的说明,请参考第二章中的加载本地库和注册本地方法食谱,Java Native Interface

  2. 右键点击项目 CubeG1,选择Android Tools | 添加本地支持

  3. cookbook.chapter4.gl1x包下添加三个 Java 文件,分别为MyActivity.javaMySurfaceViewMyRenderer.javaMyActivity.java与上一个食谱中使用的一致。

    MySurfaceView.java扩展了GLSurfaceView,包含处理触摸事件的代码:

    public class MySurfaceView extends GLSurfaceView {
      private MyRenderer mRenderer;
      private float mPreviousX;
       private float mPreviousY;
       private final float TOUCH_SCALE_FACTOR = 180.0f / 320;
      public MySurfaceView(Context context) {
        super(context);
        mRenderer = new MyRenderer();
        this.setRenderer(mRenderer);
        //control whether continuously drawing or on-demand
        this.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
      }
    
      public boolean onTouchEvent(final MotionEvent event) {
        float x = event.getX();
           float y = event.getY();
           switch (event.getAction()) {
           case MotionEvent.ACTION_MOVE:
               float dx = x - mPreviousX;
               float dy = y - mPreviousY;
               mRenderer.mAngleX += dx * TOUCH_SCALE_FACTOR;
               mRenderer.mAngleY += dy * TOUCH_SCALE_FACTOR;
               requestRender();
           }
           mPreviousX = x;
           mPreviousY = y;
           return true;
       }
    }
    

    MyRenderer.java实现了一个渲染器,以调用本地方法渲染图形:

    public class MyRenderer implements GLSurfaceView.Renderer{
       public float mAngleX;
       public float mAngleY;
      @Override
      public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        naInitGL1x();
      }
      @Override
      public void onDrawFrame(GL10 gl) {
        naDrawGraphics(mAngleX, mAngleY);
      }
      @Override
      public void onSurfaceChanged(GL10 gl, int width, int height) {
        naSurfaceChanged(width, height);
      }
    }
    
  4. jni文件夹下添加CubeG1.cppCube.cppCube.h文件。请参考下载的项目以获取完整内容。让我们列出CubeG1.cpp中的naInitGL1xnaSurfaceChangednaDrawGraphics本地方法以及Cube.cpp中的绘制和光照方法的代码:

    CubeG1.cpp设置 OpenGL ES 环境和光照:

    void naInitGL1x(JNIEnv* env, jclass clazz) {
      glDisable(GL_DITHER);
      glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);
      glClearColor(0.0f, 0.0f, 0.0f, 1.0f);    glEnable(GL_CULL_FACE);    
      glClearDepthf(1.0f);  glEnable(GL_DEPTH_TEST);  
      glDepthFunc(GL_LEQUAL);    //type of depth test
      glShadeModel(GL_SMOOTH);    
      glLightModelx(GL_LIGHT_MODEL_TWO_SIDE, 0);
      float globalAmbientLight[4] = {0.5, 0.5, 0.5, 1.0};
      glLightModelfv(GL_LIGHT_MODEL_AMBIENT, globalAmbientLight);
      GLfloat lightOneDiffuseLight[4] = {1.0, 1.0, 1.0, 1.0};
      GLfloat lightOneSpecularLight[4] = {1.0, 1.0, 1.0, 1.0};
      glLightfv(GL_LIGHT0, GL_DIFFUSE, lightOneDiffuseLight);
      glLightfv(GL_LIGHT0, GL_SPECULAR, lightOneSpecularLight);
      glEnable(GL_LIGHTING);
      glEnable(GL_LIGHT0);
    }
    void naSurfaceChanged(JNIEnv* env, jclass clazz, int width, int height) {
      glViewport(0, 0, width, height);
       float ratio = (float) width / height;
       glMatrixMode(GL_PROJECTION);
       glLoadIdentity();
       glOrthof(-ratio, ratio, -1, 1, -10, 10);
    }
    void naDrawGraphics(JNIEnv* env, jclass clazz, float pAngleX, float pAngleY) {
      glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
       glMatrixMode(GL_MODELVIEW);
       glLoadIdentity();
       glRotatef(pAngleX, 0, 1, 0);  //rotate around y-axis
       glRotatef(pAngleY, 1, 0, 0);  //rotate around x-axis
      glScalef(0.3f, 0.3f, 0.3f);      // Scale down
    mCube.lighting();
      mCube.draw();
      float lightOnePosition[4] = {0.0, 0.0, 1.0, 0.0};  
      glLightfv(GL_LIGHT0, GL_POSITION, lightOnePosition);
    }
    

    Cube.cpp绘制一个 3D 立方体并点亮它:

    void Cube::draw() {
      glEnableClientState(GL_VERTEX_ARRAY);
      glVertexPointer(3, GL_FLOAT, 0, vertices);
      glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_BYTE, indices);
      glDisableClientState(GL_VERTEX_ARRAY);
    }
    void Cube::lighting() {
      GLfloat cubeOneAmbientFraction[4] = {0.0, 0.5, 0.5, 1.0};
      GLfloat cubeOneDiffuseFraction[4] = {0.8, 0.0, 0.0, 1.0};
      GLfloat cubeSpecularFraction[4] = {0.0, 0.0, 0.0, 1.0};
      GLfloat cubeEmissionFraction[4] = {0.0, 0.0, 0.0, 1.0};
      glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT, cubeOneAmbientFraction);
      glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, cubeOneDiffuseFraction);
      glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, cubeSpecularFraction);
      glMaterialfv(GL_FRONT_AND_BACK, GL_EMISSION, cubeEmissionFraction);
      glMaterialf(GL_FRONT_AND_BACK, GL_SHININESS, 60.0);
    }
    
  5. jni文件夹下添加Android.mk文件,内容如下:

    LOCAL_PATH := $(call my-dir)
    include $(CLEAR_VARS)
    LOCAL_MODULE    := CubeG1
    LOCAL_SRC_FILES := Cube.cpp CubeG1.cpp
    LOCAL_LDLIBS := -lGLESv1_CM -llog
    include $(BUILD_SHARED_LIBRARY)
    
  6. 构建 Android NDK 应用程序并在 Android 设备上运行。应用程序将显示一个立方体,我们可以触摸它使其旋转:如何操作...

工作原理...

本食谱讨论了如何使用 OpenGL ES 1.x API 绘制 3D 图形。注意,我们将在Andorid.mk文件中需要加载 OpenGL ES 库GLESv1_CM,并在本地源代码中包含头文件GLES/gl.h

  • 在 OpenGL ES 中绘制 3D 对象:绘制 3D 对象与绘制 2D 对象类似。在Cube::draw方法中,我们首先设置顶点缓冲区,然后调用glDrawElements来绘制立方体的六个面。我们使用GL_TRIANGLES作为图元。因为每个面包含两个三角形,所以有 12 个三角形和 36 个顶点。

  • 触摸事件处理:在MySurfaceView.java中,我们重写onTouchEvent方法以检测屏幕上的图形移动,并改变MyRenderer的旋转角度属性。我们调用requestRender方法,请求渲染器重新绘制图形。

  • OpenGL ES 中的光照和材质:光照模型分为两类,即局部光照和全局光照。局部光照只考虑直接光照,因此可以对单个对象进行光照计算。与之相对的是,全局光照考虑了从其他对象和环境反射的间接光照,因此计算成本更高。OpenGL ES 1.x 使用局部光照,而全局光照可以使用OpenGL 着色语言GLSL)在 OpenGL ES 2.0 中进行编程。这里,我们只讨论 OpenGL ES 1.x 中的光照。

当考虑光照时,OpenGL ES 中涉及三个参与者,包括摄像机位置、光源和物体的材质。摄像机位置始终在默认位置(0, 0, 0),并朝向负 z 轴,如前面的食谱所述。光源可以提供独立的环境光、漫反射光和镜面光。材质可以反射不同数量的环境光、漫反射光和镜面光。此外,材质也可能发射光。每种光都由 RGB 分量组成:

  • 环境光:它近似于场景中无处不在的恒定光照量。

  • 漫反射光:它近似于来自远距离方向光源的光(例如,阳光)。当反射光照射到表面时,它在所有方向上均匀散射。

  • 镜面光:它近似于光滑表面反射的光。其强度取决于观察者与从表面反射的射线方向之间的角度。

  • 发射光:某些材质可以发光。

请注意,光源中的 RGB 值表示颜色分量的强度,而在材质中则指反射这些颜色的比例。为了理解光源和材质如何影响观察者对物体的感知,可以考虑一束白光照射在表面上,如果表面只反射光的蓝色分量,那么观察者看到的表面将是蓝色的。如果光是纯红色的,那么观察者看到的表面将是黑色的。

以下步骤可以在 OpenGL ES 中设置简单的光照:

  1. 设置光照模型参数。这是通过glLightModelfv完成的。Android NDK OpenGL ES 支持两个参数,包括GL_LIGHT_MODEL_AMBIENTGL_LIGHT_MODEL_TWO_SIDE。第一个允许我们指定全局环境光,第二个允许我们指定是否要在表面的背面计算光照。

  2. 启用、配置并放置一个或多个光源。这是通过glLightfv方法完成的。我们可以分别配置环境光、漫反射光和镜面光。光源位置也通过glLightfvGL_POSITION一起配置。在CubeG1.cpp中,我们使用了以下代码:

    float lightOnePosition[4] = {0.0, 0.0, 1.0, 0.0};  
    glLightfv(GL_LIGHT0, GL_POSITION, lightOnePosition);
    

    位置的第四个值表示光源是位置的还是方向的。当值设置为0时,光为方向光,模拟一个远距离的光源(阳光)。光线在撞击表面时是平行的,位置的(x, y, z)值指的是光的传播方向。如果第四个值设置为1,光为位置光,类似于灯泡。这里的(x, y, z)值指的是光源的位置,光线从不同的角度撞击表面。请注意,光源向所有方向发射强度相等的光。以下图像说明了这两种光源:

    它是如何工作的...

除了位置光和方向光,还有聚光灯:

  1. 我们也将通过调用以下方法来启用光照和光源

    glEnable(GL_LIGHTING);
    

    以及

    glEnable(GL_LIGHTx);
    
  2. 为所有对象的每个顶点定义法向量。这些法向量决定了物体相对于光源的方向。在我们的代码中,我们依赖 OpenGL ES 的默认法向量。

  3. 定义材质。这可以通过glMaterialfglMaterialfv方法来完成。在我们的示例代码中,我们将漫反射光的红色分量指定为0.8,而将绿色和蓝色分量保持为 0。因此,最终的立方体看起来是红色的。

使用 OpenGL ES 1.x API 将纹理映射到 3D 对象

纹理映射是一种将图像覆盖到物体表面以创建更真实场景的技术。这个菜谱涵盖了如何在 OpenGL ES 1.x 中添加纹理。

准备就绪

建议读者在阅读本节内容之前,先阅读《使用 OpenGL ES 1.x API 绘制 3D 图形并照亮场景》的菜谱。

如何操作...

以下步骤创建了一个展示如何将纹理映射到 3D 对象的 Android 项目:

  1. 创建一个名为DiceG1的 Android 应用程序。将包名设置为cookbook.chapter4.gl1x。如果你需要更详细的说明,请参考第二章《Java 本地接口》中的《加载本地库和注册本地方法》菜谱。

  2. 在项目CubeG1上点击右键,选择Android Tools | 添加本地支持

  3. cookbook.chapter4.diceg1包下添加三个 Java 文件,分别为MyActivity.javaMySurfaceView.javaMyRenderer.javaMyActivity.javaMySurfaceView.java与之前的配方相似。

  4. MyRenderer.java代码如下:

    public class MyRenderer implements GLSurfaceView.Renderer{
       public float mAngleX;
       public float mAngleY;
       private Context mContext;
       public MyRenderer(Context pContext) {
         super();
         mContext = pContext;
       }
      @Override
      public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        //call native methods to load the textures
        LoadTexture(R.drawable.dice41, mContext, 0);
        LoadTexture(R.drawable.dice42, mContext, 1);
        LoadTexture(R.drawable.dice43, mContext, 2);
        LoadTexture(R.drawable.dice44, mContext, 3);
        LoadTexture(R.drawable.dice45, mContext, 4);
        LoadTexture(R.drawable.dice46, mContext, 5);
        naInitGL1x();
      }
    … …
      private void LoadTexture(int resId, Context context, int texIdx) {
        //Get the texture from the Android resource directory
        InputStream is = context.getResources().openRawResource(resId);
        Bitmap bitmap = null;
        try {
          BitmapFactory.Options options = new BitmapFactory.Options();
          options.inPreferredConfig = Bitmap.Config.ARGB_8888;
          bitmap = BitmapFactory.decodeStream(is, null, options);
          naLoadTexture(bitmap, bitmap.getWidth(), bitmap.getHeight(), texIdx);
        } finally {
          try {
            is.close();
            is = null;
          } catch (IOException e) {
          }
        }
        if (null != bitmap) {
          bitmap.recycle();
        }
      }
    }
    
  5. jni文件夹下添加DiceG1.cppCube.cppCube.hmylog.h文件。请参考下载的项目以获取完整内容。这里,我们列出DiceG1.cpp中的fornaLoadTexturenaInitGL1x本地方法以及Cube.cpp中的draw方法的代码:

    void naLoadTexture(JNIEnv* env, jclass clazz, jobject pBitmap, int pWidth, int pHeight, int pId) {
      int lRet;
      AndroidBitmapInfo lInfo;
      void* l_Bitmap;
      GLint format;
      GLenum type;
      if ((lRet = AndroidBitmap_getInfo(env, pBitmap, &lInfo)) < 0) {
        return;
      }
      if (lInfo.format == ANDROID_BITMAP_FORMAT_RGB_565) {
        format = GL_RGB;
        type = GL_UNSIGNED_SHORT_5_6_5;
      } else if (lInfo.format == ANDROID_BITMAP_FORMAT_RGBA_8888) {
        format = GL_RGBA;
        type = GL_UNSIGNED_BYTE;
      } else {
        return;
      }
      if ((lRet = AndroidBitmap_lockPixels(env, pBitmap, &l_Bitmap)) < 0) {
        return;
      }
      glGenTextures(1, &texIds[pId]);
      glBindTexture(GL_TEXTURE_2D, texIds[pId]);
      glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
      glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
      glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
      glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
      glTexImage2D(GL_TEXTURE_2D, 0, format, pWidth, pHeight, 0, format, type, l_Bitmap);
      AndroidBitmap_unlockPixels(env, pBitmap);
    }
    void naInitGL1x(JNIEnv* env, jclass clazz) {
      glDisable(GL_DITHER);  
      glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);
      glClearColor(0.0f, 0.0f, 0.0f, 1.0f);  
      glEnable(GL_CULL_FACE);    
      glClearDepthf(1.0f);  
      glEnable(GL_DEPTH_TEST);  
      glDepthFunc(GL_LEQUAL);    
      glShadeModel(GL_SMOOTH);   
      mCube.setTexCoords(texIds);
      glTexEnvx(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
      glEnable(GL_TEXTURE_2D);
    }
    Cube.cpp: drawing the cube and mapping texture
    void Cube::draw() {
      glEnableClientState(GL_VERTEX_ARRAY);
      glEnableClientState(GL_TEXTURE_COORD_ARRAY);  // Enable texture-coords-array
      glFrontFace(GL_CW);
    
      glBindTexture(GL_TEXTURE_2D, texIds[0]);
      glTexCoordPointer(2, GL_FLOAT, 0, texCoords);
      glVertexPointer(3, GL_FLOAT, 0, vertices);
      glDrawElements(GL_TRIANGLES, 18, GL_UNSIGNED_BYTE, indices);
    
    ….
      glDisableClientState(GL_VERTEX_ARRAY);
      glDisableClientState(GL_TEXTURE_COORD_ARRAY);
    }
    
  6. jni文件夹下添加Android.mk文件,内容如下:

    LOCAL_PATH := $(call my-dir)
    include $(CLEAR_VARS)
    LOCAL_MODULE    := DiceG1
    LOCAL_SRC_FILES := Cube.cpp DiceG1.cpp
    LOCAL_LDLIBS := -lGLESv1_CM -llog -ljnigraphics
    include $(BUILD_SHARED_LIBRARY)
    
  7. 构建 Android NDK 应用程序并在 Android 设备上运行。该应用将显示一个纹理为骰子的立方体:如何操作...

工作原理...

这个配方给 3D 立方体添加了一个纹理,使其看起来像骰子。

  • 纹理坐标:纹理通常是 2D 图像。纹理坐标(s, t)通常被归一化到[0.0, 1.0],如下图所示。纹理图像在st轴上被映射到[0, 1]工作原理...

  • 加载纹理:在 OpenGL ES 中映射纹理的第一步是加载它们。在我们的示例中,我们使用 Android SDK 从可绘制资源中读取图像文件,并将位图传递给本地代码。本地方法naLoadTexture锁定位图图像并执行以下 OpenGL 操作。

    • 创建 glGenTexture 纹理:这生成纹理 ID。

    • 绑定纹理:glBindTexture。这告诉 OpenGL 我们要使用的纹理 id。

    • 设置纹理过滤:使用glTexParameterGL_TEXTURE_MIN_FILTERGL_TEXTURE_MAG_FILTER(这将在后面讨论)。

    • 设置纹理包装:使用glTexParameterGL_TEXTURE_WRAP_SGL_TEXTURE_WRAP_T(这将在后面讨论)。

    • 将图像数据加载到 OpenGL 中:(glTexImage2D)我们需要指定图像数据、宽度、高度、颜色格式等。

  • 纹理包装:纹理在st轴上被映射到[0, 1]。但是,我们可以指定超出范围的纹理坐标。一旦发生这种情况,将应用包装。典型的纹理包装设置如下:

    • GL_CLAMP:将纹理坐标限制在[0.0, 1.0]

    • GL_REPEAT:重复纹理。这创建了一个重复的模式。

  • 纹理过滤:通常纹理图像的分辨率与对象不同。如果纹理较小,则会进行放大处理;如果纹理较大,则会进行缩小处理。通常使用以下两种方法:

    • GL_NEAREST:使用与被纹理化的像素中心最近的纹理元素。

    • GL_LINEAR:对基于与被纹理化的像素最近的四个纹理元素进行插值计算颜色值。

  • 设置纹理环境:在我们将纹理映射到对象之前,可以调用 glTexEnvf 来控制当片段被纹理化时如何解释纹理值。我们可以配置 GL_TEXTURE_ENV_COLORGL_TEXTURE_ENV_MODE。在我们的示例项目中,我们使用了 GL_REPLACE 作为 GL_TEXTURE_ENV_MODE,这简单地将立方体片段替换为纹理值。

  • 映射纹理:我们绘制 3D 立方体的每个面并通过 glDrawElement 映射纹理。必须通过调用 glEnableClientState 启用 GL_TEXTURE_COORD_ARRAY。在绘制每个接口之前,我们通过调用 glBindTexture 绑定到相应的纹理。

还有更多...

在我们的本地代码中,我们使用了 Android 本地位图 API 从 Java 代码接收纹理位图对象。这个 API 的更多细节将在第七章,其他 Android NDK API中进行介绍。

使用 OpenGL ES 2.0 API 绘制 3D 图形

前面的食谱描述了在 Android NDK 上的 OpenGL ES 1.x。这个食谱涵盖了如何在 Android NDK 中使用 OpenGL ES 2.0。

准备就绪

建议读者在阅读这个食谱之前先阅读本章的介绍。在以下食谱中涵盖了大量的图形基础;建议我们首先阅读它们:

  • 使用 OpenGL ES 1.x API 绘制 2D 图形和应用变换

  • 使用 OpenGL ES 1.x API 绘制 3D 图形并照亮场景

如何操作...

以下步骤使用 Android NDK 中的 OpenGL ES 2.0 API 创建一个渲染 3D 立方体的 Android 项目:

  1. 创建一个名为 CubeG2 的 Android 应用程序。将包名设置为 cookbook.chapter4.cubeg2。如果你需要更详细的说明,请参考第二章的加载本地库和注册本地方法一节,Java Native Interface

  2. 在项目 CubeG2 上右键点击,选择 Android Tools | 添加本地支持

  3. 添加三个 Java 文件,分别为 MyActivity.javaMyRenderer.javaMySurfaceView.java。我们只列出了部分 MyRenderer.java 代码,因为其他两个文件 MyActivity.javaMySurfaceView.java 与前一个食谱中的文件相似:

    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        String vertexShaderStr = LoadShaderStr(mContext, R.raw.vshader);
        String fragmentShaderStr = LoadShaderStr(mContext, R.raw.fshader);
        naInitGL20(vertexShaderStr, fragmentShaderStr);
    }
    @Override
    public void onDrawFrame(GL10 gl) {
      naDrawGraphics(mAngleX, mAngleY);
    }
    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
      naSurfaceChanged(width, height);
    }
    
  4. jni 文件夹下添加 Cube.cppmatrix.cppCubeG2.cppCube.hmatrix.hmylog.h 文件。文件内容总结如下:

    • Cube.cpp 和 Cube.h:它们定义了一个 Cube 对象和方法来绘制 3D 立方体。

    • matrix.cpp 和 matrix.h:这些矩阵操作,包括创建平移、缩放和旋转矩阵以及矩阵乘法。

    • CubeG2.cpp:它们创建并加载着色器。它们还创建、链接并使用程序,并对 3D 立方体应用变换。

    • mylog.h:它们定义了用于 Android NDK 日志记录的宏。

    在这里,我们列出了 Cube.cppCubeG2.cpp 的部分内容。

    Cube.cpp

    …
    void Cube::draw(GLuint pvPositionHandle) {
      glVertexAttribPointer(pvPositionHandle, 3, GL_FLOAT, GL_FALSE, 0, vertices);
      glEnableVertexAttribArray(pvPositionHandle);
      glDrawArrays(GL_TRIANGLES, 0, 36);
    }
    ...
    

    CubeG2.cpp:它包含了 loadShadercreateProgramnaInitGL20naDrawGraphics 方法,下面将进行解释:

    • loadShader:这个方法创建一个着色器,附加源代码,并编译着色器:

      GLuint loadShader(GLenum shaderType, const char* pSource) {
         GLuint shader = glCreateShader(shaderType);
         if (shader) {
             glShaderSource(shader, 1, &pSource, NULL);
             glCompileShader(shader);
             GLint compiled = 0;
             glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
             if (!compiled) {
                 GLint infoLen = 0;
                 glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &infoLen);
                 if (infoLen) {
                     char* buf = (char*) malloc(infoLen);
                     if (buf) {
                         glGetShaderInfoLog(shader, infoLen, NULL, buf);
                         free(buf);
                     }
                     glDeleteShader(shader);
                     shader = 0;
                 }
             }
         }
         return shader;
      }
      
    • createProgram:这个方法创建一个程序对象,附加着色器,并链接程序:

      GLuint createProgram(const char* pVertexSource, const char* pFragmentSource) {
         GLuint vertexShader = loadShader(GL_VERTEX_SHADER, pVertexSource);
         GLuint pixelShader = loadShader(GL_FRAGMENT_SHADER, pFragmentSource);
         GLuint program = glCreateProgram();
         if (program) {
             glAttachShader(program, vertexShader);
             glAttachShader(program, pixelShader);
             glLinkProgram(program);
         }
         return program;
      }
      
    • naInitGL20:这个方法设置 OpenGL ES 2.0 环境,获取着色器源字符串,以及获取着色器属性和统一变量的位置:

      void naInitGL20(JNIEnv* env, jclass clazz, jstring vertexShaderStr, jstring fragmentShaderStr) {
        glDisable(GL_DITHER);  
        glClearColor(0.0f, 0.0f, 0.0f, 1.0f);  
      glClearDepthf(1.0f);  
        glEnable(GL_DEPTH_TEST);  
        glDepthFunc(GL_LEQUAL);    
          const char *vertexStr, *fragmentStr;
        vertexStr = env->GetStringUTFChars(vertexShaderStr, NULL);
        fragmentStr = env->GetStringUTFChars(fragmentShaderStr, NULL);
        setupShaders(vertexStr, fragmentStr);
        env->ReleaseStringUTFChars(vertexShaderStr, vertexStr);
        env->ReleaseStringUTFChars(fragmentShaderStr, fragmentStr);
        gvPositionHandle = glGetAttribLocation(gProgram, "vPosition");
        gmvP = glGetUniformLocation(gProgram, "mvp");
      
      }
      
    • naDrawGraphics:这个方法应用模型变换(旋转、缩放和平移)和投影变换:

      void naDrawGraphics(JNIEnv* env, jclass clazz, float pAngleX, float pAngleY) {
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        glClearColor(0.0, 0.0, 0.0, 1.0f);
        glUseProgram(gProgram);
      //  GL1x: glRotatef(pAngleX, 0, 1, 0);  //rotate around y-axis
      //  GL1x: glRotatef(pAngleY, 1, 0, 0);  //rotate around x-axis
        //rotate
        rotate_matrix(pAngleX, 0.0, 1.0, 0.0, aRotate);
        rotate_matrix(pAngleY, 1.0, 0.0, 0.0, aModelView);
        multiply_matrix(aRotate, aModelView, aModelView);
      //  GL1x: glScalef(0.3f, 0.3f, 0.3f);      // Scale down
        scale_matrix(0.5, 0.5, 0.5, aScale);
        multiply_matrix(aScale, aModelView, aModelView);
      // GL1x: glTranslate(0.0f, 0.0f, -3.5f);
        translate_matrix(0.0f, 0.0f, -3.5f, aTranslate);
        multiply_matrix(aTranslate, aModelView, aModelView);
      //  gluPerspective(45, aspect, 0.1, 100);
        perspective_matrix(45.0, (float)gWidth/(float)gHeight, 0.1, 100.0, aPerspective);
        multiply_matrix(aPerspective, aModelView, aMVP);
        glUniformMatrix4fv(gmvP, 1, GL_FALSE, aMVP);
        mCube.draw(gvPositionHandle);
      }
      
  5. res 文件夹下创建一个名为 raw 的文件夹,并向其中添加以下两个文件:

    • vshader:这是顶点着色器的源代码:

      attribute vec4 vPosition;
      uniform mat4 mvp;
      void main() 
      {
         gl_Position = mvp * vPosition;
      }
      
    • fshader:这是片段着色器的源代码:

      void main()
      {
         gl_FragColor = vec4(0.0,0.5,0.0,1.0);
      }
      
  6. jni 文件夹下添加 Android.mk 文件,如下所示。注意,我们必须通过 LOCAL_LDLIBS := -lGLESv2 链接到 OpenGL ES 2.0:

    LOCAL_PATH := $(call my-dir)
    include $(CLEAR_VARS)
    LOCAL_MODULE    := CubeG2
    LOCAL_SRC_FILES := matrix.cpp Cube.cpp CubeG2.cpp
    LOCAL_LDLIBS := -lGLESv2 -llog
    include $(BUILD_SHARED_LIBRARY)
    
  7. AndroidManifest.xml 文件中的 <application>...</application> 之前添加以下行,表示安卓应用使用 OpenGL ES 2.0 功能:

    <uses-feature android:glEsVersion="0x00020000" android:required="true" />
    
  8. 构建安卓 NDK 应用程序并在安卓设备上运行。该应用将显示一个立方体,我们可以触摸以旋转立方体:如何操作...

它是如何工作的...

示例项目使用 OpenGL ES 2.0 渲染了一个 3D 立方体。OpenGL ES 2.0 提供了一个可编程管线,可以提供顶点着色器和片段着色器来控制顶点和片段的处理方式:

  • 顶点着色器:它对每个顶点执行。通常使用它进行变换、光照、纹理映射等。

  • 片段着色器:它对光栅化器产生的每个片段执行。一个典型的处理是向每个片段添加颜色。

着色器是使用 OpenGL 着色语言编程的,下面将讨论这一点。

OpenGL 着色语言(GLSL)

在此,我们简要介绍 GLSL。

  • 数据类型:它们主要有四种类型,包括 boolintfloatsampler。对于前三种类型还有向量类型——bvec2bvec3bvec4 分别指 2D、3D 和 4D 布尔向量。ivec2ivec3ivec4 代表整数向量。vec2vec3vec4 指浮点向量。采样器用于纹理采样,必须是统一变量。

  • 属性、统一变量和着色器间变量:着色器包括三种输入和输出类型,包括统一变量、属性和着色器间变量。这三种类型都必须是全局的:

    • 统一变量:它是只读类型的,在渲染过程中不需要更改。例如,光源位置。

    • 属性:它是只读类型的,仅作为顶点着色器的输入。它对每个顶点都不同。例如,顶点位置。

    • 着色器间变量:它用于将数据从顶点着色器传递到片段着色器。在顶点着色器中它是可读可写的,但在片段着色器中仅可读。

  • 内置类型:GLSL 有各种内置的属性、统一变量和着色器间的变量。以下我们突出介绍其中的一些:

    • gl_Vertex:它是一个属性——一个表示顶点位置的 4D 向量。

    • gl_Color:这是一个属性——表示顶点颜色的 4D 向量。

    • gl_ModelViewMatrix:这是一个统一变量——4x4 的模型视图矩阵。

    • gl_ModelViewProjectionMatrix:这是一个统一变量。4x4 的模型视图投影矩阵。

    • gl_Position:它仅作为顶点着色器输出可用。它是一个 4D 向量,表示最终处理的顶点位置。

    • gl_FragColor:它仅作为片段着色器输出可用。它是一个 4D 向量,表示最终要写入帧缓冲区的颜色。

如何使用着色器:

在我们的示例项目中,顶点着色器程序简单地将每个立方体顶点与模型视图投影矩阵相乘,而片段着色器将每个片段设置为绿色。要使用着色器源代码,应遵循以下步骤:

  1. 创建着色器:调用了以下 OpenGL ES 2.0 方法:

    • glCreateShader:它创建一个GL_VERTEX_SHADERGL_FRAGMENT_SHADER着色器。它返回一个非零值,通过这个值可以引用着色器。

    • glShaderSource:它将源代码放入着色器对象中。之前存储的源代码将被完全替换。

    • glCompileShader:它编译着色器对象中的源代码。

  2. 创建程序并附加着色器:调用了以下方法:

    • glCreateProgram:它创建一个空的程序对象,可以向其附加着色器。程序对象本质上是提供一种机制,将所有需要一起执行的内容链接起来。

    • glAttachShader:它将着色器附加到程序对象上。

    • glLinkProgram:它链接一个程序对象。如果程序对象附加了任何GL_VERTEX_SHADER对象,它们将被用来在顶点处理器上创建一个可执行文件。如果附加了任何GL_FRAGMENT_SHADER着色器,它们将被用来在片段处理器上创建一个可执行文件。

  3. 使用程序:我们使用以下调用向着色器传递数据并执行 OpenGL 操作:

    • glUseProgram:将程序对象作为当前渲染状态的一部分安装。

    • glGetAttribLocation:它返回一个属性变量的位置。

    • glVertexAttribPointer:它指定了在渲染时要使用的通用顶点属性数组的存储位置和数据格式。

    • glEnableVertexAttribArray:它启用一个顶点属性数组。

    • glGetUniformLocation:它返回一个统一变量的位置。

    • glUniform:它指定一个统一变量的值。

    • glDrawArrays:它从数组数据中渲染图元。

还有更多...

示例项目通过矩阵操作执行模型视图变换和投影变换。这些变换的细节很繁琐,不在本书的讨论范围内,因此这里不予介绍。但是,代码中提供了详细的注释。感兴趣的读者也可以轻松地在网上找到这些操作的资源。

使用 EGL 显示图形

除了我们在上一个配方中描述的 GLSurfaceView 显示机制外,还可以使用 EGL 显示 OpenGL 图形。

准备就绪

建议读者在阅读本节之前先阅读 使用 OpenGL ES 1.x API 绘制 3D 图形和点亮场景 的配方。

如何操作...

以下步骤描述了如何创建一个演示 EGL 用法的 Android 项目:

  1. 创建一个名为 EGLDemo 的 Android 应用程序。将包名设置为 cookbook.chapter4.egl。如果你需要更详细的说明,请参考 第二章 Java Native Interface 中的 加载本地库和注册本地方法 配方。

  2. 在项目 EGLDemo 上右键点击,选择 Android Tools | 添加本地支持

  3. 添加两个 Java 文件,分别是 EGLDemoActivity.javaMySurfaceView.javaEGLDemoActivity.javaContentView 设置为 MySurfaceView 的实例,并在 Android 活动回调函数中开始和停止渲染:

    … …
    public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    myView = new MySurfaceView(this);
    this.setContentView(myView);
    }
    protected void onResume() {
    super.onResume();
    myView.startRenderer();
    }
    … …
    protected void onStop() {
    super.onStop();
    myView.destroyRender();
    }
    … …
    
  4. MySurfaceView.java 执行的角色类似于 GLSurfaceView。它与本地渲染器交互来管理显示表面和处理触摸事件:

    public class MySurfaceView extends SurfaceView implements SurfaceHolder.Callback {
    … …
    public MySurfaceView(Context context) {
    super(context);
    this.getHolder().addCallback(this);
    }
    … …
    public boolean onTouchEvent(final MotionEvent event) {
    float x = event.getX();
    float y = event.getY();
    switch (event.getAction()) {
    case MotionEvent.ACTION_MOVE:
        float dx = x - mPreviousX;
        float dy = y - mPreviousY;
        mAngleX += dx * TOUCH_SCALE_FACTOR;
        mAngleY += dy * TOUCH_SCALE_FACTOR;
        naRequestRenderer(mAngleX, mAngleY);
    }
    mPreviousX = x;
    mPreviousY = y;
    return true;
    }
    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width,int height) {
    naSurfaceChanged(holder.getSurface());
    }
    @Override
    public void surfaceCreated(SurfaceHolder holder) {}
    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
    naSurfaceDestroyed();
    }
    }
    
  5. 以下代码应添加到 jni 文件夹中:

    • Cube.cpp 和 Cube.h:使用 OpenGL 1.x API 绘制 3D 立方体。

    • OldRenderMethods.cpp 和 OldRenderMethods.h:初始化 OpenGL 1.x,执行变换,绘制图形等。这类似于 在 OpenGL 1.x 中绘制 3D 图形 配方中的相应方法。

    • Renderer.cpp 和 Renderer.h:模拟 android.opengl.GLSurfaceView.Renderer。它设置 EGL 上下文,管理显示等。

    • renderAFrame:设置事件类型,然后通知渲染线程处理事件:

      void Renderer::renderAFrame(float pAngleX, float pAngleY) {
      pthread_mutex_lock(&mMutex);
      mAngleX = pAngleX; mAngleY = pAngleY;
      mRendererEvent = RTE_DRAW_FRAME;
      pthread_mutex_unlock(&mMutex);
      pthread_cond_signal(&mCondVar); 
      }
      
    • renderThreadRun:在一个单独的线程中运行,处理各种事件,包括表面更改、绘制一帧等:

      void Renderer::renderThreadRun() {
          bool ifRendering = true;
          while (ifRendering) {
              pthread_mutex_lock(&mMutex);
              pthread_cond_wait(&mCondVar, &mMutex);
              switch (mRendererEvent) {
              … …
                  case RTE_DRAW_FRAME:
                      mRendererEvent = RTE_NONE;
                      pthread_mutex_unlock(&mMutex);
                      if (EGL_NO_DISPLAY!=mDisplay) {
                  naDrawGraphics(mAngleX, mAngleY);
                  eglSwapBuffers(mDisplay, mSurface);
                  }
                      }
                      break;
                  ……
              }
      }
      }
      
    • initDisplay:设置 EGL 上下文:

      bool Renderer::initDisplay() {
      const EGLint attribs[] = {
          EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
          EGL_BLUE_SIZE, 8,
          EGL_GREEN_SIZE, 8,
          EGL_RED_SIZE, 8,
          EGL_NONE};
      EGLint width, height, format;
      EGLint numConfigs;
      EGLConfig config;
      EGLSurface surface;
      EGLContext context;
      EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
      eglInitialize(display, 0, 0);
      eglChooseConfig(display, attribs, &config, 1, &numConfigs);
      eglGetConfigAttrib(display, config, EGL_NATIVE_VISUAL_ID, &format);
      ANativeWindow_setBuffersGeometry(mWindow, 0, 0, format);
      surface = eglCreateWindowSurface(display, config, mWindow, NULL);
      context = eglCreateContext(display, config, NULL, NULL);
      if (eglMakeCurrent(display, surface, surface, context) == EGL_FALSE) {
          return -1;
      }
      eglQuerySurface(display, surface, EGL_WIDTH, &width);
      eglQuerySurface(display, surface, EGL_HEIGHT, &height);
        … ...
      }
      
    • EGLDemo.cpp:注册本地方法并包装本地代码。以下两个方法被使用:

      naSurfaceChanged:它获取与 Java Surface 对象关联的本地窗口,并初始化 EGL 和 OpenGL:

      void naSurfaceChanged(JNIEnv* env, jclass clazz, jobject pSurface) {
      gWindow = ANativeWindow_fromSurface(env, pSurface);
      gRenderer->initEGLAndOpenGL1x(gWindow);
      }
      

      naRequestRenderer:渲染一帧,由 MySurfaceView 中的 touch 事件处理程序调用:

      void naRequestRenderer(JNIEnv* env, jclass clazz, float pAngleX, float pAngleY) {
      gRenderer->renderAFrame(pAngleX, pAngleY);
      }
      
  6. jni 文件夹下添加 Android.mk 文件,内容如下:

    LOCAL_PATH := $(call my-dir)
    include $(CLEAR_VARS)
    LOCAL_MODULE := EGLDemo
    LOCAL_SRC_FILES := Cube.cpp OldRenderMethods.cpp Renderer.cpp EGLDemo.cpp
    LOCAL_LDLIBS := -llog -landroid -lEGL -lGLESv1_CM
    include $(BUILD_SHARED_LIBRARY)
    
  7. 构建 Android NDK 应用程序并在 Android 设备上运行。应用程序将显示一个立方体,我们可以触摸它使其旋转:如何操作...

它是如何工作的...

EGL 是 OpenGL ES 与底层本地窗口系统之间的接口。根据 Khronos EGL 网页(www.khronos.org/egl)的说明,它处理包括 OpenGL ES 在内的其他 Khronos 2D 和 3D API 的图形上下文管理、表面绑定和渲染同步。

提示

EGL是一个在嵌入式系统中广泛使用的跨平台 API,包括 Android 和 iPhone(苹果实现的 EGL 称为EAGL)。许多桌面平台也支持 EGL。不同的实现可能不是 100%兼容,但通常 EGL 代码的移植工作不会很繁重。

以下步骤描述了如何设置和操作 EGL 及其与 OpenGL 的集成:

  1. 获取并初始化显示连接:EGL 需要知道内容应该显示在哪里,因此我们将需要获取一个显示连接并初始化它。这是使用以下两个方法完成的:

    • eglGetDisplay:它获取原生显示的 EGL 显示连接。如果输入参数是EGL_DEFAULT_DISPLAY,则返回默认显示连接。

    • eglInitialize:它初始化通过eglGetDisplay获取的 EGL 显示连接。

  2. 配置 EGL:这是通过eglChooseConfig完成的。

    eglChooseConfig返回与attrib_list参数指定的要求相匹配的 EGL 帧缓冲区配置列表。属性是一个属性和相应期望值对的数组,以EGL_NONE结束。在我们的代码中,我们简单指定EGL_SURFACE_TYPEEGL_WINDOW_BIT,颜色组件大小为 8 位。

  3. 创建一个渲染表面,用于放置显示内容:这是通过eglCreateWindowSurface完成的。

    eglCreateWindowSurface,给定 EGL 显示连接、EGL 帧缓冲区配置和原生窗口,返回一个新的 EGL 窗口表面。

    在我们的代码中,我们从SurfaceView开始,并将其关联的android.view.Surface值传递给原生代码。在原生代码中,我们获取其原生窗口,并最终为 OpenGL 绘制创建 EGL 窗口表面。

  4. 创建 EGL 渲染上下文并将其设为当前:这是通过eglCreateContexteglMakeCurrent完成的。

    • eglCreateContext:它创建一个新的 EGL 渲染上下文,用于渲染到 EGL 绘制表面。

    • eglMakeCurrent:它将 EGL 上下文附加到 EGL 绘制和读取表面。在我们的代码中,创建的窗口表面被用作读取和绘制表面。

  5. OpenGL 绘制:这在前面的食谱中已经介绍过了。

  6. 交换 EGL 表面内部缓冲区以显示内容:这是通过eglSwapBuffers调用完成的。

    eglSwapBuffers将 EGL 表面颜色缓冲区发布到原生窗口。这有效地在屏幕上显示绘制内容。

    EGL 内部维护两个缓冲区。前缓冲区的内容被显示,而绘制可以在后缓冲区进行。当我们决定显示新的绘制内容时,我们交换这两个缓冲区。

  7. 当我们想要停止渲染时,释放 EGL 上下文,销毁 EGL 表面,终止 EGL 显示连接:

    • 使用EGL_NO_SURFACEEGL_NO_CONTEXTeglMakeCurrent释放当前上下文。

    • eglDestroySurface销毁一个 EGL 表面。

    • eglTerminate 终止了 EGL 显示连接

窗口管理

我们的代码使用 Android 原生窗口管理 API 调用来获取原生窗口并配置它。调用了以下方法:

  • ANativeWindow_fromSurface:它返回与 Java 表面对象关联的原生窗口。返回的引用应该传递给 ANativeWindow_release,以确保没有内存泄漏。

  • ANativeWindow_setBuffersGeometry:它设置窗口缓冲区的大小和格式。在我们的代码中,我们将宽度和高度指定为 0,在这种情况下,将使用窗口的基本值。

请注意,我们将在 Android.mk 文件中链接到 Android 库(LOCAL_LDLIBS := -landroid),因为它是 Android 原生应用程序 API 的一部分,我们将在下一章中详细介绍。

还有更多...

渲染器在一个单独的线程中运行事件循环。我们使用了POSIX 线程pthreads)调用创建原生线程,将其与主线程同步等。我们将在第六章,Android NDK Multithreading中详细讲解 pthread

第五章:Android 本地应用程序 API

在本章中,我们将涵盖以下内容:

  • 使用 native_activity.h 接口创建本地活动

  • 使用 Android 本地应用程序胶水创建本地活动

  • 在 Android NDK 中管理本地窗口

  • 在 Android NDK 中检测和处理输入事件

  • 在 Android NDK 中访问传感器

  • 在 Android NDK 中管理资产

引言

感谢 Android 本地应用程序 API,从 Android API 级别 9(Android 2.3,姜饼)起,就有可能用纯本地代码编写 Android 应用程序。也就是说,不需要任何 Java 代码。Android 本地 API 在<NDK root>/platforms/android-<API level>/arch-arm/usr/include/android/文件夹下的几个头文件中定义。根据这些头文件中定义的函数提供的功能,它们可以分为以下几类:

  • 活动生命周期管理:

    • native_activity.h

    • looper.h

  • 窗口管理:

    • rect.h

    • window.h

    • native_window.h

    • native_window_jni.h

  • 输入(包括按键和动作事件)和传感器事件:

    • input.h

    • keycodes.h

    • sensor.h

  • 资产、配置和存储管理:

    • configuration.h

    • asset_manager.h

    • asset_manager_jni.h

    • storage_manager.h

    • obb.h

此外,Android NDK 还提供了一个名为本地应用程序胶水的静态库,以帮助创建和管理本地活动。该库的源代码可以在sources/android/native_app_glue/目录下找到。

在本章中,我们首先会介绍使用native_acitivity.h提供的简单回调模型创建本地活动,以及本地应用程序胶水库支持的更复杂但灵活的两个线程模型。然后,我们将讨论在 Android NDK 中的窗口管理,我们将在本地代码中在屏幕上绘制内容。接下来介绍输入事件处理和传感器访问。最后,我们将介绍资产管理,它管理我们项目assets文件夹下的文件。请注意,本章涵盖的 API 可以完全摆脱 Java 代码,但我们不必这样做。《在 Android NDK 中管理资产》一节提供了一个在混合代码 Android 项目中使用资产管理 API 的示例。

在开始之前,我们需要牢记,尽管在本地活动中不需要 Java 代码,但 Android 应用程序仍然在 Dalvik VM 上运行,许多 Android 平台功能是通过 JNI 访问的。Android 本地应用程序 API 只是为我们隐藏了 Java 世界。

使用 native_activity.h 接口创建本地活动

Android 本地应用程序 API 允许我们创建本地活动,这使得用纯本地代码编写 Android 应用程序成为可能。本节介绍如何使用纯 C/C++代码编写简单的 Android 应用程序。

准备就绪

期望读者对如何调用 JNI 函数有基本了解。第二章,Java Native Interface 详细介绍了 JNI,建议在阅读当前部分之前至少阅读该章节或以下内容:

  • 在安卓 NDK 中操作字符串

  • 在 NDK 中调用实例和静态方法

如何操作…

创建一个没有一行 Java 代码的简单安卓 NDK 应用程序的以下步骤:

  1. 创建一个名为 NativeActivityOne 的安卓应用程序。将包名设置为 cookbook.chapter5.nativeactivityone。如果你需要更详细的说明,请参考 第二章,Java Native Interface 中的 加载本地库和注册本地方法 部分。

  2. 右键点击 NativeActivityOne 项目,选择 Android Tools | 添加本地支持

  3. 按照以下方式更改 AndroidManifest.xml 文件:

    <manifest 
       package="cookbook.chapter5.nativeactivityone"
       android:versionCode="1"
       android:versionName="1.0">
       <uses-sdk android:minSdkVersion="9"/>
       <application android:label="@string/app_name"
           android:icon="@drawable/ic_launcher"
           android:hasCode="true">        
      <activity android:name="android.app.NativeActivity"
          android:label="@string/app_name"
          android:configChanges="orientation|keyboardHidden">  
               <meta-data android:name="android.app.lib_name"
                 android:value="NativeActivityOne" />
               <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.LAUNCHER" />
               </intent-filter>
             </activity>   
       </application>
    </manifest>
    

    我们应该确保在前一个文件中正确设置以下内容:

    • 活动名称必须设置为 android.app.NativeActivity

    • android.app.lib_name 元数据的值必须设置为本地模块名称,不带 lib 前缀和 .so 后缀。

    • android:hasCode 需要设置为 true,表示应用程序包含代码。注意 <NDK 根目录>/docs/NATIVE-ACTIVITY.HTML 中的文档提供了一个将 android:hasCode 设置为 falseAndroidManifest.xml 文件示例,这将不允许应用程序启动。

  4. jni 文件夹下添加两个名为 NativeActivityOne.cppmylog.h 的文件。ANativeActivity_onCreate 方法应该在 NativeActivityOne.cpp 中实现。以下是实现的一个示例:

    void ANativeActivity_onCreate(ANativeActivity* activity,
           void* savedState, size_t savedStateSize) {
      printInfo(activity);
      activity->callbacks->onStart = onStart;
      activity->callbacks->onResume = onResume;
      activity->callbacks->onSaveInstanceState = onSaveInstanceState;
      activity->callbacks->onPause = onPause;
      activity->callbacks->onStop = onStop;
      activity->callbacks->onDestroy = onDestroy;
      activity->callbacks->onWindowFocusChanged = onWindowFocusChanged;
      activity->callbacks->onNativeWindowCreated = onNativeWindowCreated;
      activity->callbacks->onNativeWindowResized = onNativeWindowResized;
      activity->callbacks->onNativeWindowRedrawNeeded = onNativeWindowRedrawNeeded;
      activity->callbacks->onNativeWindowDestroyed = onNativeWindowDestroyed;
      activity->callbacks->onInputQueueCreated = onInputQueueCreated;
      activity->callbacks->onInputQueueDestroyed = onInputQueueDestroyed;
      activity->callbacks->onContentRectChanged = onContentRectChanged;
      activity->callbacks->onConfigurationChanged = onConfigurationChanged;
      activity->callbacks->onLowMemory = onLowMemory;
      activity->instance = NULL;
    }
    
  5. jni 文件夹下添加 Android.mk 文件:

    LOCAL_PATH := $(call my-dir)
    include $(CLEAR_VARS)
    LOCAL_MODULE    := NativeActivityOne
    LOCAL_SRC_FILES := NativeActivityOne.cpp
    LOCAL_LDLIBS    := -landroid -llog
    include $(BUILD_SHARED_LIBRARY)
    
  6. 构建安卓应用程序并在模拟器或设备上运行。启动一个终端并使用以下命令显示 logcat 输出:

    $ adb logcat -v time NativeActivityOne:I *:S
    

    或者,你也可以使用 Eclipse 中的 logcat 视图来查看 logcat 输出。

    当应用程序启动时,你应该能够看到以下 logcat 输出:

    如何操作…

    如截图所示,执行了几个安卓活动生命周期回调函数。我们可以操作手机以执行其他回调。例如,长按主页按钮然后按返回按钮将导致 onWindowFocusChanged 回调执行。

工作原理…

在我们的示例中,我们创建了一个简单的“纯”本地应用程序,当安卓框架调用我们定义的回调函数时输出日志。"纯"本地应用程序实际上并不是完全本地化的。尽管我们没有编写一行 Java 代码,安卓框架仍然在 Dalvik VM 上运行一些 Java 代码。

Android 框架提供了一个android.app.NativeActivity.java类,帮助我们创建一个“本地”活动。在一个典型的 Java 活动中,我们扩展android.app.Activity并覆盖活动生命周期方法。NativeActivity也是android.app.Activity的一个子类,做类似的事情。在本地活动的开始,NativeActivity.java将调用ANativeActivity_onCreate,这在native_activity.h中声明,并由我们实现。在ANativeActivity_onCreate方法中,我们可以注册我们的回调方法来处理活动生命周期事件和用户输入。在运行时,NativeActivity将在相应事件发生时调用这些本地回调方法。

总之,NativeActivity是一个封装,它为我们的本地代码隐藏了管理的 Android Java 世界,并公开了native_activity.h中定义的本地接口。

ANativeActivity数据结构:本地代码中的每个回调方法都接受一个ANativeActivity结构的实例。Android NDK 在native_acitivity.h中定义了ANativeActivity数据结构,如下所示:

typedef struct ANativeActivity {
   struct ANativeActivityCallbacks* callbacks;
   JavaVM* vm;
   JNIEnv* env;
   jobject clazz;
   const char* internalDataPath;
   const char* externalDataPath;
   int32_t sdkVersion;
   void* instance;
   AAssetManager* assetManager;
} ANativeActivity;

上述代码的各种属性解释如下:

  • callbacks:这是一个定义了 Android 框架将在主 UI 线程中调用的所有回调的数据结构。

  • vm:它是应用程序进程的全局 Java VM 句柄。它被用于某些 JNI 函数中。

  • env:这是一个JNIEnv接口指针。JNIEnv通过局部存储数据使用(更多详情请参考第二章中的在 Android NDK 中操作字符串食谱,Java Native 接口),因此这个字段只能通过主 UI 线程访问。

  • clazz:这是由 Android 框架创建的android.app.NativeActivity对象的引用。它可以用来访问android.app.NativeActivity Java 类中的字段和方法。在我们的代码中,我们访问了android.app.NativeActivitytoString方法。

  • internalDataPath:它是应用程序的内部数据目录路径。

  • externalDataPath:它是应用程序的外部数据目录路径。

    提示

    internalDataPathexternalDataPath在 Android 2.3.x 版本中是NULL。这是一个已知的错误,从 Android 3.0 开始已经修复。如果我们针对的是低于 Android 3.0 的设备,那么我们需要寻找其他方法来获取内部和外部数据目录。

  • sdkVersion:这是 Android 平台的 SDK 版本号。注意,这指的是运行应用的设备/模拟器的版本,而不是我们开发中使用的 SDK 版本。

  • instance:框架不使用它。我们可以用它来存储用户定义的数据并在需要时传递。

  • assetManager:这是指向应用程序资源管理器实例的指针。我们需要它来访问assets数据。我们将在本章的在 Android NDK 中管理资源食谱中详细讨论它。

还有更多内容…

native_activity.h接口提供了一个简单的单线程回调机制,它允许我们编写不涉及 Java 代码的活动。但是,这种单线程方法意味着我们必须快速从本地回调方法中返回。否则,应用程序将无法响应用户操作(例如,当我们触摸屏幕或按下菜单按钮时,应用无法响应,因为 GUI 线程正忙于执行回调函数)。

解决这个问题的方法之一是使用多线程。例如,许多游戏需要几秒钟来加载。我们需要将加载工作放到后台线程中,这样 UI 就可以显示加载进度并响应用户输入。Android NDK 附带一个名为android_native_app_glue的静态库,以帮助我们处理此类情况。这个库的细节在使用 Android 本地应用胶水创建本地活动的食谱中有所介绍。

提示

Java 活动中也存在类似的问题。例如,如果我们编写一个在onCreate中搜索整个设备图片的 Java 活动,应用程序将会无响应。我们可以使用AsyncTask在后台搜索和加载图片,并让主 UI 线程显示进度条并响应用户输入。

使用 Android 本地应用胶水创建本地活动

上一个食谱描述了native_activity.h中定义的接口如何让我们创建本地活动。然而,所有定义的回调都是在主 UI 线程中调用的,这意味着我们不能在回调中进行繁重的处理。

Android SDK 提供了AsyncTaskHandlerRunnableThread等,帮助我们后台处理事情并与主 UI 线程通信。Android NDK 提供了一个名为android_native_app_glue的静态库,以帮助我们在单独的线程中执行回调函数并处理用户输入。本食谱将详细讨论android_native_app_glue库。

准备就绪

android_native_app_glue库是建立在native_activity.h接口之上的。因此,建议读者在阅读这个食谱之前先阅读使用 native_activity.h 接口创建本地活动的食谱。

如何操作…

以下步骤基于android_native_app_glue库创建一个简单的 Android NDK 应用程序:

  1. 创建一个名为NativeActivityTwo的 Android 应用程序。将包名设置为cookbook.chapter5.nativeactivitytwo。如果你需要更详细的说明,请参考第二章中的加载本地库和注册本地方法食谱,Java Native Interface

  2. 右键点击NativeActivityTwo项目,选择Android Tools | 添加本地支持

  3. 修改AndroidManifest.xml文件如下:

    <manifest 
       package="cookbook.chapter5.nativeactivitytwo"
       android:versionCode="1"
       android:versionName="1.0">
       <uses-sdk android:minSdkVersion="9"/>
       <application android:label="@string/app_name"
           android:icon="@drawable/ic_launcher"
           android:hasCode="true">
        <activity android:name="android.app.NativeActivity"
            android:label="@string/app_name"
            android:configChanges="orientation|keyboardHidden">        
               <meta-data android:name="android.app.lib_name"
                 android:value="NativeActivityTwo" />
               <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.LAUNCHER" />
               </intent-filter>
           </activity>
       </application>
    </manifest>
    
  4. jni文件夹下添加两个名为NativeActivityTwo.cppmylog.h的文件。以下是NativeActivityTwo.cpp的代码:

    #include <jni.h>
    #include <android_native_app_glue.h>
    #include "mylog.h"
    void handle_activity_lifecycle_events(struct android_app* app, int32_t cmd) {
      LOGI(2, "%d: dummy data %d", cmd, *((int*)(app->userData)));
    }
    void android_main(struct android_app* app) {
      app_dummy();    // Make sure glue isn't stripped.
      int dummyData = 111;
      app->userData = &dummyData;
      app->onAppCmd = handle_activity_lifecycle_events;
      while (1) {
        int ident, events;
        struct android_poll_source* source;
    if ((ident=ALooper_pollAll(-1, NULL, &events, (void**)&source)) >= 0) {
          source->process(app, source);
        }
      }
    }
    
  5. jni目录下添加Android.mk文件:

    LOCAL_PATH := $(call my-dir)
    include $(CLEAR_VARS)
    LOCAL_MODULE    := NativeActivityTwo
    LOCAL_SRC_FILES := NativeActivityTwo.cpp
    LOCAL_LDLIBS    := -llog -landroid
    LOCAL_STATIC_LIBRARIES := android_native_app_glue
    include $(BUILD_SHARED_LIBRARY)
    $(call import-module,android/native_app_glue)
    
  6. 构建 Android 应用程序并在模拟器或设备上运行。启动一个终端并使用以下命令显示 logcat 输出:

    adb logcat -v time NativeActivityTwo:I *:S
    

    当应用程序启动时,你应该能够看到以下 logcat 输出,并且设备屏幕将显示一个黑屏:

    如何操作…

    按下返回键时,将显示以下输出:

    如何操作…

它的工作原理是…

本示例演示了如何使用android_native_app_glue库创建一个本地活动。

使用android_native_app_glue库应遵循以下步骤:

  • 实现一个名为android_main的函数。这个函数应该实现一个事件循环,持续地轮询事件。这个方法将在库创建的后台线程中运行。

  • 默认情况下,后台线程附带了两个事件队列,包括活动生命周期事件队列和输入事件队列。当使用库创建的 looper 轮询事件时,你可以通过检查返回的标识符(LOOPER_ID_MAINLOOPER_ID_INPUT)来确定事件来自哪里。也可以将附加的事件队列附加到后台线程。

  • 当返回一个事件时,数据指针将指向一个android_poll_source数据结构。我们可以调用这个结构的 process 函数。这个过程是一个函数指针,对于活动生命周期事件,它指向android_app->onAppCmd;对于输入事件,它指向android_app->onInputEvent。我们可以提供自己的处理函数,并将相应的函数指针指向这些函数。

在我们的示例中,我们实现了一个名为handle_activity_lifecycle_events的简单函数,并将android_app->onAppCmd函数指针指向它。这个函数只是简单地打印cmd值以及与android_app数据结构一起传递的用户数据。cmdandroid_native_app_glue.h中定义为枚举。例如,当应用启动时,cmd值为1011016,分别对应于APP_CMD_STARTAPP_CMD_RESUMEAPP_CMD_INPUT_CHANGEDAPP_CMD_INIT_WINDOWAPP_CMD_GAINED_FOCUS

android_native_app_glue库内部机制:你可以在 Android NDK 的sources/android/native_app_glue目录下找到android_native_app_glue库的源代码。它仅由两个文件组成,分别是android_native_app_glue.candroid_native_app_glue.h。我们首先描述代码的流程,然后详细讨论一些重要的方面。

提示

由于提供了native_app_glue的源代码,我们可以在必要时修改它,尽管在大多数情况下并不需要。

android_native_app_glue是建立在native_activity.h接口之上的。如下代码所示(从sources/android/native_app_glue/android_native_app_glue.c提取),它实现了ANativeActivity_onCreate函数,在其中注册回调函数并调用android_app_create函数。请注意,返回的android_app实例由原生活动的instance字段指向,可以传递给各种回调函数:

void ANativeActivity_onCreate(ANativeActivity* activity,
        void* savedState, size_t savedStateSize) {
    LOGV("Creating: %p\n", activity);
    activity->callbacks->onDestroy = onDestroy;
    activity->callbacks->onStart = onStart;
    activity->callbacks->onResume = onResume;
    … …
    activity->callbacks->onNativeWindowCreated = onNativeWindowCreated;
    activity->callbacks->onNativeWindowDestroyed = onNativeWindowDestroyed;
    activity->callbacks->onInputQueueCreated = onInputQueueCreated;
    activity->callbacks->onInputQueueDestroyed = onInputQueueDestroyed;
    activity->instance = android_app_create(activity, savedState, savedStateSize);
}

android_app_create函数(如下代码片段所示)初始化android_app数据结构的一个实例,该结构在android_native_app_glue.h中定义。这个函数为线程间通信创建了一个单向管道。之后,它生成一个新的线程(之后我们称之为后台线程)以初始化的android_app数据作为输入参数运行android_app_entry函数。主线程将等待后台线程启动,然后返回:

static struct android_app* android_app_create(ANativeActivity* activity, void* savedState, size_t savedStateSize) {
    struct android_app* android_app = (struct android_app*)malloc(sizeof(struct android_app));
    memset(android_app, 0, sizeof(struct android_app));
    android_app->activity = activity;

    pthread_mutex_init(&android_app->mutex, NULL);
    pthread_cond_init(&android_app->cond, NULL);
  ……
    int msgpipe[2];
    if (pipe(msgpipe)) {
        LOGE("could not create pipe: %s", strerror(errno));
        return NULL;
    }
    android_app->msgread = msgpipe[0];
    android_app->msgwrite = msgpipe[1];

    pthread_attr_t attr; 
    pthread_attr_init(&attr);
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
    pthread_create(&android_app->thread, &attr, android_app_entry, android_app);
    // Wait for thread to start.
    pthread_mutex_lock(&android_app->mutex);
    while (!android_app->running) {
        pthread_cond_wait(&android_app->cond, &android_app->mutex);
    }
    pthread_mutex_unlock(&android_app->mutex);
    return android_app;
}

后台线程从android_app_entry函数开始(如下代码片段所示),在其中创建一个循环器。两个事件队列将被附加到循环器上。活动生命周期事件队列被附加到android_app_entry函数上。当活动的输入队列被创建时,输入队列被附加(到android_native_app_glue.candroid_app_pre_exec_cmd函数)。在附加了活动生命周期事件队列之后,后台线程向主线程发出信号,表明它已经在运行。然后它调用名为android_main的函数,并传入android_app数据。android_main是我们需要实现的函数,正如我们的示例代码所示。它必须在一个循环中运行,直到活动退出:

static void* android_app_entry(void* param) {
    struct android_app* android_app = (struct android_app*)param;
  … …
  //Attach life cycle event queue with identifier LOOPER_ID_MAIN
    android_app->cmdPollSource.id = LOOPER_ID_MAIN;
    android_app->cmdPollSource.app = android_app;
    android_app->cmdPollSource.process = process_cmd;
    android_app->inputPollSource.id = LOOPER_ID_INPUT;
    android_app->inputPollSource.app = android_app;
    android_app->inputPollSource.process = process_input;
    ALooper* looper = ALooper_prepare(ALOOPER_PREPARE_ALLOW_NON_CALLBACKS);
    ALooper_addFd(looper, android_app->msgread, LOOPER_ID_MAIN, ALOOPER_EVENT_INPUT, NULL, &android_app->cmdPollSource);
    android_app->looper = looper;

    pthread_mutex_lock(&android_app->mutex);
    android_app->running = 1;
    pthread_cond_broadcast(&android_app->cond);
    pthread_mutex_unlock(&android_app->mutex);
    android_main(android_app);
    android_app_destroy(android_app);
    return NULL;
}

下图展示了主线程和后台线程如何共同工作以创建多线程原生活动:

工作原理

我们以活动生命周期事件队列为例。主线程调用回调函数,这些函数只是向管道的写端写入数据,而在android_main函数中实现的真正循环将轮询事件。一旦检测到事件,该函数就会调用事件处理程序,从管道的读端读取确切的命令并进行处理。android_native_app_glue库为我们实现了所有主线程的工作以及部分后台线程的工作。我们只需要提供轮询循环和事件处理程序,正如我们的示例代码所示。

管道:主线程通过在android_app_create函数中调用pipe方法创建一个单向管道。这个方法接受一个包含两个整数的数组。函数返回后,第一个整数将被设置为代表管道读端的文件描述符,而第二个整数将被设置为代表管道写端的文件描述符。

管道通常用于进程间通信IPC),但在这里它被用于主 UI 线程与在android_app_entry创建的后台线程之间的通信。当一个活动生命周期事件发生时,主线程将执行在ANativeActivity_onCreate注册的相应回调函数。回调函数只是将一个命令写入管道的写入端,然后等待来自后台线程的信号。后台线程应该不断地轮询事件,一旦检测到生命周期事件,它将从管道的读取端读取确切的事件,通知主线程解除阻塞并处理事件。因为信号在收到命令后立即发送,且在实际处理事件之前,主线程可以快速从回调函数返回,而无需担心事件可能需要长时间处理。

不同的操作系统对管道的实现各不相同。安卓系统实现的管道是“半双工”的,即通信是单向的。也就是说,一个文件描述符只能写,另一个文件描述符只能读。某些操作系统中的管道是“全双工”的,两个文件描述符都可以读写。

循环器是一个事件跟踪设施,它允许我们为一个线程的事件循环附加一个或多个事件队列。每个事件队列都有一个关联的文件描述符。一个事件是在文件描述符上可用的数据。为了使用循环器,我们需要包含android/looper.h头文件。

该库为我们在后台线程中创建的事件循环附加了两个事件队列,包括活动生命周期事件队列和输入事件队列。为了使用循环器,应按以下步骤操作:

  1. 为当前线程创建或获取一个循环器:这是通过ALooper_prepare函数完成的:

    ALooper* ALooper_prepare(int opts);
    

    该函数准备与调用线程关联的循环器并返回它。如果循环器不存在,它会创建一个,将其与线程关联,并返回它。

  2. 附加一个事件队列:这是通过ALooper_addFd完成的。该函数具有以下原型:

    int ALooper_addFd(ALooper* looper, int fd, int ident, int events, ALooper_callbackFunc callback, void* data);
    

    该函数有两种使用方式。首先,如果将callback设置为NULL,则ident集合将由ALooper_pollOnceALooper_pollAll返回。其次,如果callbackNULL,则将执行回调函数,并忽略identandroid_native_app_glue库采用第一种方法将新的事件队列附加到 looper 上。输入参数fd表示与事件队列相关联的文件描述符。ident是事件队列中事件的标识符,可用于分类事件。当callback设置为NULL时,标识符必须大于零。在库源代码中,callback被设置为NULLdata指向将随标识符一起在轮询时返回的私有数据。

    在库中,此函数被调用来将活动生命周期事件队列附加到后台线程。输入事件队列通过特定于输入队列的函数AInputQueue_attachLooper进行附加,我们将在在 NDK 中检测和处理输入事件的菜谱中进行讨论。

  3. 轮询事件:可以通过以下两个函数之一来完成:

    int ALooper_pollOnce(int timeoutMillis, int* outFd, int* outEvents, void** outData);
    int ALooper_pollAll(int timeoutMillis, int* outFd, int* outEvents, void** outData);
    

    当在ALooper_addFd中将callback设置为NULL时,这两种方法是等价的。它们具有相同的输入参数。timeoutMillis指定轮询的超时时间。如果设置为 0,则函数立即返回;如果设置为负数,它们将无限期等待直到发生事件。当与 looper 关联的任何输入队列发生事件时,函数将返回标识符(大于零)。在这种情况下,outFdoutEventsoutData将被设置为与事件关联的文件描述符、轮询事件和数据。否则,它们将被设置为NULL

  4. 分离事件队列:这是通过以下函数完成的:

    int ALooper_removeFd(ALooper* looper, int fd);
    

    它接受与事件队列相关联的 looper 和文件描述符,并将队列从 looper 上分离。

在 Android NDK 中管理原生窗口

本章前面的菜谱仅提供了带有 logcat 输出的简单示例。这个菜谱将讨论如何在 Android NDK 中管理原生窗口。

准备就绪

建议读者在阅读这个菜谱之前阅读以下菜谱:

  • 使用 native_activity.h 接口创建原生活动

  • 使用 Android 原生应用胶水创建原生活动

还要回顾一下,在第四章,Android NDK OpenGL ES API中的使用 EGL 显示图形菜谱中简要介绍了原生窗口管理。

如何操作…

以下步骤创建示例应用程序:

  1. 创建一个名为NativeWindowManagement的 Android 应用程序。将包名设置为cookbook.chapter5.nativewindowmanagement。如果你需要更详细的说明,请参考第二章,Java Native Interface中的加载本地库和注册本地方法的菜谱。

  2. 右键点击NativeWindowManagement项目,选择Android Tools | 添加原生支持

  3. 更新AndroidManifest.xml。具体细节请参考之前的食谱或下载的代码。注意,元数据android.app.lib_name的值必须为NativeWindowManagement

  4. jni文件夹下添加两个名为NativeWindowManagement.cppmylog.h的文件。NativeWindowManagement.cpp是基于之前食谱修改的。以下代码段显示了更新的部分:

    void drawSomething(struct android_app* app) {
      ANativeWindow_Buffer lWindowBuffer;
      ANativeWindow* lWindow = app->window;
      ANativeWindow_setBuffersGeometry(lWindow, 0, 0, WINDOW_FORMAT_RGBA_8888);
      if (ANativeWindow_lock(lWindow, &lWindowBuffer, NULL) < 0) {
        return;
      }
      memset(lWindowBuffer.bits, 0, lWindowBuffer.stride*lWindowBuffer.height*sizeof(uint32_t));
      int sqh = 150, sqw = 100;
      int wst = lWindowBuffer.stride/2 - sqw/2;
      int wed = wst + sqw;
      int hst = lWindowBuffer.height/2 - sqh/2;
      int hed = hst + sqh;
      for (int i = hst; i < hed; ++i) {
        for (int j = wst; j < wed; ++j) {
          ((char*)(lWindowBuffer.bits))[(i*lWindowBuffer.stride + j)*sizeof(uint32_t)] = (char)255;      //R
          ((char*)(lWindowBuffer.bits))[(i*lWindowBuffer.stride + j)*sizeof(uint32_t) + 1] = (char)0;    //G
          ((char*)(lWindowBuffer.bits))[(i*lWindowBuffer.stride + j)*sizeof(uint32_t) + 2] = (char)0;    //B
          ((char*)(lWindowBuffer.bits))[(i*lWindowBuffer.stride + j)*sizeof(uint32_t) + 3] = (char)255;    //A
        }
      }
      ANativeWindow_unlockAndPost(lWindow);
    }
    
    void handle_activity_lifecycle_events(struct android_app* app, int32_t cmd) {
      LOGI(2, "%d: dummy data %d", cmd, *((int*)(app->userData)));
      switch (cmd) {
      case APP_CMD_INIT_WINDOW:
        drawSomething(app);
        break;
      }
    }
    
  5. jni文件夹下添加Android.mk文件,与之前食谱中使用的类似。你只需要将模块名称替换为NativeWindowManagement,源文件替换为NativeWindowManagement.cpp

  6. 构建 Android 应用程序并在模拟器或设备上运行。启动终端并使用以下命令显示 logcat 输出:

    $ adb logcat -v time NativeWindowManagement:I *:S
    

    当应用程序启动时,我们将看到以下 logcat 输出:

    如何操作…

    设备屏幕将在屏幕中心显示一个红色矩形,如下所示:

    如何操作…

工作原理…

原生窗口管理的 NDK 接口在window.hrect.hnative_window_jni.hnative_window.h头文件中定义。前两个只是定义了一些常量和数据结构。native_window_jni.h定义了一个名为ANativeWindow_fromSurface的单一函数,它帮助我们从一个 Java 表面对象获取原生窗口。我们在第四章,使用 EGL 显示图形的食谱中说明了这个函数。这里,我们关注native_window.h中提供的函数。

执行以下步骤在手机屏幕上绘制一个正方形:

  1. 设置窗口缓冲区格式和大小:这是通过ANativeWindow_setBuffersGeometry函数完成的:

    int32_t ANativeWindow_setBuffersGeometry(ANativeWindow* window, int32_t width, int32_t height, int32_t format);
    

    此函数更新与输入参数 window 引用的原生窗口关联的原生窗口缓冲区。根据其余输入参数,窗口大小和格式会发生变化。在native_window.h中定义了三种格式,包括WINDOW_FORMAT_RGBA_8888WINDOW_FORMAT_RGBX_8888WINDOW_FORMAT_RGB_565。如果大小或格式设置为0,则将使用原生窗口的基本值。

  2. 锁定窗口下一次绘图表面:这是通过ANativeWindow_lock函数完成的:

    int32_t ANativeWindow_lock(ANativeWindow* window, ANativeWindow_Buffer* outBuffer,  ARect* inOutDirtyBounds);
    

    在此调用返回后,输入参数outBuffer将引用用于绘图的窗口缓冲区。

  3. 清除缓冲区:这是可选的。有时我们可能只想覆盖窗口缓冲区的一部分。在我们的示例中,我们调用了memset将所有数据设置为0

  4. 在缓冲区中绘制内容:在我们的示例中,我们首先计算矩形的开始和结束宽度与高度,然后将矩形区域的红色和 alpha 字节设置为255。这将显示一个红色矩形。

  5. 解锁窗口的绘图表面并向显示发布新缓冲区:这是通过ANativeWindow_unlockAndPost函数完成的:

    int32_t ANativeWindow_unlockAndPost(ANativeWindow* window);
    

在 Android NDK 中检测和处理输入事件

输入事件对于 Android 应用中的用户交互至关重要。这个食谱讨论了如何在 Android NDK 中检测和处理输入事件。

准备就绪

我们将进一步开发上一个食谱中的示例。在阅读这个食谱之前,请先阅读在 Android NDK 中管理本地窗口的食谱。

如何操作…

以下步骤将创建一个示例应用程序,该程序在本地代码中检测和处理输入事件:

  1. 创建一个名为NativeInputs的 Android 应用程序。将包名设置为cookbook.chapter5.nativeinputs。如果你需要更详细的说明,请参考第二章,Java Native Interface中的加载本地库和注册本地方法的食谱。

  2. 右键点击NativeInputs项目,选择Android Tools | 添加本地支持

  3. 更新AndroidManifest.xml。具体细节请参考上一个食谱或下载的代码。注意,元数据android.app.lib_name的值必须为NativeInputs

  4. jni文件夹下添加两个名为NativeInputs.cppmylog.h的文件。NativeInputs.cpp是基于上一个食谱修改的。让我们在这里看一部分它的代码:

    • handle_input_events:这是输入事件的事件处理方法。请注意,当检测到具有移动动作(AINPUT_EVENT_TYPE_MOTION)的移动事件时,我们会更新app->userData并将app->redrawNeeded设置为1

      int mPreviousX = -1;
      int32_t handle_input_events(struct android_app* app, AInputEvent* event) {
        int etype = AInputEvent_getType(event);
        switch (etype) {
        case AINPUT_EVENT_TYPE_KEY:
      … ...    
          break;
        case AINPUT_EVENT_TYPE_MOTION:
          int32_t action, posX, pointer_index;
          action = AMotionEvent_getAction(event);
          pointer_index = (action&AMOTION_EVENT_ACTION_POINTER_INDEX_MASK) >> AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT;
          posX = AMotionEvent_getX(event, pointer_index);
          if (action == AMOTION_EVENT_ACTION_MOVE) {
            int xMove = posX - mPreviousX;
            USERDATA* userData = (USERDATA*)app->userData;
            userData->xMove = xMove;
            app->redrawNeeded = 1;
          }
          mPreviousX = posX;
          break;
        }
      }
      
    • android_main:我们更新了 while true 循环。当设置app->redrawNeeded时,我们重新绘制矩形:

      void android_main(struct android_app* app) {
      … ...
      while (1) {
          int ident, events;
          struct android_poll_source* source;
          if ((ident=ALooper_pollOnce(app->redrawNeeded?0:-1, NULL, &events, (void**)&source)) >= 0) {
            if (NULL!=source) {
              source->process(app, source);
            }
            if (app->redrawNeeded) {
              drawSomething(app);
            }
          }
      }
      }
      
  5. jni文件夹下添加Android.mk文件,与上一个食谱类似。我们只需要将模块名称替换为NativeInputs,将源文件替换为NativeInputs.cpp

  6. 构建 Android 应用程序并在模拟器或设备上运行。我们可以移动屏幕上的图形来观察矩形水平移动:如何操作…

工作原理…

这个食谱讨论了在 Android NDK 中使用android_native_app_glue库处理输入事件。

android_native_app_glue 中的输入事件队列android_native_app_glue默认为我们附加了输入事件队列。

  1. 当为活动创建输入队列时,主线程会调用onInputQueueCreated回调,该回调将APP_CMD_INPUT_CHANGED写入我们在上一个食谱中描述的管道的写入端。后台线程将接收命令并调用AInputQueue_attachLooper函数,将输入队列附加到后台线程循环器。

  2. 当发生输入事件时,它将被process_input处理(在 while true 循环中我们调用的source->process函数指针指向process_input,如果事件是输入事件)。在process_input内部,首先调用AInputQueue_getEvent来获取事件。然后,调用AInputQueue_preDispatchEvent来发送预分派的键。这可能导致它在使用者提供的应用程序之前被当前的输入法编辑器IME)消耗掉。接下来是android_app->onInputEvent,这是一个指向由我们提供的事件处理器的函数指针。如果我们没有提供事件处理器,它会被设置为NULL。之后,调用AInputQueue_finishEvent来表示事件处理结束。

  3. 最后,当输入队列被销毁时,主线程会调用onInputQueueDestroyed回调,这也会写入APP_CMD_INPUT_CHANGED。后台线程将读取命令并调用名为AInputQueue_detachLooper的函数,以将输入队列从线程循环器中分离。

事件处理器:在handle_input_events函数中,我们首先调用了AInputEvent_getType来获取输入事件类型。android/input.h头文件定义了两种输入事件类型,即AINPUT_EVENT_TYPE_KEYAINPUT_EVENT_TYPE_MOTION。第一种事件类型表示输入事件是按键事件,而第二种表示它是动作事件。

我们调用了AKeyEvent_getActionAKeyEvent_getFlagsAKeyEvent_getKeyCode来获取按键事件的行为、标志和键码,并打印出描述它的字符串。另一方面,我们调用了AMotionEvent_getActionAMotionEvent_getX来获取动作事件的行为和x位置。注意,AMotionEvent_getX函数需要第二个输入参数作为指针索引。通过以下代码获取指针索引:

pointer_index = (action&AMOTION_EVENT_ACTION_POINTER_INDEX_MASK) >> AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT;

andoid/input.h中还有更多的输入事件函数。

在 Android NDK 中访问传感器

许多 Android 设备内置有传感器,用于检测和测量运动、方向和其他环境条件。在 Android NDK 中可以访问这些传感器。本食谱将详细讨论如何进行操作。

准备就绪

本食谱提供的示例基于前两个食谱中的示例代码。建议读者先阅读它们:

  • 在 Android NDK 中管理原生窗口

  • 在 Android NDK 中检测和处理输入事件

如何操作...

以下步骤开发了一个示例 Android 应用程序,演示了如何从 Android NDK 访问传感器:

  1. 创建一个名为nativesensors的 Android 应用程序。将包名设置为cookbook.chapter5.nativesensors。如果你需要更详细的说明,请参考第二章的加载原生库和注册原生方法食谱,Java Native Interface

  2. 右键点击nativesensors项目,选择Android Tools | Add Native Support

  3. 更新AndroidManifest.xml。具体细节请参考上一个配方或下载的代码。注意,元数据android.app.lib_name的值必须为nativesensors

  4. jni文件夹下添加两个名为nativesensors.cppmylog.h的文件。让我们展示一下nativesensors.cpp中的部分代码。

    • handle_activity_lifecycle_events:此函数处理活动生命周期事件。当活动处于焦点时,我们启用传感器,当活动失去焦点时禁用它。这通过避免在活动未处于焦点时读取传感器来节省电池寿命:

      void handle_activity_lifecycle_events(struct android_app* app, int32_t cmd) {
        USERDATA* userData;
        switch (cmd) {
      …...
        case APP_CMD_SAVE_STATE:
          // save current state
          userData = (USERDATA*)(app->userData);
          app->savedState = malloc(sizeof(SAVED_USERDATA));
          *((SAVED_USERDATA*)app->savedState) = userData->drawingData;
          app->savedStateSize = sizeof(SAVED_USERDATA);
          break;
        case APP_CMD_GAINED_FOCUS:
          userData = (USERDATA*)(app->userData);
          if (NULL != userData->accelerometerSensor) {
            ASensorEventQueue_enableSensor(userData->sensorEventQueue,
                userData->accelerometerSensor);
            ASensorEventQueue_setEventRate(userData->sensorEventQueue,
                userData->accelerometerSensor, (1000L/60)*1000);
          }
          break;
        case APP_CMD_LOST_FOCUS:
          USERDATA userData = *(USERDATA*) app->userData;
          if (NULL!=userData.accelerometerSensor) {      ASensorEventQueue_disableSensor(userData.sensorEventQueue, userData.accelerometerSensor);
          }
          break;
        }
      }
      
    • android_main:我们不断地轮询事件,并处理由LOOPER_ID_USER标识的传感器事件:

      void android_main(struct android_app* app) {
      … ...
      while (0==app->destroyRequested) {
        int ident, events;
        struct android_poll_source* source;
        if ((ident=ALooper_pollOnce(-1, NULL, &events, (void**)&source)) >= 0) {
          if (LOOPER_ID_USER == ident) {
          ASensorEvent event;
          while (ASensorEventQueue_getEvents(userData.sensorEventQueue,
              &event, 1) > 0) {
            int64_t currentTime = get_time();
            … ...
            if ((currentTime - lastTime) > TIME_THRESHOLD) {
              long diff = currentTime - lastTime;
              float speedX = (event.acceleration.x - lastX)/diff*10000;
              float speedY = (event.acceleration.y - lastY)/diff*10000;
              float speedZ = (event.acceleration.z - lastZ)/diff*10000;
              float speed = fabs(speedX + speedY + speedZ);
      …...        
            }
          }
          }
        }
      }
      
      ASensorManager_destroyEventQueue(userData.sensorManager, userData.sensorEventQueue);
      }
      
      
  5. jni文件夹下添加Android.mk文件,该文件与上一个配方中使用的类似。我们只需要将模块名称替换为nativesensors,并将源文件替换为nativesensors.cpp

  6. 构建 Android 应用程序并在模拟器或设备上运行。我们可以摇动设备来观察矩形水平移动:如何操作…

工作原理…

在我们的示例中,我们使用了加速度传感器来检测手机摇动。然后,根据手机摇动的速度,我们将红色矩形移动到手机屏幕的一侧。一旦矩形到达手机屏幕的边缘,它就会开始移动到另一边缘。

示例代码提供了一个简单的算法来确定是否发生了摇动。存在更复杂和准确的算法,并且可以实施。我们还可以调整SHAKE_TIMEOUTSHAKE_COUNT_THRESHOLD常数以微调算法。

示例的重要部分是如何访问传感器。让我们总结一下步骤:

  1. 获取传感器管理器的引用:这是通过使用以下函数完成的:

    ASensorManager* ASensorManager_getInstance();
    
  2. 获取给定类型的默认传感器:我们还可以获取所有可用传感器的列表。这是通过分别使用以下两个函数完成的:

    ASensor const* ASensorManager_getDefaultSensor(ASensorManager* manager, int type);
    int ASensorManager_getSensorList(ASensorManager* manager, ASensorList* list);
    

    可用类型在android/sensor.h中定义。在我们的示例中,我们打印出所有传感器名称和类型,但只使用ASENSOR_TYPE_ACCELEROMETER

  3. 创建一个新的传感器队列并将其附加到线程的 looper 上:这是通过使用以下ASensorManager_createEventQueue函数完成的:

    ASensorEventQueue* ASensorManager_createEventQueue(ASensorManager* manager, ALooper* looper, int ident, ALooper_callbackFunc callback, void* data);
    

    此函数的使用与使用 Android 原生应用胶水创建原生活动配方中的ALooper_addFd函数的使用类似,以及在 Android NDK 中检测和处理输入事件配方中的AInputQueue_attachLooper。在我们的示例中,我们将ident设置为LOOPER_ID_USER。请注意,我们还可以通过更改android_native_app_glue.h的代码并在此处定义新的 looper ID。

  4. 启用并配置传感器

    int ASensorEventQueue_enableSensor(ASensorEventQueue* queue, ASensor const* sensor);
    int ASensorEventQueue_setEventRate(ASensorEventQueue* queue, ASensor const* sensor, int32_t usec);
    

    第一个函数启用由传感器输入参数引用的传感器。第二个函数为引用由传感器输入参数的传感器设置事件传递率,以微秒为单位。在我们的示例中,当活动获得焦点时调用了这两个函数。

  5. 轮询事件并从队列中获取可用事件:如前一个菜谱所示,通过调用ALooper_pollOnce进行轮询。如果返回的事件标识符是LOOPER_ID_USER,我们知道这是一个传感器事件,我们可以使用以下函数来获取它:

    ssize_t ASensorEventQueue_getEvents(ASensorEventQueue* queue, ASensorEvent* events, size_t count);
    

    count表示我们想要获取的最大可用事件数。在我们的示例中,我们将其设置为1。也可以定义一个ASensorEvent数组,一次性获取多个事件。

  6. 处理传感器事件:传感器事件由ASensorEvent数据结构表示,可以在android/sensor.h中找到(该文件的确切路径为<Android NDK 根目录>/platforms/android-<版本>/arch-arm/usr/include/android/sensor.h)。在我们的示例中,我们访问了 x、y、z 轴上的加速度读数,并使用这些读数来确定是否发生了手机摇晃。

  7. 禁用传感器:访问完传感器后,你可以使用以下函数禁用它:

    int ASensorEventQueue_disableSensor(ASensorEventQueue* queue, ASensor const* sensor);
    
  8. 销毁传感器事件队列并释放与其相关的所有资源

    int ASensorManager_destroyEventQueue(ASensorManager* manager, ASensorEventQueue* queue);
    

在 Android NDK 中管理资源

资源为 Android 应用提供了一种包含各种类型文件的方式,包括文本、图像、音频、视频等。本菜谱讨论了如何从 Android NDK 加载资源文件。

准备就绪

我们将修改在第四章的在 OpenGL ES 1.x 中映射纹理菜谱中开发的示例,Android NDK OpenGL ES API。建议读者阅读该菜谱或先查看代码。

如何操作…

以下步骤描述了如何开发示例应用程序:

  1. 创建一个名为NativeAssets的 Android 应用程序。将包名设置为cookbook.chapter5.nativeassets。如果你需要更详细的说明,请参考第二章的加载本地库和注册本地方法菜谱,Java Native Interface

  2. NativeAssets项目上右键点击,选择Android Tools | Add Native Support

  3. cookbook.chapter5.nativeassets包下添加三个 Java 文件,分别为MyActivity.javaMySurfaceView.javaMyRenderer.java。前两个文件与第四章中的在 OpenGL ES 1.x 中映射纹理菜谱中的对应文件完全相同,Android NDK OpenGL ES API。最后一个文件略有改动,其中naLoadTexture本地方法签名更新如下:

    private static native void naLoadTexture(AssetManager pAssetManager);
    

    onSurfaceCreated方法中,我们通过传递一个 Java AssetManager实例来调用本地方法:

    naLoadTexture(mContext.getAssets());
    
  4. jni文件夹下创建两个文件夹,分别为dicelibpng-1.5.12。在libpng-1.5.12文件夹中,我们放置了 libpng 的源文件,可以从sourceforge.net/projects/libpng/files/下载。

    dice文件夹中,我们添加了Cube.cppCube.hmylog.hDiceG1.cpp文件。前三个文件与第四章中在 OpenGL ES 1.x 中映射纹理的示例相同,Android NDK OpenGL ES APIDiceG1.cpp文件通过添加从assets文件夹读取.png资产文件的程序进行了更新。下面是更新代码的一部分:

    • readPng:这是在png_set_read_fn中使用的回调函数。它从asset文件中读取数据:

      void readPng(png_structp pPngPtr, png_bytep pBuf, png_size_t pCount) {
        AAsset* assetF = (AAsset*)png_get_io_ptr(pPngPtr);
        AAsset_read(assetF, pBuf, pCount);
      }
      
    • naLoadTexture:它读取assets顶级目录下的所有.png文件,并将数据加载到 OpenGL 中进行纹理映射:

      void naLoadTexture(JNIEnv* env, jclass clazz, jobject pAssetManager) {
        AAssetManager* assetManager = AAssetManager_fromJava(env, pAssetManager);
        AAssetDir* texDir = AAssetManager_openDir(assetManager, "");
        const char* texFn;
        int pId = 0;
        while (NULL != (texFn = AAssetDir_getNextFileName(texDir))) {
          AAsset* assetF = AAssetManager_open(assetManager, texFn, AASSET_MODE_UNKNOWN);
          //read the png header
          png_byte header[8];
         png_byte *imageData;
          …...
          if (8 != AAsset_read(assetF, header, 8)) {
            goto FEND;
          }
          …...
          //init png reading by setting a read callback
          png_set_read_fn(pngPtr, assetF, readPng);
          …...
          // Loads image data into OpenGL.
          glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, type, imageData);
      FEND:
          AAsset_close(assetF);
          pId++;
        }
        AAssetDir_close(texDir);
      }
      
  5. jnijni/dicejni/libpng-1.5.12下分别添加一个Android.mk文件。jni顶级文件夹下的Android.mk文件如下。这仅指示 Android 构建系统包含jni文件夹下每个子目录中的Android.mk文件:

    LOCAL_PATH := $(call my-dir)
    include $(call all-subdir-makefiles)
    

    jni/libpng-1.5.12文件夹下的Android.mk文件如下。这将编译libpng作为本地静态库:

    LOCAL_PATH := $(call my-dir)
    include $(CLEAR_VARS)
    LOCAL_CFLAGS := 
    LOCAL_MODULE    := libpng
    LOCAL_SRC_FILES :=\
      png.c \
      pngerror.c \
      pngget.c \
      pngmem.c \
      pngpread.c \
      pngread.c \
      pngrio.c \
      pngrtran.c \
      pngrutil.c \
      pngset.c \
      pngtrans.c \
      pngwio.c \
      pngwrite.c \
      pngwtran.c \
      pngwutil.c 
    LOCAL_LDLIBS := -lz
    include $(BUILD_STATIC_LIBRARY)
    

    jni/dice文件夹下的Android.mk文件如下:

    LOCAL_PATH := $(call my-dir)
    include $(CLEAR_VARS)
    LOCAL_MODULE    := DiceG1NativeAssets
    LOCAL_C_INCLUDES := $(LOCAL_PATH)/../libpng-1.5.12/
    LOCAL_STATIC_LIBRARIES := libpng
    LOCAL_SRC_FILES := Cube.cpp DiceG1.cpp
    LOCAL_LDLIBS := -lGLESv1_CM -llog -landroid -lz
    include $(BUILD_SHARED_LIBRARY)
    
  6. 构建 Android NDK 应用程序并在 Android 设备上运行。该应用程序将显示一个作为骰子的纹理立方体;这与我们在第四章中Android NDK OpenGL ES API看到的相同。如何操作…

工作原理…

在示例中,我们从assets文件夹加载了.png文件,并将其用作 OpenGL 纹理。你可以使用以下步骤读取assets

  1. 从 Java AssetManager 对象获取本地 AAssetManager 对象:这是通过AAssetManager_fromJava函数完成的,该函数在asset_manager_jni.h中定义。

  2. 打开资产目录:这是通过 AAssetManager_openDir 完成的。

    AAssetDir* AAssetManager_openDir(AAssetManager* mgr, const char* dirName);
    

    要打开顶级目录"assets",我们将 dirName 设置为""。对于子目录,我们需要提供目录名称。

  3. 获取资产文件名

    const char* AAssetDir_getNextFileName(AAssetDir* assetDir);
    

    遍历输入参数assetDir所引用的asset目录下的文件。如果所有文件都已返回或没有文件,则返回NULL

  4. 打开资产文件:这是通过使用AAssetManager_open完成的:

    AAsset* AAssetManager_open(AAssetManager* mgr, const char* filename, int mode);
    

    文件名应设置为asset文件名,其中mode可以是以下之一:

    • AASSET_MODE_UNKNOWN:不知道数据将如何被访问

    • AASSET_MODE_RANDOM:读取块,并向前和向后查找

    • AASSET_MODE_STREAMING:顺序读取,偶尔向前查找

    • AASSET_MODE_BUFFER:尝试将内容加载到内存中,以便快速小读取

  5. 读取资产文件:这是通过使用AAsset_read完成的。

    int AAsset_read(AAsset* asset, void* buf, size_t count);
    

    输入参数 buf 指的是读取后数据放置的位置,而 count 表示我们想要读取的字节数。实际读取的字节数将被返回,可能与 count 不一致。

  6. 关闭资产文件:这是通过使用 AAsset_close 函数完成的。

  7. 关闭资产目录:这是通过使用 AAssetDir_close 函数完成的。

还有更多内容…

在这个例子中,我们将 libpng 作为本地静态库构建。这是读取 .png 文件所必需的,因为 Android NDK 没有提供访问 .png 文件的 API。我们将在第八章,移植和使用现有库中讨论如何利用现有库开发 Android NDK 应用程序。

第六章:Android NDK 多线程

本章将涵盖以下内容:

  • 在 Android NDK 中创建和终止本地线程

  • 在 Android NDK 中使用互斥锁同步本地线程

  • 在 Android NDK 中使用条件变量同步本地线程

  • 在 Android NDK 中使用读写锁同步本地线程

  • 在 Android NDK 中用信号量同步本地线程

  • 在 Android NDK 中调度本地线程

  • 在 Android NDK 中为本地线程管理数据

简介

大多数非琐碎的 Android 应用都使用不止一个线程,因此多线程编程对 Android 开发至关重要。在 Android NDK 中,POSIX 线程pthreads)被包含在 Android 的 Bionic C 库中,以支持多线程。本章主要讨论pthread.hsemaphore.h头文件中定义的 API 函数,这些文件可以在 Android NDK 的platforms/android-<API level>/arch-arm/usr/include/目录下找到。

我们将首先介绍线程的创建和终止。在所有多线程应用程序中,同步非常重要,因此我们讨论了 Android NDK 中四种常用的同步技术,包括互斥锁、条件变量、读写锁和信号量。然后我们说明了线程调度,最后描述了如何为线程管理数据。

作为一本实用书籍,我们将不涉及多线程编程背后的理论。读者需要了解多线程的基础知识,包括并发、互斥、死锁等。

此外,pthreads 编程是一个复杂的话题,有专门针对 pthreads 编程的书籍。本章将仅关注 Android NDK 编程环境下的基本内容。感兴趣的读者可以参考Bradford NicolsDick ButtlarJacqueline Proulx Farrell所著的《Pthreads Programming: A POSIX Standard for Better Multiprocessing》以获取更多信息。

在 Android NDK 中创建和终止本地线程

本食谱讨论如何在 Android NDK 中创建和终止本地线程。

准备就绪…

读者需要知道如何创建一个 Android NDK 项目。我们可以参考第一章中的编写 Hello NDK 程序食谱,详细说明操作步骤。

如何操作...

以下步骤描述了如何创建一个具有多个本地线程的简单 Android 应用程序:

  1. 创建一个名为NativeThreadsCreation的 Android 应用程序。将包名设置为cookbook.chapter6.nativethreadscreation。更多详细说明请参考第二章中的加载本地库和注册本地方法食谱。

  2. 右键点击项目NativeThreadsCreation,选择Android Tools | Add Native Support

  3. cookbook.chapter6.nativethreadscreation 包下添加一个名为 MainActivity.java 的 Java 文件。这个 Java 文件简单加载原生库 NativeThreadsCreation 并调用原生的 jni_start_threads 方法。

  4. jni 文件夹下添加 mylog.hNativeThreadsCreation.cpp 文件。mylog.h 文件包含了安卓原生 logcat 实用功能函数,而 NativeThreadsCreation.cpp 文件包含了启动多线程的原生代码。部分代码如下所示。

    jni_start_threads 函数启动两个线程并等待这两个线程结束:

    void jni_start_threads() {
      pthread_t th1, th2;
      int threadNum1 = 1, threadNum2 = 2;
      int ret;
      ret = pthread_create(&th1, NULL, run_by_thread, (void*)&threadNum1);
      ret = pthread_create(&th2, NULL, run_by_thread, (void*)&threadNum2);
      void *status;
      ret = pthread_join(th1, &status);
      int* st = (int*)status;
      LOGI(1, "thread 1 end %d %d", ret, *st);
      ret = pthread_join(th2, &status);
      st = (int*)status;
      LOGI(1, "thread 2 end %d %d", ret, *st);
    }
    

    run_by_thread 函数被原生线程执行:

    int retStatus;
    void *run_by_thread(void *arg) {
      int cnt = 3, i;
      int* threadNum = (int*)arg;
      for (i = 0; i < cnt; ++i) {
        sleep(1);
        LOGI(1, "thread %d: %d", *threadNum, i);
      }
      if (1 == *threadNum) {
        retStatus = 100;
        return (void*)&retStatus;
      } else if (2 == *threadNum) {
        retStatus = 200;
        pthread_exit((void*)&retStatus);
      }
    }
    
  5. jni 文件夹中添加一个 Android.mk 文件,并包含以下代码:

    LOCAL_PATH := $(call my-dir)
    include $(CLEAR_VARS)
    LOCAL_MODULE := NativeThreadsCreation
    LOCAL_SRC_FILES := NativeThreadsCreation.cpp
    LOCAL_LDLIBS := -llog
    include $(BUILD_SHARED_LIBRARY)
    
  6. 构建并运行安卓项目,并使用以下命令监控 logcat 输出:

    $ adb logcat -v time NativeThreadsCreation:I *:S
    

    以下是 logcat 输出的截图:

    如何操作...

工作原理...

本教程展示了如何在安卓 NDK 中创建和终止线程。

使用 pthreads 构建

传统上,pthread 被实现为一个外部库,必须通过提供链接器标志 -lpthread 来链接。安卓的 Bionic C 库有自己的 pthread 实现。因此,在我们的项目中,Android.mk 文件不使用 -lpthread

线程创建

正如我们的代码所示,可以使用 pthread_create 函数创建一个线程,该函数具有以下原型:

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg);

这个函数使用 attr 输入参数指定的属性创建并启动一个新线程。如果 attr 设置为 NULL,则使用默认属性。start_routine 参数指向新创建的线程要执行的函数,arg 作为函数的输入参数。当函数返回时,thread 输入参数将指向存储线程 ID 的位置,返回值为零表示成功,或其他值表示错误。

在我们的示例代码中,我们创建了两个线程来执行 run_by_thread 函数。我们传递一个整数指针作为输入参数给 run_by_thread 函数。

线程终止

线程在从 start_routine 函数返回后终止,或者我们显式调用 pthread_exitpthread_exit 函数具有以下原型:

void pthread_exit(void *value_ptr);

这个函数终止调用线程,并将 value_ptr 指向的值返回给与调用线程成功 join 的任何线程。这也在我们的示例代码中进行了演示。我们对创建的两个线程调用了 pthread_joinpthread_join 函数具有以下原型:

int pthread_join(pthread_t thread, void **value_ptr);

该函数挂起调用线程的执行,直到由第一个输入参数指定的线程终止。当函数成功返回时,第二个参数可用于获取已终止线程的退出状态,正如我们的示例代码所示。

此外,我们之前看到的logcat截图显示,从线程中返回相当于调用pthread_exit。因此,无论调用哪种方法,我们都可以获取退出状态。

注意

Android Bionic C 库 pthread 不支持pthread_cancel。因此,如果我们正在移植使用pthread_cancel的代码,我们需要重构代码以摆脱它。

在 Android NDK 中用互斥锁同步本地线程

本食谱讨论如何在 Android NDK 中使用 pthread 互斥锁。

如何操作...

以下步骤帮助创建一个演示 pthread 互斥锁使用的 Android 项目:

  1. 创建一个名为NativeThreadsMutex的 Android 应用程序。将包名设置为cookbook.chapter6.nativethreadsmutex。更多详细说明请参考第二章中的加载本地库和注册本地方法食谱,Java Native Interface

  2. 右键点击项目NativeThreadsMutex,选择Android Tools | 添加本地支持

  3. cookbook.chapter6.nativethreadsmutex包下添加一个名为MainActivity.java的 Java 文件。这个 Java 文件只是加载本地NativeThreadsMutex库并调用本地jni_start_threads方法。

  4. jni文件夹中添加两个名为mylog.hNativeThreadsMutex.cpp的文件。NativeThreadsMutex.cpp包含启动两个线程的代码。这两个线程将更新一个共享计数器。部分代码如下所示:

    run_by_thread1函数由第一个本地线程执行:

    int cnt = 0;
    int THR = 10;
    void *run_by_thread1(void *arg) {
      int* threadNum = (int*)arg;
      while (cnt < THR) {
        pthread_mutex_lock(&mux1);
        while ( pthread_mutex_trylock(&mux2) ) {
          pthread_mutex_unlock(&mux1);  //avoid deadlock
          usleep(50000);  //if failed to get mux2, release mux1 first
          pthread_mutex_lock(&mux1);
        }
        ++cnt;
        LOGI(1, "thread %d: cnt = %d", *threadNum, cnt);
        pthread_mutex_unlock(&mux1);
        pthread_mutex_unlock(&mux2);
        sleep(1);
      }
    }
    

    run_by_thread2函数由第二个本地线程执行:

    void *run_by_thread2(void *arg) {
      int* threadNum = (int*)arg;
      while (cnt < THR) {
        pthread_mutex_lock(&mux2);
        while ( pthread_mutex_trylock(&mux1) ) {
          pthread_mutex_unlock(&mux2);  //avoid deadlock
          usleep(50000);   //if failed to get mux2, release mux1 first
          pthread_mutex_lock(&mux2);
        }
        ++cnt;
        LOGI(1, "thread %d: cnt = %d", *threadNum, cnt);
        pthread_mutex_unlock(&mux2);
        pthread_mutex_unlock(&mux1);
        sleep(1);
      }
    }
    
  5. jni文件夹中添加一个Android.mk文件,内容如下:

    LOCAL_PATH := $(call my-dir)
    include $(CLEAR_VARS)
    LOCAL_MODULE := NativeThreadsMutex
    LOCAL_SRC_FILES := NativeThreadsMutex.cpp
    LOCAL_LDLIBS := -llog
    include $(BUILD_SHARED_LIBRARY)
    
  6. 构建并运行 Android 项目,并使用以下命令监控logcat输出。

    $ adb logcat -v time NativeThreadsMutex:I *:S
    

    logcat输出如下所示:

    如何操作...

  7. 我们还在NativeThreadsMutex.cpp中实现了一个本地方法jni_start_threads_dead,这可能会导致死锁(可能需要运行几次代码才能产生死锁情况)。如果在MainActivity.java中调用jni_start_threads_dead,两个线程将会启动然后如以下logcat输出所示阻塞:如何操作...

    如此截图所示,两个线程在启动后无法继续执行。

工作原理...

示例项目演示了如何使用互斥锁来同步本地线程。以下是详细信息:

初始化和销毁互斥锁

可以使用pthread_mutex_init函数初始化互斥锁,该函数具有以下原型:

int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);

输入参数 mutex 是要初始化的互斥锁的指针,attr表示互斥锁的属性。如果attr设置为NULL,将使用默认属性。如果互斥锁初始化成功,该函数将返回零,否则返回非零值。

注意

pthread.h中定义了一个宏PTHREAD_MUTEX_INITIALIZER,用于使用默认属性初始化互斥量。

当我们完成互斥量的使用后,可以通过pthread_mutex_destroy函数销毁它,该函数具有以下原型:

int pthread_mutex_destroy(pthread_mutex_t *mutex);

输入参数是指向要销毁的互斥量的指针。

在我们的示例项目中,我们创建了两个互斥量mux1mux2,以同步两个线程对共享计数器cnt的访问。两个线程退出后,我们销毁了这些互斥量。

使用互斥量

以下是可用于锁定和解锁互斥量的四个函数:

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_lock_timeout_np(pthread_mutex_t *mutex, unsigned msecs);

在所有四个函数中,输入参数指的是正在使用的mutex对象。零返回值表示互斥量锁定或解锁成功。最后一个函数允许我们指定以毫秒为单位的等待超时。如果超时后无法获取互斥量,它将返回EBUSY表示失败。

注意

pthread_mutex_timedlock函数在一些 pthread 实现中被定义,允许我们指定一个超时值。然而,这个函数在 Android Bionic C 库中是不可用的。

我们在之前的示例中演示了这些函数的使用。在run_by_thread1函数中,我们首先通过pthread_mutex_lock锁定mux1,然后通过pthread_mutex_trylock尝试锁定mux2。如果无法锁定mux2,我们就解锁mux1,休眠 50 毫秒,然后再次尝试。如果可以锁定mux2,我们就更新共享计数器cnt,记录其当前值,然后释放两个互斥量。另一个函数run_by_thread2run_by_thread1类似,不同之处在于它首先锁定mux2,然后锁定mux1。这两个函数由两个线程执行。这可以通过以下图示说明:

使用互斥量

如前面的图所示,线程 1 需要获取mux1,然后是mux2以更新cnt,而线程 2 需要获取mux2,然后是mux1以更新cnt。如果线程 1 锁定了mux1而线程 2 锁定了mux2,那么这两个线程都无法继续。这对应于pthread_mutex_trylock返回非零值的情况。如果发生这种情况,一个线程将放弃其互斥量,以便另一个线程可以继续更新共享计数器cnt并释放两个互斥量。请注意,我们可以在代码中将pthread_mutex_trylock替换为pthread_mutex_lock_timeout_np。鼓励读者亲自尝试。

我们还实现了一个本地方法jni_start_threads_dead,它很可能会造成死锁。线程设置与上一个案例相似,但我们使用pthread_mutex_lock而不是pthread_mutex_trylock,并且线程不会放弃它们已经锁定的互斥量。这可以如下所示图示:

使用互斥量

线程 1 尝试锁定mux1然后是mux2,而线程 2 尝试锁定mux2然后是mux1。如果线程 1 锁定了mux1而线程 2 锁定了mux2,那么这两个线程都无法继续。因为它们不会放弃已经获得的互斥锁,这两个线程将被永久阻塞。这被称为死锁。

还有更多...

回想一下,函数pthread_mutex_init的第二个输入参数是指向pthread_mutexattr_t的指针。pthread.h中定义了一些函数来初始化、操作和销毁互斥属性,包括:

  • pthread_mutexattr_init

  • pthread_mutexattr_destroy

  • pthread_mutexattr_gettype

  • pthread_mutexattr_settype

  • pthread_mutexattr_setpshared

  • pthread_mutexattr_getpshared

感兴趣的读者可以查看pthread.h头文件以获取更多信息。

在 Android NDK 中使用条件变量同步本地线程

前一个食谱讨论了如何使用互斥锁来同步线程。这个食谱描述了如何使用条件变量。

如何操作...

以下步骤将帮助我们创建一个展示 pthread 条件变量使用的 Android 项目:

  1. 创建一个名为NativeThreadsCondVar的 Android 应用程序。将包名设置为cookbook.chapter6.nativethreadscondvar。有关更详细的说明,请参考第二章中的加载本地库和注册本地方法食谱,Java Native Interface

  2. 右键点击项目NativeThreadsCondVar,选择Android Tools | Add Native Support

  3. cookbook.chapter6.nativethreadscondvar包下添加一个名为MainActivity.java的 Java 文件。这个 Java 文件简单加载了本地库NativeThreadsCondVar并调用了本地方法jni_start_threads

  4. jni文件夹下添加两个名为mylog.hNativeThreadsCondVar.cpp的文件。NativeThreadsCondVar.cpp包含了启动两个线程的代码。这两个线程将更新一个共享计数器。部分代码如下所示:

    jni_start_threads函数初始化互斥锁、条件变量并创建两个线程:

    pthread_mutex_t mux;
    pthread_cond_t cond;
    void jni_start_threads() {
      pthread_t th1, th2;
      int threadNum1 = 1, threadNum2 = 2;
      int ret;
      pthread_mutex_init(&mux, NULL);
      pthread_cond_init(&cond, NULL);
      ret = pthread_create(&th1, NULL, run_by_thread1, 
    void*)&threadNum1);
      LOGI(1, "thread 1 started");
      ret = pthread_create(&th2, NULL, run_by_thread2, 
    void*)&threadNum2);
      LOGI(1, "thread 2 started");
      ret = pthread_join(th1, NULL);
      LOGI(1, "thread 1 end %d", ret);
      ret = pthread_join(th2, NULL);
      LOGI(1, "thread 2 end %d", ret);
      pthread_mutex_destroy(&mux);
      pthread_cond_destroy(&cond);
    }
    

    run_by_thread1函数由第一个本地线程执行:

    int cnt = 0;
    int THR = 10, THR2 = 5;
    void *run_by_thread1(void *arg) {
      int* threadNum = (int*)arg;
      pthread_mutex_lock(&mux);
      while (cnt != THR2) {
          LOGI(1, "thread %d: about to wait", *threadNum);
          pthread_cond_wait(&cond, &mux);
      }
      ++cnt;
      LOGI(1, "thread %d: cnt = %d", *threadNum, cnt);
      pthread_mutex_unlock(&mux);
    }
    

    run_by_thread2函数由第二个本地线程执行:

    void *run_by_thread2(void *arg) {
      int* threadNum = (int*)arg;
      while (cnt < THR) {
        pthread_mutex_lock(&mux);
        if (cnt == THR2) {
          pthread_cond_signal(&cond);
        } else {
          ++cnt;
          LOGI(1, "thread %d: cnt = %d", *threadNum, cnt);
        }
        pthread_mutex_unlock(&mux);
        sleep(1);
      }
    }
    
  5. jni文件夹下添加一个名为Android.mk的文件,内容如下:

    LOCAL_PATH := $(call my-dir)
    include $(CLEAR_VARS)
    LOCAL_MODULE    := NativeThreadsCondVar
    LOCAL_SRC_FILES := NativeThreadsCondVar.cpp
    LOCAL_LDLIBS    := -llog
    include $(BUILD_SHARED_LIBRARY)
    
  6. 构建并运行 Android 项目,使用以下命令监控logcat输出:

    $ adb logcat -v time NativeThreadsCondVar:I *:S
    

    logcat输出如下所示:

    如何操作...

工作原理...

当互斥锁控制线程间共享数据的访问时,条件变量允许线程根据数据的实际值进行同步。典型用例是一个线程等待条件被满足。没有条件变量,线程需要不断地检查条件(通常称为轮询)。条件变量允许我们在不消耗资源的轮询情况下处理这种情况。

初始化和销毁条件变量

pthread_cond_init函数用于初始化条件变量。它具有以下原型:

int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);

该函数使用attr参数引用的属性初始化cond输入参数指向的条件变量。如果attr设置为NULL,则使用默认属性。

类似于互斥量,pthread.h中定义了一个宏PTHREAD_COND_INITIALIZER,用于使用默认属性初始化条件变量。

完成条件变量的使用后,我们可以通过调用pthread_cond_destroy来销毁它,该函数具有以下原型:

int pthread_cond_destroy(pthread_cond_t *cond);

在我们的示例代码中,我们调用了这两个函数来初始化和销毁名为cond的条件变量。

使用条件变量:

以下三个函数通常用于操作条件变量:

int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);

所有这三个函数接受正在使用的条件变量的指针。第一个函数还接受关联互斥量的指针作为第二个参数。请注意,条件变量必须与关联的互斥量一起使用。

第一个函数应在关联互斥量锁定后被调用;否则该函数行为是未定义的。它会使得调用线程在条件变量上阻塞。此外,关联的互斥量会自动且原子性地解锁,以便另一个线程可以使用它。

第二个和第三个函数用于解除之前在条件变量上阻塞的线程。pthread_cond_broadcast将解除在cond指向的条件变量上阻塞的所有线程,而pthread_cond_signal将至少解除在cond上阻塞的一个线程。如果没有任何线程在由cond指定的条件变量上阻塞,这两个函数不起作用。如果有多个线程需要解除阻塞,其顺序取决于调度策略,这将在本章后面的在 Android NDK 中调度本地线程的菜谱中讨论。

这些函数的使用在我们的示例代码中有演示。在run_by_thread1函数中,线程一将锁定关联的互斥量,然后在条件变量cond上等待。这将导致线程一释放互斥量mux。在run_by_thread2函数中,线程二将获取mux并增加共享计数器cnt

cnt增加到五时,线程二调用pthread_cond_signal来解除线程一的阻塞并释放mux。线程一会自动且原子性地锁定mux(注意,在唤醒时不需要pthread_mutex_lock调用),然后将cnt从五增加到六,并最终退出。线程二将继续增加cnt的值到 10 并退出。这就解释了前面的截图。

注意

我们将 pthread_cond_wait(&cond, &mux) 函数放入一个 while 循环中以处理虚假唤醒。虚假唤醒是指即使没有线程信号条件,线程也会被唤醒的情况。建议我们总是在 pthread_cond_wait 返回时检查条件。你可以参考 pubs.opengroup.org/onlinepubs/7908799/xsh/pthread_cond_wait.html 了解更多信息。

还有更多...

示例项目演示了条件变量如何用于本地线程同步。我们将在下一节中详细介绍。

条件变量属性函数

在我们的示例代码中,通过将 pthread_cond_init 的第二个参数指定为 NULL 来使用默认属性创建条件变量。pthread.h 定义了一些函数来初始化和操作条件变量属性。这些函数包括 pthread_condattr_initpthread_condattr_getpsharedpthread_condattr_setpsharedpthread_condattr_destroy。我们不讨论这些函数,因为它们不常使用。感兴趣的读者可以参考位于 platforms/android-<API level>/arch-arm/usr/include/pthread.h 头文件以获取更多信息。

定时条件变量函数

pthread.h 还定义了一些允许我们为条件变量的等待指定超时值的函数。它们如下所示:

int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t * mutex, const struct timespec *abstime);
int pthread_cond_timedwait_monotonic_np(pthread_cond_t *cond, pthread_mutex_t        *mutex, const struct timespec  *abstime);
int pthread_cond_timedwait_relative_np(pthread_cond_t *cond, pthread_mutex_t        *mutex, const struct timespec  *reltime);
int pthread_cond_timeout_np(pthread_cond_t *cond, pthread_mutex_t * mutex, unsigned msecs);

前两个函数 pthread_cond_timedwaitpthread_cond_timedwait_monotonic_np 允许我们指定一个绝对时间值。当系统时间等于或超过指定时间时,将返回超时错误。这两个函数之间的区别在于,第一个函数使用的是挂钟时间,而第二个函数使用的是 CLOCK_MONOTONIC 时钟。系统挂钟时间可以向前或向后跳跃(例如,配置为使用网络时间协议的机器的挂钟时间可能在时钟同步时发生变化),而 CLOCK_MONOTONIC 是自过去某个固定点以来经过的绝对时间,它不能突然改变。

注意

Android pthread.h 还定义了一个函数 pthread_cond_timedwait_monotonic,该函数已被弃用。它在功能上等同于 pthread_cond_timedwait_monotonic_np。我们应该始终使用 pthread_cond_timedwait_monotonic_np

最后两个函数 pthread_cond_timedwait_relative_nppthread_cond_timeout_np 允许我们指定一个相对于当前时间的相对超时值。不同之处在于,一个函数将超时值指定为 timespec 结构,而另一个则指定为毫秒数。

注意

本菜谱中介绍的一些方法以np结尾,表示“不可移植”。这意味着这些函数可能不会在其他 pthread 库中实现。如果我们设计的程序也想在其他非 Android 平台上工作,我们应该避免使用这些函数。

在 Android NDK 中使用读写锁同步本地线程

前两个菜谱涵盖了使用互斥量和条件变量进行线程同步。本菜谱讨论了 Android NDK 中的读写锁。

准备工作...

建议读者在阅读本节之前先阅读前两个菜谱,在 Android NDK 中使用互斥量同步本地线程在 Android NDK 中使用条件变量同步本地线程

如何操作...

以下步骤将帮助你创建一个展示 pthread 读写锁用法的 Android 项目:

  1. 创建一个名为NativeThreadsRWLock的 Android 应用。设置包名为cookbook.chapter6.nativethreadsrwlock。关于更详细的说明,请参考第二章中的加载本地库和注册本地方法部分,Java Native Interface

  2. 右键点击项目NativeThreadsRWLock,选择Android Tools | Add Native Support

  3. cookbook.chapter6.nativethreadsrwlock包下添加一个名为MainActivity.java的 Java 文件。这个 Java 文件简单加载了本地库NativeThreadsRWLock并调用了本地方法jni_start_threads

  4. jni文件夹下添加两个名为mylog.hNativeThreadsRWLock.cpp的文件。NativeThreadsRWLock.cpp中的一部分代码如下所示:

    jni_start_threads启动pNumOfReader个读线程和pNumOfWriter个写线程:

    void jni_start_threads(JNIEnv *pEnv, jobject pObj, int pNumOfReader, int pNumOfWriter) {
      pthread_t *ths;
      int i, ret;
      int *thNum;
      ths = (pthread_t*)malloc(sizeof(pthread_t)*(pNumOfReader+pNumOfWriter));
      thNum = (int*)malloc(sizeof(int)*(pNumOfReader+pNumOfWriter));
      pthread_rwlock_init(&rwlock, NULL);
      for (i = 0; i < pNumOfReader + pNumOfWriter; ++i) {
        thNum[i] = i;
        if (i < pNumOfReader) {
          ret = pthread_create(&ths[i], NULL, run_by_read_thread, (void*)&(thNum[i]));
        } else {
          ret = pthread_create(&ths[i], NULL, run_by_write_thread, (void*)&(thNum[i]));
        }
      }
      for (i = 0; i < pNumOfReader+pNumOfWriter; ++i) {
        ret = pthread_join(ths[i], NULL);
      }
      pthread_rwlock_destroy(&rwlock);
      free(thNum);
      free(ths);
    }
    

    run_by_read_thread函数由读线程执行:

    void *run_by_read_thread(void *arg) {
      int* threadNum = (int*)arg;
      int ifRun = 1;
      int accessTimes = 0;
      int ifPrint = 1;
      while (ifRun) {
        if (!pthread_rwlock_rdlock(&rwlock)) {
          if (100000*numOfWriter == sharedCnt) {
            ifRun = 0;
          }
          if (0 <= sharedCnt && ifPrint) {
            LOGI(1, "reader thread %d sharedCnt value before processing %d\n", *threadNum, sharedCnt);
            int j, k;//some dummy processing
            for (j = 0; j < 100000; ++j) {
              k = j*2;
              k = sqrt(k);
            }
            ifPrint = 0;
            LOGI(1, "reader thread %d sharedCnt value after processing %d %d\n", *threadNum, sharedCnt, k);
          }
          if ((++accessTimes) == INT_MAX/5) {
            accessTimes = 0;
            LOGI(1, "reader thread %d still running: %d\n", *threadNum, sharedCnt);
          }
          pthread_rwlock_unlock(&rwlock);
        }
      }
      LOGI(1, "reader thread %d return %d\n", *threadNum, sharedCnt);
      return NULL;
    }
    

    run_by_write_thread函数由写线程执行:

    void *run_by_write_thread(void *arg) {
      int cnt = 100000, i, j, k;
      int* threadNum = (int*)arg;
      for (i = 0; i < cnt; ++i) {
        if (!pthread_rwlock_wrlock(&rwlock)) {
          int lastShCnt = sharedCnt;
          for (j = 0; j < 10; ++j) {  //some dummy processing
            k = j*2;
            k = sqrt(k);
          }
          sharedCnt = lastShCnt + 1;
          pthread_rwlock_unlock(&rwlock);
        }
      }
      LOGI(1, "writer thread %d return %d %d\n", *threadNum, sharedCnt, k);
      return NULL;
    }
    
  5. jni文件夹下添加一个Android.mk文件,内容如下:

    LOCAL_PATH := $(call my-dir)
    include $(CLEAR_VARS)
    LOCAL_MODULE    := NativeThreadsRWLock
    LOCAL_SRC_FILES := NativeThreadsRWLock.cpp
    LOCAL_LDLIBS    := -llog
    include $(BUILD_SHARED_LIBRARY)
    
  6. 构建并运行 Android 项目,并使用以下命令监控logcat输出:

    $ adb logcat -v time NativeThreadsRWLock:I *:S
    

    logcat输出如下所示:

    如何操作...

工作原理...

读写锁在内部实现时使用了互斥量和条件变量。它具有以下规则:

  • 如果一个线程尝试获取一个资源的读锁,只要没有其他线程持有该资源的写锁,它就可以成功。

  • 如果一个线程尝试获取一个资源的写锁,只有当没有其他线程持有该资源的写锁或读锁时,它才能成功。

  • 读写锁保证只有一个线程可以修改(需要获取写锁)资源,同时允许多个线程读取资源(需要获取读锁)。它还确保在资源被更改时不会发生读取操作。以下部分我们将描述 Android pthread.h提供的读写锁功能。

初始化和销毁读写锁

下面定义了两个函数来初始化和销毁读写锁:

int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

pthread_rwlock_init 函数用于初始化由 rwlock 参数指向的读写锁,并使用 attr 参数引用的属性。如果 attr 设置为 NULL,将使用默认属性。pthread_rwlock_destroy 函数接受一个指向读写锁的指针,并销毁它。

注意

还定义了一个宏 PTHREAD_RWLOCK_INITIALIZER 来初始化读写锁。在这种情况下使用默认属性。

使用读写锁

下面定义了两个函数来分别获取读锁和写锁:

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);

这两个函数都接受一个读写锁的指针,并返回零表示成功。如果无法获取锁,调用线程将被阻塞,直到锁可用或发生错误。

以下函数定义用于解锁读锁或写锁:

int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

在我们的示例代码中,我们演示了这些函数的使用。在 run_by_read_thread 函数中,读线程需要获取读锁才能访问共享资源 sharedCnt 的值。在 run_by_write_thread 函数中,写线程在更新共享资源 sharedCnt 之前需要获取写锁。

如果我们移除锁定和解锁读写的代码,构建应用程序,并重新运行它,输出将如下截图所示:

使用读写锁

如输出所示,当启用读写锁时,共享资源 sharedCnt 被更新为一个小于最终值的值。原因在下图中说明:

使用读写锁

在这个图表中,两个写者获取了共享计数器的相同值(N),并且都将其从 N 更新到 N+1。当它们将值写回共享计数器时,尽管两个写者两次更新,共享计数器仍从 N 更新到 N+1。这说明了为什么我们需要写锁。同时注意,由于写者更新了值,读线程对 sharedCnt 的两次读取(处理前一次和处理后一次)给出了两个不同的值。有时这可能不是我们想要的,这就是为什么有时需要读锁的原因。

还有更多...

pthread.h 中还定义了一些其他的读写锁函数。

定时的读/写锁和尝试锁

安卓的 pthread.h 定义了以下两个函数,允许调用线程在尝试获取读锁或写锁时指定超时值:

int pthread_rwlock_timedrdlock(pthread_rwlock_t *rwlock, const struct timespec *abs_timeout);
int pthread_rwlock_timedwrlock(pthread_rwlock_t *rwlock, const struct timespec *abs_timeout);

此外,以下两个函数允许调用线程在不阻塞自身的情况下获取读锁或写锁。如果锁不可用,这些函数将返回非零值而不是阻塞:

int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);

读写锁属性函数

Android 的pthread.h定义了一组函数来初始化和操作读写锁属性,可以作为第二个参数传递给pthread_rwlock_init。这些函数包括pthread_rwlockattr_initpthread_rwlockattr_destroypthread_rwlockattr_setpsharedpthread_rwlockattr_getpshared。它们在 Android NDK 开发中不常使用,因此这里不进行讨论。

在 Android NDK 中用信号量同步本地线程。

我们在前三个食谱中已经介绍了互斥锁、条件变量和读写锁。这是在 Android NDK 上关于线程同步的最后一个食谱,我们将讨论信号量。

准备工作...

读者在阅读本食谱之前,应阅读前三个食谱,在 Android NDK 中使用互斥锁同步本地线程在 Android NDK 中使用条件变量同步本地线程在 Android NDK 中使用读写锁同步本地线程

如何操作...

以下步骤将帮助您创建一个展示 pthread 读写锁使用的 Android 项目:

  1. 创建一个名为NativeThreadsSemaphore的 Android 应用程序。将包名设置为cookbook.chapter6.nativethreadssemaphore。更多详细说明请参考第二章中的加载本地库和注册本地方法食谱,Java Native Interface

  2. 右键点击项目NativeThreadsSemaphore,选择Android Tools | 添加本地支持

  3. cookbook.chapter6.nativethreadssemaphore包下添加一个名为MainActivity.java的 Java 文件。这个 Java 文件仅加载本地库NativeThreadsSemaphore并调用本地方法jni_start_threads

  4. jni文件夹下添加两个名为mylog.hNativeThreadsSemaphore.cpp的文件。NativeThreadsSemaphore.cpp中的一部分代码如下所示:

    jni_start_threads创建pNumOfConsumer个消费者线程,pNumOfProducer个生产者线程,以及numOfSlots个插槽:

    void jni_start_threads(JNIEnv *pEnv, jobject pObj, int pNumOfConsumer, int pNumOfProducer, int numOfSlots) {
      pthread_t *ths;
      int i, ret;
      int *thNum;
      pthread_mutex_init(&mux, NULL);
      sem_init(&emptySem, 0, numOfSlots);
      sem_init(&fullSem, 0, 0);
      ths = (pthread_t*)malloc(sizeof(pthread_t)*(pNumOfConsumer+pNumOfProducer));
      thNum = (int*)malloc(sizeof(int)*(pNumOfConsumer+pNumOfProducer));
      for (i = 0; i < pNumOfConsumer + pNumOfProducer; ++i) {
        thNum[i] = i;
        if (i < pNumOfConsumer) {
          ret = pthread_create(&ths[i], NULL, 
    un_by_consumer_thread, (void*)&(thNum[i]));
        } else {
          ret = pthread_create(&ths[i], NULL, run_by_producer_thread, (void*)&(thNum[i]));
        }
      }
      for (i = 0; i < pNumOfConsumer+pNumOfProducer; ++i) {
        ret = pthread_join(ths[i], NULL);
      }
      sem_destroy(&emptySem);
      sem_destroy(&fullSem);
      pthread_mutex_destroy(&mux);
      free(thNum);
      free(ths);
    }
    

    run_by_consumer_thread是由消费者线程执行的功能:

    void *run_by_consumer_thread(void *arg) {
      int* threadNum = (int*)arg;
      int i;
      for (i = 0; i < 4; ++i) {
        sem_wait(&fullSem);
        pthread_mutex_lock(&mux);
        --numOfItems;
        pthread_mutex_unlock(&mux);
        sem_post(&emptySem);
      }
      return NULL;
    }
    

    run_by_producer_thread是由生产者线程执行的功能:

    void *run_by_producer_thread(void *arg) {
      int* threadNum = (int*)arg;
      int i;
      for (i = 0; i < 4; ++i) {
        sem_wait(&emptySem);
        pthread_mutex_lock(&mux);
        ++numOfItems;
        pthread_mutex_unlock(&mux);
        sem_post(&fullSem);
      }
      return NULL;
    }
    
  5. jni文件夹下添加一个名为Android.mk的文件,内容如下:

    LOCAL_PATH := $(call my-dir)
    include $(CLEAR_VARS)
    LOCAL_MODULE    := NativeThreadsSemaphore
    LOCAL_SRC_FILES := NativeThreadsSemaphore.cpp
    LOCAL_LDLIBS    := -llog
    include $(BUILD_SHARED_LIBRARY)
    
  6. 构建并运行 Android 项目,并使用以下命令监控logcat输出:

    $ adb logcat -v time NativeThreadsSemaphore:I *:S
    

    logcat输出如下截图所示:

    如何操作...

工作原理...

信号量本质上是一个整数计数器。信号量支持两种主要操作:

  • 等待(Wait):尝试减少信号量的值。如果在值为零的信号量上调用 wait,调用线程将被阻塞,直到在其他地方调用post以增加信号量的值。

  • 发布(Post):尝试增加信号量的值。

信号量相关的函数定义在semaphore.h中,而不是pthread.h。接下来,我们将描述一些关键函数。

注意

Android 不支持进程间互斥锁、条件变量和信号量。Android 使用 IntentBinder 等进行进程间通信和同步。

初始化和销毁信号量

以下三个函数被定义用于初始化或销毁信号量:

extern int sem_init(sem_t *sem, int pshared, unsigned int value);
extern int    sem_init(sem_t *, int, unsigned int value);
extern int    sem_destroy(sem_t *);

前两个函数用于初始化信号量。它们都使用输入参数 value 指示的值初始化指向输入参数 sem 的信号量。第一个函数还接受一个参数 pshared,对于线程同步应将其设置为零。如果设置为非零,信号量可以在进程间共享,这在 Android 上不支持,因此不进行讨论。

使用信号量

以下函数被定义用于使用信号量。

extern int    sem_trywait(sem_t *);
extern int    sem_wait(sem_t *);
extern int    sem_post(sem_t *);
extern int    sem_getvalue(sem_t *, int *);

前两个函数用于等待信号量。如果信号量的值不为零,则值会减一。如果值为零,第一个函数将返回非零值以表示失败,而第二个函数将阻塞调用线程。第三个函数用于将信号量的值增加一,最后一个函数用于查询信号量的值。注意,值是通过第二个输入参数返回,而不是通过返回值。

注意

Android 中的 semaphore.h 也定义了一个名为 sem_timedwait 的函数,允许我们在等待信号量时指定一个超时值。

在我们的示例项目中,我们使用了两个信号量 emptySemfullSem,以及一个互斥锁 mux。应用程序将创建一些生产者线程和消费者线程。emptySem 信号量用于指示可用于存储生产者线程生产的项目的空位数量,而 fullSem 指的是消费者线程可以消费的项目数量。互斥锁 mux 用于确保一次没有两个线程可以访问共享计数器 numOfItems

生产者线程将需要在 emptySem 信号量上等待。当它被解锁时,生产者获得了一个空位。它会锁定 mux 然后更新共享计数 numOfItems,这意味着产生了新的项目。因此,它将调用 fullSempost 函数以增加其值。

另一方面,消费者线程将在 fullSem 上等待。当它被解锁时,消费者已经消费了一个项目。它会锁定 mux 然后更新共享计数 numOfItems。由于消费了一个项目,一个新的空位变得可用,因此消费者线程将调用 emptySem 的 post 方法以增加其值。

注意

互斥锁 mux 也可以被二进制信号量替代。二进制信号量的可能值被限制为零和一。

在 Android NDK 中调度本地线程

本食谱讨论如何在 Android NDK 中调度本地线程。

准备就绪...

建议读者阅读第二章中的在 JNI 中操作类从原生代码调用静态和实例方法的食谱,以及本章中的在 Android NDK 中创建和终止原生线程的食谱。

如何操作...

以下步骤将帮助我们创建一个演示在 Android NDK 中线程调度的 Android 项目:

  1. 创建一个名为NativeThreadsSchedule的 Android 应用程序。将包名设置为cookbook.chapter6.nativethreadsschedule。更多详细说明请参考第二章中的加载原生库和注册原生方法部分,Java Native Interface

  2. 右键点击项目NativeThreadsSchedule,选择Android Tools | Add Native Support

  3. cookbook.chapter6.nativethreadsschedule包下添加一个名为MainActivity.java的 Java 文件。这个 Java 文件简单加载了原生库NativeThreadsSchedule并调用原生方法。

  4. jni文件夹下添加五个名为mylog.hNativeThreadsSchedule.hNativeThreadsSchedule.cppSetPriority.cppJNIProcessSetThreadPriority.cpp的文件。最后三个文件的部分代码如下所示:

    • NativeThreadsSchedule.cpp文件包含了在pthread.h中定义的线程调度函数的演示源代码

      jni_thread_scope展示了如何设置原生线程的争用范围:

      void jni_thread_scope() {
        pthread_attr_t attr;
        int ret;
        pid_t fpId = fork();
        if (0 == fpId) {
          pthread_attr_init(&attr);
          int ret = pthread_attr_setscope(&attr, PTHREAD_SCOPE_PROCESS);
          pthread_t thFive[5];
          int threadNums[5];
    int i;
          for (i = 0; i < 5; ++i) {
            threadNums[i] = i;      ret = pthread_create(&thFive[i], &attr, run_by_thread, (void*)&(threadNums[i]));
          }
          for (i = 0; i < 5; ++i) {
            ret = pthread_join(thFive[i], NULL);
          }
        } else {
          pthread_attr_init(&attr);
          int ret = pthread_attr_setscope(&attr, PTHREAD_SCOPE_SYSTEM);
          pthread_t th1;
          int threadNum1 = 0;
          ret = pthread_create(&th1, &attr, run_by_thread, (void*)&threadNum1);
          ret = pthread_join(th1, NULL);
        }
        //code executed by both processes
        pthread_attr_destroy(&attr);
      }
      

      jni_thread_fifo展示了如何设置原生线程的调度策略和优先级:

      void jni_thread_fifo() {
        pthread_attr_t attr;
        int ret;
        pid_t fpId = fork();
        struct sched_param prio;
        if (0 == fpId) {
          //the child process
          pthread_attr_init(&attr);
          pthread_t thFive[5];
          int threadNums[5];
          int i;
          for (i = 0; i < 5; ++i) {
            if (i == 4) {
              prio.sched_priority = 10;
            } else {
              prio.sched_priority = 1;
            }
            ret = pthread_attr_setschedpolicy(&attr, SCHED_FIFO);
            ret = pthread_attr_setschedparam(&attr, &prio);
            threadNums[i] = i;
            ret = pthread_create(&thFive[i], &attr, run_by_thread, (void*)&(threadNums[i]));
            pthread_attr_t lattr;
            struct sched_param lprio;
            int lpolicy;
            pthread_getattr_np(thFive[i], &lattr);
            pthread_attr_getschedpolicy(&lattr, &lpolicy);
            pthread_attr_getschedparam(&lattr, &lprio);
            pthread_attr_destroy(&lattr);
          }
          for (i = 0; i < 5; ++i) {
            ret = pthread_join(thFive[i], NULL);
          }
        } else {
          //the parent process
          pthread_attr_init(&attr);
          prio.sched_priority = 10;
          ret = pthread_attr_setschedpolicy(&attr, SCHED_FIFO);
          ret = pthread_attr_setschedparam(&attr, &prio);
          pthread_t th1;
          int threadNum1 = 0;
          ret = pthread_create(&th1, &attr, run_by_thread, (void*)&threadNum1);
          pthread_attr_t lattr;
          struct sched_param lprio;
          int lpolicy;
          pthread_getattr_np(th1, &lattr);
          pthread_attr_getschedpolicy(&lattr, &lpolicy);
          pthread_attr_getschedparam(&lattr, &lprio);
          pthread_attr_destroy(&lattr);
          ret = pthread_join(th1, NULL);
        }
        //code executed by both processes
        pthread_attr_destroy(&attr);
      }
      

      run_by_thread是每个原生线程实际要执行的功能:

      void *run_by_thread(void *arg) {
        int cnt = 18000000, i;
        int* threadNum = (int*)arg;
        for (i = 1; i < cnt; ++i) {
          if (0 == i%6000000) {
            LOGI(1, "process %d thread %d: %d", getpid(), *threadNum, i);
          }
        }
        LOGI(1, "process %d thread %d return", getpid(), *threadNum);
      }
      
    • SetPriority.cpp文件包含了通过setpriority配置线程 nice 值的源代码

      jni_thread_set_priority方法创建了并加入了五个原生方法:

      void jni_thread_set_priority() {
        int ret;
        pthread_t thFive[5];
        int threadNums[5];
        int i;
        for (i = 0; i < 5; ++i) {
          threadNums[i] = i;
          ret = pthread_create(&thFive[i], NULL, run_by_thread2, (void*)&(threadNums[i]));
        }
        for (i = 0; i < 5; ++i) {
          ret = pthread_join(thFive[i], NULL);
        }
      }
      

      run_by_thread2函数由每个原生线程执行:

      void *run_by_thread2(void *arg) {
        int cnt = 18000000, i;
        int* threadNum = (int*)arg;
        switch (*threadNum) {
        case 0:
          setpriority(PRIO_PROCESS, 0, 21);
          break;
        case 1:
          setpriority(PRIO_PROCESS, 0, 10);
          break;
        case 2:
          setpriority(PRIO_PROCESS, 0, 0);
          break;
        case 3:
          setpriority(PRIO_PROCESS, 0, -10);
          break;
        case 4:
          setpriority(PRIO_PROCESS, 0, -21);
          break;
        default:
          break;
        }
        for (i = 1; i < cnt; ++i) {
          if (0 == i%6000000) {
            int prio = getpriority(PRIO_PROCESS, 0);
            LOGI(1, "thread %d (prio = %d): %d", *threadNum, prio, i);
          }
        }
        int prio = getpriority(PRIO_PROCESS, 0);
        LOGI(1, "thread %d (prio = %d): %d return", *threadNum, prio, i);
      }
      
    • JNIProcessSetThreadPriority.cpp文件包含了通过android.os.Process.setThreadPriority Java 方法配置线程 nice 值的源代码

      jni_process_setThreadPriority方法创建了并加入了五个原生线程:

      void jni_process_setThreadPriority() {
        int ret;
        pthread_t thFive[5];
        int threadNums[5];
        int i;
        for (i = 0; i < 5; ++i) {
          threadNums[i] = i;
          ret = pthread_create(&thFive[i], NULL, run_by_thread3, (void*)&(threadNums[i]));
          if(ret) {
            LOGE(1, "cannot create the thread %d: %d", i, ret);
          }
          LOGI(1, "thread %d started", i);
        }
        for (i = 0; i < 5; ++i) {
          ret = pthread_join(thFive[i], NULL);
          LOGI(1, "join returned for thread %d", i);
        }
      }
      

      run_by_thread3函数由每个原生线程执行。在这里设置线程的 nice 值:

      void *run_by_thread3(void *arg) {
        int cnt = 18000000, i;
        int* threadNum = (int*)arg;
        JNIEnv *env;
        jmethodID setThreadPriorityMID;
        cachedJvm->AttachCurrentThread(&env, NULL);
        jclass procClass = env->FindClass("android/os/Process");
        setThreadPriorityMID = env->GetStaticMethodID(procClass, "setThreadPriority", "(I)V");
        switch (*threadNum) {
        case 0:
          env->CallStaticVoidMethod(procClass, setThreadPriorityMID, 21);
          break;
        case 1:
          env->CallStaticVoidMethod(procClass, setThreadPriorityMID, 10);
          break;
        case 2:
          env->CallStaticVoidMethod(procClass, setThreadPriorityMID, 0);
          break;
        case 3:
          env->CallStaticVoidMethod(procClass, setThreadPriorityMID, -10);
          break;
        case 4:
          env->CallStaticVoidMethod(procClass, setThreadPriorityMID, -21);
          break;
        default:
          break;
      
       }
        //we can also use getThreadPriority(int tid) through JNI interface
        for (i = 1; i < cnt; ++i) {
          if (0 == i%6000000) {
            int prio = getpriority(PRIO_PROCESS, 0);
            LOGI(1, "thread %d (prio = %d): %d", *threadNum, prio, i);
          }
        }
        int prio = getpriority(PRIO_PROCESS, 0);
        LOGI(1, "thread %d (prio = %d): %d return", *threadNum, prio, i);
        cachedJvm->DetachCurrentThread();
      }
      
  5. jni文件夹下添加一个Android.mk文件,内容如下:

    LOCAL_PATH := $(call my-dir)
    include $(CLEAR_VARS)
    LOCAL_MODULE    := NativeThreadsSchedule
    LOCAL_SRC_FILES := NativeThreadsSchedule.cpp
    LOCAL_LDLIBS    := -llog
    include $(BUILD_SHARED_LIBRARY)
    
  6. MainActivity.java中,除了jni_thread_scope,禁用所有原生方法。构建项目并运行它。启动一个终端并输入以下命令,以监控logcat的输出:

    $ adb logcat -v time NativeThreadsSchedule:I *:S
    

    下面的截图显示了输出结果:

    如何操作...

  7. MainActivity.java中,除了jni_thread_fifo,禁用所有原生方法。构建项目并运行它。logcat的输出在下图的截图中显示:如何操作...

  8. MainActivity.java中,除了jni_thread_set_priority之外,禁用所有本地方法。构建项目并运行它。logcat的输出显示在以下屏幕截图中:如何操作...

  9. MainActivity.java中,除了jni_process_setThreadPriority之外,禁用所有本地方法。构建项目并运行它。logcat的输出显示在以下屏幕截图中:如何操作...

它的工作原理...

我们可以通过设置调度竞争范围、线程优先级和调度策略来调度本地线程:

  • 调度竞争范围:它决定了当调度器调度线程时,线程必须与之竞争的线程

  • 线程优先级:它决定了当 CPU 可用时,调度器更有可能选择哪个线程

  • 调度策略:它决定了调度器如何调度具有相同优先级的线程

调整这些设置的一种方式是通过线程属性。以下函数在pthread.h中定义,用于初始化和销毁pthread_attr_t实例:

int pthread_attr_init(pthread_attr_t * attr); 
int pthread_attr_destroy(pthread_attr_t * attr);

在这两个函数中,输入参数是一个指向pthread_attr_t对象的指针。接下来我们将详细描述竞争范围、线程优先级和调度策略。

调度竞争范围

在典型的 pthread 实现中定义了两种范围,即PTHREAD_SCOPE_SYSTEMPTHREAD_SCOPE_PROCESS。系统范围的线程与系统范围内的所有其他线程竞争 CPU。另一方面,进程范围的线程与同一进程中的其他线程进行调度。

安卓 Bionic pthread.h定义了以下两个函数用于设置和获取线程范围:

int pthread_attr_setscope(pthread_attr_t *attr, int  scope); 
int pthread_attr_getscope(pthread_attr_t const *attr);

这两个函数接受一个指向 pthread 属性对象的指针作为输入参数。set函数还包括第二个参数,让我们传递范围常量。这两个函数返回零表示成功,返回非零值表示失败。

结果显示,带有PTHREAD_SCOPE_PROCESS作为第二个输入参数的pthread_attr_setscope不被安卓支持。换句话说,安卓本地线程总是具有系统范围。如NativeThreadsSchedule.cpp中的jni_thread_scope所示,使用PTHREAD_SCOPE_PROCESS调用pthread_attr_setscope将返回非零值。

我们之前在本地方法jni_thread_scope中演示了这两个函数的用法。我们在方法中创建了两个进程。子进程运行五个线程,而父进程只运行一个线程。因为它们都是系统范围的线程,所以无论它们属于哪个进程,调度器都会为它们分配大致相同的 CPU 时间片,因此它们都会在大致相同的时间完成,如本食谱的如何操作...部分的第 6 步中的第一个logcat屏幕截图所示。

注意

我们在代码中调用了fork来创建一个进程。这仅用于演示目的。强烈建议不要在 Android 上使用fork创建本地进程,因为本地进程不会被 Android 框架管理,行为不当的本地进程可能会占用大量 CPU 周期并导致安全漏洞。

调度策略和线程优先级

每个线程都关联有一个调度策略和优先级。当 CPU 可用时,优先级更高的线程更有可能被调度器选中。如果多个线程具有相同的优先级,调度策略将决定如何调度它们。在 Android 的pthread.h中定义的策略包括SCHED_OTHERSCHED_FIFOSCHED_RR

注意事项

优先级值的合法范围与调度策略相关联。

SCHED_FIFO:在先进先出(FIFO)策略中,线程将获得 CPU 直到它退出或阻塞。如果被阻塞,它将被放在其优先级队列的末尾,队列前面的线程将被交给 CPU。此策略允许的优先级范围是 1 到 99。

SCHED_RR轮询(RR)策略与 FIFO 类似,不同之处在于每个线程只允许运行一定时间,称为量子。当一个线程完成其量子,它将被中断并放在其优先级队列的末尾。此策略允许的优先级范围也是 1 到 99。

SCHED_OTHER:这是默认的调度策略。它也允许线程只运行有限次数,但算法可能不同于SCHED_RR,并且更为复杂。此策略下的所有线程优先级为 0。

熟悉 pthread 编程的人可能熟悉 pthread 策略和优先级函数,包括:

  • pthread_attr_setschedpolicy

  • pthread_attr_getschedpolicy

  • pthread_attr_setschedparam

  • pthread_attr_getschedparam

这些函数在 Android 上不能按预期工作,尽管它们在 Android 的pthread.h头文件中有定义。因此,我们在这里不讨论细节。

在我们的示例项目中,我们实现了一个本地方法jni_thread_fifo,它试图将调度策略设置为SCHED_FIFO以及线程优先级。正如第二个logcat截图所示,这些设置并没有影响到线程。

总结来说,所有 Android 线程都是系统范围的线程,优先级为 0,调度策略为SCHED_OTHER

使用 nice 值/级别进行调度

Nice 值/级别是另一个可能影响调度器的因素。它也常被称为优先级,但在这里我们将使用 nice 值来与我们之前讨论的线程优先级区分开来。

我们使用以下两种方法来调整 nice 值:

  • 调用 setpriority:这在SetPriority.cpp中有所展示。我们创建了五个具有不同 nice 值的线程,如何操作部分的第 8 步的第三个logcat截图显示,具有较低 nice 值的线程先返回。

  • 调用 android.os.Process.setThreadPriority:这在JNIProcessSetThreadPriority.cpp中有说明。如如何操作部分的第 9 步的第四个logcat截图所示,我们得到了与调用setpriority类似的结果。实际上,setThreadPriority内部调用了setpriority

在 Android NDK 中管理原生线程数据

当我们想在函数间保存线程范围内的数据时,有几种选择,包括全局变量、参数传递和线程特定数据键。本食谱将讨论这三种选择,重点放在线程特定数据键上。

准备就绪...

建议读者在阅读本章中的在 Android NDK 中创建和终止原生线程食谱和在 Android NDK 中使用互斥锁同步原生线程食谱后再阅读本食谱。

如何操作...

以下步骤将帮助我们创建一个展示 Android NDK 数据管理的项目:

  1. 创建一个名为NativeThreadsData的 Android 应用程序。将包名设置为cookbook.chapter6.nativethreadsdata。如果你需要更详细的说明,请参考第二章,Java Native Interface中的加载原生库和注册原生方法食谱。

  2. 右键点击项目NativeThreadsData,选择Android Tools | 添加原生支持

  3. cookbook.chapter6.nativethreadsdata包下添加一个名为MainActivity.java的 Java 文件。这个 Java 文件只是加载了原生库NativeThreadsData并调用了原生方法。

  4. jni文件夹下添加mylog.hNativeThreadsData.cpp文件。mylog.h包含 Android 原生logcat工具函数,而NativeThreadsData.cpp文件包含启动多个线程的原生代码。部分代码如下所示:

    jni_start_threads函数启动了n个线程,其中n由变量pNumOfThreads指定:

    void jni_start_threads(JNIEnv *pEnv, jobject pObj, int pNumOfThreads) {
      pthread_t *ths;
      int i, ret;
      int *thNum;
      ths = (pthread_t*)malloc(sizeof(pthread_t)*pNumOfThreads);
      thNum = (int*)malloc(sizeof(int)*pNumOfThreads);
      pthread_mutex_init(&mux, NULL);
      pthread_key_create(&muxCntKey, free_muxCnt);
      for (i = 0; i < pNumOfThreads; ++i) {
        thNum[i] = i;
        ret = pthread_create(&ths[i], NULL, run_by_thread, (void*)&(thNum[i]));
      }
      for (i = 0; i < pNumOfThreads; ++i) {
        ret = pthread_join(ths[i], NULL);
      }
      pthread_key_delete(muxCntKey);
      pthread_mutex_destroy(&mux);
      free(thNum);
      free(ths);
    }
    

    thread_step_1函数由线程执行。它获取与线程特定键关联的数据,并使用它来计算互斥锁被锁定的次数:

    void thread_step_1() {
      struct timeval st, cu;
      long stt, cut;
      int *muxCntData = (int*)pthread_getspecific(muxCntKey);
      gettimeofday(&st, NULL);
      stt = st.tv_sec*1000 + st.tv_usec/1000;
      do {
               pthread_mutex_lock(&mux);
        (*muxCntData)++;
               pthread_mutex_unlock(&mux);
        gettimeofday(&st, NULL);
        cut = st.tv_sec*1000 + st.tv_usec/1000;
         } while (cut - stt < 10000);
    }
    

    thread_step_2函数由线程执行。它获取与线程特定键关联的数据并打印出来:

    void thread_step_2(int thNum) {
      int *muxCntData = (int*)pthread_getspecific(muxCntKey);
      LOGI(1, "thread %d: mux usage count: %d\n", thNum, *muxCntData);
    }
    

    run_by_thread函数由线程执行:

    void *run_by_thread(void *arg) {
      int* threadNum = (int*)arg;
      int *muxCntData = (int*)malloc(sizeof(int));
      *muxCntData = 0;
      pthread_setspecific(muxCntKey, (void*)muxCntData);
      thread_step_1();
      thread_step_2(*threadNum);
      return NULL;
    }
    
  5. jni文件夹下添加一个Android.mk文件,内容如下:

    LOCAL_PATH := $(call my-dir)
    include $(CLEAR_VARS)
    LOCAL_MODULE    := NativeThreadsData
    LOCAL_SRC_FILES := NativeThreadsData.cpp
    LOCAL_LDLIBS    := -llog
    include $(BUILD_SHARED_LIBRARY)
    
  6. 构建并运行 Android 项目,并使用以下命令监控logcat输出:

    $ adb logcat -v time NativeThreadsData:I *:S
    

    logcat输出在下图中显示:

    如何操作...

工作原理...

在我们的示例项目中,我们展示了使用全局变量、参数和线程特定数据键传递数据的方法:

  • 互斥量 mux 被声明为一个全局变量,每个线程都可以访问它。

  • 每个线程都被分配一个作为输入参数的线程编号。在 run_by_thread 函数中,每个线程将接收到的线程编号传递给另一个函数 thread_step_2

  • 我们定义了一个线程特定键 muxCntKey。每个线程都可以将自身的值与该键关联。在我们的代码中,我们使用这个键来存储一个线程锁定互斥量 mux 的次数。

接下来我们将详细讨论线程特定数据键。

创建和删除线程特定数据键

以下两个函数在 pthread.h 中定义,分别用于创建和删除线程特定数据键:

int pthread_key_create(pthread_key_t *key, void (*destructor_function)(void *));
int pthread_key_delete (pthread_key_t key);

pthread_key_create 函数接收一个指向 pthread_key_t 结构的指针和一个函数指针,该函数指针指向与每个键值相关联的销毁函数。销毁函数是可选的,可以被设置为 NULL。在我们的示例中,我们调用了 pthread_key_create 来创建名为 muxCntKey 的键。

pthread_key_create 函数返回零表示成功,其他值表示失败。如果成功,第一个输入参数 key 将指向新创建的键,并且所有活动线程中新的键关联的值是 NULL。如果在键创建后创建了一个新线程,新线程也会将 NULL 与键关联。

当一个线程退出时,与键关联的值被设置为 NULL,然后调用与键关联的销毁函数,并将键之前关联的值作为唯一的输入参数。在我们的示例代码中,我们定义了一个销毁函数 free_muxCnt,用于释放与键 muxCntKey 关联的数据的内存。

pthread_key_delete 的使用相对简单。它接收由 pthread_key_create 创建的键并删除它。成功时返回零,失败时返回非零值。

设置和获取线程特定数据

安卓的 pthread.h 为线程特定数据管理定义了以下两个函数:

int pthread_setspecific(pthread_key_t key, const void *value);
void *pthread_getspecific(pthread_key_t key);

pthread_setspecific 函数接收一个先前创建的数据键和一个指向要与键关联的数据的指针。它返回零表示成功,否则返回非零值。不同的线程可以调用这个函数,将不同的值绑定到同一个键上。

pthread_getspecific 函数接收一个先前创建的数据和键,并返回调用线程中与键关联的数据的指针。

在我们的示例代码中的 run_by_thread 函数中,我们将一个初始化为零的整数变量与 muxCntKey 键关联。在 thread_step_1 函数中,我们获取与键关联的整数变量,并使用它来计算 mux 被锁定的次数。在 thread_step_2 函数中,我们再次获取与 muxCntKey 关联的整数变量并打印其值。

第七章:其他 Android NDK API

在本章中,我们将涵盖以下内容:

  • 使用 Android NDK 中的 jnigraphics 库进行编程

  • 使用 Android NDK 中的动态链接库进行编程

  • 使用 Android NDK 中的 zlib 压缩库进行编程

  • 使用 Android NDK 中的 OpenSL ES 音频库进行音频编程

  • 使用 Android NDK 中的 OpenMAX AL 多媒体库进行编程

引言

在前三章中,我们已经涵盖了 Android NDK OpenGL ES API(第四章,Android NDK OpenGL ES API)、Native Application API(第五章,Android Native Application API)和 Multithreading API(第六章,Android NDK Multithreading)。这是关于 Android NDK API 说明的最后一章,我们将介绍更多库,包括jnigraphics库、动态链接库、zlib压缩库、OpenSL ES 音频库和 OpenMAX AL 多媒体库。

我们首先介绍两个小型库,jnigraphics和动态链接器,它们的 API 函数较少,易于使用。然后我们描述zlib压缩库,该库可用于以.zlib.gzip格式压缩和解压数据。OpenSL ES 音频库和 OpenMAX AL 多媒体库是两个相对较新的 API,在 Android 的新版本上可用。这两个库中的 API 函数尚未冻结,仍在发展中。正如 NDK OpenSL ES 和 OpenMAX AL 文档所述,由于 Android 上的库开发并不追求源代码兼容性,因此这两个库的未来版本可能需要我们更新代码。

请注意,OpenSL ES 和 OpenMAX AL 是相当复杂的库,拥有大量的 API 函数。我们只能通过简单示例介绍这两个库的基本用法。感兴趣的读者应查阅库文档以获取更多详细信息。

使用 Android NDK 中的 jnigraphics 库进行编程

jnigraphics库提供了一个基于 C 的接口,使本地代码能够访问 Java 位图对象的像素缓冲区,该接口在 Android 2.2 系统映像及更高版本上作为一个稳定的本地 API 提供。本节讨论如何使用jnigraphics库。

准备工作…

读者应该知道如何创建一个 Android NDK 项目。我们可以参考第一章《Hello NDK》中的编写一个 Hello NDK 程序一节获取详细说明。

如何操作…

以下步骤描述了如何创建一个简单的 Android 应用,该应用演示了jnigraphics库的使用方法:

  1. 创建一个名为JNIGraphics的 Android 应用。将包名设置为cookbook.chapter7.JNIGraphics。更多详细说明请参考第二章《Java Native Interface》中的加载本地库和注册本地方法一节。

  2. 右键点击项目JNIGraphics,选择Android Tools | Add Native Support

  3. cookbook.chapter7.JNIGraphics包中添加两个名为MainActivity.javaRenderView.java的 Java 文件。RenderView.java加载JNIGraphics本地库,调用本地naDemoJniGraphics方法处理位图,并最终显示位图。MainActivity.java文件创建一个位图,将其传递给RenderView类,并将RenderView类设置为它的内容视图。

  4. jni文件夹下添加mylog.hJNIGraphics.cpp文件。mylog.h包含 Android 本地logcat实用函数,而JNIGraphics.cpp文件包含使用jnigraphics库函数处理位图的本地代码。JNIGraphics.cpp文件中的部分代码如下所示:

    void naDemoJniGraphics(JNIEnv* pEnv, jclass clazz, jobject pBitmap) {
      int lRet, i, j;
      AndroidBitmapInfo lInfo;
      void* lBitmap;
      //1\. retrieve information about the bitmap
      if ((lRet = AndroidBitmap_getInfo(pEnv, pBitmap, &lInfo)) < 0) {
        return;
      }
      if (lInfo.format != ANDROID_BITMAP_FORMAT_RGBA_8888) {
        return;
      }
      //2\. lock the pixel buffer and retrieve a pointer to it
      if ((lRet = AndroidBitmap_lockPixels(pEnv, pBitmap, &lBitmap)) < 0) {
        LOGE(1, "AndroidBitmap_lockPixels() failed! error = %d", lRet);
      }
      //3\. manipulate the pixel buffer
      unsigned char *pixelBuf = (unsigned char*)lBitmap;
      for (i = 0; i < lInfo.height; ++i) {
        for (j = 0; j < lInfo.width; ++j) {
        unsigned char *pixelP = pixelBuf + i*lInfo.stride + j*4;
        *pixelP = (unsigned char)0x00;	//remove R component
    //    *(pixelP+1) = (unsigned char)0x00;	//remove G component
    //    *(pixelP+2) = (unsigned char)0x00;	//remove B component
    //    LOGI(1, "%d:%d:%d:%d", *pixelP, *(pixelP+1), *(pixelP+2), *(pixelP+3));}
      }
      //4\. unlock the bitmap
      AndroidBitmap_unlockPixels(pEnv, pBitmap);
    }
    
  5. jni文件夹中添加一个Android.mk文件,内容如下:

    LOCAL_PATH := $(call my-dir)
    include $(CLEAR_VARS)
    LOCAL_MODULE    := JNIGraphics
    LOCAL_SRC_FILES := JNIGraphics.cpp
    LOCAL_LDLIBS := -llog -ljnigraphics
    include $(BUILD_SHARED_LIBRARY)
    
  6. 构建并运行 Android 项目。我们可以启用代码从位图中移除不同的组件。以下屏幕截图分别显示了原始图片以及移除了红色、绿色和蓝色组件的图片:如何操作...

它的工作原理...

在我们的示例项目中,我们通过将位图传递给本地naDemoJniGraphics函数的一个 RGB 组件设置为 0 来修改位图。

注意

jnigraphics库仅适用于 Android API 级别 8(Android 2.2,Froyo)及更高版本。

使用jnigraphics库应遵循以下步骤:

  1. 在使用jnigraphicsAPI 的源代码中包含<android/bitmap.h>头文件。

  2. Android.mk文件中包含以下行以链接到jnigraphics库。

    LOCAL_LDLIBS += -ljnigraphics
    
  3. 在源代码中,调用AndroidBitmap_getInfo函数来获取关于位图对象的信息。AndroidBitmap_getInfo函数具有以下原型:

    int AndroidBitmap_getInfo(JNIEnv* env, jobject jbitmap, AndroidBitmapInfo* info);
    

    该函数接受指向JNIEnv结构的指针、位图对象的引用以及指向AndroidBitmapInfo结构的指针。如果调用成功,info指向的数据结构将被填充。

    AndroidBitmapInfo的定义如下:

    typedef struct {
    uint32_t    width;
    	uint32_t    height;
    uint32_t    stride;
    int32_t     format;
    uint32_t    flags; 
    } AndroidBitmapInfo;
    

    widthheight表示位图的像素宽度和高度。stride指的是像素缓冲区行之间跳过的字节数。该数字不得小于宽度字节。在大多数情况下,stridewidth相同。然而,有时像素缓冲区包含填充,所以stride可能比位图width大。

    format是颜色格式,可以是bitmap.h头文件中定义的ANDROID_BITMAP_FORMAT_RGBA_8888ANDROID_BITMAP_FORMAT_RGB_565ANDROID_BITMAP_FORMAT_RGBA_4444ANDROID_BITMAP_FORMAT_A_8ANDROID_BITMAP_FORMAT_NONE

    在我们的示例中,我们使用ANDROID_BITMAP_FORMAT_RGBA_8888作为位图格式。因此,每个像素占用 4 个字节。

  4. 通过调用AndroidBitmap_lockPixels函数锁定像素地址:

    int AndroidBitmap_lockPixels(JNIEnv* env, jobject jbitmap, void** addrPtr);
    

    如果调用成功,*addrPtr 指针将指向位图的像素。一旦像素地址被锁定,在像素地址被解锁之前,像素的内存不会移动。

  5. 在本地代码中操作像素缓冲区。

  6. 通过调用 AndroidBitmap_unlockPixels 来解锁像素地址:

    int AndroidBitmap_unlockPixels(JNIEnv* env, jobject jbitmap);
    

    请注意,如果 AndroidBitmap_lockPixels 函数调用成功,则必须调用此函数。

    注意

    jnigraphics 函数在成功时返回 ANDROID_BITMAP_RESUT_SUCCESS,其值为 0。失败时返回负值。

还有更多内容...

回顾我们在第四章,Android NDK OpenGL ES API使用 OpenGL ES 1.x API 将纹理映射到 3D 对象示例中使用了 jnigraphics 库来加载纹理。我们可以重新访问该示例,了解我们如何使用 jnigraphics 库的另一个例子。

在 Android NDK 中使用动态链接库进行编程

动态加载是一种在运行时将库加载到内存中,并执行库中定义的函数或访问变量的技术。它允许应用程序在没有这些库的情况下启动。

在本书的几乎每个示例中,我们都看到了动态加载。当我们调用 System.loadLibrarySystem.load 函数来加载本地库时,我们就是在使用动态加载。

自从 Android 1.5 起,Android NDK 就提供了动态链接库以支持 NDK 中的动态加载。本示例讨论动态链接库函数。

准备就绪...

期望读者知道如何创建一个 Android NDK 项目。你可以参考第一章的编写一个 Hello NDK 程序示例,Hello NDK 以获取详细说明。

如何操作...

以下步骤描述了如何使用动态链接库创建一个 Android 应用程序,以加载数学库并计算 2 的平方根。

  1. 创建一个名为 DynamicLinker 的 Android 应用程序。将包名设置为 cookbook.chapter7.dynamiclinker。更多详细说明请参考第二章,Java Native Interface加载本地库和注册本地方法示例。

  2. 右键点击 DynamicLinker 项目,选择 Android Tools | Add Native Support

  3. cookbook.chapter7.dynamiclinker 包下添加一个名为 MainActivity.java 的 Java 文件。这个 Java 文件简单加载了本地 DynamicLinker 库,并调用了本地 naDLDemo 方法。

  4. jni 文件夹下添加 mylog.hDynamicLinker.cpp 文件。OpenSLESDemo.cpp 文件中的一部分代码在以下代码中显示。

    naDLDemo 加载了 libm.so 库,获取了 sqrt 函数的地址,并以输入参数 2.0 调用该函数:

    void naDLDemo(JNIEnv* pEnv, jclass clazz) {
      void *handle;
      double (*sqrt)(double);
      const char *error;
      handle = dlopen("libm.so", RTLD_LAZY);
      if (!handle) {
        LOGI(1, "%s\n", dlerror());
        return;
      }
      dlerror();    /* Clear any existing error */
      *(void **) (&sqrt) = dlsym(handle, "sqrt");
      if ((error = dlerror()) != NULL)  {
        LOGI(1, "%s\n", error);
        return;
      }
      LOGI(1, "%f\n", (*sqrt)(2.0));
    }
    
  5. jni 文件夹下添加一个 Android.mk 文件,内容如下:

    LOCAL_PATH := $(call my-dir)
    include $(CLEAR_VARS)
    LOCAL_MODULE    := DynamicLinker
    LOCAL_SRC_FILES := DynamicLinker.cpp
    LOCAL_LDLIBS := -llog -ldl
    include $(BUILD_SHARED_LIBRARY)
    
  6. 构建并运行 Android 项目,使用以下命令监控 logcat 输出:

    $ adb logcat -v time DynamicLinker:I *:S
    

    logcat输出的屏幕截图如下所示:

    如何操作...

它的工作原理...

为了使用动态加载库libdl.so进行构建,我们必须在Android.mk文件中添加以下行:

LOCAL_LDLIBS := -ldl

以下函数在dlfcn.h头文件中由 Android 动态链接库定义:

void*        dlopen(const char*  filename, int flag);
int          dlclose(void*  handle);
const char*  dlerror(void);
void*        dlsym(void*  handle, const char*  symbol);
int          dladdr(const void* addr, Dl_info *info);

dlopen函数动态加载库。第一个参数指示库名称,而第二个参数指的是加载模式,描述了dlopen如何解析未定义的符号。当一个对象文件(例如共享库、可执行文件等)被加载时,它可能包含对符号的引用,这些符号的地址在另一个对象文件被加载之前是未知的(这类符号被称为未定义符号)。在使用这些引用访问符号之前,需要解析这些引用。以下两种模式决定了解析何时发生:

  • RTLD_NOW:当对象文件被加载时,未定义的符号将被解析。这意味着解析在dlopen函数返回之前发生。如果执行了解析但从未访问过引用,这可能是浪费。

  • RTLD_LAZY:解析可以在dlopen函数返回后执行,即当代码执行时解析未定义的符号。

以下两种模式决定了已加载对象中符号的可见性。它们可以与前面提到的两种模式进行 OR 运算:

  • RTLD_LOCAL:符号对另一个对象不可用

  • RTLD_GLOBAL:符号将对随后加载的对象可用

dlopen函数在成功时返回一个句柄。该句柄应用于后续对dlsymdlclose的调用。

dlclose函数只是减少了加载库句柄的引用计数。如果引用计数减少到零,将卸载库。

dlerror函数返回一个字符串,以描述自上次调用dlerror以来在调用dlopendlsymdlclose时发生的最新错误。如果没有发生此类错误,它将返回NULL

dlsym函数返回输入参数句柄所引用的已加载动态库中给定符号的内存地址。返回的地址可以用来访问该符号。

dladdr函数接收一个地址,并尝试通过DI_info类型的info参数返回有关该地址和库的更多信息。DI_info数据结构定义如下代码片段所示:

typedef struct {
   const char *dli_fname;  
   void       *dli_fbase;  
   const char *dli_sname;  
   void       *dli_saddr;  
} Dl_info;

dli_fname表示输入参数addr引用的共享对象的路径。dli_fbase是共享对象加载的地址。dli_sname表示地址低于addr的最近符号的名称,而dli_saddr是名为dli_sname的符号的地址。

在我们的示例中,我们演示了前四个函数的用法。我们通过 dlopen 加载数学库,通过 dlsym 获取 sqrt 函数的地址,通过 dlerror 检查错误,并通过 dlclose 关闭库。

有关动态加载库的更多详细信息,请参考 tldp.org/HOWTO/Program-Library-HOWTO/dl-libraries.htmllinux.die.net/man/3/dlopen

在 Android NDK 中使用 zlib 压缩库进行编程

zlib 是一个广泛使用的、无损的数据压缩库,适用于 Android 1.5 系统镜像或更高版本。本食谱讨论了 zlib 函数的基本用法。

准备中...

期望读者知道如何创建一个 Android NDK 项目。我们可以参考 第一章 的 编写一个 Hello NDK 程序 食谱,Hello NDK 以获取详细说明。

如何操作...

以下步骤描述了如何创建一个简单的 Android 应用程序,该程序演示了 zlib 库的用法:

  1. 创建一个名为 ZlibDemo 的 Android 应用程序。将包名设置为 cookbook.chapter7.zlibdemo。有关更详细的说明,请参考 第二章 的 加载本地库和注册本地方法 食谱,Java Native Interface

  2. 在项目 ZlibDemo 上右键点击,选择 Android Tools | 添加本地支持

  3. cookbook.chapter7.zlibdemo 包中添加一个名为 MainActivity.java 的 Java 文件。MainActivity.java 文件加载 ZlibDemo 本地库,并调用本地方法。

  4. jni 文件夹下添加 mylog.hZlibDemo.cppGzFileDemo.cpp 文件。mylog.h 头文件包含了 Android 本地的 logcat 实用功能函数,而 ZlibDemo.cppGzFileDemo.cpp 文件包含了压缩和解压缩的代码。ZlibDemo.cppGzFileDemo.cpp 的一部分代码在以下代码中展示。

    ZlibDemo.cpp 包含了在内存中压缩和解压缩数据的本地代码。

    compressUtil 在内存中压缩和解压缩数据。

    void compressUtil(unsigned long originalDataLen) {
      int rv;
      int compressBufBound = compressBound(originalDataLen);
      compressedBuf = (unsigned char*) malloc(sizeof(unsigned char)*compressBufBound);
      unsigned long compressedDataLen = compressBufBound;
      rv = compress2(compressedBuf, &compressedDataLen, dataBuf, originalDataLen, 6);
      if (Z_OK != rv) {
        LOGE(1, "compression error");
        free(compressedBuf);
        return;
      }
      unsigned long decompressedDataLen = S_BUF_SIZE;
      rv = uncompress(decompressedBuf, &decompressedDataLen, compressedBuf, compressedDataLen);
      if (Z_OK != rv) {
        LOGE(1, "decompression error");
        free(compressedBuf);
        return;
      }
      if (0 == memcmp(dataBuf, decompressedBuf, originalDataLen)) {
        LOGI(1, "decompressed data same as original data");
      }   //free resource
      free(compressedBuf);
    }
    
  5. naCompressAndDecompress 生成压缩数据并调用 compressUtil 函数来压缩和解压缩生成的数据:

    void naCompressAndDecompress(JNIEnv* pEnv, jclass clazz) {
      unsigned long originalDataLen = getOriginalDataLen();
      LOGI(1, "---------data with repeated bytes---------")
      generateOriginalData(originalDataLen);
      compressUtil(originalDataLen);
      LOGI(1, "---------data with random bytes---------")
      generateOriginalDataRandom(originalDataLen);
      compressUtil(originalDataLen);
    }
    

    GzFileDemo.cpp 包含了本地代码,用于压缩和解压缩文件中的数据。

    writeToFile 函数将字符串写入到 gzip 文件中。在写入时会应用压缩:

    int writeToFile() {
      gzFile file;
      file = gzopen("/sdcard/test.gz", "w6");
      if (NULL == file) {
        LOGE(1, "cannot open file to write");
        return 0;
      }
      const char* dataStr = "hello, Android NDK!";
      int bytesWritten = gzwrite(file, dataStr, strlen(dataStr));
      gzclose(file);
      return bytesWritten;
    }
    

    readFromFilegzip 文件中读取数据。在读取时会应用解压缩:

    void readFromFile(int pBytesToRead) {
      gzFile file;
      file = gzopen("/sdcard/test.gz", "r6");
      if (NULL == file) {
        LOGE(1, "cannot open file to read");
        return;
      }
      char readStr[100];
      int bytesRead = gzread(file, readStr, pBytesToRead);
      gzclose(file);
      LOGI(1, "%d: %s", bytesRead, readStr);
    }
    
  6. jni 文件夹下添加一个 Android.mk 文件,内容如下:

    LOCAL_PATH := $(call my-dir)
    include $(CLEAR_VARS)
    LOCAL_MODULE    := ZlibDemo
    LOCAL_SRC_FILES := ZlibDemo.cpp GzFileDemo.cpp
    LOCAL_LDLIBS := -llog -lz
    include $(BUILD_SHARED_LIBRARY)
    
  7. 启用 naCompressAndDecompress 函数并禁用 naGzFileDemo 函数,构建并运行应用程序。我们可以使用以下命令监控 logcat 输出:

    $ adb logcat -v time ZlibDemo:I *:S
    

    logcat 输出的屏幕截图如下所示:

    如何操作...

    启用 naGzFileDemo 函数并禁用 naCompressAndDecompress 函数,构建并运行应用程序。logcat 输出在以下屏幕截图中显示:

    如何操作...

工作原理...

zlib 库为内存数据和文件提供压缩和解压缩功能。我们演示了这两种用例。在 ZlibDemo.cpp 文件中,我们创建了两个数据缓冲区,一个包含重复的字节,另一个包含随机的字节。我们按照以下步骤压缩和解压缩数据:

  1. 计算压缩后大小的上限。这是通过以下函数完成的:

    uLong compressBound(uLong sourceLen);
    

    该函数返回在 sourceLen 字节的源数据上调用 compresscompress2 函数后压缩数据的最大大小。

  2. 为存储压缩数据分配内存。

  3. 压缩数据。这是通过以下函数完成的:

    int compress2(Bytef *dest,   uLongf *destLen, const Bytef *source, uLong sourceLen, int level);
    

    这个函数接受五个输入参数。sourcesourceLen 指的是源数据缓冲区和源数据长度。destdestLen 指示用于存储压缩数据的数据缓冲区和这个缓冲区的大小。destLen 的值必须在调用函数时至少为 compressBound 返回的值。当函数返回时,destLen 被设置为压缩数据的实际大小。最后一个输入参数 level 可以在 0 到 9 之间取值,其中 1 表示最快的速度,9 表示最佳的压缩率。在我们的示例中,我们将其值设置为 6,以在速度和压缩之间取得平衡。

    注意

    我们还可以使用压缩函数来压缩数据,该函数没有级别输入参数。相反,它假设一个默认级别,相当于 6。

  4. 解压缩数据。这是通过使用 uncompress 函数完成的:

    int uncompress(Bytef *dest,   uLongf *destLen, const Bytef *source, uLong sourceLen);
    

    输入参数与 compress2 函数的含义相同。

  5. 将解压缩的数据与原始数据比较。这只是简单的检查。

    默认情况下,这些函数使用 zlib 格式来处理压缩数据。

    这个库还支持以 gzip 格式读写文件。这在 GzFileDemo.cpp 中有演示。这些函数的使用类似于 stdio 文件读写函数。

我们遵循的步骤将压缩数据写入 gzip 文件,然后从中读取未压缩数据,如下所示:

  1. 打开一个 gzip 文件以供写入。这是通过以下函数完成的:

    gzFile gzopen(const char *path, const char *mode);
    

    该函数接受一个文件名和打开模式,并在成功时返回一个 gzFile 对象。该模式类似于 fopen 函数,但有一个可选的压缩级别。在我们的示例中,我们用 w6 调用 gzopen 以指定压缩级别为 6。

  2. 将数据写入 gzip 文件。这是通过以下函数完成的:

    int gzwrite(gzFile file, voidpc buf, unsigned len);
    

    此函数将未压缩数据写入压缩文件中。输入参数file指的是压缩文件,buf指的是未压缩数据缓冲区,而len表示要写入的字节数。函数返回实际写入的未压缩数据数量。

  3. 关闭gzip文件。这是通过以下函数完成的:

    int ZEXPORT    gzclose(gzFile file);
    

    调用此函数将刷新所有挂起的输出并关闭压缩文件。

  4. 打开文件以供读取。我们向gzopen函数传递了r6

  5. 从压缩文件中读取数据。这是通过gzread函数完成的。

    int gzread(gzFile file, voidp buf, unsigned len);
    

    该函数从文件中读取len个字节到buf中。它返回实际读取的字节数。

    注意

    zlib库支持两种压缩格式,zlibgzipzlib旨在紧凑且快速,因此最适合在内存和通信通道中使用。另一方面,gzip专为文件系统上的单个文件压缩设计,它有一个更大的头部来维护目录信息,并且比zlib使用更慢的校验方法。

为了使用zlib库,我们必须在源代码中包含zlib.h头文件,并在Android.mk中添加以下行以链接到libz.so库:

LOCAL_LDLIBS := -lz

还有更多...

回顾第五章中的管理 Android NDK 的资产一节,Android Native Application API,我们编译了libpng库,它需要zlib库。

我们只介绍了zlib库提供的一些函数。更多信息,您可以参考platforms/android-<version>/arch-arm/usr/include/文件夹中的zlib.hzconf.h头文件。zlib库的详细文档可以在www.zlib.net/manual.html找到。

使用 Android NDK 中的 OpenSL ES 音频库进行音频编程

OpenSL ES 是一个 C 语言级别的应用程序音频库。Android NDK 原生音频 API 基于 OpenSL ES 1.0.1 标准,并带有 Android 特定的扩展。该 API 适用于 Android 2.3 或更高版本,某些功能仅在 Android 4.0 或更高版本上支持。此库中的 API 函数尚未冻结,仍在发展中。此库的未来版本可能需要我们更新代码。本节在 Android 环境下介绍 OpenSL ES API。

准备就绪...

在开始使用 OpenSL ES 编码之前,了解这个库的一些基本知识是至关重要的。OpenSL ES 代表嵌入式系统的开放声音库,它是一个跨平台、免版税、使用 C 语言的应用程序级别 API,供开发者访问嵌入式系统的音频功能。该库规范定义了如音频播放和录制、音频效果和控制、2D 和 3D 音频、高级 MIDI 等功能。根据支持的功能,OpenSL ES 定义了三个配置文件,包括电话、音乐和游戏。

然而,Android 原生音频 API 并不符合这三个配置文件中的任何一个,因为它没有实现任何配置文件中的所有功能。此外,Android 实现了一些特定于 Android 的功能,例如 Android 缓冲队列。关于在 Android 上支持的功能的详细描述,我们可以参考随 Android NDK 提供的docs/opensles/文件夹下的 OpenSL ES for Android 文档。

尽管 OpenSL ES API 是用 C 语言实现的,但它通过基于对象和接口构建库,采用了面向对象的方法:

  • 对象:对象是一组资源和它们状态的抽象。每个对象在创建时都会分配一个类型,而类型决定了对象可以执行的任务集合。这类似于 C++中的类概念。

  • 接口:接口是一组对象可以提供的特性的抽象。这些特性以一组方法和每种接口类型的精确特性集合的形式暴露给我们。在代码中,接口类型通过接口 ID 来识别。

需要注意的是,对象在代码中没有实际的表现形式。我们通过接口改变对象的状态和访问其特性。一个对象可以有一个或多个接口实例。然而,一个单一对象的两个实例不能是同一类型。此外,给定的接口实例只能属于一个对象。这种关系可以如下所示的关系图进行说明:

准备就绪...

如图中所示,对象 1 和对象 2 具有不同的类型,因此暴露了不同的接口。对象 1 有三个接口实例,所有实例类型都不同。而对象 2 有另外两个不同类型的接口实例。注意对象 1 的接口 2 和对象 2 的接口 4 具有相同的类型,这意味着对象 1 和对象 2 都支持通过 Interface Type B 的接口暴露的特性。

如何操作...

以下步骤描述了如何使用原生音频库创建一个简单的 Android 应用程序以录制和播放音频:

  1. 创建一个名为OpenSLESDemo的 Android 应用程序。将包名设置为cookbook.chapter7.opensles。更多详细说明请参考第二章的加载本地库和注册本地方法部分,Java Native Interface

  2. 右键点击项目OpenSLESDemo,选择Android Tools | Add Native Support

  3. cookbook.chapter7.opensles包中添加一个名为MainActivity.java的 Java 文件。这个 Java 文件仅加载本地库OpenSLESDemo,并调用本地方法来录制和播放音频。

  4. jni文件夹中添加mylog.hcommon.hplay.crecord.cOpenSLESDemo.cpp文件。play.crecord.cOpenSLESDemo.cpp文件中的一部分代码在以下代码片段中展示。

    record.c包含创建音频录音器对象并录制音频的代码。

    createAudioRecorder创建并实现一个音频播放器对象,并获得录音和缓冲队列接口:

    jboolean createAudioRecorder() {
       SLresult result;
       SLDataLocator_IODevice loc_dev = {SL_DATALOCATOR_IODEVICE, SL_IODEVICE_AUDIOINPUT, SL_DEFAULTDEVICEID_AUDIOINPUT, NULL};
       SLDataSource audioSrc = {&loc_dev, NULL};
       SLDataLocator_AndroidSimpleBufferQueue loc_bq = {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 1};
       SLDataFormat_PCM format_pcm = {SL_DATAFORMAT_PCM, 1, SL_SAMPLINGRATE_16,
           SL_PCMSAMPLEFORMAT_FIXED_16, SL_PCMSAMPLEFORMAT_FIXED_16,
           SL_SPEAKER_FRONT_CENTER, SL_BYTEORDER_LITTLEENDIAN};
       SLDataSink audioSnk = {&loc_bq, &format_pcm};
       const SLInterfaceID id[1] = {SL_IID_ANDROIDSIMPLEBUFFERQUEUE};
       const SLboolean req[1] = {SL_BOOLEAN_TRUE};
       result = (*engineEngine)->CreateAudioRecorder(engineEngine, &recorderObject, &audioSrc,
               &audioSnk, 1, id, req);
         result = (*recorderObject)->Realize(recorderObject, SL_BOOLEAN_FALSE);
       result = (*recorderObject)->GetInterface(recorderObject, SL_IID_RECORD, &recorderRecord);
       result = (*recorderObject)->GetInterface(recorderObject, SL_IID_ANDROIDSIMPLEBUFFERQUEUE, &recorderBufferQueue);
       result = (*recorderBufferQueue)->RegisterCallback(recorderBufferQueue, bqRecorderCallback, NULL);
       return JNI_TRUE;
    }
    

    startRecording将缓冲区入队以存储录音音频,并将音频对象状态设置为录音状态:

    void startRecording() {
       SLresult result;
       recordF = fopen("/sdcard/test.pcm", "wb");
       result = (*recorderRecord)->SetRecordState(recorderRecord, SL_RECORDSTATE_STOPPED);
       result = (*recorderBufferQueue)->Clear(recorderBufferQueue);
       recordCnt = 0;
       result = (*recorderBufferQueue)->Enqueue(recorderBufferQueue, recorderBuffer,
               RECORDER_FRAMES * sizeof(short));
       result = (*recorderRecord)->SetRecordState(recorderRecord, SL_RECORDSTATE_RECORDING);
    }
    

    每当缓冲队列准备好接受新的数据块时,就会调用bqRecorderCallback回调方法。这发生在缓冲区填满音频数据时:

    void bqRecorderCallback(SLAndroidSimpleBufferQueueItf bq, void *context) {
       int numOfRecords = fwrite(recorderBuffer, sizeof(short), RECORDER_FRAMES, recordF);
       fflush(recordF);
       recordCnt++;
       SLresult result;
       if (recordCnt*5 < RECORD_TIME) {
        result = (*recorderBufferQueue)->Enqueue(recorderBufferQueue, recorderBuffer,
            RECORDER_FRAMES * sizeof(short));
       } else {
        result = (*recorderRecord)->SetRecordState(recorderRecord, SL_RECORDSTATE_STOPPED);
        if (SL_RESULT_SUCCESS == result) {
          fclose(recordF);
        }
       }
    }
    

    play.c包含创建音频播放器对象并播放音频的代码。

    createBufferQueueAudioPlayer创建并实现一个从缓冲队列播放音频的音频播放器对象:

    void createBufferQueueAudioPlayer() {
       SLresult result;
       SLDataLocator_AndroidSimpleBufferQueue loc_bufq = {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 1};
       SLDataFormat_PCM format_pcm = {SL_DATAFORMAT_PCM, 1, SL_SAMPLINGRATE_16,
           SL_PCMSAMPLEFORMAT_FIXED_16, SL_PCMSAMPLEFORMAT_FIXED_16,
           SL_SPEAKER_FRONT_CENTER, SL_BYTEORDER_LITTLEENDIAN};
       SLDataSource audioSrc = {&loc_bufq, &format_pcm};
       SLDataLocator_OutputMix loc_outmix = {SL_DATALOCATOR_OUTPUTMIX, outputMixObject};
       SLDataSink audioSnk = {&loc_outmix, NULL};
       const SLInterfaceID ids[3] = {SL_IID_BUFFERQUEUE, SL_IID_EFFECTSEND, SL_IID_VOLUME};
       const SLboolean req[3] = {SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE};
       result = (*engineEngine)->CreateAudioPlayer(engineEngine, &bqPlayerObject, &audioSrc, &audioSnk, 3, ids, req);
       result = (*bqPlayerObject)->Realize(bqPlayerObject, SL_BOOLEAN_FALSE);
       result = (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_PLAY, &bqPlayerPlay);
       result = (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_BUFFERQUEUE,
               &bqPlayerBufferQueue);
       result = (*bqPlayerBufferQueue)->RegisterCallback(bqPlayerBufferQueue, bqPlayerCallback, NULL);
       result = (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_EFFECTSEND,
               &bqPlayerEffectSend);
       result = (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_VOLUME, &bqPlayerVolume);
    }
    

    startPlayingtest.cpm文件填充缓冲区数据并开始播放:

    jboolean startPlaying() {
      SLresult result;
      recordF = fopen("/sdcard/test.pcm", "rb");
      noMoreData = 0;
      int numOfRecords = fread(recorderBuffer, sizeof(short), RECORDER_FRAMES, recordF);
      if (RECORDER_FRAMES != numOfRecords) {
        if (numOfRecords <= 0) {
          return JNI_TRUE;
        }
        noMoreData = 1;
      }   
    result = (*bqPlayerBufferQueue)->Enqueue(bqPlayerBufferQueue, recorderBuffer, RECORDER_FRAMES * sizeof(short));
      result = (*bqPlayerPlay)->SetPlayState(bqPlayerPlay, SL_PLAYSTATE_PLAYING);
      return JNI_TRUE;
    }
    

    bqPlayerCallback每次缓冲队列准备好接受新的缓冲区时,都会调用这个回调方法。这发生在缓冲区播放完毕时:

    void bqPlayerCallback(SLAndroidSimpleBufferQueueItf bq, void *context) {
       if (!noMoreData) {
            SLresult result;
    int numOfRecords = fread(recorderBuffer, sizeof(short), RECORDER_FRAMES, recordF);
      if (RECORDER_FRAMES != numOfRecords) {
        if (numOfRecords <= 0) {
          noMoreData = 1;
          (*bqPlayerPlay)->SetPlayState(bqPlayerPlay, SL_PLAYSTATE_STOPPED);
          fclose(recordF);
          return;
        }
        noMoreData = 1;
      } 
      result = (*bqPlayerBufferQueue)->Enqueue(bqPlayerBufferQueue, recorderBuffer,  RECORDER_FRAMES * sizeof(short));
       } else {
         (*bqPlayerPlay)->SetPlayState(bqPlayerPlay, SL_PLAYSTATE_STOPPED);
         fclose(recordF);
       }
    }
    

    OpenSLESDemo.cpp文件包含创建 OpenSL ES 引擎对象、释放对象以及注册本地方法的代码:

    naCreateEngine创建引擎对象并输出混合对象。

    void naCreateEngine(JNIEnv* env, jclass clazz) {
       SLresult result;
       result = slCreateEngine(&engineObject, 0, NULL, 0, NULL, NULL);
       result = (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);
       result = (*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineEngine);
       const SLInterfaceID ids[1] = {SL_IID_ENVIRONMENTALREVERB};
       const SLboolean req[1] = {SL_BOOLEAN_FALSE};
       result = (*engineEngine)->CreateOutputMix(engineEngine, &outputMixObject, 1, ids, req);
       result = (*outputMixObject)->Realize(outputMixObject, SL_BOOLEAN_FALSE);
       result = (*outputMixObject)->GetInterface(outputMixObject, SL_IID_ENVIRONMENTALREVERB,
               &outputMixEnvironmentalReverb);
       if (SL_RESULT_SUCCESS == result) {
            result = (*outputMixEnvironmentalReverb)->SetEnvironmentalReverbProperties(
                   outputMixEnvironmentalReverb, &reverbSettings);
       }
    }
    
  5. AndroidManifest.xml文件中添加以下权限。

    <uses-permission android:name="android.permission.RECORD_AUDIO"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"></uses-permission>
    
  6. jni文件夹中添加一个Android.mk文件,内容如下:

    LOCAL_PATH := $(call my-dir)
    include $(CLEAR_VARS)
    LOCAL_MODULE    := OpenSLESDemo
    LOCAL_SRC_FILES := OpenSLESDemo.cpp record.c play.c
    LOCAL_LDLIBS := -llog
    LOCAL_LDLIBS    += -lOpenSLES
    include $(BUILD_SHARED_LIBRARY)
    
  7. 构建并运行 Android 项目,并使用以下命令监控logcat输出:

    $ adb logcat -v time OpenSLESDemo:I *:S
    
  8. 应用的 GUI 如下截图所示:如何操作...

    • 我们可以通过点击录音按钮开始音频录音。录音将持续 15 秒。logcat输出将如下截图所示:如何操作...

    • 录音完成后,将在 Android 设备上创建一个/sdcard/test.pcm文件。我们可以点击播放按钮来播放音频文件。logcat输出将如下截图所示:

    如何操作...

工作原理...

本示例项目展示了如何使用 OpenSL ES 音频库。我们首先解释一些关键概念,然后描述我们是如何使用录音和播放 API 的。

对象创建

对象在代码中没有实际的表现形式,对象的创建是通过接口完成的。每个创建对象的方法都返回一个SLObjectInf接口,该接口可用于执行对象的基本操作并访问对象的其它接口。对象创建的步骤如下所述:

  1. 创建一个引擎对象。引擎对象是 OpenSL ES API 的入口点。创建引擎对象是通过全局函数slCreateEngine()完成的,该函数返回一个SLObjectItf接口。

  2. 实现引擎对象。在对象被实现之前,不能使用该对象。我们将在下一节详细讨论这一点。

  3. 通过SLObjectItf接口的GetInterface()方法获取引擎对象的SLEngineItf接口。

  4. 调用SLEngineItf接口提供的对象创建方法。成功后,将返回新创建对象的SLObjectItf接口。

  5. 实现新创建的对象。

  6. 通过对象的SLObjectItf接口操作创建的对象或访问其他接口。

  7. 完成对象操作后,调用SLObjectItf接口的Destroy()方法来释放对象及其资源。

在我们的示例项目中,我们在OpenSLESDemo.cppnaCreateEngine函数中创建了并实现了引擎对象,并获得了SLEngineItf接口。然后,我们调用了SLEngineItf接口暴露的CreateAudioRecorder()方法,在record.ccreateAudioRecorder函数中创建了一个音频录音对象。在同一个函数中,我们还实现了录音对象,并通过对象创建时返回的SLObjectItf接口访问了对象的其他几个接口。完成录音对象后,我们调用了Destroy()方法来释放对象及其资源,如OpenSLESDemo.cpp中的naShutdown函数所示。

在对象创建时需要注意的另一件事是接口请求。对象创建方法通常接受与接口相关的三个参数,如SLEngineItf接口的CreateAudioPlayer方法所示,以下代码片段展示了这一点:

SLresult (*CreateAudioPlayer) (
SLEngineItf self,
SLObjectItf * pPlayer,
SLDataSource *pAudioSrc,
SLDataSink *pAudioSnk,
SLuint32 numInterfaces,
const SLInterfaceID * pInterfaceIds,
const SLboolean * pInterfaceRequired
);

最后三个输入参数与接口相关。numInterfaces参数表示我们请求访问的接口数量。pInterfaceIds是一个包含numInterfaces接口 ID 的数组,表示对象应该支持的接口类型。pInterfaceRequired是一个SLboolean数组,指定请求的接口是可选的还是必需的。在我们的音频播放器示例中,我们调用了CreateAudioPlayer方法来请求三种类型的接口(分别由SL_IID_BUFFERQUEUESL_IID_EFFECTSENDSL_IID_VOLUME表示的SLAndroidSimpleBufferQueueItfSLEffectSendItfSLVolumeItf)。由于req数组中的所有元素都是true,因此所有接口都是必需的。如果对象无法提供任何接口,对象创建将失败:

const SLInterfaceID ids[3] = {SL_IID_BUFFERQUEUE, SL_IID_EFFECTSEND, SL_IID_VOLUME};
const SLboolean req[3] = {SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE,  SL_BOOLEAN_TRUE};
result = (*engineEngine)->CreateAudioPlayer(engineEngine, &bqPlayerObject, &audioSrc, &audioSnk, 3, ids, req);

请注意,一个对象可以具有隐式和显式接口。隐式接口对类型的每个对象都是可用的。例如,SLObjectItf接口是所有类型所有对象的隐式接口。在对象创建方法中,不需要请求隐式接口。然而,如果我们想要访问一些显式接口,必须在方法中请求它们。

有关接口的更多信息,请参考OpenSL ES 1.0.1 Specification文档中的第 3.1.6 节对象与接口之间的关系

改变对象的状态

对象创建方法创建一个对象并将其置于未实现状态。在这种状态下,对象的资源尚未分配,因此无法使用。

我们需要调用对象的SLObjectItf接口的Realize()方法,使对象过渡到实现状态,在该状态下分配资源并且可以访问接口。

一旦我们完成了对象操作,我们调用Destroy()方法来释放对象及其资源。这个调用内部将对象转移到未实现阶段,在该阶段释放资源。因此,在释放对象本身之前,首先释放资源。

在这个食谱中,我们使用我们的示例项目展示了录制和播放 API。

使用和构建 OpenSL ES 音频库

为了调用 API 函数,我们必须向我们的代码中添加以下行:

#include <SLES/OpenSLES.h>

如果我们也使用安卓特有的功能,我们应该包含另一个头文件:

#include <SLES/OpenSLES_Android.h>

Android.mk文件中,我们必须添加以下行以链接到本地 OpenSL ES 音频库:

LOCAL_LDLIBS += libOpenSLES

OpenSL ES 音频录制

因为 MIME 数据格式和SLAudioEncoderItf接口对安卓上的音频录音机不可用,我们只能以 PCM 格式录制音频。我们的示例展示了如何以 PCM 格式录制音频并将数据保存到文件中。这可以用以下图示说明:

OpenSL ES 音频录制

record.ccreateAudioRecorder函数中,我们创建并实现了音频录音对象。我们将音频输入设置为数据源,将安卓缓冲队列设置为数据接收端。注意,我们注册了bqRecorderCallback函数作为缓冲队列的回调函数。每当缓冲队列准备好新的缓冲区时,将调用bqRecorderCallback函数将缓冲区数据保存到test.cpm文件中,并将缓冲区重新入队以录制新的音频数据。在startRecording函数中,我们开始录音。

注意事项

OpenSL ES 中的回调函数是从内部非应用程序线程执行的。这些线程不由 Dalvik VM 管理,因此它们无法访问 JNI。这些线程对 OpenSL ES 实现至关重要,因此回调函数不应该阻塞或执行任何繁重的处理任务。

如果当回调函数被触发时我们需要执行繁重任务,我们应该发布一个事件给另一个线程来处理这些任务。

这同样适用于我们将在下一个食谱中介绍的 OpenMAX AL 库。更详细的信息可以从 NDK OpenSL ES 文档的docs/opensles/文件夹中获得。

OpenSL ES 音频播放

安卓的 OpenSL ES 库为音频播放提供了许多功能。我们可以播放编码的音频文件,包括 mp3、aac 等。我们的示例展示了如何播放 PCM 音频。这可以如下所示图示:

OpenSL ES 音频播放

我们在 OpenSLESDemo.cppnaCreateEngine 函数中创建了引擎对象和输出混合对象。在 play.ccreateBufferQueueAudioPlayer 函数中创建了音频播放器对象,以 Android 缓冲队列作为数据源和输出混合对象作为数据接收器。通过 SLAndroidSimpleBufferQueueItf 接口注册了 bqPlayerCallback 函数作为回调方法。每当播放器播放完一个缓冲区,缓冲队列就准备好接收新数据,此时会调用回调函数 bqPlayerCallback。该方法从 test.pcm 文件读取数据到缓冲区并将其入队。

startPlaying 函数中,我们将初始数据读取到缓冲区,并将播放器状态设置为 SL_PLAYSTATE_PLAYING

还有更多...

OpenSL ES 是一个复杂的库,其规范超过 500 页。在开发 OpenSL ES 应用程序时,规范是一个很好的参考,它可以通过 Android NDK 获得。

Android NDK 还附带了一个本地音频示例,演示了更多 OpenSL ES 函数的使用。

在 Android NDK 中使用 OpenMAX AL 多媒体库进行编程

OpenMAX AL 是一个用 C 语言编写的应用层多媒体库。Android NDK 多媒体 API 基于 OpenMAX AL 1.0.1 标准,并带有 Android 特定的扩展。该 API 可用于 Android 4.0 或更高版本。需要注意的是,API 正在不断发展,Android NDK 团队提到,未来版本的 OpenMAX AL API 可能会要求开发者更改他们的代码。

准备就绪...

在开始使用 OpenMAX AL 库进行编程之前,了解一些关于库的基础知识是很重要的。我们将在以下文本中简要描述该库。

OpenMAX AL 指的是 Open Media AccelerationOpenMAX)库的应用层接口。它是一个免版税、跨平台、使用 C 语言的 应用层 API,供开发者创建多媒体应用程序。其主要特性包括媒体记录、媒体播放、媒体控制(例如,亮度控制)和效果。与 OpenSL ES 库相比,OpenMAX AL 提供了视频和音频的功能,但它缺少 OpenSL ES 可以提供的某些音频功能,如 3D 音频和音频效果。某些应用程序可能需要同时使用这两个库。

OpenMAX AL 定义了两个配置文件,分别是媒体播放和媒体播放器/记录器。Android 并没有实现这两个配置文件所需的所有功能,因此 Android 中的 OpenMAX AL 库不符合任何一个配置文件。此外,Android 还实现了一些特定于 Android 的功能。

Android OpenMAX AL 实现提供的主要功能是处理 MPEG-2 传输流的能力。我们可以对流进行解复用,解码视频和音频,并将它们作为音频输出或渲染到手机屏幕。这个库允许我们在将媒体数据传递以供展示之前完全控制它。例如,我们可以在渲染视频数据之前调用 OpenGL ES 函数以应用图形效果。

要了解 Android 支持的内容,我们可以参考随 Android NDK 提供的 OpenMAX AL for Android 文档,位于 docs/openmaxal/ 文件夹中。

OpenMAX AL 库的设计与 OpenSL ES 库类似。它们都采用面向对象的方法,基本概念包括对象和接口都是相同的。读者应参考之前的食谱以获取这些概念的详细解释。

如何操作...

以下步骤描述了如何使用 OpenMAX AL 函数创建一个简单的 Android 视频播放应用程序:

  1. 创建一个名为 OpenMAXSLDemo 的 Android 应用程序。将包名设置为 cookbook.chapter7.openmaxsldemo。有关更详细的说明,请参考 第二章,Java 本地接口中的加载本地库和注册本地方法食谱。

  2. 右键点击项目 OpenMAXSLDemo,选择 Android Tools | 添加本地支持

  3. 在包 cookbook.chapter7.openmaxsldemo 中添加一个名为 MainActivity.java 的 Java 文件。这个 Java 文件加载本地库 OpenMAXSLDemo,设置视图,并调用本地方法来播放视频。

  4. jni 文件夹中添加 mylog.hOpenMAXSLDemo.c 文件。OpenMAXSLDemo.c 的一部分代码在以下代码片段中显示。

    naCreateEngine 创建并实现引擎对象和输出混合对象。

    void naCreateEngine(JNIEnv* env, jclass clazz) {
       XAresult res;
       res = xaCreateEngine(&engineObject, 0, NULL, 0, NULL, NULL);
       res = (*engineObject)->Realize(engineObject, XA_BOOLEAN_FALSE);
       res = (*engineObject)->GetInterface(engineObject, XA_IID_ENGINE, &engineEngine);
       res = (*engineEngine)->CreateOutputMix(engineEngine, &outputMixObject, 0, NULL, NULL);
       res = (*outputMixObject)->Realize(outputMixObject, XA_BOOLEAN_FALSE);
    }
    

    naCreateStreamingMediaPlayer 创建并实现具有数据源和数据接收器的媒体播放器对象。它获取缓冲队列接口,并将 AndroidBufferQueueCallback 函数注册为回调函数。回调函数将在处理完缓冲区后被调用:

    jboolean naCreateStreamingMediaPlayer(JNIEnv* env, jclass clazz, jstring filename) {
       XAresult res;
       const char *utf8FileName = (*env)->GetStringUTFChars(env, filename, NULL);
       file = fopen(utf8FileName, "rb");
       XADataLocator_AndroidBufferQueue loc_abq = { XA_DATALOCATOR_ANDROIDBUFFERQUEUE, NB_BUFFERS };
       XADataFormat_MIME format_mime = {XA_DATAFORMAT_MIME, XA_ANDROID_MIME_MP2TS, XA_CONTAINERTYPE_MPEG_TS };
       XADataSource dataSrc = {&loc_abq, &format_mime};
       XADataLocator_OutputMix loc_outmix = { XA_DATALOCATOR_OUTPUTMIX, outputMixObject };
       XADataSink audioSnk = { &loc_outmix, NULL };
       XADataLocator_NativeDisplay loc_nd = {XA_DATALOCATOR_NATIVEDISPLAY,       
               (void*)theNativeWindow, NULL};
       XADataSink imageVideoSink = {&loc_nd, NULL};
       XAboolean required[NB_MAXAL_INTERFACES] = {XA_BOOLEAN_TRUE, XA_BOOLEAN_TRUE};
       XAInterfaceID iidArray[NB_MAXAL_INTERFACES] = {XA_IID_PLAY, XA_IID_ANDROIDBUFFERQUEUESOURCE};
       res = (*engineEngine)->CreateMediaPlayer(engineEngine, &playerObj, &dataSrc, NULL,   &audioSnk, &imageVideoSink, NULL, NULL, NB_MAXAL_INTERFACES, iidArray, required );
       (*env)->ReleaseStringUTFChars(env, filename, utf8FileName);
       res = (*playerObj)->Realize(playerObj, XA_BOOLEAN_FALSE);
       res = (*playerObj)->GetInterface(playerObj, XA_IID_PLAY, &playerPlayItf);
       res = (*playerObj)->GetInterface(playerObj, XA_IID_ANDROIDBUFFERQUEUESOURCE, &playerBQItf);
       res = (*playerBQItf)->SetCallbackEventsMask(playerBQItf, XA_ANDROIDBUFFERQUEUEEVENT_PROCESSED);
       res = (*playerBQItf)->RegisterCallback(playerBQItf, AndroidBufferQueueCallback, NULL);
       if (!enqueueInitialBuffers(JNI_FALSE)) {
           return JNI_FALSE;
       }
       res = (*playerPlayItf)->SetPlayState(playerPlayItf, XA_PLAYSTATE_PAUSED);
       res = (*playerPlayItf)->SetPlayState(playerPlayItf, XA_PLAYSTATE_PLAYING);
       return JNI_TRUE;
    }
    

    AndroidBufferQueueCallback 是注册的回调函数,用于用媒体数据重新填充缓冲区或处理命令:

    XAresult AndroidBufferQueueCallback(XAAndroidBufferQueueItf caller, void *pCallbackContext, void *pBufferContext,  void *pBufferData, XAuint32 dataSize,  XAuint32 dataUsed, const XAAndroidBufferItem *pItems, XAuint32 itemsLength) {
       XAresult res;
       int ok;
       ok = pthread_mutex_lock(&mutex);
       if (discontinuity) {
           if (!reachedEof) {
               res = (*playerBQItf)->Clear(playerBQItf);
               rewind(file);
                (void) enqueueInitialBuffers(JNI_TRUE);
           }
           discontinuity = JNI_FALSE;
           ok = pthread_cond_signal(&cond);
           goto exit;
       }
       if ((pBufferData == NULL) && (pBufferContext != NULL)) {
           const int processedCommand = *(int *)pBufferContext;
           if (kEosBufferCntxt == processedCommand) {
               goto exit;
           }
       }
       if (reachedEof) {
           goto exit;
       }
       size_t nbRead;
       size_t bytesRead;
       bytesRead = fread(pBufferData, 1, BUFFER_SIZE, file);
       if (bytesRead > 0) {
           if ((bytesRead % MPEG2_TS_PACKET_SIZE) != 0) {
               LOGI(2, "Dropping last packet because it is not whole");
           }
           size_t packetsRead = bytesRead / MPEG2_TS_PACKET_SIZE;
           size_t bufferSize = packetsRead * MPEG2_TS_PACKET_SIZE;
           res = (*caller)->Enqueue(caller, NULL, pBufferData, bufferSize, NULL, 0);
       } else {
           XAAndroidBufferItem msgEos[1];
           msgEos[0].itemKey = XA_ANDROID_ITEMKEY_EOS;
           msgEos[0].itemSize = 0;
           res = (*caller)->Enqueue(caller, (void *)&kEosBufferCntxt, NULL, 0, msgEos, sizeof(XAuint32)*2);
           reachedEof = JNI_TRUE;
       }
    exit:
       ok = pthread_mutex_unlock(&mutex);
       return XA_RESULT_SUCCESS;
    }
    
  5. jni 文件夹中添加一个 Android.mk 文件,内容如下:

    LOCAL_PATH := $(call my-dir)
    include $(CLEAR_VARS)
    LOCAL_MODULE    := OpenMAXSLDemo
    LOCAL_SRC_FILES := OpenMAXSLDemo.c
    LOCAL_LDLIBS := -llog
    LOCAL_LDLIBS    += -landroid
    LOCAL_LDLIBS    += -lOpenMAXAL
    include $(BUILD_SHARED_LIBRARY)
    
  6. 我们可以使用 samples/native-media/ 目录中可用的 NativeMedia.ts 视频文件进行测试。以下命令可以将视频文件放入测试 Android 设备的 /sdcard/ 目录中:

    $ adb push NativeMedia.ts /sdcard/
    
  7. 构建并启动 Android 应用程序。我们可以看到如下截图所示的 GUI:如何操作...

    我们可以按下 播放 开始播放视频。

它是如何工作的...

在此食谱中,我们使用了 OpenMAX AL 库来实现一个简单的视频播放器。

使用 OpenMAX AL 多媒体库进行构建和使用:

为了调用 API 函数,我们必须在代码中添加以下行:

#include <OMXAL/OpenMAXAL.h>

如果我们也在使用 Android 特定的功能,我们应该包含另一个头文件:

#include <OMXAL/OpenMAXAL_Android.h>

Android.mk 文件中,我们必须添加以下行以链接到 OpenMAX AL 多媒体库:

LOCAL_LDLIBS += libOpenMAXAL

OpenMAX AL 视频播放

我们的示例项目是随 Android NDK 一起提供的原生媒体项目的简化版本。下图说明了应用程序的工作原理:

OpenMAX AL 视频播放

在我们的代码中,在 naCreateEngine 函数中创建并实现了引擎和输出混合对象。在 naCreateStreamingMediaPlayerfunction 函数中,我们创建并实现了媒体播放器对象,将音频数据接收器设置为输出混合,视频数据接收器设置为本地显示,数据源设置为 Android 缓冲队列。

当一个缓冲区被消耗时,会调用回调函数 AndroidBufferQueueCallback,我们在其中用 NativeMedia.ts 文件中的数据重新填充缓冲区,并将其加入缓冲队列。

还有更多……

OpenMAX AL 是一个复杂的库。在开发具有 OpenMAX AL 的应用程序时,规范是一个很好的参考,并且它随 Android NDK 一起提供。Android NDK 还附带了一个原生媒体示例,这个示例很好地展示了如何使用 API。

第八章:使用 Android NDK 移植和使用现有库

在本章中,我们将介绍以下食谱:

  • 使用 Android NDK 构建系统将库作为共享库模块移植

  • 使用 Android NDK 构建系统将库作为静态库模块移植

  • 使用 Android NDK 工具链移植使用现有构建系统的库

  • 将库作为预构建库使用

  • 在多个项目中使用 import-module 引入库

  • 移植需要 RTTI、异常和 STL 支持的库

引言

对于桌面计算领域有许多 C/C++库。如果我们能在 Android 平台上重用它们,这些库可以为我们节省大量的努力。Android NDK 使这成为可能。在本章中,我们将讨论如何使用 NDK 将现有库移植到 Android。

我们将首先介绍如何使用 Android NDK 构建系统构建库。我们可以将库构建为静态库模块或共享库模块。本章将讨论这两种方式的区别。

我们还可以将 Android NDK 工具链作为独立的交叉编译器使用,这将在下一节介绍。然后,我们将描述如何使用编译后的库作为预构建模块。

我们经常在多个 Android 项目中使用同一个库。我们可以使用 import-module 功能将相同的库模块链接到多个项目,同时保持库的单个副本。

许多 C++库需要 STL、C++异常和运行时类型信息RTTI)的支持,这些在 Android 默认的 C++运行时库中是不可用的。我们将通过使用流行的 boost 库作为示例,说明如何启用这些支持。

使用 Android NDK 构建系统将库作为共享库模块移植

本食谱将讨论如何使用 Android NDK 构建系统将现有库作为一个共享库进行移植。我们将以开源的 libbmp 库为例。

准备工作

建议读者在阅读本节之前先阅读第三章中的在命令行构建 Android NDK 应用程序食谱,构建和调试 NDK 应用程序

如何操作...

以下步骤描述了如何创建我们的示例 Android 项目,演示如何将 libbmp 库作为共享库进行移植:

  1. 创建一个名为 PortingShared 的 Android 应用程序,并具有本地支持。将包名设置为 cookbook.chapter8.portingshared。如果你需要更详细的说明,请参考第二章中的加载本地库和注册本地方法食谱,Java Native Interface

  2. cookbook.chapter8.portingshared 包下添加一个 Java 文件 MainActivity.java。这个 Java 文件简单加载共享库 .bmpPortingShared,并调用本地方法 naCreateABmp

  3. code.google.com/p/libbmp/downloads/list下载libbmp库,并解压存档文件。在jni文件夹下创建一个名为libbmp的文件夹,并将提取的文件夹中的src/bmpfile.csrc/bmpfile.h文件复制到libbmp文件夹。

  4. 如果您使用的是 NDK r8 及以下版本,请从bmpfile.h中删除以下代码:

    #ifndef uint8_t
    typedef unsigned char uint8_t;
    #endif
    #ifndef uint16_t
    typedef unsigned short uint16_t;
    #endif
    #ifndef uint32_t
    typedef unsigned int uint32_t;
    #endif
    
  5. 然后,添加以下代码行:

    #include <stdint.h>
    

    注意

    bmpfile.h的代码更改仅适用于 Android NDK r8 及以下版本。编译库将返回错误"error: redefinition of typedef 'uint8_t'"。这是 NDK 构建系统中的一个错误,因为uint8_t的定义被#ifndef预处理指令包围。从 NDK r8b 开始,这个问题已被修复,如果我们使用 r8b 及以上版本,则无需更改代码。

  6. libbmp文件夹下创建一个Android.mk文件,以将libbmp编译为共享库libbmp.so。此Android.mk文件的内容如下:

    LOCAL_PATH := $(call my-dir)
    include $(CLEAR_VARS)
    LOCAL_MODULE    := libbmp
    LOCAL_SRC_FILES := bmpfile.c
    include $(BUILD_SHARED_LIBRARY)
    
  7. jni文件夹下创建另一个名为libbmptest的文件夹。在其下添加mylog.hPortingShared.c文件。PortingShared.c实现了本地方法naCreateABmp,该方法使用libbmp库中定义的函数来创建位图图像并将其保存到/sdcard/test_shared.bmp。如果您的设备上没有/sdcard目录,您需要更改目录:

    void naCreateABmp(JNIEnv* env, jclass clazz, jint width, jint height, jint depth) {
      bmpfile_t *bmp;
      int i, j;
      rgb_pixel_t pixel = {128, 64, 0, 0};
      for (i = 10, j = 10; j < height; ++i, ++j) {
        bmp_set_pixel(bmp, i, j, pixel);
        pixel.red++;
        pixel.green++;
        pixel.blue++;
        bmp_set_pixel(bmp, i + 1, j, pixel);
        bmp_set_pixel(bmp, i, j + 1, pixel);
      }
      bmp_save(bmp, "/sdcard/test_shared.bmp");
      bmp_destroy(bmp);
    }
    
  8. libbmptest文件夹下创建另一个Android.mk文件,以将PortingShared.c文件编译为另一个共享库libPortingShared.so。此Android.mk文件的内容如下:

    LOCAL_PATH := $(call my-dir)
    include $(CLEAR_VARS)
    LOCAL_MODULE    := PortingShared
    LOCAL_C_INCLUDES := $(LOCAL_PATH)/../libbmp/
    LOCAL_SRC_FILES := PortingShared.c
    LOCAL_SHARED_LIBRARIES := libbmp
    LOCAL_LDLIBS := -llog
    include $(BUILD_SHARED_LIBRARY)
    
  9. jni文件夹下创建一个Android.mk文件,内容如下:

    LOCAL_PATH := $(call my-dir)
    include $(call all-subdir-makefiles)
    
  10. AndroidManifest.xml文件添加WRITE_EXTERNAL_STORAGE权限,如下所示:

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    
  11. 构建并运行 Android 项目。在 Android 设备的sdcard文件夹中应创建一个位图文件test_shared.bmp。我们可以使用以下命令获取该文件:

    $ adb pull /sdcard/test_shared.bmp .
    

    以下是.bmp文件:

    如何操作...

工作原理...

示例项目演示了如何将libbmp代码作为共享库进行移植并在本地代码PortingShared.c中使用。

共享库:共享库可以被多个可执行文件和库共享。Android 本地代码通常被编译为共享库并由 Java 代码加载。实际上,Android 构建系统只将共享库打包到应用程序的apk文件中。因此,我们必须至少提供一个共享库来包含我们的本地代码。

注意

我们仍然可以使用静态库来生成共享库,正如我们将在将库作为静态库模块与 Android NDK 构建系统的配方中看到的那样。

我们的示例项目构建了两个共享库,分别是libbmp.solibPortingShared.so。我们可以在项目的libs文件夹下找到这些库。libPortingShared.so依赖于libbmp.so,因为PortingShared.c调用了libbmp库中定义的函数。

在我们的 Java 文件中,我们需要在libPortingShared.so之前加载libbmp.so,如下所示:

static {
       System.loadLibrary("bmp");
         System.loadLibrary("PortingShared");
}

理解 Android.mk 文件:Android NDK 提供了一个易于使用的构建系统,使我们免于编写 makefile。然而,我们仍然需要通过Android.mkApplication.mk向系统提供一些基本输入。本节仅讨论Android.mk

Android.mk文件是一个 GNU makefile 片段,它向 Android 构建系统描述源文件。源文件被分组到模块中。每个模块都是一个静态或共享库。Android NDK 提供了一些预定义的变量和宏。这里,我们将简要介绍本节中使用的那些。我们将在后续的菜谱中介绍更多预定义的变量和宏,你也可以参考 Android NDK 的docs/ANDROID-MK.html获取更多信息。

  • CLEAR_VARS:此变量指向一个脚本,它取消定义几乎所有模块描述变量,除了LOCAL_PATH。我们必须在每个新模块之前包含它,如下所示:

    include $(CLEAR_VARS)
    
  • BUILD_SHARED_LIBRARY:此变量指向一个构建脚本,它根据模块描述确定如何从列出的源构建共享库。包含此变量时,我们必须定义LOCAL_MODULELOCAL_SRC_FILES,如下所示:

    include $(BUILD_SHARED_LIBRARY)
    

    包含它将生成共享库lib$(LOCAL_MODULE).so

  • my-dir:必须使用$(call <macro>)来评估它。my-dir宏返回最后一个包含的 makefile 的路径,这通常是包含当前Android.mk文件的目录。它通常用于定义LOCAL_PATH,如下所示:

    LOCAL_PATH := $(call my-dir)
    
  • all-subdir-makefiles:此宏返回当前my-dir路径下所有子目录中的Android.mk文件列表。在我们的示例中,我们在jni目录下的Android.mk文件中使用了这个宏,如下所示:

    include $(call all-subdir-makefiles)
    

    这将包含libbmplibbmptest目录下的两个Android.mk文件。

  • LOCAL_PATH:这是一个模块描述变量,用于定位源文件的路径。它通常与my-dir宏一起使用,如下所示:

    LOCAL_PATH := $(call my-dir)
    
  • LOCAL_MODULE:这是一个模块描述变量,用于定义我们模块的名称。请注意,它必须在所有模块名称中唯一,并且不能包含任何空格。

  • LOCAL_SRC_FILES:这是一个模块描述变量,用于列出构建模块时使用的源文件。注意,这些源文件应该是相对于LOCAL_PATH的路径。

  • LOCAL_C_INCLUDES:这是一个可选的模块描述变量,它提供将附加到编译时包含搜索路径的路径列表。这些路径应该是相对于 NDK 根目录的。在我们的示例项目的libbmptest文件夹下的Android.mk中,我们使用这个变量如下:

    LOCAL_C_INCLUDES := $(LOCAL_PATH)/../libbmp/
    
  • LOCAL_SHARED_LIBRARIES:这是一个可选的模块描述变量,提供当前模块依赖的共享库列表。在libbmptest文件夹下的Android.mk中,我们使用这个变量来包含libbmp.so共享库:

    LOCAL_SHARED_LIBRARIES := libbmp
    
  • LOCAL_LDLIBS:这是一个可选的模块描述变量,提供链接器标志列表。它用于传递带有-l前缀的系统库。在我们的示例项目中,我们使用它来链接系统日志库:

    LOCAL_LDLIBS := -llog
    

有了前面的描述,现在可以很容易地理解我们示例项目中使用的三个Android.mk文件。jni下的Android.mk简单地包含了另外两个Android.mk文件。libbmp文件夹下的Android.mklibbmp源代码编译为共享库libbmp.so,而libbmptest文件夹下的Android.mkPortingShared.c编译为依赖于libbmp.so库的libPortingShared.so共享库。

另请参阅

可以在本地代码中使用共享库,正如我们在第六章的使用 Android NDK 动态链接库进行编程食谱中演示的那样,其他 Android NDK API

使用 Android NDK 构建系统将库作为静态库模块移植

前一个食谱讨论了如何将库作为共享库模块移植,以libbmp库为例。在本食谱中,我们将展示如何将libbmp库作为静态库移植。

准备就绪

建议读者在阅读本食谱之前,先阅读第三章的在命令行构建 Android NDK 应用程序食谱,构建和调试 NDK 应用程序

如何操作...

以下步骤描述了如何创建我们的示例 Android 项目,演示如何将libbmp库作为静态库移植:

  1. 创建一个名为PortingStatic的具有本地支持的 Android 应用程序。将包名设置为cookbook.chapter8.portingstatic。如果你需要更详细的说明,请参考第二章的加载本地库和注册本地方法食谱,Java Native Interface

  2. cookbook.chapter8.portingstatic包下添加一个 Java 文件MainActivity.java。这个 Java 文件简单地加载共享库PortingStatic,并调用本地方法naCreateABmp

  3. 按照第 3 步的使用 Android NDK 构建系统将库作为共享库模块移植食谱下载libbmp库并进行修改。

  4. libbmp文件夹下创建一个Android.mk文件,以编译libbmp为静态库libbmp.a。这个Android.mk文件的内容如下:

    LOCAL_PATH := $(call my-dir)
    include $(CLEAR_VARS)
    LOCAL_MODULE    := libbmp
    LOCAL_SRC_FILES := bmpfile.c
    include $(BUILD_STATIC_LIBRARY)
    
  5. jni文件夹下创建另一个文件夹libbmptest。向其中添加mylog.hPortingStatic.c文件。注意,它的代码与之前章节中的naCreateABmp方法相同,只是.bmp文件名从test_shared.bmp更改为test_static.bmp

  6. libbmptest文件夹下创建另一个Android.mk文件,以编译PortingStatic.c文件作为共享库libPortingStatic.so。这个Android.mk文件的内容如下:

    LOCAL_PATH := $(call my-dir
    include $(CLEAR_VARS)
    LOCAL_MODULE    := PortingStatic
    LOCAL_C_INCLUDES := $(LOCAL_PATH)/../libbmp/
    LOCAL_SRC_FILES := PortingStatic.c
    LOCAL_STATIC_LIBRARIES := libbmp
    LOCAL_LDLIBS := -llog
    include $(BUILD_SHARED_LIBRARY)
    
  7. jni文件夹下创建一个Android.mk文件,内容如下:

    LOCAL_PATH := $(call my-dir)
    include $(call all-subdir-makefiles)
    
  8. AndroidManifest.xml文件添加WRITE_EXTERNAL_STORAGE权限,如下所示:

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    
  9. 构建并运行 Android 项目。应该在 Android 设备的sdcard文件夹中创建位图文件test_static.bmp。我们可以使用以下命令获取该文件:

    $ adb pull /sdcard/test_static.bmp .
    

    这个文件与上一个食谱中使用的test_static.bmp文件相同。

工作原理...

在示例项目中,我们将libbmp构建为静态库libbmp.a,可以在obj/local/armeabi/文件夹下找到。我们在本地代码PortingStatic.c中调用了在libbmp中定义的函数。

静态库仅仅是从源代码编译的对象文件的归档。在 Android NDK 中,它们被构建为以".a"后缀结尾的文件。静态库在构建时由编译器或链接器复制到目标可执行文件或库中。在 Android NDK 中,静态库仅用于构建共享库,因为只有共享库会被打包到apk文件中以便部署。

我们的示例项目构建了一个静态库libbmp.a和一个共享库libPortingStatic.solibPortingStatic.so共享库位于libs/armeabi文件夹下,将被复制到应用程序的apk文件中。libbmp.a库用于构建libPortingStatic.so共享库。如果你使用 Eclipse 项目资源管理器检查libPortingStatic.so库的符号,你会发现libbmp中定义的函数的符号被包含在内。以下截图展示了这一点:

工作原理...

函数bmp_createbmp_destroy等在libbmp中定义,并包含在共享库libPortingStatic.so中。

在我们的 Java 代码中,我们需要使用以下代码加载共享库:

static {
       System.loadLibrary("PortingStatic");
}

理解 Android.mk 文件:上一个食谱已经描述了在这三个Android.mk文件中使用的预定义变量和宏的大部分内容。因此,我们只涉及那些在上一个食谱中没有看到的内容:

  • BUILD_STATIC_LIBRARY:该变量指向一个构建脚本,该脚本将收集模块的信息并确定如何从源代码构建静态库。通常在另一个模块的LOCAL_STATIC_LIBRARIES中列出构建的模块。这个变量通常在Android.mk中如下包含:

    include $(BUILD_STATIC_LIBRARY)
    

    在我们的示例项目中,我们在jni/libbmp文件夹下的Android.mk文件中包含了这个变量。

  • LOCAL_STATIC_LIBRARIES:这是一个模块描述变量,它提供当前模块应链接到的静态库列表。它只在共享库模块中有意义。

    在我们的项目中,我们使用这个变量链接到libbmp.a静态库,如jni/libbmptest/文件夹下的Android.mk文件所示。

    LOCAL_STATIC_LIBRARIES := libbmp
    
  • LOCAL_WHOLE_STATIC_LIBRARIES:这是LOCAL_STATIC_LIBRARIES变量的一个变体。它指示列出的静态库应该作为完整的归档链接。这将强制将静态库中的所有对象文件添加到当前的共享库模块中。

静态库与共享库:现在你已经了解了如何将现有库作为静态库或共享库移植,你可能会问哪个更好。答案可能如你所料,取决于我们的需求。

当你移植一个大型库,并且只使用了库提供的一小部分功能时,静态库是一个好的选择。Android NDK 构建系统可以在构建时解决依赖关系,并且只将最终共享库中使用的那部分复制。这意味着库的大小更小,相应的apk文件大小也更小。

注意事项

有时,我们需要强制将整个静态库构建到最终的共享库中(例如,几个静态库之间存在循环依赖)。我们可以在Android.mk中使用LOCAL_WHOLE_STATIC_LIBRARIES变量或"--whole-archive"链接器标志。

当你需要移植一个将被多个 Android 应用使用的库时,共享库是一个更好的选择。假设你想要构建两个 Android 应用,一个是视频播放器,一个是视频编辑器。这两个应用都需要一个第三方codec库,你可以使用 NDK 将其移植到 Android 上。在这种情况下,你可以将库作为一个共享库单独放在一个apk文件中(例如,MX Player 将codecs库放在单独的apk文件中),这样两个应用可以在运行时加载同一个库。这意味着用户只需下载一次库就可以使用这两个应用。

另一个可能需要共享库的情况是,一个库L被多个共享库使用。如果L是一个静态库,每个共享库将包含其代码的副本,并因代码重复(例如,重复的全局变量)而造成问题。

另请参阅

实际上,我们之前使用 Android NDK 构建系统将一个库作为静态库移植过。回想一下我们在第五章的在 Android NDK 上管理资产菜谱中,如何将libpng作为静态库移植的。

使用 Android NDK 工具链移植带有现有构建系统的库

前两个食谱讨论了如何使用 Android NDK 构建系统移植库。然而,许多开源项目都有自己的构建系统,有时在Android.mk文件中列出所有源文件会很麻烦。幸运的是,Android NDK 工具链也可以作为一个独立的交叉编译器使用,我们可以将交叉编译器用在开源项目的现有构建系统中。这个食谱将讨论如何使用现有的构建系统移植库。

如何操作...

以下步骤描述了如何创建我们的示例项目,该项目展示了如何使用现有的构建系统移植开源libbmp库:

  1. 创建一个名为 PortingWithBuildSystem 的 Android 应用程序,并支持本地原生代码。将包名设置为cookbook.chapter8.portingwithbuildsystem。如果你需要更详细的说明,请参考第二章的加载本地库和注册本地方法食谱,Java Native Interface

  2. cookbook.chapter8.portingwithbuildsystem包下添加一个 Java 文件MainActivity.java。这个 Java 文件简单地加载共享库PortingWithBuildSystem,并调用本地方法naCreateABmp

  3. code.google.com/p/libbmp/downloads/list下载libbmp库,并将归档文件解压到jni文件夹。这将在jni文件夹下创建一个libbmp-0.1.3文件夹,内容如下:如何操作...

  4. 按照食谱将库作为共享库模块与 Android NDK 构建系统一起移植的第 3 步,更新src/bmpfile.h

  5. libbmp-0.1.3文件夹下添加一个 bash shell 脚本文件build_android.sh,内容如下:

    #!/bin/bash
    NDK=<path to Android ndk folder>/android-ndk-r8b
    SYSROOT=$NDK/platforms/android-8/arch-arm/
    CFLAGS="-mthumb"
    LDFLAGS="-Wl,--fix-cortex-a8"
    export CC="$NDK/toolchains/arm-linux-androideabi-4.4.3/prebuilt/linux-x86/bin/arm-linux-androideabi-gcc --sysroot=$SYSROOT"
    ./configure \
       --host=arm-linux-androideabi \
       --disable-shared \
       --prefix=$(pwd) \
       --exec-prefix=$(pwd) 
    make clean
    make 
    make install
    
  6. 使用以下命令为build_android.sh文件添加执行权限:

    $ sudo chmod +x build_android.sh
    
  7. 在命令行终端,转到libbmp-0.1.3目录,输入以下命令来构建库:

    $ ./build_android.sh
    

    构建将会因为以下错误而失败:

    如何操作...

    这是因为libbmp-0.1.3文件夹下的config.guessconfig.sub脚本过时了(这两个文件的第一行表明时间戳是2009-08-19)。我们需要时间戳为2010-05-20或之后的脚本副本。可以在gcc.gnu.org/svn/gcc/branches/cilkplus/config.guess找到config.guess脚本,在gcc.gnu.org/svn/gcc/branches/cilkplus/config.sub找到config.sub脚本。

  8. 再次尝试执行build_android.sh脚本。这次它成功完成了。我们应当在jni/libbmp-0.1.3/lib文件夹下找到libbmp.a静态库,在jni/libbmp-0.1.3/include文件夹下找到bmpfile.h

工作原理...

许多现有的开源库可以通过 shell 命令"./configure; make; make install"来构建。在我们的示例项目中,我们编写了一个build_android.sh脚本来使用 Android NDK 交叉编译器执行这三个步骤。

以下是我们使用 Android NDK 交叉编译器移植库时应该考虑的事项列表:

  1. 选择合适的工具链:根据我们目标设备(ARM、x86 或 MIPS)的 CPU 架构,你需要选择相应的工具链。以下工具链可在 Android NDK r8d 的toolchains文件夹下找到:

    • 对于基于 ARM 的设备arm-linux-androideabi-4.4.3arm-linux-androideabi-4.6arm-linux-androideabi-4.7,以及arm-linux-androideabi-clang3.1

    • 对于基于 MIPS 的设备mipsel-linux-android-4.4.3mipsel-linux-android-4.6mipsel-linux-android-4.7,以及mipsel-linux-android-clang3.1

    • 对于基于 x86 的设备x86-4.4.3x86-4.6x86-4.7,以及x86-clang3.1

  2. 选择 sysroot:根据我们想要针对的 Android 原生 API 级别和 CPU 架构,你需要选择合适的 sysroot。编译器在编译时会查找sysroot目录下的头文件和库。

    sysroot的路径遵循以下格式:

    $NDK/platforms/android-<level>/arch-<arch>/
    

    $NDK指的是 Android NDK 的根目录,<level>指的是 Android API 级别,<arch>表示 CPU 架构。在你的build_android.sh脚本中,SYSROOT定义如下:

    SYSROOT=$NDK/platforms/android-8/arch-arm/
    
  3. 指定交叉编译器:库现有的构建系统通常有一种方法让我们指定交叉编译器。这通常是通过配置选项或环境变量来实现的。

    libbmp中,我们可以输入"./configure --help"命令来了解如何设置编译器。compiler命令是通过环境变量CC指定的,而环境变量CFLAGSLDFLAGS用于指定编译器标志和链接器标志。在你的build_android.sh脚本中,这三个环境变量如下设置:

    export CFLAGS="-mthumb"
    export LDFLAGS="-Wl,--fix-cortex-a8"
    export CC="$NDK/toolchains/arm-linux-androideabi-4.4.3/prebuilt/linux-x86/bin/arm-linux-androideabi-gcc --sysroot=$SYSROOT"
    

    注意

    "-mthumb"编译器标志表示你将使用 thumb 指令集而不是 ARM 指令集。"-wl, --fix-cortex-a8"链接器标志是为了绕过某些 Cortex-A8 实现中的 CPU 错误。

  4. 指定头文件和库二进制文件的输出位置:你通常希望将库放在jni/<library folder>/下。

    libbmp的情况下,库二进制文件安装在PREFIX/lib文件夹下,头文件安装在EPREFIX/include文件夹下。因此,我们通过向配置脚本传递以下选项,将PREFIXEPREFIX设置为jni/libbmp-0.1.3

    --prefix=$(pwd) \
    --exec-prefix=$(pwd)
    
  5. 构建并安装库:你可以简单地执行"make; make install;"来构建和安装库。

还有更多...

在你的build_android.sh脚本中,我们已经禁用了共享库。如果你删除了这行"--disable-shared \",构建将在jni/libbmp-0.1.3/lib/文件夹下生成共享库(libbmp.so)和静态库(libbmp.a)。

在你的示例项目中,我们直接使用了 NDK 工具链。这种方法有一个严重的限制,即你不能使用任何 C++ STL 函数,且 C++异常和 RTTI 不支持。实际上,Android NDK 允许你使用脚本$NDK/build/tools/make-standalone-toolchain.sh创建一个自定义的工具链安装。假设你的目标是 Android API 级别 8;你可以使用以下命令在/tmp/my-android-toolchain文件夹中安装工具链。

$ANDROID_NDK/build/tools/make-standalone-toolchain.sh --platform=android-8 --install-dir=/tmp/my-android-toolchain

你可以使用以下命令来使用这个工具链:

export PATH=/tmp/my-android-toolchain/bin:$PATH
export CC=arm-linux-androideabi-gcc

请注意,安装的工具链将在/tmp/my-android-toolchain/arm-linux-androideabi/lib/文件夹下拥有几个库(libgnustl_shared.solibstdc++.alibsupc++.a)。你可以链接这些库以启用异常、RTTI 和 STL 函数支持。我们将在需要 RTTI 的库移植配方中进一步讨论异常和 STL 支持。

有关将 Android 工具链作为独立编译器使用的更多信息,请参见 Android NDK 中的docs/STANDALONE-TOOLCHAIN.html

将库作为预构建库使用

上一个配方描述了如何使用自己的构建系统构建现有库。我们获得了开源libbmp库的编译静态库libbmp.a。这个配方将讨论如何使用预构建库。

如何操作...

以下步骤构建了一个使用预构建库的 Android NDK 应用程序。请注意,示例项目基于我们之前配方的操作。如果你还没有完成之前的配方,现在应该去做。

  1. 打开你在之前配方中创建的PortingWithBuildSystem项目。在cookbook.chapter8.portingwithbuildsystem包下添加一个 Java 文件MainActivity.java。这个 Java 文件只是加载共享库PortingWithBuildSystem,并调用本地方法naCreateABmp

  2. 在此目录下添加mylog.hPortingWithBuildSystem.c文件。PortingWithBuildSystem.c实现了本地方法naCreateABmp

  3. jni文件夹下创建一个Android.mk文件,以编译PortingWithBuildSystem.c作为共享库libPortingWithBuildSystem.so。此Android.mk文件的内容如下:

    LOCAL_PATH := $(call my-dir)
    include $(CLEAR_VARS)
    LOCAL_MODULE := libbmp-prebuilt
    LOCAL_SRC_FILES := libbmp-0.1.3/lib/libbmp.a
    LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/libbmp-0.1.3/include/
    include $(PREBUILT_STATIC_LIBRARY)
    include $(CLEAR_VARS)
    LOCAL_MODULE    := PortingWithBuildSystem
    LOCAL_SRC_FILES := PortingWithBuildSystem.c
    LOCAL_STATIC_LIBRARIES := libbmp-prebuilt
    LOCAL_LDLIBS := -llog
    include $(BUILD_SHARED_LIBRARY)
    
  4. AndroidManifest.xml文件中添加WRITE_EXTERNAL_STORAGE权限,如下所示:

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    
  5. 构建并运行 Android 项目。应该在 Android 设备的sdcard文件夹中创建位图文件test_bs_static.bmp。我们可以使用以下命令来获取该文件:

    $ adb pull /sdcard/test_bs_static.bmp .
    

    该文件与本章中将库作为共享库模块与 Android NDK 构建系统的配方中显示的test_static.bmp文件相同。

它的工作原理...

预构建库有两种常见用例:

  • 你想使用第三方开发者的库,而只提供了库的二进制文件

  • 你已经构建了一个库,并希望在不重新编译的情况下使用该库

你的示例项目属于第二种情况。让我们看看在 Android NDK 中使用预构建库时需要考虑的事项:

  1. 声明一个预构建库模块:在 Android NDK 中,构建模块可以是静态库或共享库。你已经看到了如何用源代码声明一个模块。当模块基于预构建的库时,声明方式类似。

    i. 声明模块名称:这是通过LOCAL_MODULE模块描述变量完成的。在你的示例项目中,使用以下行定义模块名称:

    	LOCAL_MODULE := libbmp-prebuilt
    

    ii. 列出预构建库的源代码:你需要将预构建库的路径提供给LOCAL_SRC_FILES变量。注意,该路径是相对于LOCAL_PATH的。在你的示例项目中,以下列方式列出libbmp.a静态库的路径:

    	LOCAL_SRC_FILES := libbmp-0.1.3/lib/libbmp.a
    

    iii. 导出库头文件:这是通过LOCAL_EXPORT_C_INCLUDES模块描述变量完成的。该变量确保任何依赖预构建库模块的模块都会自动将库头文件的路径追加到LOCAL_C_INCLUDES中。注意,这一步是可选的,因为我们可以显式地将库头文件的路径添加到任何依赖预构建库模块的模块中。然而,最好是将头文件导出,而不是将路径添加到每个依赖预构建库模块的模块中。

    在你的示例项目中,通过在Android.mk文件中添加以下行来导出库头文件:

    LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/libbmp-0.1.3/include/
    

    iv. 导出编译器和/或链接器标志:这可以通过LOCAL_EXPORT_CFLAGSLOCAL_EXPORT_CPPFLAGSLOCAL_EXPORT_LDLIBS来完成。这一步也是可选的,我们在你的示例项目中不会使用它们。你可以参考 Android NDK 中的docs/ANDROID-MK.html获取关于这些模块描述变量的更详细信息。

    v. 声明构建类型:对于共享预构建库,你需要包含PREBUILT_SHARED_LIBRARY,对于静态预构建库,需要包含PREBUILT_STATIC_LIBRARY。在你的示例项目中,使用以下行来声明你想要构建一个预构建的静态库模块:

    	include $(PREBUILT_STATIC_LIBRARY) 
    
  2. 使用预构建的库模块:一旦你有了预构建的库模块,你只需在任何依赖该预构建库的模块的LOCAL_STATIC_LIBRARIESLOCAL_SHARED_LIBRARIES声明中列出模块名称即可。这在你的示例项目的Android.mk文件中有展示:

    LOCAL_STATIC_LIBRARIES := libbmp-prebuilt
    
  3. 用于调试的预构建库:Android NDK 建议你提供包含调试符号的预构建库二进制文件,以便使用ndk-gdb进行调试。当你将库打包进apk文件时,将使用 Android NDK 创建的剥离版本(位于项目的libs/<abi>/文件夹中)。

    提示

    我们不讨论如何生成库的调试版本,因为这取决于库是如何构建的。通常,库的文档将包含如何生成调试构建的说明。如果您直接使用 GCC 构建库,那么您可以参考gcc.gnu.org/onlinedocs/gcc/Debugging-Options.html了解各种调试选项。

使用 import-module 在多个项目中使用库

您可能经常需要在多个项目中使用同一个库。您可以将库放入每个项目的jni文件夹中并分别构建它们。然而,维护同一库的多个副本是件麻烦事。例如,当库有新版本发布,您想要更新库时,您将不得不更新每个库副本。

幸运的是,Android NDK 提供了一个功能,允许我们在 NDK 项目的主源代码树之外维护一个库模块,并通过在Android.mk文件中使用简单的命令导入该模块。让我们讨论一下如何在此配方中导入一个模块。

如何操作...

以下步骤描述了如何在项目的jni文件夹之外声明和导入一个模块:

  1. 创建一个名为ImportModule的具有本地支持的 Android 应用程序。将包名设置为cookbook.chapter8.importmodule。请参考第二章,Java Native Interface中的加载本地库和注册本地方法的配方,以获取更详细的说明。

  2. cookbook.chapter8.importmodule包下添加一个 Java 文件MainActivity.java。这个 Java 文件仅加载共享库ImportModule,并调用本地方法naCreateABmp

  3. code.google.com/p/libbmp/downloads/list下载libbmp库并提取归档文件。在项目下创建一个名为modules的文件夹,并在modules文件夹下创建一个libbmp-0.1.3文件夹。将提取的文件夹中的src/bmpfile.csrc/bmpfile.h文件复制到libbmp-0.1.3文件夹。

  4. 按照第 3 步使用 Android NDK 构建系统将库作为共享库模块移植的配方更新src/bmpfile.h

  5. libbmp-0.1.3文件夹下创建一个Android.mk文件,以编译静态库libbmp.alibbmp。这个Android.mk文件的内容如下:

    LOCAL_PATH := $(call my-dir)
    include $(CLEAR_VARS)
    LOCAL_MODULE := libbmp
    LOCAL_SRC_FILES := bmpfile.c
    LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)
    include $(BUILD_STATIC_LIBRARY)
    
  6. 向其添加mylog.hImportModule.c文件。ImportModule.c实现了本地方法naCreateABmp

  7. jni文件夹下创建一个Android.mk文件,以编译共享库libImportModule.soImportModule.c。这个Android.mk文件的内容如下:

    LOCAL_PATH := $(call my-dir)
    include $(CLEAR_VARS)
    LOCAL_MODULE    := ImportModule
    LOCAL_SRC_FILES := ImportModule.c
    LOCAL_LDLIBS := -llog
    LOCAL_STATIC_LIBRARIES := libbmp
    include $(BUILD_SHARED_LIBRARY)
    $(call import-add-path,$(LOCAL_PATH)/../modules)
    $(call import-module,libbmp-0.1.3)
    
  8. AndroidManifest.xml文件添加WRITE_EXTERNAL_STORAGE权限,如下所示:

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    
  9. 构建并运行 Android 项目。应该在 Android 设备的sdcard文件夹中创建一个位图文件test_bs_static.bmp。您可以使用以下命令获取该文件:

    $ adb pull /sdcard/test_im.bmp .
    

    该文件与本章中的使用 Android NDK 构建系统将库作为共享库模块移植的配方中显示的test_static.bmp相同。

它的工作原理...

在您的示例项目中,您在项目的jni文件夹外部创建了一个模块,然后导入该模块以构建共享库libImportModule.so。在声明和导入模块时,应执行以下步骤:

  1. 声明导入模块:声明导入模块时没有什么特别的。由于导入模块通常被多个 NDK 项目使用,因此在声明导入模块时,导出头文件(使用LOCAL_EXPORT_C_INCLUDES)、编译器标志(LOCAL_EXPORT_CFLAGSLOCAL_EXPORT_CPPFLAGS)和链接器标志(LOCAL_EXPORT_LDLIBS)是一个好习惯。

    在我们的示例项目中,您声明了一个导入的静态库模块libbmp

  2. 决定放置导入模块的位置:Android NDK 构建系统将在NDK_MODULE_PATH中定义的路径中搜索导入模块。默认情况下,Android NDK 目录的sources文件夹会添加到NDK_MODULE_PATH中。因此,您只需将导入模块文件夹放在sources文件夹下,Android NDK 构建系统就能找到它。

    或者,您可以将导入模块文件夹放在任何地方,并将路径追加到NDK_MODULE_PATH。在我们的示例项目中,将导入的libbmp模块放在modules文件夹中。

  3. 追加导入路径:当将导入模块文件夹放置在 Android NDK 的sources目录下时,这不需要。否则,您需要通过向NDK_MODULE_PATH追加路径来告诉 Android NDK 构建系统导入模块的位置。import-add-path宏由 NDK 提供,以帮助您追加路径。

    在您的示例项目中,您通过在jni/Android.mk中的以下这行代码将modules文件夹追加到NDK_MODULE_PATH

    $(call import-add-path,$(LOCAL_PATH)/../modules)
    
  4. 导入模块:Android NDK 提供了一个import-module宏来导入一个模块。这个宏接受一个相对路径,指向导入模块文件夹,该文件夹中包含导入模块的Android.mk文件。Android NDK 构建系统将在NDK_MODULE_PATH中定义的所有路径中搜索导入模块。

    在您的示例项目中,您通过在jni/Android.mk文件中以下这行代码导入了模块:

    $(call import-module,libbmp-0.1.3)
    

    NDK 构建系统将在所有NDK_MODULE_PATH目录中搜索导入模块的libbmp-0.1.3/Android.mk文件。

  5. 使用该模块:使用导入模块就像使用其他任何库模块一样。您需要通过在LOCAL_STATIC_LIBRARIES中列出静态库导入模块,在LOCAL_SHARED_LIBRARIES中列出共享库导入模块来进行链接。

有关如何导入模块的更多信息,您可以参考 Android NDK 中的docs/IMPORT-MODULE.html

移植需要 RTTI、异常和 STL 支持的库。

Android 平台在/system/lib/libstdc++.so提供了一个 C++运行时库。这个默认的运行时库不提供 C++异常处理和 RTTI,对标准 C++库的支持也有限。幸运的是,Android NDK 提供了对默认 C++运行时库的替代方案,这使得大量需要异常处理、RTTI 和 STL 支持的现有库的移植成为可能。本食谱讨论如何移植一个需要 RTTI、异常处理和 STL 支持的 C++库。你会广泛使用boost库作为例子。

如何操作...

以下步骤描述了如何为 Android NDK 构建和使用boost库:

  1. 使用以下命令安装自定义的 Android 工具链:

    $ANDROID_NDK/build/tools/make-standalone-toolchain.sh --platform=android-9 --install-dir=/tmp/my-android-toolchain
    

    这应该在/tmp/my-android-toolchain文件夹中安装工具链。

  2. 创建一个名为PortingBoost的具有本地支持的 Android 应用程序。将包名设置为cookbook.chapter8.portingboost。更详细的说明,请参考第二章,Java Native Interface中的加载本地库和注册本地方法食谱。

  3. cookbook.chapter8.portingboost包下添加一个 Java 文件MainActivity.java。这个 Java 文件简单地加载共享库PortingBoost,并调用本地方法naExtractSubject

  4. sourceforge.net/projects/boost/files/boost/下载 boost 库。在这个食谱中,你将构建boost库 1.51.0。将下载的归档文件解压到jni文件夹中。这将创建一个名为boost_1_51_0的文件夹在jni文件夹下,如下所示:如何操作...

  5. 在命令行终端,进入boost_1_51_0目录。输入以下命令:

    $ ./bootstrap.sh
    
  6. 编辑jni/boost_1_51_0/tools/build/v2目录下的user-config.jam文件。在文件末尾添加以下内容。关于 boost 配置的更多信息,你可以参考www.boost.org/boost-build2/doc/html/bbv2/overview/configuration.html

    NDK_TOOLCHAIN = /tmp/my-android-toolchain ;
    using gcc : android4.6 :
       $(NDK_TOOLCHAIN)/bin/arm-linux-androideabi-g++ :
       <archiver>$(NDK_TOOLCHAIN)/bin/arm-linux-androideabi-ar
       <ranlib>$(NDK_TOOLCHAIN)/bin/arm-linux-androideabi-ranlib
       <compileflags>--sysroot=$(NDK_TOOLCHAIN)/sysroot
       <compileflags>-I$(NDK_TOOLCHAIN)/arm-linux-androideabi/include/c++/4.6
       <compileflags>-I$(NDK_TOOLCHAIN)/arm-linux-androideabi/include/c++/4.6/arm-linux-androideabi
       <compileflags>-DNDEBUG
       <compileflags>-D__GLIBC__
       <compileflags>-DBOOST_FILESYSTEM_VERSION=3
       <compileflags>-lstdc++
       <compileflags>-mthumb
       <compileflags>-fno-strict-aliasing
       <compileflags>-O2
           ;
    
  7. 尝试使用以下命令构建boost库:

    $ ./b2 --without-python --without-mpi  toolset=gcc-android4.6 link=static runtime-link=static target-os=linux --stagedir=android > log.txt &
    

    这个命令将在后台执行boost构建。你可以使用以下命令监控构建输出:

    $ tail -f log.txt
    

    构建完成需要一些时间。有些目标构建可能会失败。我们可以通过log.txt文件检查错误。

    第一个错误是找不到sys/statvfs.h文件。你可以通过更新libs/filesystem/src/operations.cpp文件来修复这个问题。更新的部分如下所示:

    #   include <sys/types.h>
    #   include <sys/stat.h>
    #   if !defined(__APPLE__) && !defined(__OpenBSD__) && !defined(__ANDROID__)
    #     include <sys/statvfs.h>
    #     define BOOST_STATVFS statvfs
    #     define BOOST_STATVFS_F_FRSIZE vfs.f_frsize
    #   else
    #     ifdef __OpenBSD__
    #       include <sys/param.h>
    #     elif defined(__ANDROID__)
    #         include <sys/vfs.h>
    #     endif
    #     include <sys/mount.h>
    #     define BOOST_STATVFS statfs
    #     define BOOST_STATVFS_F_FRSIZE   static_cast<boost::uintmax_t>(vfs.f_bsize)
    #   endif
    

    第二个错误是找不到bzlib.h文件。这是因为 Android 上可用bzip。你可以在jni/boost_1_51_0/tools/build/v2/user-config.jam文件顶部添加以下行来禁用bzip

    modules.poke : NO_BZIP2 : 1 ;
    

    第三个错误是 PAGE_SIZE 在此作用域中没有声明。您可以通过在 boost_1_51_0/boost/thread/thread.hppboost_1_51_0/boost/thread/pthread/thread_data.hpp 中添加以下行来修复此问题:

    #define PAGE_SIZE sysconf(_SC_PAGESIZE)
    
  8. 使用第 5 步的相同命令再次尝试构建库。这次库将成功构建。

  9. jni 文件夹下添加 mylog.hPortingBoost.cpp 文件。PortingBoost.cpp 文件包含本地方法 naExtractSubject 的实现。该函数将使用 boost 库的 regex_match 方法,将输入字符串 pInputStr 的每一行与正则表达式匹配:

    void naExtractSubject(JNIEnv* pEnv, jclass clazz, jstring pInputStr) {
       std::string line;
       boost::regex pat( "^Subject: (Re: |Aw: )*(.*)" );
       const char *str;
       str = pEnv->GetStringUTFChars(pInputStr, NULL);
       std::stringstream stream;  
       stream << str;
       while (1) {
           std::getline(stream, line);
           LOGI(1, "%s", line.c_str());
           if (!stream.good()) {
             break;
           }
           boost::smatch matches;
           if (boost::regex_match(line, matches, pat)) {
               LOGI(1, "matched: %s", matches[0].str().c_str());
           } else {
             LOGI(1, "not matched");
           }
       }
    }
    
  10. jni 文件夹下添加一个 Android.mk 文件,内容如下:

    LOCAL_PATH := $(call my-dir)
    include $(CLEAR_VARS)
    LOCAL_MODULE := boost_regex
    LOCAL_SRC_FILES := boost_1_51_0/android/lib/libboost_regex.a
    LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/boost_1_51_0
    include $(PREBUILT_STATIC_LIBRARY)
    include $(CLEAR_VARS)
    LOCAL_MODULE    := PortingBoost
    LOCAL_SRC_FILES := PortingBoost.cpp
    LOCAL_LDLIBS := -llog
    LOCAL_STATIC_LIBRARIES := boost_regex
    include $(BUILD_SHARED_LIBRARY)
    
  11. jni 文件夹下添加一个 Application.mk 文件,内容如下:

    APP_STL := gnustl_static
    APP_CPPFLAGS := -fexceptions
    
  12. 构建并运行项目。您可以使用以下命令监控 logcat 输出:

    $ adb logcat -v time PortingBoost:I *:S
    

    以下是 logcat 输出的截图:

    如何操作...

它的工作原理...

在您的示例项目中,首先使用 Android 工具链作为独立编译器构建了 boost 库。然后,您将 boost 中的 regex 库作为预构建模块使用。注意,boost 库需要支持 C++ 异常和 STL。让我们讨论如何在 Android NDK 上启用这些特性的支持。

Android NDK 中的 C++ 运行时:默认情况下,Android 带有一个最小的 C++ 运行时库位于 /system/lib/libstdc++.so。该库不支持大多数 C++ 标准库函数、C++ 异常和 RTTI。幸运的是,Android NDK 提供了额外的 C++ 运行时库供我们使用。以下表格总结了 NDK r8 中不同运行时库提供的特性:

C++ 标准库 C++ 异常 C++ RTTI
system 最小化
gabi++ 最小化 否(NDK r8d 或更高版本为是)
stlport 否(NDK r8d 或更高版本为是)
gnustl

注意

自从 Android NDK r8d 开始,gabi++stlport 中增加了 C++ 异常支持。

系统库指的是随 Android 系统默认提供的值。这里只支持最小的 C++ 标准库,并且不支持 C++ 异常和 RTTI。支持的 C++ 头文件包括以下内容:

cassert, cctype, cerrno, cfloat, climits, cmath, csetjmp, csignal, cstddef, cstdint, cstdio, cstdlib, cstring, ctime, cwchar, new, stl_pair.h, typeinfo, utility
  • gabi++ 是一个运行时库,除了支持系统默认提供的 C++ 函数外,还支持 RTTI。

  • stlport 提供了一套完整的 C++ 标准库头文件和 RTTI,但不支持 C++ 异常。实际上,Android NDK 的 stlport 是基于 gabi++ 的。

  • gnustl 是 GNU 标准的 C++ 库。它附带了一套完整的 C++ 头文件,并支持 C++ 异常和 RTTI。

    提示

    共享库文件 gnustl 命名为 libgnustl_shared.so,而不是在其他平台上使用的 libstdc++.so。这是因为名称 libstdc++.so 被系统默认的 C++ 运行时使用。

Android NDK 构建系统允许我们在Application.mk文件中指定要链接的 C++ 库运行时。根据库类型(共享或静态)以及要使用的运行时,我们可以如下定义APP_STL

静态库 共享库
gabi++ gabi++_static gabi++_shared
stlport stlport_static stlport_shared
gnustl gnustl_static gnustl_shared

在你的示例项目中,在Application.mk中添加以下行,以使用gnustl静态库:

APP_STL := gnustl_static

提示

你只能将静态 C++ 库链接到一个共享库中。如果一个项目使用多个共享库,并且所有库都链接到静态 C++ 库,每个共享库都会在其二进制文件中包含该库代码的副本。这会导致一些问题,因为 C++ 运行时库使用的一些全局变量会被重复。

这些库的源代码、头文件和二进制文件可以在 Android NDK 的sources/cxx-stl文件夹中找到。你也可以参考docs/CPLUSPLUS-SUPPORT.html获取更多信息。

启用 C++ 异常支持:默认情况下,所有 C++ 源文件都是使用-fno-exceptions编译的。为了启用 C++ 异常,你需要选择一个支持异常的 C++ 库(gnustl_staticgnustl_shared),并执行以下操作之一:

  • Android.mk中,将异常添加到LOCAL_CPP_FEATURES中,如下所示:

    LOCAL_CPP_FEATURES += exceptions
    
  • Android.mk中,将-fexceptions添加到LOCAL_CPPFLAGS中,如下所示:

    LOCAL_CPPFLAGS += -fexceptions
    
  • Application.mk中,添加以下行:

    APP_CPPFLAGS += -fexceptions
    

启用 C++ RTTI 支持:默认情况下,C++ 源文件是使用-fno-rtti编译的。为了启用 RTTI 支持,你需要使用一个支持 RTTI 的 C++ 库,并执行以下操作之一:

  • Android.mk中,将rtti添加到LOCAL_CPP_FEATURES中,如下所示:

    LOCAL_CPP_FEATURES += rtti
    
  • Android.mk中,将-frtti添加到LOCAL_CPPFLAGS中,如下所示:

    LOCAL_CPPFLAGS += -frtti
    
  • Application.mk中,将-frtti添加到APP_CPPFLAGS中,如下所示:

    APP_CPPFLAGS += -frtti
    

第九章:使用 NDK 将现有应用程序移植到 Android

在本章中,我们将涵盖以下内容:

  • 使用 NDK 构建系统将命令行可执行文件移植到 Android

  • 使用 NDK 独立编译器将命令行可执行文件移植到 Android

  • 为移植的 Android 应用程序添加 GUI

  • 在移植时使用后台线程

简介

上一章涵盖了使用 NDK 将本地库移植到 Android 的各种技术。本章讨论了本地应用程序的移植。

我们将首先介绍如何使用 Android NDK 构建系统和 NDK 提供的独立编译器为 Android 构建本地命令行应用程序。然后,我们为移植的应用程序添加一个图形用户界面(GUI)。最后,我们说明如何使用后台线程进行繁重处理,并将进度更新消息从本地代码发送到 Java UI 线程以进行 GUI 更新。

我们将在本章中使用开源的 Fugenschnitzer 程序。它是一个基于Seam Carving算法的内容感知图像调整大小程序。该算法的基本思想是通过搜索并操作原始图像中的接缝(一个接缝是从上到下或从左到右连接像素的路径)来改变图像的大小。该算法能够在尝试保留重要信息的同时调整图像大小。对于对程序和算法感兴趣的读者,可以访问fugenschnitzer.sourceforge.net/main_en.html了解更多详情。否则,我们可以忽略算法,专注于移植过程。

使用 NDK 构建系统将命令行可执行文件移植到 Android

本食谱讨论了如何使用 NDK 构建系统将命令行可执行文件移植到 Android。我们将以开源的 Fugenschnitzer 程序(fusch)为例。

准备工作

在阅读本章之前,你应该先阅读第八章中的使用 Android NDK 构建系统将库作为静态库移植的食谱,使用 Android NDK 移植和使用现有库

如何操作...

以下步骤描述了如何使用 NDK 构建系统将fusch程序移植到 Android:

  1. 创建一个名为PortingExecutable的具有本地支持的 Android 应用程序。将包名设置为cookbook.chapter9.portingexecutable。如果你需要更详细的说明,请参考第二章中的加载本地库和注册本地方法的食谱,Java 本地接口

  2. 删除项目jni文件夹下的现有内容。

  3. fugenschnitzer.sourceforge.net/main_en.html下载fusch库和命令行应用程序的源代码。解压归档文件,并将它们分别放入jni/fuschjni/fusch_lib文件夹中。

  4. sourceforge.net/projects/libpng/files/libpng12/1.2.50/下载libpng 1.2.50,并将文件解压到jni/libpng-1.2.50文件夹中。最新版本的libpng无法工作,因为接口不同。

  5. jni/libpng-1.2.50文件夹下添加一个Android.mk文件,以将libpng构建为一个静态库模块。该文件具有以下内容:

    LOCAL_PATH := $(call my-dir)
    include $(CLEAR_VARS)
    LOCAL_CFLAGS := 
    LOCAL_MODULE    := libpng
    LOCAL_SRC_FILES :=\
      png.c \
      pngerror.c \
      pngget.c \
      pngmem.c \
      pngpread.c \
      pngread.c \
      pngrio.c \
      pngrtran.c \
      pngrutil.c \
      pngset.c \
      pngtrans.c \
      pngwio.c \
      pngwrite.c \
      pngwtran.c \
      pngwutil.c 
    LOCAL_LDLIBS := -lz
    LOCAL_EXPORT_LDLIBS := -lz
    LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)
    include $(BUILD_STATIC_LIBRARY)
    
  6. jni/fusch_lib文件夹下添加一个Android.mk文件,以将libseamcarv构建为一个静态库模块。文件内容如下:

    LOCAL_PATH := $(call my-dir)
    include $(CLEAR_VARS)
    LOCAL_MODULE    := libseamcarv
    LOCAL_SRC_FILES :=\
      sc_core.c  \
      sc_carve.c  \
      sc_color.c  \
      sc_shift.c \
      sc_mgmnt.c \
      seamcarv.c
    LOCAL_CFLAGS := -std=c99 
    LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)
    include $(BUILD_STATIC_LIBRARY)
    
  7. jni/fusch文件夹下添加第三个Android.mk文件,以构建使用libpng-1.2.50fusch_lib两个文件夹中构建的两个静态库的fusch可执行文件。

    LOCAL_PATH := $(call my-dir)
    include $(CLEAR_VARS)
    LOCAL_MODULE    := fusch
    LOCAL_SRC_FILES := fusch.c
    LOCAL_CFLAGS := -std=c99
    LOCAL_STATIC_LIBRARIES := libpng libseamcarv
    include $(BUILD_EXECUTABLE)
    
  8. jni文件夹下添加第四个Android.mk文件,以包含其子文件夹下的Android.mk文件。

    LOCAL_PATH := $(call my-dir)
    include $(call all-subdir-makefiles)
    
  9. 构建应用程序,你会在libs/armeabi文件夹下看到一个名为fusch的二进制文件。我们可以使用以下命令将此二进制文件放入已越狱的 Android 设备或模拟器中:

    $ adb push fusch /data/data/
    
  10. 请注意,我们无法在未越狱的 Android 设备上复制并执行二进制文件,因为我们无法获得执行权限。

  11. 在控制台上启动第一个命令行。我们可以使用以下命令授予二进制文件执行权限并执行它:

    $ adb shell
    # cd /data/data
    # chmod 755 fusch
    # ./fusch
    

    这将输出程序的帮助信息。

  12. 启动第二个命令行终端。使用以下命令将测试 PNG 文件cookbook_ch9_test.png(位于示例项目源代码的assets文件夹中)推送到测试设备或模拟器中:

    $ adb push cookbook_ch9_test.png /data/data/
    
  13. 回到第一个命令行终端,使用以下命令再次执行fusch程序:

    # ./fusch cookbook_ch9_test.png 1.png h-200
    
  14. 程序将花费一些时间将输入图像从 800 x 600 调整到 600 x 600。一旦完成,我们可以在第二个命令行终端使用以下命令获取处理后的图像:

    $ adb pull /data/data/1.png .
    
  15. 以下屏幕截图显示了左侧的原始图像和右侧的处理后图像:如何操作...

工作原理...

示例项目演示了如何将fusch程序作为命令行可执行文件移植到 Android。我们在Android.mk文件中向 Android NDK 构建系统描述了源代码,NDK 构建系统处理其余部分。

移植命令行可执行文件的操作步骤如下:

  1. 确定库依赖关系。在我们的示例程序中,fusch依赖于libseamcarv(位于fusch_lib文件夹中)和libpng,而libpng随后又依赖于zlib

  2. 如果 Android 系统上没有可用的库,将其作为静态库模块移植。这是我们示例应用程序中的libseamcarvlibpng的情况。但是因为 Android 上有zlib,所以我们只需链接到它即可。

  3. 将可执行文件作为单独的模块移植,并将其链接到库模块。

理解 Android.mk 文件

我们在第八章《使用 Android NDK 移植和使用现有库》中已经介绍了大部分Android.mk变量和宏。这里我们将介绍另外两个预定义变量。你也可以查阅 Android NDK 文件docs/ANDROID-MK.html获取更多关于宏和变量的信息。

  • LOCAL_CFLAGS:一个模块描述变量。它允许我们为构建 C 和 C++源文件指定额外的编译器选项或宏定义。另一个具有类似功能的变量是LOCAL_CPPFLAGS,但它仅用于 C++源文件。在我们示例项目中,在构建libseamcarvfusch时,我们向编译器传递了-std=c99。这要求编译器接受 ISO C99 C 语言标准的语法。如果在构建时未指定该标志,将导致编译错误。

    注意

    也可以使用LOCAL_CFLAGS += I<包含路径>来指定包含路径。但是,建议我们使用LOCAL_C_INCLUDES,因为LOCAL_C_INCLUDES路径也将用于ndk-gdb本地调试。

  • BUILD_EXECUTABLE:一个 GNU make 变量。它指向一个构建脚本,该脚本收集了我们想要构建的可执行文件的所有信息,并确定如何构建它。它与BUILD_SHARED_LIBRARYBUILD_STATIC_LIBRARY类似,不同之处在于它用于构建可执行文件。在我们示例项目中构建fusch时使用了它。

    include $(BUILD_EXECUTABLE)
    

通过本章的解释以及第八章《使用 Android NDK 移植和使用现有库》的知识,现在理解我们示例应用程序中使用的四个Android.mk文件已经相当容易了。我们将libpnglibseamcarv作为两个静态库模块进行移植。我们导出依赖的库(通过LOCAL_EXPORT_LDLIBS)和头文件(通过LOCAL_EXPORT_C_INCLUDES),这样在使用模块时它们会被自动包含。在移植libpng时,我们还链接了 Android 系统上可用的zlib库(通过LOCAL_LDLIBS)。最后,我们通过引用这两个库模块(通过LOCAL_STATIC_LIBRARIES)来移植fusch程序。

使用 NDK 独立编译器将命令行可执行文件移植到 Android。

上一个食谱介绍了如何使用 NDK 构建系统将命令行可执行文件移植到 Android。这个食谱描述了如何使用 Android NDK 工具链作为独立编译器来实现这一点。

准备工作

在继续之前,建议您阅读第八章中的使用现有构建系统移植库一节,使用 Android NDK 移植和利用现有库

如何操作...

以下步骤描述了如何使用 NDK 工具链直接将fusch程序移植到 Android:

  1. 创建一个名为PortingExecutableBuildSystem的具有本地支持的 Android 应用。设置包名为cookbook.chapter9.portingexecutablebuildsystem。如果您需要更详细的说明,请参考第二章中的加载本地库和注册本地方法一节,Java 本地接口

  2. 删除项目jni文件夹下的现有内容。

  3. fugenschnitzer.sourceforge.net/main_en.html下载fusch库和命令行应用的源代码。解压归档文件,并将它们分别放入jni/fuschjni/fusch_lib文件夹。

  4. sourceforge.net/projects/libpng/files/libpng12/1.2.50/下载libpng 1.2.50,并将文件解压到jni/libpng-1.2.50文件夹。最新版本的libpng不能工作,因为接口已经改变。将libpng-1.2.50下的config.guess脚本替换为gcc.gnu.org/svn/gcc/branches/cilkplus/config.guess的内容,config.sub替换为gcc.gnu.org/svn/gcc/branches/cilkplus/config.sub的脚本。

  5. jni/libpng-1.2.50文件夹下添加一个build_android.sh文件来构建libpng。文件内容如下:

    #!/bin/bash
    NDK=~/Desktop/android/android-ndk-r8b
    SYSROOT=$NDK/platforms/android-8/arch-arm/
    export CFLAGS="-fpic \
       -ffunction-sections \
       -funwind-tables \
       -D__ARM_ARCH_5__ -D__ARM_ARCH_5T__ \
       -D__ARM_ARCH_5E__ -D__ARM_ARCH_5TE__ \
      -Wno-psabi \
      -march=armv5te \
       -mtune=xscale \
       -msoft-float \
      -mthumb \
       -Os \
      -DANDROID \
       -fomit-frame-pointer \
       -fno-strict-aliasing \
       -finline-limit=64"
    export LDFLAGS="-lz"
    export CC="$NDK/toolchains/arm-linux-androideabi-4.4.3/prebuilt/linux-x86/bin/arm-linux-androideabi-gcc --sysroot=$SYSROOT"
    ./configure \
       --host=arm-linux-androideabi \
       --prefix=$(pwd) \
       --exec-prefix=$(pwd) \
      --enable-shared=false \
      --enable-static=true
    make clean
    make 
    make install
    
  6. jni/fusch_lib文件夹下添加一个build_android.sh文件来构建libseamcarv库。文件内容如下:

    #!/bin/bash
    NDK=~/Desktop/android/android-ndk-r8b
    SYSROOT=$NDK/platforms/android-8/arch-arm/
    export CFLAGS="-fpic \
       -ffunction-sections \
       -funwind-tables \
       -D__ARM_ARCH_5__ -D__ARM_ARCH_5T__ \
       -D__ARM_ARCH_5E__ -D__ARM_ARCH_5TE__ \
      -Wno-psabi \
      -march=armv5te \
       -mtune=xscale \
       -msoft-float \
      -mthumb \
       -Os \
       -fomit-frame-pointer \
       -fno-strict-aliasing \
       -finline-limit=64 \
      -std=c99 \
      -DANDROID "
    export CC="$NDK/toolchains/arm-linux-androideabi-4.4.3/prebuilt/linux-x86/bin/arm-linux-androideabi-gcc --sysroot=$SYSROOT"
    AR="$NDK/toolchains/arm-linux-androideabi-4.4.3/prebuilt/linux-x86/bin/arm-linux-androideabi-ar"
    SRC_FILES="\
      sc_core.c  \
      sc_carve.c  \
      sc_color.c  \
      sc_shift.c \
      sc_mgmnt.c \
      seamcarv.c"
    $CC $SRC_FILES $CFLAGS -c
    $AR cr libseamcarv.a *.o 
    
  7. jni/fusch文件夹下添加第三个build_android.sh文件,以构建使用在libpng-1.2.50fusch_lib两个文件夹下构建的两个静态库的fusch可执行文件。

    #!/bin/bash
    NDK=~/Desktop/android/android-ndk-r8b
    SYSROOT=$NDK/platforms/android-8/arch-arm
    CUR_D=$(pwd)
    export CFLAGS="-fpic \
       -ffunction-sections \
       -funwind-tables \
       -D__ARM_ARCH_5__ -D__ARM_ARCH_5T__ \
       -D__ARM_ARCH_5E__ -D__ARM_ARCH_5TE__ \
      -Wno-psabi \
      -march=armv5te \
       -mtune=xscale \
       -msoft-float \
      -mthumb \
       -Os \
       -fomit-frame-pointer \
       -fno-strict-aliasing \
       -finline-limit=64 \
      -std=c99 \
      -DANDROID \
      -I$CUR_D/../fusch_lib \
      -I$CUR_D/../libpng-1.2.50/include"
    export LDFLAGS="-Wl,--no-undefined -Wl,-z,noexecstack -Wl,-z,relro -Wl,-z,now -lz -lc -lm -lpng -lseamcarv -L$CUR_D/../fusch_lib -L$CUR_D/../libpng-1.2.50/lib"
    export CC="$NDK/toolchains/arm-linux-androideabi-4.4.3/prebuilt/linux-x86/bin/arm-linux-androideabi-gcc --sysroot=$SYSROOT"
    SRC_FILES="fusch.c"
    $CC $SRC_FILES $CFLAGS $LDFLAGS -o fusch
    
  8. 通过在libpng-1.2.50fusch_libfusch三个子文件夹中执行build_android.sh脚本来构建libpnglibseamcarv两个库以及fusch可执行文件。我们可以在libpng-1.2.50/lib文件夹下找到libpng.a,在fusch_lib文件夹下找到libseamcarv.a,在fusch文件夹下找到fusch可执行文件。

  9. 我们可以使用以下命令将二进制文件fusch放到已越狱的 Android 设备或模拟器上:

    $ cd <path to project folder>/PortingExecutableBuildSystem/jni/fusch
    $ adb push fusch /data/data/
    
  10. 请注意,由于我们无法获得权限,因此不能在未越狱的 Android 设备上复制和执行二进制文件。

  11. 启动第一个命令行终端。我们可以给二进制文件执行权限,然后使用以下命令执行它:

    $ adb shell
    # cd /data/data
    # chmod 755 fusch
    # ./fusch
    
  12. 这将打印出程序的帮助信息。

  13. 启动第二个命令行终端。使用以下命令将测试 PNG 文件cookbook_ch9_test.png(位于示例项目源代码的assets文件夹下)推送到测试设备或模拟器上:

    $ adb push cookbook_ch9_test.png /data/data/
    
  14. 回到第一个命令行终端,使用以下命令再次执行fusch程序:

    # ./fusch cookbook_ch9_test.png 1.png v-200
    
  15. 程序将花费一些时间将输入图像从 800 x 600 调整到 800 x 400。一旦完成,我们可以在第二个命令行终端使用以下命令获取处理后的图像:

    $ adb pull /data/data/1.png .
    
  16. 下图显示了左侧的原始图像和右侧的处理后图像:如何操作...

工作原理...

示例项目展示了如何使用 NDK 工具链作为独立编译器将命令行可执行文件移植到 Android。

移植可执行文件的过程与之前使用 Android NDK 构建系统的食谱类似。关键在于向独立编译器传递适当的选项。

移植 libpng

libpng附带了它自己的构建脚本。我们可以使用以下命令获取配置构建过程的选项列表:

$ ./configure –help

编译器命令、编译器标志和链接器标志可以通过环境变量CCCFLAGSLDFLAGS分别配置。在libpng-1.2.50文件夹下的build_android.sh脚本中,我们设置这些变量以使用 NDK 编译器为 ARM 架构构建。关于如何移植库的详细说明,我们可以参考使用 Android NDK 工具链的现有构建系统移植库的食谱,在第八章,移植带有其现有构建系统的库

我们现在将介绍一些编译选项。由于 Android NDK 工具链基于 GCC,我们可以参考}gcc.gnu.org/onlinedocs/gcc/Option-Summary.html详细了解每个选项。

  • -fpic:它生成适用于构建共享库的位置无关代码。

  • -ffunction-sections:此选项要求链接器执行优化,以提高代码中的引用局部性。

  • -funwind-tables:它生成用于展开调用栈的静态数据。

  • -D__ARM_ARCH_5__, -D__ARM_ARCH_5T, -D__ARM_ARCH_5E__, -D__ARM_ARCH_5TE, -DANDROID定义了__ARM_ARCH_5__, __ARM_ARCH_5T, __ARM_ARCH_5E__, __ARM_ARCH_5TE, 和ANDROID作为宏,定义等于1。例如,-DANDROID等同于-D ANDROID=1

  • -Wno-psabi:它抑制了关于va_list等的警告信息。

  • -march=armv5te:它指定目标 ARM 架构为ARMv5te

  • -mtune=xscale:它调整代码的性能,因为代码将在 xscale 处理器上运行。请注意,xscale 是一个处理器名称。

  • -msoft-float:它使用软件浮点函数。

  • -mthumb:它使用 Thumb 指令集生成代码。

  • -Os:提供针对大小的优化。

  • -fomit-frame-pointer:如果可能,帮助避免在寄存器中保存帧指针。

  • -fno-strict-aliasing:不应用严格的别名规则。这防止编译器进行不想要的优化。

  • -finline-limit=64:设置可以作为64伪指令内联的函数的大小限制。

  • -std=c99:接受c99标准语法。

当构建成功执行后,我们可以在libpng-1.2.50/lib文件夹下找到libpng.a静态库,以及在libpng-1.2.50/include文件夹下的头文件。

注意

Android NDK 构建系统本质上是为我们确定合适的编译选项并为我们调用交叉编译器。因此,我们可以从 NDK 构建系统的输出中学习传递给编译器的选项。例如,我们可以在前一个食谱中调用命令ndk-build -B V=1ndk-build -B -n,以了解 NDK 构建系统如何处理libpnglibseamcarvfusch的构建,并在本食谱中应用类似的选项。

移植 libseamcarv

libseamcarv附带一个 Makefile 但没有配置文件。我们可以修改 Makefile 或者从头开始编写构建脚本。由于库只包含几个文件,我们将直接编写构建脚本。需要遵循两个步骤:

  1. 将所有源文件编译成对象文件。这是通过在编译时传递"-c"选项完成的。

  2. 将对象文件归档成静态库。这一步是通过 NDK 工具链中的归档器arm-linux-androideabi-ar完成的。

提示

正如我们在第八章,使用 Android NDK 移植和现有库中所解释的,静态库不过是对象文件的归档,可以通过archiver程序创建。

移植 fusch

我们需要链接到我们构建的两个库,即libpnglibseamcarv。这是通过向链接器传递以下选项完成的:

-lpng -lseamcarv -L$CUR_D/../fusch_lib -L$CUR_D/../libpng-1.2.50/lib

这个"-L"选项将fusch_liblibpng-1.2.50/lib添加到库的搜索路径中,而"-l"告诉链接器链接到libpnglibseamcarv库。构建脚本将在fusch文件夹下输出名为fusch的二进制文件。

fusch程序相当简单。因此,我们可以使用 Android NDK 构建系统或独立的编译器来移植它。如果一个应用程序有更多的依赖,用Android.mk文件描述所有内容可能会很困难。因此,能够使用 NDK 工具链作为独立的编译器并利用库的现有构建脚本是非常有帮助的。

为移植的 Android 应用添加 GUI

前两个食谱展示了如何将命令行可执行文件移植到 Android。不用说,这种方法最大的缺点是它不能在未越狱的 Android 设备上执行。本食谱讨论了在将应用程序移植到 Android 时,如何通过添加 GUI 来解决这一问题。

如何操作...

以下步骤描述了如何向移植的应用添加一个简单的用户界面:

  1. 创建一个名为PortingExecutableAUI的具有本地支持的 Android 应用。将包名设置为cookbook.chapter9.portingexecutableaui。如果你需要更详细的说明,请参考第二章的加载本地库和注册本地方法部分,Java Native Interface

  2. 按照本章中使用 NDK 构建系统将命令行可执行文件移植到 Android的步骤 2 至 8 进行操作。

  3. jni/fusch文件夹下添加一个mylog.h文件。在jni/fusch/fusch.c文件的开头部分添加以下几行,然后移除原始的主方法签名行。naMain方法接受来自 Java 代码的命令,而不是命令行 shell。参数应以空格分隔:

    #ifdef ANDROID_BUILD
    #include <jni.h>
    #include "mylog.h"
    int naMain(JNIEnv* env, jclass clazz, jstring pCmdStr);
    
    jint JNI_OnLoad(JavaVM* pVm, void* reserved) {
      JNIEnv* env;
      if ((*pVm)->GetEnv(pVm, (void **)&env, JNI_VERSION_1_6) != JNI_OK) {
        return -1;
      }
      JNINativeMethod nm[1];
      nm[0].name = "naMain";
      nm[0].signature = "(Ljava/lang/String;)I";
      nm[0].fnPtr = (void*)naMain;
      jclass cls = (*env)->FindClass(env, "cookbook/chapter9/portingexecutableaui/MainActivity");
      // Register methods with env->RegisterNatives.
      (*env)->RegisterNatives(env, cls, nm, 1);
      return JNI_VERSION_1_6;
    }
    
     int naMain(JNIEnv* env, jclass clazz, jstring pCmdStr) {
      int argc = 0;
      char** argv = (char**) malloc (sizeof(char*)*4);
      *argv = "fusch";
      char** targv = argv + 1;
      argc++;
      jboolean isCopy;
       char *cmdstr = (*env)->GetStringUTFChars(env, pCmdStr, &isCopy);
       if (NULL == cmdstr) {
         LOGI(2, "get string failed");
       }
       LOGI(2, "naMain assign parse string %s", cmdstr);
       char* pch;
       pch = strtok(cmdstr, " ");
       while (NULL != pch) {
         *targv = pch;
         argc++;
         targv++;
         pch = strtok(NULL, " ");
       }
       LOGI(1, "No. of arguments: %d", argc);
       LOGI(1, "%s %s %s %s", argv[0], argv[1], argv[2], argv[3]);
    #else
     int main(int argc, char *argv[]) {
    #endif
    
  4. 在主方法的return语句之前添加以下几行以释放本地字符串:

    #ifdef ANDROID_BUILD
       (*env)->ReleaseStringUTFChars(env, pCmdStr, cmdstr);
    #endif
    
  5. 更新jni/fusch下的Android.mk文件,如下所示。更新的部分已被高亮显示:

    LOCAL_PATH := $(call my-dir)
    include $(CLEAR_VARS)
    LOCAL_MODULE    := fusch
    LOCAL_SRC_FILES := fusch.c
    LOCAL_CFLAGS := -std=c99 -DANDROID_BUILD
    LOCAL_STATIC_LIBRARIES := libpng libseamcarv
    LOCAL_LDLIBS := -llog
    include $(BUILD_SHARED_LIBRARY)
    
    
  6. cookbook.chapter9.portingexecutableaui包下添加MainActivity.java文件。Java 代码设置图形用户界面,加载共享库libfusch.so,并调用本地方法naMain

  7. res/layout文件夹下添加一个activity_main.xml文件以描述图形用户界面。

  8. AndroidManifest.xml文件中,在<application>...</application>之前添加以下行:

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    
  9. 构建并运行 Android 应用。你应该能看到一个与以下截图类似的图形用户界面:如何操作...

  10. 我们可以点击宽度高度按钮来处理默认图像。或者,我们可以加载另一个.png图像并处理它。一旦我们点击宽度高度,图形用户界面将不再响应,我们必须等待处理完成。如果出现著名的应用无响应ANR)对话框,只需点击等待

  11. 处理完成后,将加载处理过的图像并显示其尺寸。左侧的截图显示了点击宽度按钮的结果,而右侧的截图则表示高度处理的结果。请注意,图像被缩放以适应显示区域:如何操作...

工作原理...

该示例展示了如何为移植到 Android 的fusch程序添加图形用户界面。fusch源代码被修改,以便本地代码与图形用户界面接口。

通常,可以按照以下步骤向已移植到 Android 的命令行可执行文件添加图形用户界面。

  1. 用本地方法替换主方法。在我们的示例应用中,我们用naMain替换了 main。

  2. 解析本地方法的输入参数以获取命令选项,而不是从命令行读取。在我们的示例应用程序中,我们解析了第三个输入参数 pCmdStr 以获取 fusch 命令选项。这使得命令可以在 Java 代码中构建,并轻松地传递给本地代码。

  3. 将本地方法注册到 Java 类。

  4. 在 Java 代码中,图形用户界面(GUI)可以接收用户指定的各种参数值,构建命令,并将其传递给本地方法进行处理。

请注意,在我们的修改后的本地代码中,我们并没有移除原始代码。我们使用了 C 预处理器宏 ANDROID_BUILD 来控制哪些源代码部分应该被包含以构建 Android 共享库。我们在 Android.mk 文件(位于 fusch 文件夹下)中向编译器传递 -DANDROID_BUILD,以启用特定的 Android 代码。这种方法使得我们能够轻松添加对 Android 的支持,而不会破坏其他平台的代码。

本食谱中的示例应用程序有两个严重的限制。首先,主 UI 线程处理繁重的图像处理,这导致应用程序变得无响应。其次,在图像处理过程中没有进度更新。只有在图像处理完成后 GUI 才会更新。我们将在下一个食谱中解决这些问题。

在移植中使用后台线程

前一个食谱为移植的 fusch 程序添加了 GUI,但留下了两个问题——GUI 的无响应性和处理过程中没有进度更新。这个食谱讨论了如何使用后台线程来处理进程,并将进度报告给主 UI 线程。

准备就绪。

本食谱中的示例程序基于我们本章前一个食谱中开发的程序。您应该首先阅读它们。此外,建议读者阅读以下 第二章,Java Native Interface 的食谱:

  • 从本地代码调用静态和实例方法

  • 缓存 jfieldIDjmethodID 和引用数据以提高性能

如何操作...

以下步骤描述了如何使用后台线程进行繁重的处理,并将进度更新报告给 Java UI 线程:

  1. 将我们在前一个食谱中开发的 PortingExecutableAUI 项目复制到一个名为 PortingExecutableAUIAsync 的文件夹中。在 Eclipse IDE 中打开文件夹中的项目。

  2. MainActivity.java 添加以下代码:

    handlerhandler 类的实例处理从后台线程发送的消息。它将使用消息内容更新 GUI。

    public static final int MSG_TYPE_PROG = 1;
    public static final int MSG_TYPE_SUCCESS = 2;
    public static final int MSG_TYPE_FAILURE = 3;
    Handler handler = new Handler() {
      @Override
      public void handleMessage(Message msg) {
        switch(msg.what) {
          case MSG_TYPE_PROG:
            String updateMsg = (String)msg.obj;
            if (1 == msg.arg1) {
              String curText = text1.getText().toString();
              String newText = curText.substring(0, curText.lastIndexOf("\n")) + "\n" + updateMsg;
              text1.setText(newText);
            } else if (2 == msg.arg1) {
              text1.append(updateMsg);
            } else {
              text1.append("\n" + updateMsg);
            }
            break;
          case MSG_TYPE_SUCCESS:
            Uri uri = Uri.fromFile(new File(outputImageDir + outputImgFileName));
            img2.setImageURI(uri);
            text1.append("\nprocessing done!");
            text2.setText(getImageDimension(inputImagePath) + ";" + 
            getImageDimension(outputImageDir + outputImgFileName));
            break;
          case MSG_TYPE_FAILURE:
            text1.append("\nerror processing the image");
            break;
        }
      }
    };
    

    ImageProcRunnableMainActivity 的一个私有类实现了 Runnable 接口,它接受命令字符串,调用本地方法 naMain,并将结果消息发送给 Java UI 线程的处理器。这个类的实例将从后台线程中调用:

    private class ImageProcRunnable implements Runnable {
      String procCmd;
      public ImageProcRunnable(String cmd) {
        procCmd = cmd;
      }
      @Override
      public void run() {
        int res = naMain(procCmd, MainActivity.this);
        if (0 == res) {
          //success, send message to handler
          Message msg = new Message();
          msg.what = MSG_TYPE_SUCCESS;
          handler.sendMessage(msg);
        } else {
          //failure, send message to handler
          Message msg = new Message();
          msg.what = MSG_TYPE_FAILURE;
          handler.sendMessage(msg);
        }
      }
    }
    

    updateProgress:这是一个从本地代码通过 JNI 调用的方法。它向 Java UI 线程的处理程序发送一条消息:

    public void updateProgress(String pContent, int pInPlaceUpdate) {
      Message msg = new Message();
      msg.what = MSG_TYPE_PROG;
      msg.arg1 = pInPlaceUpdate;
      msg.obj = pContent;
      handler.sendMessage(msg);
    }
    
  3. 更新 fusch.c 源代码。

  4. naMain 方法中我们缓存了 JavaVM 引用,并为 MainAcitvity 对象引用 pMainActObj 获取了一个全局引用。fusch 程序使用了不止一个后台线程。我们将需要这些引用从那些后台线程调用 Java 方法:

    #ifdef ANDROID_BUILD
    int naMain(JNIEnv* env, jobject pObj, jstring pCmdStr, jobject pMainActObj);
    jint JNI_OnLoad(JavaVM* pVm, void* reserved) {
      JNIEnv* env;
      if ((*pVm)->GetEnv(pVm, (void **)&env, JNI_VERSION_1_6) != JNI_OK) {
        return -1;
      }
      cachedJvm = pVm;
      JNINativeMethod nm[1];
      nm[0].name = "naMain";
      nm[0].signature = "(Ljava/lang/String;Lcookbook/chapter9/portingexecutableaui/MainActivity;)I";
      nm[0].fnPtr = (void*)naMain;
      jclass cls = (*env)->FindClass(env, "cookbook/chapter9/portingexecutableaui/MainActivity");
      (*env)->RegisterNatives(env, cls, nm, 1);
      return JNI_VERSION_1_6;
    }
    int naMain(JNIEnv* env, jobject pObj, jstring pCmdStr, jobject pMainActObj) {
      char progBuf[500];
      jmethodID updateProgMID, toStringMID;
      jstring progStr;
      jclass mainActivityClass = (*env)->GetObjectClass(env, pMainActObj);
      cachedMainActObj = (*env)->NewGlobalRef(env, pMainActObj);
      updateProgMID = (*env)->GetMethodID(env, mainActivityClass, "updateProgress", "(Ljava/lang/String;I)V");
      if (NULL == updateProgMID) {
        LOGE(1, "error finding method updateProgress");
        return EXIT_FAILURE;
      }
      int argc = 0;
      char** argv = (char**) malloc (sizeof(char*)*4);
      *argv = "fusch";
      char** targv = argv + 1;
      argc++;
      jboolean isCopy = JNI_TRUE;
        char *cmdstr = (*env)->GetStringUTFChars(env, pCmdStr, &isCopy);
        if (NULL == cmdstr) {
           LOGI(2, "get string failed");
           return EXIT_FAILURE;
         }
         char* pch;
        pch = strtok(cmdstr, " ");
        while (NULL != pch) {
           *targv = pch;
           argc++;
           targv++;
           pch = strtok(NULL, " ");
       }
        LOGI(1, "No. of arguments: %d", argc);
         LOGI(1, "%s %s %s %s", argv[0], argv[1], argv[2], argv[3]);
    #else
     int main(int argc, char *argv[]) {
    #endif
    
  5. main 方法的 return 语句之前添加以下行,以释放本地字符串和缓存的 JavaVM 引用,避免内存泄漏:

    #ifdef ANDROID_BUILD
       (*env)->ReleaseStringUTFChars(env, pCmdStr, cmdstr);
       (*env)->DeleteGlobalRef(env, cachedMainActObj);
       cachedMainActObj = NULL;
    #endif
    
  6. 为了更新 GUI,我们向 Java 代码发送一条消息。我们需要更新源文件不同部分用于生成输出消息的代码。以下是这方面的一个示例:

    #ifdef ANDROID_BUILD
      progStr = (*env)->NewStringUTF(env, MSG[I_NOTHINGTODO]);
      (*env)->CallVoidMethod(env, pMainActObj, updateProgMID, progStr, 0);
    #else
      puts(MSG[I_NOTHINGTODO]);
    #endif
    
  7. seam_progresscarve_progress 函数是由在 naMain 启动的本地线程执行的。我们使用了缓存的 JavaVM 引用 cachedJvmMainActivity 对象引用 cachedMainActObj 来获取在 MainActivity.java 中定义的 updateProgress 方法的 jmethodID

    #ifdef ANDROID_BUILD
      char progBuf[500];
      JNIEnv *env;
      jmethodID updateProgMID;
      (*cachedJvm)->AttachCurrentThread(cachedJvm, &env, NULL);
      jstring progStr;
      jclass mainActivityClass = (*env)->GetObjectClass(env, cachedMainActObj);
      updateProgMID = (*env)->GetMethodID(env, mainActivityClass, "updateProgress", "(Ljava/lang/String;I)V");
      if (NULL == updateProgMID) {
        LOGE(1, "error finding method updateProgress at seam_progress");
        (*cachedJvm)->DetachCurrentThread(cachedJvm);
        pthread_exit((void*)NULL);
      }
    #endif
    
  8. 然后,我们可以从 seam_progresscarve_progress 调用 updateProgress 方法。以下是来自 carve_progress 函数的代码段,显示了这一点:

    #ifdef ANDROID_BUILD
      sprintf(progBuf, "%6d %6d %3d%%", max, pro, lrintf((float)(pro * 100) / max));
      progStr = (*env)->NewStringUTF(env, progBuf);
      (*env)->CallVoidMethod(env, cachedMainActObj, updateProgMID, progStr, 1);
    #else
      printf("%6d %3d%% ", pro, lrintf((float)(pro * 100) / max));
    #endif
    
  9. 构建并运行 Android 应用。你应该能看到一个与以下截图相似的图形用户界面:如何操作...

  10. 我们可以点击宽度高度按钮开始处理。左中和右截图分别显示了处理过程和结果:如何操作...

工作原理...

前面的示例显示了如何使用后台线程处理繁重的处理工作,以便 GUI 能够响应用户输入。当后台线程处理图像时,它还会向 UI 线程发送进度更新。

fusch 程序的细节实际上比所描述的核心思想要复杂一些,因为它使用了大量的并发处理。以下图表对此进行了说明:

工作原理...

一旦我们在 MainActivity.java 中点击了宽度高度按钮,将创建一个新的 Java 线程(后台线程 1),其实例为 ImageProcRunnable。此线程将调用 naMain 本地方法。

naMain 方法中使用 pthread_create 函数创建了多个本地线程。其中两个,分别标记为后台线程 2后台线程 3,将分别运行 seam_progresscarve_progress

在所有三个后台线程中,我们向绑定到 UI 线程的处理程序发送 MSG_TYPE_PROG 类型的消息。处理程序将处理这些消息并更新图形用户界面。

从本地代码发送消息

在 Java 中向处理程序发送消息很简单;我们只需调用 handler.sendMessage() 方法。但在本地代码中可能会有些麻烦。

MainActivity.java中,我们定义了一个updateProgress方法,该方法接收一个字符串和一个整数,构建一条消息,并将其发送给处理器。本地代码通过 JNI 调用这个 Java 方法以便发送消息。有两种情况:

  • 本地代码在 Java 线程中:这是前一个图中后台线程 1的情况。该线程是在 Java 代码中创建的,并调用了naMain本地方法。在naMain中,我们获取updateProgressjmethodID,并通过 JNI 函数CallVoidMethod调用updateProgress方法。更多详情,您可以参考第二章,Java Native Interface中的Calling static and instance methods from native code一节。

  • 本地代码在本地线程中:这就是后台线程 2后台线程 3发生的情况。这些线程是通过naMain中的pthread_create函数创建的。在进行任何 JNI 调用之前,我们必须调用AttachCurrentThread将本地线程附加到 Java 虚拟机。注意,我们使用了缓存的MainActivity对象引用cachedMainActObj来调用updateProgress方法。关于在 JNI 中缓存更多详情,我们可以参考第二章,Java Native Interface中的Caching jfieldID, jmethodID, and reference data to improve performance一节。

我们创建的 GUI 看起来并不完美,但它足够简单,足以说明如何使用后台线程进行繁重处理以及从本地代码发送 GUI 更新消息。

posted @ 2024-05-23 11:07  绝不原创的飞龙  阅读(5)  评论(0编辑  收藏  举报