安卓应用与-Eclipse-教程-全-

安卓应用与 Eclipse 教程(全)

原文:Android Apps with Eclipse

协议:CC BY-NC-SA 4.0

零、简介

Android 是手机市场的主要参与者之一,其市场份额正在不断增长。Android 是第一个完整、开放和免费的移动平台,它为移动应用开发者提供了无尽的机会。与所有其他平台一样,拥有一个健壮灵活的开发环境是该平台成功的关键。

Eclipse 是 Java 程序员最常用的集成开发环境(IDE)。现在 Eclipse 是 Android 应用开发人员的首选 IDE。

Android Apps with Eclipse 提供了 Eclipse 的详细概述,包括帮助 Android 开发人员快速掌握 Eclipse 并简化其日常软件开发的步骤和插图。

这本书是给谁的

这本书是为那些希望使用 Eclipse IDE 快速掌握 Android 开发速度的初学者和中级开发人员编写的。

你将学到什么

本书涵盖以下主题:

  • Android 平台如何工作以及 Android 应用开发的基础知识
  • 如何使用最流行的 Java IDE Eclipse 开发 Android 应用
  • 如何为 Android 开发安装和配置 Eclipse
  • 如何利用 Eclipse 和 Android 本地开发工具包(NDK)来满足 C/C++ 的需求
  • 如何使用 Android 的脚本层(SL4A)利用 Eclipse 编写脚本
  • 如何使用 Eclipse 调试和排除 Android 应用的故障

下载代码

这本书的源代码从[www.apress.com](http://www.apress.com)开始对读者开放。

联系作者

读者可以通过作者在[www.zdo.com/android-apps-with-eclipse](http://www.zdo.com/android-apps-with-eclipse)Android 应用与 Eclipse 网站联系他。

一、安卓优先

在这一章中,我们将从多个角度简要介绍 Android 平台。我们将从 Android 的历史开始,以更好地理解其形成背后的动机。然后,我们将探索 Android 平台架构的各种技术的完美结合,这些技术使平台能够提供卓越的移动体验。我们将强调多层 Android 安全框架,该框架使用软件和硬件来保证平台的安全。我们将简要回顾通过 Android 框架为用户级应用提供的服务应用编程接口(API ),以便与平台进行交互。最后,我们将讨论 Android 应用的部署和分发。

安卓历史

Android Inc .于 2003 年 10 月在加州硅谷成立,其理念是提供一个更加了解用户位置和偏好的移动平台。

Google 于 2005 年 8 月收购了 Android Inc .作为 Google Inc .的全资子公司。Google 的主要意图是为用户和应用开发人员提供一个由 Google technologies 支持的完全开放的平台。

2007 年 11 月,开放手机联盟作为一个为移动设备开发开放标准的联盟成立。开放手机联盟通过宣布 Android 平台开始了它的旅程。不到一年,新成员开始加入这个财团。

在开放手机联盟的保护下,Android 成为由谷歌领导的开源项目。Android 开源项目的目标是提供一个开放平台来改善用户的移动体验。

Android 是第一个完整、开放、免费的移动平台。

  • 完成:Android 平台是一个健壮、安全、易于升级的移动平台,具有全面的框架和定义良好的接口。它允许应用开发人员开发应用,并将其完全融入平台。它还提供兼容性和认证计划,因此设备制造商可以设计高度兼容的设备。
  • 开放:整个 Android 平台都是根据开源 Apache 许可条款开发和提供的。Android 不区分预装应用和第三方应用。开发人员在开发应用时可以完全访问设备功能和服务。
  • 免费:Android 平台在平台上开发应用不收取任何许可费、版税、会员费或认证费。Android 平台源代码和软件开发工具包免费提供给应用开发者。软件开发平台在许多桌面操作系统上广泛可用,允许应用开发人员使用他们选择的操作系统开发应用。

今天,Android 是手机市场的主要参与者之一。根据最近的市场分析,平均每天有 70 万台 Android 设备被激活,已经有超过 2 亿台设备被激活。Android 目前拥有 48%的手机市场份额,并且还在快速增长。

安卓版本

Android 平台的第一个测试版于 2007 年 11 月 5 日发布。从那以后,它经历了一系列的更新和漏洞修复。尽管从应用开发人员的角度来看,错误修复通常是透明的,但更新通常意味着对框架 API 的更改和添加。出于这个原因,除了 Android 平台版本号之外,第二个版本号,称为 API 级别,用于标识所支持的框架 API。

自 2009 年 4 月以来,每个 Android 版本都以基于甜点的代号发布,如克莱尔、弗罗育和姜饼。这为 Android 平台引入了第三种版本控制方案,对于首次使用 Android 应用的开发者来说,这变得更加神秘。说到 Android 应用开发,你会经常听到有人说“我的应用需要 clair 及以上”、“这个方法至少需要 API 级”、“我的手机得到了 Android 2.1 更新”之类的话。了解他们所指的是哪个版本和哪个 API 级别,以及哪些新 API 是哪个 Android 平台版本的一部分,很容易成为一项繁琐的记忆练习。您可以使用表 1-1 作为参考,在这三个版本方案之间进行映射。

注:由于 Android 平台仍在继续发展,表 1-1 可能没有涵盖最新的平台版本。有关最新列表,请参考 Android 开发者页面的 API 级别部分,位于[developer.android.com/guide/appendix/api-levels.html](http://developer.android.com/guide/appendix/api-levels.html)

Image

Image

如表 1-1 所示,在开发应用时,您应该考虑 15 个 API 级别。API 级别也决定了用户的规模,所以在开发新的 Android 应用时,明智地选择这个数字是非常重要的。

安卓手机市场高度分散。仅仅看发布日期,你可能会认为大多数 Android 用户群至少运行 Android 3.0,因为它已经存在一年了;然而,事实并非如此。由于碎片化,发布日期远远不能给出正在使用的 Android 版本的清晰视图。图 1-1 是来自 Android 平台版本仪表盘([developer.android.com/resources/dashboard/platform-versions.html](http://developer.android.com/resources/dashboard/platform-versions.html))的最新版本分布图。

Image

图 1-1。 基于市场数据的安卓版本分布

在图 1-1 中可以看到,目前大部分 Android 用户群都在运行 Android 2.3.3,姜饼。这意味着您的应用至少需要支持 API level 10,以便面向大多数 Android 用户。这也意味着你不能在你的应用中使用新版本 Android 平台中引入的最新 API 特性。在本书中,我们将使用 Android 2.3.3 开发我们的示例。

版本繁多是安卓开发者的通病。大多数应用开发人员为不同的 API 级别开发包。这解决了问题,但是这意味着需要维护不同的代码分支。

2011 年 3 月,谷歌推出了支持包作为版本问题的解决方案。支持包是一组静态库,允许应用开发人员开发支持多个 Android 平台版本的 Android 应用。支持包的主要目标是简化从单一代码库支持多个 Android 版本的过程。您可以在[developer.android.com/sdk/compatibility-library.html](http://developer.android.com/sdk/compatibility-library.html)找到有关支持包的更多信息。

Android 平台架构

Android 更像是一个完整的移动设备软件栈,而不是一个操作系统。它是针对移动需求精心优化的工具和技术的组合。

Android 依赖于久经考验的 Linux 内核来提供其操作系统功能。对于用户空间应用,Android 通过使用 Dalvik 虚拟机依赖于 Java 虚拟机技术。Android Zygote 应用进程通过服务预加载和资源共享,加快了应用的启动时间,并允许高效利用移动平台上稀缺的内存资源。所有这些成功的技术都对 Android 平台的成功起到了重要作用,如图图 1-2 所示。除了这些工具和技术之外,Android 运行时还提供了一个独特的计算环境,该环境专为向最终用户提供流畅的移动体验而定制,同时简化了开发人员的移动应用开发。

Image

图 1-2。 安卓平台架构

硬件抽象层

Android 依赖 Linux 内核作为其硬件抽象层(HAL ),并提供其操作系统功能。在 Android 开发过程中,为了适应移动需求,对 Linux 内核代码进行了多次改进。以下是最显著的特征:

  • 闹钟定时器
  • 偏执的网络安全
  • 粘合剂
  • 保持进程在休眠时从屏幕消失
  • Android 共享内存(Ashmem)
  • 进程共享内存
  • 低记忆杀手(维京黑仔)
  • 记录器

虽然应用开发人员不需要直接与这些底层组件交互,但是了解它们在整个 Android 平台中的角色是非常重要的。

闹钟定时器

Android 是为在移动平台上运行而设计的,设备的唯一电力是通过电池提供的。Android 进入各种睡眠模式,以便有效地使用有限的电池资源。当设备处于睡眠模式时,应用需要一种方法来唤醒系统,以便执行某些周期性任务。在 Android 上,这是通过闹钟定时器内核模块实现的。它允许用户空间应用调度自己在未来的某个时间运行,而不管设备的状态如何。

Android 运行时中的android.app.AlarmManager类允许用户级应用通过 API 调用与闹钟定时器进行交互。报警管理器允许应用使用报警定时器安排一个意图(意图将在下一章讨论)。当警报响起时,系统广播预定意图以启动应用。只要应用忙于执行其广播接收器的onReceive方法中的代码,报警管理器就会保持一个 CPU 唤醒锁(本章稍后会介绍)。这保证了设备不会再次进入睡眠模式,直到应用完成执行其任务。闹钟定时器在设备睡眠时保留预定的闹钟;但是,如果设备关闭并重新启动,该列表将被清除。

偏执的网络安全

网络安全是任何移动平台最重要的要求之一。为了提供广泛的安全级别,Android 在尽可能低的层将这一要求作为内核修改来处理。通过这种实现,Android 限制了调用进程的组的访问。应用应该预先请求必要的权限,以便成为这些网络组的一部分。否则,这些应用的网络访问将被阻止在内核中。

粘合剂

Android 平台架构大量使用进程间通信(IPC)。应用通过使用 IPC 与系统、电话服务以及相互之间进行通信。

注意:进程间通信(IPC) 是一种允许应用相互之间以及与操作系统本身交换数据的机制。

尽管 Android 依赖于 Linux 内核来实现与操作系统相关的功能,但它并不使用通过 Linux 内核提供的 System V IPC 机制。相反,它依赖于 Android 专用的 IPC 系统,即 Binder。

活页夹技术起源于 Be 公司的工程师,是 Be 操作系统(BeOS)的一部分。Binder 的开发继续在 PalmSource 进行,作为 Cobalt 系统的关键基础,后来作为 Linux 内核模块以 OpenBinder 项目的名义被开源。Android 的 Binder 实现完全重写了 OpenBinder 项目,以符合 Apache 许可。Binder 使用内核模块在进程间通信,如图图 1-3 所示。

Image

图 1-3。 Binder 内核模块允许两个应用进行通信

Binder 的用户空间代码在每个进程中维护一个线程池,这些线程用于将传入的 Binder 请求作为本地事件进行处理。Binder 还负责跟踪跨进程的对象引用。此外,Binder 通过在每个 Binder 请求中传输调用进程的用户和组 ID,提供了额外的安全级别。

Binder 是 Android 平台中的一个关键构造。它是整个 Android 平台的中央消息传递渠道。Android 应用通过 Binder 接口与系统、服务以及相互之间进行通信。

虽然 Binder 是作为一个底层服务实现的,但是应用开发人员并不希望直接与它进行交互。Android 运行时提供了android.os.IBinder接口作为 API,通过 Binder 与其他进程进行通信。Android 提供了 Android 界面定义语言(AIDL),该语言针对 Binder 进行了调整。

AIDL 允许您定义客户机和服务器用来相互通信的编程接口。与许多其他操作系统一样,在 Android 上,不允许进程直接访问另一个进程的内存。AIDL 提供了将对象分解成 Binder 可以理解并跨项目边界使用的原语的功能。

穿线是与 Binder 互动的最重要部分之一:

  • 从本地进程发出的调用在调用线程中执行。Binder 调用是同步的,将阻塞当前线程,直到请求被处理。如果请求预计需要很长时间才能完成,则不应该从应用的主线程发出请求。这将使应用挂起,并可能导致应用被 Android 平台终止。Binder 还通过单向属性支持非阻塞请求。
  • 来自远程进程的调用从本地进程提供的线程池中分派。服务代码应该是线程安全的,因为请求可以由这些线程中的任何一个执行。

Android SDK 提供了必要的代码生成器,将 AIDL 定义的编程接口转换成实际的 Java 类。应用开发人员只需要为生成的接口和将向客户端提供接口的 Android 服务提供实现。

保持进程在休眠时从屏幕消失

Android 旨在资源稀缺的移动平台上运行。正因为如此,Android 设备非常频繁地进入睡眠模式。虽然这允许系统有效地使用可用资源,但是当内核或应用正在进行重要处理时,设备进入睡眠模式并不可取。唤醒锁是作为内核补丁引入的,目的是允许应用在执行任务时防止系统进入睡眠模式。

Android 平台支持两种类型的唤醒锁:

  • 空闲唤醒锁可防止系统进入低功耗空闲状态。
  • 挂起唤醒锁可防止系统进入全系统挂起状态。

应用开发者通过android.os.PowerManager.WakeLock接口与唤醒锁进行交互。要使用这个接口,应用应该提前请求android.permission.WAKE_LOCK

唤醒锁应谨慎使用。阻止设备进入睡眠模式将增加功耗,最终导致电池电量耗尽。应用应在重要操作期间保持唤醒锁,并在操作完成后立即释放唤醒锁。

Android 共享内存

Android 共享内存(Ashmem))是 Android 平台上一个类似 POSIX 的共享内存子系统,作为内核模块实现。Ashmem 针对移动需求进行了高度优化,它为低内存设备提供了更好的支持。Ashmem 支持可以在多个进程之间共享的引用计数对象。

进程共享内存

除了 Ashmem,Android 还提供了第二种类型的共享内存子系统,称为进程共享内存(Pmem)。Pmem 用于在进程间共享大量物理上连续的内存。Android 媒体引擎主要使用 Pmem 在媒体引擎和应用进程之间传递大型媒体帧。

低内存黑仔

低内存杀手,也被称为维京黑仔,是 Linux 内核中其他特定于 Android 的增强功能之一。此功能允许系统在内存耗尽之前回收内存。

为了启动应用,设备必须首先将应用代码从永久存储器读取到随机存取存储器(RAM)中。由于这是一个耗时且昂贵的过程,Android 试图尽可能长时间地保持应用进程。但最终,当内存不足时,它需要将它们从 RAM 中删除。

为防止耗尽内存而删除应用的顺序取决于应用的重要性,这由用户与该应用交互的当前状态来衡量:

  • 用户当前正在与之交互的具有前台活动的应用被认为是最重要的。
  • 具有可见活动的应用(当前没有与用户交互,但仍然可见)也被认为是重要的。
  • 具有对用户不再可见的后台活动的应用被认为是不重要的,因为它的当前状态可以被保存,并且稍后当用户导航回该活动时可以被恢复。

当从内存中删除进程时,Android 从最不重要的应用开始。空进程是指没有活动、服务或广播接收器的进程。这些类型的应用被认为是最不重要的,Android 首先开始删除它们。

使用/etc/init.rc系统配置文件配置每个应用状态的阈值。表 1-2 列出了这些阈值。

Image

低内存杀手服务通过ActivityManagerService获得这些信息。

记录器

日志记录是故障排除最重要的部分,但是很难实现,尤其是在移动平台上,应用的开发和执行发生在两台不同的机器上。Android 有一个广泛的日志系统,允许系统范围内集中记录来自 Android 系统本身和应用的信息。

Android 日志系统被实现为一个内核模块,称为 logger。还提供了一组 API 调用和用户级应用来与记录器模块进行交互。

任何给定时间平台上记录的信息量使得查看和分析这些日志消息非常困难。为了简化这个过程,Android 日志记录系统将日志消息分成四个独立的日志缓冲区:

  • 主:主应用日志消息
  • 事件:系统事件
  • 无线电:与无线电相关的日志消息
  • 系统:用于调试的低级系统调试消息

这四个缓冲区作为伪设备保存在/dev/log系统目录下。因为移动平台上的输入和输出(I/O)操作非常昂贵,所以日志消息不保存在永久存储器中;相反,它们被保存在内存中。为了控制日志消息的内存利用率,logger 模块将它们放在固定大小的缓冲区中。主日志、广播日志和系统日志作为自由格式的文本消息保存在 64KB 的日志缓冲区中。事件日志消息携带二进制格式的附加信息,它们保存在 256KB 的日志缓冲区中。

还提供了一组用户级应用来查看和过滤这些日志,例如 logcat 和 Dalvik 调试监控服务器(DDMS)工具,我们将在第五章中研究这些工具。

Android 运行时提供了一组 API 调用,允许应用轻松地将其日志消息发送给日志记录器。应用日志消息通过以下类发送:

  • android.util.Log:该类用于发送应用日志消息。它提供了一组方法来指定消息的优先级,还提供了一个标记来指示哪个应用正在生成这个日志消息。
  • android.util.EventLog:这个类用于发送二进制格式的事件日志消息。
  • 这个类被 Android 运行时组件用来发送系统日志消息。它不是 Android API 的一部分,也不能从应用中访问。

合子

在大多数类似 UNIX 的操作系统中,被称为 Init 的应用被认为是所有进程的父进程。Init 在内核成功启动后启动。它的主要作用是根据系统配置启动一组其他进程。

Zygote 也称为“app 进程”,是系统启动时由 Init 启动的核心进程之一。Zygote 在 Android 平台中的角色与 Init 非常相似。它的首要任务是启动一个新的 Dalvik 虚拟机实例,并初始化核心 Android 服务,如下所示:

供电业务

活动服务

打包服务

内容服务

报警服务

窗口服务

内容提供商

电话服务

电池服务

看门狗

启动这些服务后,Zygote 开始进行它的第二项任务,这也是它名字的由来。

注:根据其字典定义,受精卵是最初形成的细胞。在单细胞生物中,受精卵分裂产生后代。

如前所述,在 Android 上,每个应用都在其专用的虚拟机实例中运行。此外,Android 应用依赖于一组类和数据对象,这些对象需要首先加载到内存中,以便应用执行其任务。当启动一个新的应用时,这会带来很大的开销。尽管有这种开销,Android 需要尽可能缩短启动时间,以便提供高度响应的用户体验。通过使用分叉,Zygote 以一种快速有效的方式解决了这个问题。

在计算中,分叉是克隆现有进程的操作。新进程拥有父进程所有内存段的精确副本,尽管两个进程都独立执行,如图 1-4 所示。写时复制是分叉中使用的一种优化策略,通过允许两个进程共享同一个内存段,直到其中一个进程试图修改它,从而延迟内存的复制。

Image

图 1-4。 Zygote 等应用共享安卓框架只读组件

由于 Android 运行时类和数据对象对于应用来说是不可变的,这使得它们成为分叉期间写时复制优化的理想候选对象。

Zygote 预加载 Android 运行时对象,并等待启动新应用的请求。当一个新的请求到达时,它不是启动一个新的虚拟机实例,而是简单地分叉。这使得新的应用可以快速启动,同时保持较低的内存占用。

达尔维克虚拟机

Java 是一种通用的、面向对象的编程语言,专门为平台无关的应用开发而设计,目标是“编写一次,在任何地方运行”Java 通过将应用代码编译成一种称为字节码的中间独立于平台的解释语言来实现这一点。在运行时,这个字节码通过另一个叫做 Java 虚拟机的 Java 实体来执行。

虚拟机是运行在主机上并解释字节码的本地应用。为了优化复杂应用的运行时,大多数虚拟机实现还支持实时(JIT)特性,这允许从字节码到本机代码的即时翻译。这允许长时间运行的应用执行得更快,因为只有在应用执行的开始才需要解释字节码。

大多数移动平台面临的最大挑战之一是缺乏应用。为了从一开始就解决这个问题,Android 依赖于久经考验的 Java 编程语言,这种语言已经拥有非常庞大的开发人员社区,以及将促进应用开发的应用、工具和组件。

Android 还依赖于高度定制的虚拟机实现,该虚拟机实现针对移动需求进行了调整。Dalvik 虚拟机是 Android 为移动平台定制的 Java 虚拟机。

Dalvik 虚拟机与其他 Java 虚拟机实现有很大不同。桌面平台上的大多数虚拟机实现都是基于基于堆栈的虚拟机模型开发的。由于移动需求,Dalvik 虚拟机基于基于寄存器的虚拟机模型。基于寄存器的虚拟机需要更长的指令来解释;然而,与基于堆栈的虚拟机相比,实际执行的指令数量非常少。这使得基于寄存器的虚拟机成为移动环境的更好选择,在移动环境中,计算能力是一种稀缺资源。

由于 Dalvik 虚拟机需要不同类型的字节码来解释,所以它不支持标准的 Java 类文件,它依赖于自己的格式,这种格式被称为 Dalvik 可执行文件(DEX)。Android 软件开发平台附带了一套工具,用于将编译后的 Java 类文件后处理成 DEX 格式。

DEX 格式也是在移动平台上存储编译后的 Java 应用代码的一种更紧凑的方式。标准 Java 应用由多个单独存储的类文件组成。DEX 将所有的类文件合并成一个大的 DEX 文件,如图图 1-5 所示。这最小化了应用代码的占用空间。

Image

图 1-5。 从 JAR 文件中的标准 Java 类文件到单个 DEX 文件的转换

DEX 格式的常量池允许将字符串、类型、字段和方法常量以及代码中的其他内容存储在一个地方,使用这些列表的索引而不是全名。这将类文件的大小减少了近 50%。

Android 平台在自己的专用虚拟机实例中运行每个应用,作为沙箱。这就对平台提出了很高的要求,因为多个虚拟机被期望在有限的 CPU 资源环境中同时运行。Dalvik 虚拟机专门针对这种环境进行了调整。

文件系统

文件系统是操作系统中非常重要的一部分。特别是在移动平台上,文件系统在满足操作系统的期望方面起着重要的作用。

移动设备依赖基于闪存的存储芯片。Android 依赖另一个 Flash 文件系统(YAFFS2)作为其主要文件系统。YAFFS2 是 Charles Manning 为 Linux 操作系统设计和编写的一个开源文件系统实现。YAFFS2 是一个高性能文件系统,专门设计用于基于 NAND 的闪存芯片。它是一个日志结构的文件系统,将数据完整性作为一个高优先级。

除了文件系统,操作系统文件和组件的组织结构在 Android 中也起着重要的作用。为了保护用户的机密信息,移动平台应该易于升级并且高度安全。Android 通过使用多个分区来组织自己,从而解决了这一需求。通过将操作系统的不同部分保存在不同的分区中,Android 提供了高级别的安全性,也使平台易于升级。

使用的分区取决于设备制造商。以下是一些最常见的问题:

  • /boot:这个分区包括引导设备所需的引导加载程序和 Linux 内核。用户应用无法写入该分区,因为修改该分区的内容可能会导致设备无法再启动。
  • /system:这个分区包含了所有预装在设备上的 Android 系统文件和应用。在升级过程中,这个分区会被最新版本的 Android 平台替换。该分区不可由用户应用写入,尽管 Android Market 应用可以使该分区暂时可写,以便更新预加载的应用。
  • /recovery:这个分区保存了一个恢复镜像,是一个备选的引导分区。它提供维护功能,以便恢复系统或执行其他任务,如进行系统备份。该分区也不能从用户应用写入。
  • /data:这个分区保存用户的应用以及用户的数据,比如联系人、信息和设置。当设备恢复出厂设置时,该分区会被擦除。
  • /cache:该分区用于存储经常访问的文件。在大多数 Android 设备上,cache不是闪存介质上的分区,而是存储在 RAM 中的虚拟分区。当设备重新启动时,该分区的内容不会保留。
  • /sdcard:这是内部存储器上的挂载点,而不是分区。连接到设备的 SD 卡以此名称安装。应用并不总是可以访问这个挂载点,因为当设备通过 USB 连接时,它可能直接挂载在主机 PC 上。

虽然 Android 并不期望应用开发人员直接使用这些分区,但是了解它们的用途在 Android 应用开发过程中非常有用。

安全

与许多其他移动平台一样,从用户的角度来看,Android 的最大要求是用户应用和数据的安全性和完整性。Android 的设计考虑到了安全性。

Android 架构在平台的多个层面提供安全性。这个广泛的安全框架也通过 Android 运行时向开发人员公开。精通安全的开发人员可以很容易地依赖这些 API,以便为他们的应用及其使用的数据提供高级别的安全性。不太熟悉安全性的开发人员已经受到默认安全设置的保护。

Android 通过使用来自硬件和软件的多种安全特性来提供高级别的安全性。虽然它被设计为在各种硬件平台上工作,但 Android 仍然利用了特定于硬件的安全功能,如 ARMv6 的永不执行功能。

Android 平台是建立在 Linux 内核之上的。Linux 内核本身已经在许多安全敏感的环境中使用了很多年。Linux 内核为 Android 提供了几个关键的安全特性,如下所示:

  • 基于用户的权限模型
  • 进程隔离
  • 安全 IPC 机制
  • 从内核本身移除不必要功能的能力

Linux 内核是为多用户平台设计的。虽然 Android 是单用户环境,但它仍然利用了基于用户的权限模型。Android 在虚拟机沙箱中运行应用,将它们视为系统中的不同用户。仅仅依靠基于用户的权限模型,Android 就可以通过阻止应用访问其他应用的数据和内存来轻松保护系统。

在 Android 上,服务和硬件资源也通过基于用户的权限模型得到保护。这些资源都有自己的保护组。在应用部署期间,应用请求访问这些资源。如果用户同意请求,应用将成为这些资源组的成员。如果应用不是该资源组的成员,则不允许它访问任何附加资源。

除了操作系统提供的安全功能,Android 还使用 ProPolice 增强了 Android 平台二进制文件,以保护它们免受堆栈缓冲区溢出攻击。

文件系统保护也是自 Android 3.0 以来可用的 Android 新功能之一。它允许 Android 使用 AES-128 算法加密整个存储介质。这可以防止其他人在不知道使用的密钥的情况下访问用户的数据。

设备管理是自 Android 2.2 以来可用的其他安全功能之一。它允许管理员远程实施安全策略,并在设备丢失或被盗时远程擦除设备。

服务

Android 平台不仅限于 Linux 内核提供的特性。Android 运行时为应用开发人员提供了许多服务。以下是提供的主要服务。

  • 可访问性服务:该服务通过android.view.accessibility.AccessibilityManager类提供。它是一个系统级服务,充当可访问性事件的事件调度程序,并提供一组 API 来查询系统的可访问性状态。
  • 账户服务:该服务通过android.accounts.AccountManager类提供。它是用户在线账户的集中注册。它允许应用在用户批准后使用用户的帐户访问在线资源。
  • 活动服务:该服务通过android.app.ActivityManager类提供。它允许应用与系统中运行的活动进行交互。
  • 报警服务:该服务通过android.app.AlarmManager类提供。它允许应用向报警服务注册,以便在将来的某个时间安排执行。
  • 音频服务:该服务通过android.media.AudioManager类提供。它允许应用控制音量和铃声模式。
  • 剪贴板服务:该服务通过android.content.ClipboardManager类提供。它允许应用将数据放入系统剪贴板,并从剪贴板中检索数据。
  • 连接服务:该服务通过android.net.ConnectivityManager类提供。它允许应用查询网络连接的状态。当网络连接发生变化时,它也会生成事件。
  • 设备策略服务:该服务通过android.app.admin.DevicePolicyManager类提供。设备管理 API 提供系统级的设备管理功能。它允许开发在企业环境中有用的安全感知应用。
  • 下载服务:该服务通过android.app.DownloadManager类提供。它处理长时间运行的 HTTP 下载。应用可以使用此服务请求将 URI 下载到特定的目标文件。下载服务负责 HTTP 交互,并在失败、连接更改和系统重启后重试下载。
  • 投件箱服务:该服务通过android.os.DropBoxManager类提供。它提供系统范围的、面向数据的日志存储。它从应用崩溃、内核日志和其他来源收集数据。数据不会直接发送到任何地方,但是调试工具可能会扫描并上传条目进行处理。
  • 输入法服务:该服务通过android.view.inputmethod.InputMethodManager类提供。它允许应用通过提供的方法与输入法框架(IMF)进行交互。
  • 通知服务:该服务通过android.app.NotificationManager类提供。它允许应用通知用户发生的事件。后台运行的服务仅通过此服务与用户通信。
  • 位置服务:该服务通过android.location.LocationManager类提供。它允许应用获得设备当前位置的定期更新。
  • 近场通信服务:该服务通过android.nfc.NfcManager类提供。它允许应用使用设备的近场通信(NFC)功能。
  • 包服务:该服务通过android.content.pm.PackageManager类提供。它允许应用检索与当前安装在系统上的应用包相关的信息。
  • 电力服务:该服务通过android.os.PowerManager类提供。它允许应用控制设备的电源状态。它允许应用保持唤醒锁,以防止设备在执行任务时进入睡眠模式。
  • 传感器服务:该服务通过android.hardware.SensorManager类提供。它允许应用访问设备的传感器。
  • 电话服务:该服务通过android.telephony.TelephonyManager类提供。它允许应用与移动设备的电话功能进行交互。它还为应用生成事件来监视电话状态的变化。
  • UI 模式服务:该服务通过android.app.UiModeManager类提供。它允许应用控制设备的用户界面(UI)模式,例如禁用汽车模式。
  • USB 服务:该服务通过android.hardware.usb.UsbManager类提供。它允许应用查询 USB 的状态,并通过 USB 设备与设备通信。
  • 振动器服务:该服务通过android.os.Vibrator类提供。它允许应用控制设备上的振动器。
  • 壁纸服务:该服务通过android.service.wallpaper.WallpaperService类提供。它允许应用在后台显示动态壁纸。
  • Wi-Fi 点对点服务:该服务通过android.net.wifi.p2p.WifiP2pManager类提供。它允许应用发现可用的对等点,并通过 Wi-Fi 网络建立对等连接。
  • Wi-Fi 服务:该服务通过android.net.wifi.WifiManager类提供。它允许应用管理 Wi-Fi 连接。应用可以列出和更新已配置的网络,访问接入点扫描的结果,以及建立和断开连接。

Android 部署和分发

因为 Android 平台是一个免费的平台,它不收取任何许可费、版税、会员费或认证费来开发和分发平台上的应用。

Android 平台让应用开发者决定如何分发和赚钱他们的应用。应用开发者可以以免费软件、共享软件、广告赞助或付费的方式分发他们的应用。

Android 平台附带了一个默认的市场,Google Play,以前称为 Android Market,是谷歌为 Android 设备开发的在线商店。与 Android 平台不同,Android Market 应用不是开源的。它仅适用于符合谷歌兼容性要求的设备。客户端部分预装在 Android 设备上,名为 Market。用户可以使用该应用搜索和下载 Android 应用。市场应用还通过通知用户软件更新来保持已安装的 Android 应用最新。

应用开发者使用 Android 市场的服务器部分。通过基于 web 的界面,应用开发人员可以上传他们的应用进行发布。

Android Market 对分布式应用运行一组测试,但它不对从 Android Market 下载的应用承担任何责任。在安装过程中,Android Market 应用显示应用请求的列表权限,并在继续安装之前获得用户的隐式权限。

虽然大多数 Android 设备预装了谷歌的 Android Market 应用,但 Android 平台支持其他应用分发渠道。GetJar 和 Amazon Appstore 是 Android 应用分发的两个替代方案。

总结

我们在这一章开始时简要概述了 Android 的历史和现有的 Android 版本。然后,我们探讨了 Android 平台和 Linux 内核的核心,并简要回顾了为交付卓越的移动平台而对 Linux 内核进行的特定于 Android 的更改和添加。

我们解释了 Android 选择 Java 技术作为 Android 应用基础的原因,以及 Dalvik 虚拟机为优化移动计算的 Java 技术而提供的独特功能。我们探索了 Zygote,这个应用进程使 Android 应用具有快速的启动时间和较小的内存占用。我们还研究了 Android 多层安全框架。然后,我们简要介绍了允许应用与 Android 平台交互的 Android 框架服务。最后,我们讨论了 Android 的开发和发行。

在下一章,我们将关注 Android 应用架构。

二、应用架构

理解 Android 应用的架构是开发可靠应用的关键。在这一章中,我们将开始探索 Android 应用架构。

首先,我们将简要回顾 Android 框架提供的基本组件,如活动、服务、广播接收器、内容提供者和用户界面组件。然后,我们将非常详细地检查活动和服务生命周期。接下来,我们将完成打包 Android 应用以进行部署的过程。最后,我们将研究 Android manifest 文件,以及它在 Android 应用开发中的作用和重要性。

安卓组件

Android 和其他移动平台的主要区别在于应用的定义。

其他移动平台将应用定义为在自己的沙箱中运行的独立程序,与周围平台的交互有限。移动平台提供 API,允许应用使用平台服务和数据存储,如地址簿,以提供丰富的用户体验。然而,这种通信总是单向的,这意味着应用可以使用平台服务,但是平台和其他应用不能访问另一个应用提供的服务。

在 Android 上,应用就像是模块。每个应用都由一组组件组成,平台和其他应用都可以访问这些组件。Android 上的每个新应用都通过提供一组新的组件来扩展平台,并为其他应用开发者提供更多机会。应用开发人员不需要决定一组 API 或一个契约来实现应用之间的互操作性。

Android 框架定义了四个主要组件:活动、服务、广播接收器和内容提供者。每个应用不需要使用所有这些组件,但是正确使用它们可以让应用完全融入平台。

活动和意图

活动是应用最重要的组成部分。它对应于一个显示屏。用户只能通过活动与 Android 应用进行交互。一个应用可以由一个或多个活动组成。每个活动都允许用户执行特定的任务。为了模块化,期望每个活动做一个单独的任务。

用户发起一个新的活动是为了完成某项任务。这些意图在 Android 框架中被捕获为意图。意图是对要执行的操作的抽象描述。它使用被动数据结构提供不同组件之间的后期运行时绑定。

Android 保留了从意图到活动的映射,并基于给定的意图发起正确的活动。对于某些意图,可能有多个活动可以完成该任务。在这种情况下,Android 会向用户提供这些活动的列表以供选择。

一项复杂的任务可能涉及不止一项活动。在这种情况下,当用户从一个活动移动到另一个活动时,活动保存在活动堆栈中。

为了更好地理解活动的概念,让我们想象一个简单的用例,其中用户正在发送电子邮件:

  1. 用户按下屏幕上的撰写电子邮件按钮。
  2. 该代码捕获用户的意图,将电子邮件消息组成一个意图对象,并将该意图提供给 Android 框架。
  3. Android 检查它的注册表,提取能够满足这个意图的活动,并将这个新活动添加到活动堆栈的顶部。开始时,活动会占据整个屏幕。
  4. 用户按下选择接收者按钮。用户的意图在一个新的意图对象中被捕获,Android 框架再次检查它的注册表并启动联系人列表活动。
  5. 用户从列表中选择一个或多个接收者,并选择完成按钮。结果,该活动将用户的选择返回到 Android 框架,并通过使之前的活动再次可见,将自己从活动堆栈中移除。
  6. 收到 Android 框架的结果后,撰写电子邮件活动会相应地在用户界面中填充所选收件人的列表。
  7. 完成消息后,用户单击 Send 按钮,电子邮件就被发送了。
  8. 撰写电子邮件活动将其自身从活动堆栈中移除,用户返回到他或她开始的屏幕。

应用不仅限于使用自己的活动。在任务流期间,应用可以利用由平台或其他应用提供的其他活动。例如,要从用户的地址簿中选择一个联系人,应用可以使用平台已经提供的活动,而不是编写新的活动。这种方法促进了活动的重用,也提供了整个平台的一致性。

活动是为与用户互动而设计的。当它们对用户不再可见时,Android 可能会随时终止它们,以便释放内存资源。出于这个原因,活动不适合执行预计需要很长时间才能完成的任务,例如从互联网上下载文件。Android 框架提供了运行这些类型任务的服务组件。

服务

服务在后台运行。服务不提供用户界面,它们不能直接与用户交互。Android 并没有限制它们的生存期,只要系统有足够的资源来执行前台任务,它就允许它们继续在后台运行。应用可以提供与用户交互的活动,以便控制服务。

例如,假设我们正在开发一个音乐播放器应用。我们希望让用户选择一个音乐文件,并在继续使用设备的同时收听它。初始活动可以与用户交互以选择歌曲;但是,活动不能直接播放歌曲,因为活动的生存期受到其可见性的限制。我们将需要有一个服务,将在后台运行,以便应用可以继续播放歌曲,而用户正在用设备做其他任务。在任何给定的时间,用户都可以启动一个活动来控制服务,因为服务本身不能直接与用户交互。

与活动一样,应用并不局限于它自己的服务。应用还可以使用由平台或其他应用提供的服务。例如,为了连续接收全球定位系统(GPS)坐标,应用可以启动由平台提供的 GPS 服务。

服务也是通过意向启动的。根据设计,在任何给定时间,只有一个服务实例可以运行。Android 框架在第一个请求到达时启动服务,然后将后续请求交付给已经运行的实例。

有时服务可能需要用户的关注。服务使用通知来通知用户服务的当前状态。例如,在我们的音乐播放器应用中,当新歌曲开始播放时,可以在通知栏上显示带有歌曲名称的通知来通知用户。

广播接收器

应用不仅与用户交互,还通过生成和消费事件与平台和其他应用交互。在 Android 上,这些事件也以意图的形式交付。

为了接收某些类型的事件,应用可以通过提供一个广播接收器来注册一组意图。当系统中生成匹配事件时,Android 会将该事件传递给广播接收器。

例如,假设我们想让我们的应用在手机开机时自动启动。在我们的应用中,我们指定应用对接收设备启动事件感兴趣。当设备启动时,它会广播事件。只有感兴趣的应用通过它们的广播接收器接收该事件。

内容提供商

内容提供商允许 Android 应用与平台和其他应用交换数据。与其他组件不同,内容提供者不依赖意图。相反,内容提供者使用内容 URIs 形式的标准接口,并以一个或多个表的形式提供对数据的访问,这些表类似于关系数据库中的表。这些表的结构通过Contract类与外部应用通信。Contract类不是内容提供商框架的一部分。内容提供商开发人员应该定义并使Contract类对外部应用可用。

当应用发出内容提供商查询时,Android 通过注册中心将给定的 URI 与适当的内容提供商进行匹配。Android 框架检查以确保应用具有必要的特权,并将请求发送给相应的内容提供商。响应以光标的形式返回到发出请求的应用。然后,应用通过光标提供的界面检索和操作数据。

视图、小工具、布局和菜单

视图对象是 Android 平台上用户界面的基本单元。一个视图对象是一个数据结构,其属性存储布局参数和屏幕上矩形区域的内容。它提供了处理其绘图和布局测量所需的方法。

一个小部件是一个视图对象,允许应用与用户交互。Android 运行时提供了一组丰富的小部件,使应用开发人员能够轻松开发全面的用户界面。Android 应用开发人员并不局限于使用 Android 运行时提供的小部件。通过派生新的视图对象,开发人员可以从头开始创建新的小部件,或者基于现有的小部件创建新的小部件。通过基类android.view.View提供一个小部件。

一个布局用于表达视图层次以及每个视图组件应该如何在显示器上定位。由于 Android 设备的大小、分辨率和方向差异很大,这种布局允许应用开发人员根据设备的规格动态定位视图组件。Android 运行时提供了一组丰富的布局组件,允许基于一组不同的约束来定位视图。通过基类android.view.ViewGroup提供布局。以下是一些常见的布局对象:

  • 框架布局:这是最简单的布局对象类型。它是通过android.widget.FrameLayout类提供的。这是一种基本布局,只能容纳一个视图对象,该对象将占据框架视图所覆盖的整个空间。
  • 线性布局:该布局允许为视图对象分配权重,并相应地定位它们。它是通过android.widget.LinearLayout类提供的。它可以根据其配置垂直或水平定位视图对象。所有视图对象都被卡住,一个接一个。可以使用配置参数引入余量。此外,可以指定一个视图对象来填充整个空白显示区域。
  • 表格布局:该布局允许视图对象以类似表格的格式按行和列放置。它是通过android.widget.TableLayout类提供的。尽管它遵循表格格式,但它不在单元格周围提供边框。此外,单元格不能跨列。
  • 相对布局:该布局允许视图对象在显示器上相对定位。它是通过android.widget.RelativeLayout类提供的。它是最高级的布局组件之一。

除了小部件和布局,应用菜单对于用户界面开发也非常重要。应用菜单为应用功能和设置提供了可靠的界面。

使用 Android 设备上的硬菜单按钮和软菜单按钮来显示菜单。菜单正在慢慢失去其重要性,并被 Android 平台更高版本的动作栏所取代。从 Android 3.0 开始,主机设备不再需要提供硬菜单按钮。

Android 用户界面是根据应用的功能将视图、小部件、布局和菜单结合起来形成的。Android 框架允许应用将它们的用户界面动态定义为应用代码的一部分,或者它们可以依赖于特定于 Android 平台的基于 XML 的用户界面定义语言。这种基于 XML 的语言允许在实际应用逻辑之外设计和管理视图代码。此外,应用开发人员可以在不改变应用逻辑的情况下,为纵向和横向显示设计不同的用户界面。

Android 应用可以从应用代码中控制和填充视图对象。由于 Android 平台的用户界面架构,用户界面预计只能从主 UI 线程进行修改。不支持从应用线程修改用户界面,这可能会在应用运行时导致问题。Android 运行时提供了一个增强的消息队列系统,允许应用开发人员通过主 UI 线程调度与用户界面相关的任务。

尽管 Android 应用可以操作视图对象,但是由于保持用户界面与数据模型一致的复杂性,具有大量用户界面和数据模型组件的应用可能更难开发。为了解决这个问题,Android 运行时提供了将数据绑定到视图的适配器。这允许用户界面组件自动反映对数据模型的任何更改。视图对象android.widget.Galleryandroid.widget.ListViewandroid.widget.Spinner是使用适配器将数据绑定到 Android 平台上的视图对象的很好的例子。

资源

Android 架构鼓励用户尽可能地将应用资源从应用源代码中外部化。通过将资源外部化,Android 应用可以根据设备配置和当前语言环境使用不同的图形和文本资源。Android 平台目前支持以下资源:

  • 动画资源
  • 色彩资源
  • 可提取资源
  • 布局资源
  • 菜单资源
  • 字符串资源
  • 样式资源
  • 价值资源

应用资源放在应用的res目录中。有不同的子目录来分组不同的资源。

在编译期间,Android 生成一个资源类,允许应用在代码中引用这些资源。

数据存储

Android 平台提供了多种保存持久应用数据的方法。以下是一些备选方案:

  • 共享偏好:这种方法允许应用将数据存储为键/值对。Android 框架带有实用功能,允许开发人员轻松维护共享偏好。共享首选项只支持基本数据类型,应用应该执行将数据转换为基本类型所需的任何封送处理。Android 平台还保证共享的偏好设置会被保存,即使应用被终止。
  • 内部和外部存储:这种方法允许应用开发人员将任何类型的数据作为普通文件存储在平台上。Android 框架提供了一组实用函数,允许应用开发人员轻松地进行这些文件操作,而无需知道这些文件的实际位置。
  • SQLite 数据库:使用 SQLite 数据库允许应用开发人员轻松地存储和检索结构化数据。SQLite 在应用的进程空间中提供了一个关系数据库。虽然 SQLite 功能是通过本地库提供的,但 Android 框架包括一组实用函数和类,允许应用开发人员轻松地与 SQLite 数据库进行交互。

Android 生命周期

Android 应用的生命周期比桌面应用的生命周期复杂得多。桌面应用的生命周期由用户直接控制。用户可以选择在任何给定的时间启动和终止应用。然而,在 Android 上,平台管理应用的生命周期,以便高效地使用稀缺的系统资源。

活动生命周期

活动生命周期是活动从第一次创建到被销毁所经历的一组状态。

Android 框架提供了一套生命周期方法,允许应用在活动生命周期发生变化时做出适当的调整。例如,如果活动对用户不再可见,它就没有理由消耗 CPU 周期在屏幕上显示动画。在这种情况下,应用应该停止执行任何 CPU 密集型操作,以便让前台应用获得足够的系统资源来提供流畅的用户体验。

android.app.Activity类中定义了七种生命周期方法:

public class Activity {     protected void onCreate(Bundle savedInstanceState);     protected void onStart();     protected void onRestart();     protected void onResume();     protected void onPause();     protected void onStop();     protected void onDestroy(); }

图 2-1 用这些方法说明了活动生命周期状态机。

Image

图 2-1。 活动生命周期状态机

这些活动生命周期方法的工作方式如下:

  • onCreate:创建活动时调用该方法。它初始化活动并创建视图。该方法还接受一个Bundle对象,该对象包含活动上次运行时的冻结状态。活动使用这个包来恢复其先前的状态。这个方法调用后面总是跟着onStart
  • onStart:当活动变得可见时,调用这个方法。如果该活动处于前台,则随后调用onResume。如果活动变得隐蔽,则接下来是onStop
  • onRestart:当活动重新显示给用户时,调用这个方法。其次是onStart
  • onResume:每次活动来到前台与用户交互时,都会调用这个方法。
  • onPause:该方法在活动即将进入后台,但尚未终止时调用。这个回调主要用于保存任何持久状态。这也是停止任何 CPU 密集型操作并释放任何系统资源(如相机)的好地方。当应用处于暂停状态时,如果需要为前台应用回收资源,系统可以随时决定终止应用。由于这个原因,应用应该在这个调用中保存它的当前状态,因为它可能没有第二次机会。根据用户与前台应用的交互,这个调用之后是onResumeonStop
  • onStop:当用户看不到活动时,调用这个方法。如果活动即将到达前台,则随后调用onRestart,如果活动即将终止,则调用onDestroy
  • onDestroy:这个方法在活动被销毁的时候调用。这可能是因为活动即将结束,或者因为系统需要释放资源。预计应用将在此时释放其资源。

注意:当覆盖这些生命周期方法时,不要忘记调用超类。Android 本身也需要密切监控这些生命周期事件,以便正常运行。

活动应该通过在onPause方法中保存其状态来结束,尽管onStoponDestroy方法跟在它后面。Android 平台保证应用在执行onPause方法中的任何工作时,应用进程不会被终止;在执行onStoponDestroy方法时,应用可能会被终止。但是,您应该非常小心,不要在onPause方法上花费太多时间,因为 Android 平台和用户都在等待这个方法完成,然后再将下一个活动放到前台。在onPause方法上花费太多时间会让系统看起来对用户的请求不负责任。

服务生命周期

服务生命周期类似于活动生命周期,但有几个很大的区别。因为服务不直接与用户交互,所以它们的生命周期不像活动那样依赖于用户的操作。因为服务不关心可见性,所以生命周期方法onPauseonResumeonStop对它们不适用。

android.app.Service类中定义了三种生命周期方法:

public abstract class Service {     public void onCreate();     public int onStartCommand(Intent intent, int flags, int startId);     public void onDestroy(); }

图 2-2 展示了使用这些方法的服务生命周期状态机。

Image

图 2-2。 服务生命周期状态机

这些服务生命周期方法的工作方式如下:

  • onCreate:当Context.startService(Intent)方法被应用使用并且服务还没有运行时,这个方法被调用。由于服务被设计成单例的,所以服务在其生命周期中只得到一次onCreate调用。

  • onStartCommand: This method is called each time the Context.startService(Intent) method is used by the application. A service may end up processing multiple requests, so it is possible for the service to receive multiple onStartCommand calls during its lifetime. If the service is already busy processing the previous request, it is expected that the service will queue this new request.

    注意:当前台应用需要更多资源时,Android 平台可能会决定销毁一个正在运行的服务,然后在资源条件改善时重新启动它。如果您的服务需要存储持久数据以便在重启后继续正常运行,那么最好在调用onStartCommand期间存储这些数据。

  • onDestroy:该方法在服务即将被 Android 平台销毁时调用。

包装

Android 包文件(APK)文件格式用于打包和分发 Android 应用。APK 文件实际上是压缩文件格式的档案文件。除了应用类文件的打包方式之外,它们部分遵循 JAR 文件格式。APK 文件包含以下内容:

  • META-INF/MANIFEST.MF:这是包文件本身的 JAR 清单文件。
  • META-INF/CERT.SF:包含了包文件中包含的文件的 SHA1 哈希。该文件由应用开发人员的证书签名。
  • META-INF/CERT.RSA:这是用于签署CERT.SF文件的证书的公钥。
  • AndroidManifest.xml:这是应用的清单文件。它是 Android 应用最重要的组件之一,我们将在下一节简要探讨它。
  • classes.dex:这是 DEX 格式的应用类文件。
  • assets:这很特别,因为在生成 APK 文件时,其内容不会被压缩。这允许 Android 平台在运行时直接向 APK 文件提供文件描述符,因此应用可以轻松地访问资源,而无需将它们提取到设备中。Android 开发人员希望将大型资源文件保存在assets目录中,以最小化应用在已安装设备上的占用空间。
  • res:该目录包含应用资源。
  • resources.arsc:包含视图定义和字符串资源。

APK 文件用证书签名,证书的私钥由应用开发人员持有。该证书确定了应用的作者,以及 APK 文件中包含的文件的完整性。与许多其他移动平台相比,Android 不要求这些证书由认证机构签名。Android 开发人员可以生成并使用自签名证书来签署他们的应用。

Android 平台在软件更新期间使用证书,以确保更新来自与创建已经安装在系统上的应用的作者相同的作者。除了更新之外,Android 平台还依赖于证书,同时在安装期间向应用授予签名级别的权限。

安卓清单

Android 应用通过一个名为AndroidManifest.xml的清单文件向系统描述。所有 Android 应用的根目录中都应该有这个文件。Android 清单文件向系统呈现关于应用的基本信息,以便让 Android 平台正确运行应用的代码,并在安装期间授予必要的权限。

Android 清单文件向系统提供以下信息:

  • 它包括应用的名称、包名和版本号。
  • 它表示运行应用所需的 API 的最低版本。
  • 它描述了应用的组件(活动、服务、广播接收器和内容提供者)以及它们在处理意图方面的能力。
  • 它声明需要哪些权限才能访问 Android 运行时的受保护部分,并与系统上运行的其他应用进行交互。
  • 它声明了其他应用需要拥有的权限,以便与该应用的组件进行交互。
  • 它列出了应用在运行时为了运行而必须链接的库。

Android 清单文件是一个 XML 格式的纯文本文件。下面是一个AndroidManifest.xml文件的例子:

`


                


            
        


        

    


`

总结

本章通过简要回顾最基本的 Android 组件,包括活动、服务、广播接收器、内容提供者和其他用户界面组件,介绍了 Android 应用架构。我们试图阐明 Android 开发中最令人困惑的概念之一:活动和服务生命周期。然后我们探索了打包 Android 应用的过程,并仔细查看了 Android 清单文件。接下来的章节展示了这些概念的应用实例。

三、Eclipse 优先

Eclipse 是一个集成开发环境,我们将在整个 Android 开发过程中使用它。Eclipse 不仅仅是一个简单的代码编辑器。它是一个非常强大和复杂的工具平台。从这个角度来看,这一章非常重要,因为我们将通过为 Eclipse 设置合适的工作环境来为后面的章节建立框架。

本章将介绍 Eclipse,强调最常用的 Eclipse 组件。熟悉 Eclipse 是顺利体验 Android 开发的关键。

Eclipse 历史

1995 年,Sun Microsystems 向公众发布了 Java 编程语言的第一个公共实现。Java 的到来将开发者社区分成了两个群体:一个以微软技术和工具为中心,另一个以 Java 平台为中心。

Visual Studio 是微软的工具平台,以集成的方式提供对所有微软技术的访问。市场上有许多成功的 Java 开发工具,但是它们没有像微软技术那样紧密集成。

在 20 世纪 90 年代末,IBM 是 Java 的主要参与者。IBM 当时的主要目标是让开发人员更接近 Java 中间件。IBM 知道理想的开发环境必须由来自 IBM、第三方和客户内部工具的异构组合组成。IBM 的对象技术国际(OTI)实验室是 VisualAge 产品家族的幕后支持者,他们在构建集成开发环境方面有着丰富的经验。作为第一个步骤,VisualAge for Java Micro Edition 是作为完全使用 Java 编程语言的集成开发环境的重新实现而开发的。后来,VisualAge for Java Micro Edition 代码被用作 Eclipse 平台的基础。

IBM 已经意识到,仅仅在这个新平台上拥有 IBM 产品不足以获得开发人员社区的广泛采用。集成第三方工具是 Eclipse 平台成功的关键。2001 年,IBM 决定为 Eclipse 平台采用开源许可和操作模型。IBM 和其他八个组织一起建立了 Eclipse 联盟。该联盟的主要运营原则是推动 Eclipse 平台的营销和关系,同时将 Eclipse 源代码的控制权留给开放源代码社区。

2003 年,Eclipse 平台及其快速增长的开源和商业扩展集开始受到开发人员的欢迎。2004 年,Eclipse Foundation,一个拥有自己的专业和独立员工的非营利组织,接管了 Eclipse 平台的全部控制权。Eclipse 现在是领先的 Java 开发环境。由于其独特和可扩展的架构,它也被用作许多其他编程语言的开发环境。

Eclipse 架构

作为一名 Android 开发人员,您不需要与 Eclipse 平台的内部交互。然而,了解它的架构将使您更容易概念化和理解 Eclipse 的一般工作方式。

Eclipse 平台主要是为构建集成开发环境而设计的。它是一个高度可扩展的平台,而不是一套特定任务的定制工具。

Eclipse 平台定义了机制和规则,并通过提供一组定义良好的 API,允许在它们之上构建工具。Eclipse 平台是围绕插件的概念构建的,如图 3-1 所示。

Image

图 3-1。 Eclipse 平台架构概述

插件是 Eclipse 平台的最小单元。它们是结构化的代码束,为平台贡献了一组功能。插件可以单独开发、分发和部署。Eclipse 平台也允许插件具有可扩展性。插件可以通过定义良好的 API 提供一组扩展点,供其他插件扩展其功能。

Eclipse 平台中的每个子系统都基于一组插件。例如,Eclipse 的 Java 开发工具是一组插件,以集成的方式为平台提供 Java 开发功能。Java 开发工具插件也是可扩展的。

在本书中,我们将使用 Android 开发工具插件。这些扩展了现有的 Java 开发工具,以便提供特定于 Android 的开发工具和功能。

Eclipse 平台的核心,也称为 Eclipse 运行时,负责提供插件可以工作和互操作的基础设施。Eclipse 运行时还提供了任何实用服务器,这将使开发人员更容易开发新的插件。在撰写本文时,最新版本是 Eclipse Indigo 3.7.2。

在接下来的小节中,我们将为 Eclipse 设置合适的工作环境。

安装 Java 开发工具包

Eclipse 是一个基于 Java 的应用,它需要一个 Java 虚拟机才能运行。在安装 Eclipse 之前,您需要安装 Java 开发工具包(JDK),而不仅仅是 Java 运行时版本(JRE)。Eclipse 支持多种 JDK 风格,比如 IBM JDK、OpenJDK 和 Oracle JDK(以前称为 Sun JDK)。在本节中,我们将假设您正在安装 Oracle JDK,它是最初的 JDK 实现,支持更广泛的平台。

JDK 的版本也需要与 Dalvik 虚拟机兼容,因为我们将使用 Eclipse 进行 Android 开发。在撰写本文时,Dalvik 虚拟机支持 Java 编译器兼容级别 1.5 和 1.6。尽管新版本的 JDK 可以配置为在这些合规级别上工作,但是从相应的 JDK 版本 JDK 6 开始要容易得多。

使用您最喜欢的网络浏览器,导航至[www.oracle.com/technetwork/java/javase/downloads/index.html](http://www.oracle.com/technetwork/java/javase/downloads/index.html)。如图 3-2 所示,你会看到一个下载选项列表。

Image

图 3-2。 甲骨文网站上的 Java 下载页面

因为我们想下载 JDK 6 而不是 JDK 的最新版本,所以向下滚动到下载页面的 Java SE 6 部分。在撰写本文时,JDK 6 的最新版本是 Update 31。点击 JDK 6 旁边的下载按钮继续。

目前,Oracle JDK 不提供 Mac OS X 平台的安装包,因为安装程序是通过苹果的软件更新分发的。对于所有其他主要平台,Oracle JDK 安装程序都列在该页面上,如图 3-3 所示。每个操作系统的安装过程各不相同。为了使安装过程尽可能顺利,我们将在下面的章节中介绍三种主要的操作系统——Windows、Mac OS X 和 Linux。

Image

图 3-3。 主要操作系统的 Oracle JDK 安装包列表

在 Windows 上安装 JDK

Oracle JDK 是作为 Microsoft Windows 操作系统的可执行安装程序提供的。安装向导将指导您在机器上安装 JDK,如图图 3-4 所示。

Image

图 3-4。 甲骨文 JDK 6 安装向导

安装向导将首先安装 JDK,然后安装 JRE。在安装过程中,向导会询问目标目录以及要安装的组件。您可以在这里继续使用默认值。记下 JDK 部件的安装目录。

安装向导将自动进行必要的系统更改。安装过程完成后,JDK 就可以使用了。

在 Windows 上,安装向导不会自动将 Java 二进制目录添加到系统变量Path中,所以您需要这样做。转到控制面板并选择系统(或从开始菜单中选择运行,然后执行sysdm.cpl)以打开系统属性对话框。切换到高级选项卡,点击环境变量按钮,如图图 3-5 所示。

Image

图 3-5。 系统属性对话框的高级标签

如图 3-6 所示,环境变量对话框分为两部分:上面的是用户的,下面的是系统的。

Image

图 3-6。 环境变量对话框

在“系统变量”窗格中,单击“新建”按钮定义新的环境变量。将变量名设置为JAVA_HOME,将变量值设置为 JDK 安装目录,如图 3-7 中所示。单击确定按钮保存变量。

Image

图 3-7。设置 JAVA_HOME 系统环境变量

在系统变量列表中,双击Path变量,将;%JAVA_HOME%\bin追加到变量值,如图图 3-8 所示。

Image

图 3-8。设置 Path 系统环境变量

现在,可以从命令提示符轻松访问 JDK。为了验证安装,通过选择启动附件命令提示符打开命令提示符窗口。使用命令提示符,执行javac –version。如果安装成功,你会看到 JDK 版本号,如图图 3-9 所示。

Image

图 3-9。 Windows 命令行提示显示 JDK 版本

在 Mac OS X 上安装 JDK

苹果 Mac OS X 操作系统出厂时已经安装了 JDK。它基于 Oracle JDK,但由 Apple 配置以更好地与 Mac OS X 集成。JDK 的新版本可通过软件更新窗口获得,如图 3-10 所示。确保主机上安装了 JDK 6 或更高版本。

Image

图 3-10。 Mac OS X 软件更新窗口显示 JDK

在 Linux 上安装 JDK

JDK 安装过程因 Linux 发行版而异。由于 Oracle JDK 的许可条款,它不包含在任何 Linux 发行版中。某些发行版附带了一个存根应用,它允许您安装 Oracle JDK,而无需通过 web 下载过程。如本章前面的图 3-3 所示,Oracle 的网站为 Linux 系统提供了两种类型的安装包:

  • 名称以-rpm.bin 结尾的安装包包含一组 Red Hat Package Manager (RPM)格式的可安装包。如果您使用的是 Linux 发行版,如 Red Hat Enterprise Linux、Fedora、CentOS、SUSE 或 openSUSE,您可以下载这个安装包。
  • 名称以结尾的安装包。bin 包含一个自解压 ZIP 存档文件。这个安装包可以在任何 Linux 发行版上运行,尽管在安装之后需要一些手动的系统配置。

在本节中,我们将假设您运行的是支持 RPM 的 Linux 发行版。

下载 RPM 格式的安装包后,打开一个终端窗口。如图 3-11 所示,首先调用chmod +x jdk-6u31-linux-i586-rpm.bin启用安装程序上的执行位。要开始安装,在命令行上调用sudo ./jdk-6u31-linux-i586-rpm.bin。根据 JDK 的版本,用适当的文件名替换jdk-6u31-linux-i586-rpm.bin

Image

图 3-11。 在 Linux 上安装 Oracle JDK

注意:在 Linux 操作系统上,安装软件包需要超级用户(root)权限。在启动rpm之前,sudo命令将提示输入密码以授予超级用户权限。

安装 Eclipse

安装 Eclipse 是一个相当简单的过程,尽管不同操作系统的安装过程有所不同。在这一节中,我们将再次介绍三种主要的操作系统:Windows、Mac OS X 和 Linux。

使用您最喜欢的 web 浏览器,导航到 Eclipse web 站点,[www.eclipse.org](http://www.eclipse.org),如图图 3-12 所示(在您阅读本文时,该站点可能看起来有所不同)。在本书中,我们将使用最新版本的 Eclipse。在撰写本文时,最新版本是 Eclipse Indigo 3.7.1。

Image

图 3-12。 月食网站

点击下载页面的链接。如图 3-13 所示,您将看到一长串您选择的操作系统(在本例中是 Windows)的可下载 Eclipse 版本。在这个列表中,Eclipse Classic 是您可以下载的最基本的 Eclipse 包。它只包含 Eclipse 平台和 Java 开发工具(JDT)。您当然可以从这个包开始,然后安装您选择的其他插件。

Image

图 3-13。 月食下载页面

这个列表中的其他 Eclipse 包只是包含了一组常用的插件,这些插件是用 Eclipse 平台为主要编程语言预先打包的。你可以在[www.eclipse.org/downloads/compare.php](http://www.eclipse.org/downloads/compare.php)找到这些包的详细对比。

在 Windows 上安装 Eclipse

Microsoft Windows 的 Eclipse 安装包以 ZIP 存档文件的形式提供。只需右击该文件并从上下文菜单中选择 Extract All…

如图 3-14 所示,Windows 会提示输入解压文件的目标目录。在本节中,我们将假设目标目录是 c 盘的根目录C:\

Image

图 3-14。 解压 Eclipse ZIP 包到它的目的地

当这个过程完成后,Eclipse 将被安装在C:\eclipse目录下,如图图 3-15 所示。您现在可以考虑创建一个 Eclipse 应用的快捷方式。

Image

图 3-15。安装后的 Eclipse 文件

在 Mac OS X 上安装 Eclipse

Mac OS X 的 Eclipse 安装包以 GZIP 压缩 TAR 文件的形式提供。下载完成后,Eclipse 将出现在您的Downloads目录中。根据操作系统的版本,您可能需要双击以提取存档文件(如果尚未提取)。

您可以将 Eclipse 从Downloads目录拖放到您的Applications目录,如图图 3-16 所示。

Image

图 3-16。 将 Eclipse 从下载目录移动到应用目录

稍后,您还可以将 Eclipse 添加到您的仪表板中,以便于访问。

在 Linux 上安装 Eclipse

Linux 的 Eclipse 安装包以 GZIP 压缩 TAR 文件的形式提供。打开一个终端窗口,将目录切换到您想要安装 Eclipse 的目的地,如图图 3-17 所示。要提取 Eclipse 文件,在命令行上发出tar zxf eclipse-java-indigo-SR1-linux-gtk.tar.gz,根据 Eclipse 的版本替换文件名。

Image

图 3-17。在 Linux 上安装 Eclipse

Eclipse 现在已经可以使用了。您可能会发现创建 Eclipse 应用的快捷方式非常方便。

探索月食

您现在已经准备好开始使用 Eclipse 了。在这一节中,我们将开始探索 Eclipse 及其使用的术语。

工作空间

当你启动 Eclipse 时,会提示你选择工作区目录,如图图 3-18 所示。在 Eclipse 术语中,工作区是存储项目、源代码和 Eclipse 设置的目录。

Image

图 3-18。 Eclipse 启动时的工作区选择对话框

如果使用默认设置,Eclipse 将在用户的主目录下创建一个名为workspace的新目录。在 Workspace Launcher 对话框中,您也可以将这个工作区设置为默认工作区,Eclipse 下次不会再提示它。

工作区对于组织项目非常有用。例如,我同时在两个主要工作区工作:一个是与工作相关的项目,另一个是我的车库项目。然而,许多 Eclipse 开发人员可以很好地使用单个工作区。

在您选择了您的工作空间之后,Eclipse 会用欢迎屏幕来欢迎您,如图 3-19 所示。在这里,您可以找到有用的 Eclipse 资源链接,比如教程和示例。在右上角,单击工作台链接进入主屏幕。

Image

图 3-19。 月食的开场欢迎画面

提示:选择帮助欢迎可以随时回到欢迎界面。

工作台

在 Eclipse 术语中, Workbench 指的是桌面开发环境。这是图 3-20 中所示的 Eclipse 窗口的名称。每个工作台都包含一组透视图,以及它们各自的视图、编辑器、菜单和工具栏项。

Image

图 3-20。 Eclipse 工作台

您可以一次打开多个工作台。要打开一个新工作台,选择窗口 Image 新窗口

视角

一个透视图定义了视图集和视图在工作台中的布局。每个透视图都旨在帮助完成特定类型的任务:

  • Java 透视图:这个透视图结合了开发 Java 应用时常用的视图、菜单和工具栏。
  • Debug 透视图:这个透视图包含了与故障排除和调试 Java 应用相关的视图。

Eclipse 为常见任务提供了预定义的透视图。您还可以根据自己的需求修改透视图并定义新的透视图。为此,选择窗口 Image 自定义视角

您可以使用视角切换器,如图 3-21 所示,或者选择窗口Image打开视角随时切换视角。

Image

图 3-21。 视角切换器

当您启动一个新的任务时,比如调试一个项目,Eclipse 还会提出改变透视图的建议。这允许根据当前任务在透视图之间自动切换。

编辑

编辑器允许您编辑源代码和资源文件。根据文件类型,Eclipse 支持多种编辑器风格。例如,源代码和 XML 资源文件是用不同的编辑器类型处理的。

大多数文件类型已经用 Eclipse 中正确的编辑器进行了映射。如果 Eclipse 找不到特定文件类型的内部编辑器,它就依赖操作系统来找到外部编辑器。例如,如果您试图打开一个 PNG 格式的图形文件,由于 Eclipse 没有内部图形编辑器,它将依靠操作系统的映射来启动 PNG 文件的默认编辑器。根据活动编辑器的类型,仅显示相关的工具栏和菜单项。

可以同时打开任意数量的编辑器。编辑器在编辑器区域中以单独的选项卡出现,如图图 3-22 所示。在任何给定时间,只能有一个编辑器处于活动状态。

Image

图 3-22。 编辑区

您可以通过右键单击编辑器选项卡,从上下文菜单中选择移动 Image 编辑器,并将分离的编辑器移动到您喜欢的角落位置,将编辑器区域分成多个选项卡组。

观点

视图为项目和编辑器提供了可选的表示,允许在工作台中轻松导航和访问信息。例如,Outline 视图提供了当前编辑的源文件中的方法和变量的列表,允许在编辑器中轻松导航。

Eclipse 提供了许多视图。要打开一个新视图,选择窗口 Image 显示视图。如图 3-23 所示,下拉菜单只显示最常用的视图。如需完整列表,请选择其他…

Image

图 3-23。 选择视图

根据透视图的布局,视图可以始终可见、堆叠在选项卡式笔记本中或最小化。视图可能在视图区域中嵌入了自己的工具栏和菜单。

快速查看

快速视图是隐藏视图,可以使用工作台左下角状态栏上的快速视图图标快速打开和关闭,如图图 3-24 所示。

Image

图 3-24。 状态栏上的快速查看图标

点击快速查看图标,弹出下拉菜单,如图图 3-25 所示。

Image

图 3-25。 快速查看下拉菜单

快速视图的工作方式与其他视图类似,但在不使用时不会占用任何屏幕空间。您可以将任何视图拖放到快速视图图标上,使其成为快速视图。

提示:或者,与快速视图相同类型的行为可以通过最小化视图来实现。最小化视图以类似工具栏的方式显示。它们比快速视图更容易激活,因为每个最小化视图都可以通过其图标容易地识别,该图标在显示器上总是可见的。

快速查看

快速视图是隐藏的视图,当通过组合键触发时,显示在编辑器区域的顶部。快速视图旨在提供对光标下的元素或当前活动编辑器的信息的轻松访问。快速大纲视图和快速类型层次视图就是例子。

菜单

Eclipse 有不同种类的菜单。其中一些菜单比其他的更难发现。最明显的是工作台顶部的主菜单,如图图 3-26 所示。

Image

图 3-26。 主菜单

视图也可以有自己的菜单,如视图工具栏上朝下的三角形图标所示,如图 3-27 所示。单击此图标显示视图的菜单。

Image

图 3-27。 一个查看菜单

工作台中的子窗口也提供了一个菜单,也称为系统菜单,用于窗口相关的操作。右键单击窗口的标题栏可以激活该菜单,如图图 3-28 所示。

Image

图 3-28。 一个系统菜单

编辑器和大多数视图还为各种任务集提供了上下文菜单。您可以通过右击该视图上的任意位置来访问该菜单,如图图 3-29 所示。

Image

图 3-29。 一个查看菜单

工具栏

工具栏为常见任务提供快捷方式。工作台包含多种类型的工具栏。最重要的是屏幕上方主菜单下方的工具栏,如图图 3-30 所示。该工具栏包含最常用的 Eclipse 任务的图标。

Image

图 3-30。 顶部工具栏

根据所关注的编辑器或视图,工具栏项目可能会在启用和禁用状态之间切换,以反映任务在当前上下文中的可用性。

另一个工具栏出现在工作台的右下角。它包含欢迎屏幕中提到的资源的快捷方式。

视图也可能有工具栏。这些工具栏位于视图内,视图标题的正下方,如图图 3-31 所示。

Image

图 3-31。 大纲视图工具栏

Eclipse 还提供了一个工具栏,可以方便地访问最小化视图,如图 3-32 所示。

Image

图 3-32。 工具栏用于最小化视图

项目

一个项目是 Eclipse 中最大的结构单元,用于分组和组织相关的文件、文件夹、资源、设置和其他工件。例如,Java 项目是一组源文件、资源和设置。

当前工作区中可用的项目通过项目浏览器视图呈现给用户,如图图 3-33 所示。

Image

图 3-33。 项目浏览器视图

项目可以是打开的,也可以是关闭的。当项目处于关闭状态时,它需要较少的内存,在构建期间不会被检查,并且在工作台中不可编辑。关闭项目以缩短活动项目的构建时间始终是一个好的做法。

要创建新项目,从顶部菜单栏中选择文件 Image 新项目。您将看到新项目对话框,其中列出了可用的项目类型,如图 3-34 所示。

Image

图 3-34。 新建项目向导对话框

总结

在本章中,我们首先介绍了在 Microsoft Windows、Mac OS X 和 Linux 系统上为 Eclipse 建立合适的工作环境的步骤。接下来,我们简要回顾了 Eclipse 架构,以便更好地从概念上理解 Eclipse 平台通常是如何工作的。然后我们探索了最常用的用户界面组件。例如工作区、工作台、透视图、编辑器和视图。

这一章为后面的章节奠定了基础。在下一章,我们将探索导航、重构、原型和 Eclipse 平台提供的其他高级特性。

参考文献

本章使用了以下参考资料:

  • [www.ibm.com/developerworks/rational/library/nov05/cernosek](http://www.ibm.com/developerworks/rational/library/nov05/cernosek)Eclipse 简史
  • 关于 Eclipse 基金会,[www.eclipse.org/org/](http://www.eclipse.org/org/)
  • Eclipse 平台技术概述,[www.eclipse.org/whitepapers/eclipse-overview.pdf](http://www.eclipse.org/whitepapers/eclipse-overview.pdf)
  • Eclipse 文档,[help.eclipse.org/indigo/index.jsp](http://help.eclipse.org/indigo/index.jsp)

四、掌握 Eclipse

在前一章中,我们探索了最常用的 Eclipse 组件。然而,Eclipse 提供了更多的东西。

大型复杂的项目,尤其是当涉及多个开发人员时,可能很快变得难以跟踪和导航。在这一章中,我们将探索高级的 Eclipse 导航特性,例如大纲、类型和调用层次结构以及标记,这些特性可以帮助开发人员轻松地找到代码。

除了导航之外,日常的软件开发还涉及到大量耗时且多余的任务,比如为每个成员字段编写 getters 和 setters,重构代码,以及更新对它的所有引用。在这一章中,我们将探索 Eclipse 提供的用于处理这些劳动密集型任务的大量代码生成器和代码操纵器。使用这些强大的特性使开发人员能够更快地编写代码,因为他们可以将更多的时间投入到实际的应用中。

导航

在一个复杂项目的不同组件之间导航,甚至在一个大的源代码文件中导航,很容易成为一项非常耗时的工作。能够在复杂的项目中轻松导航是任何图形开发环境的最大需求之一。Eclipse 提供了许多高级功能来简化日常开发体验;然而,这些漂亮的功能大部分都隐藏在平台中。在这里,您将学习如何使用一些导航功能,包括工作集、大纲视图、类型层次结构视图、调用层次结构视图、标记和搜索。

工作集

工作集允许对元素进行进一步分组,例如项目、文件、资源和断点,用于显示和操作目的。工作集是 Eclipse 最重要的特性之一,有助于在工作空间中导航。工作集可以用作许多视图的过滤标准,也可以使用构建系统构建工作空间的某个部分。

默认情况下,工作区中的每个元素都被视为窗口工作集的成员。为了定义一个新的工作集,首先在工作台上选择一个元素,比如一个文件,然后右键激活上下文菜单,选择分配Image工作集…,如图图 4-1 所示。

Image

图 4-1。 从上下文菜单中选择分配工作集

将出现“工作集分配”对话框,显示现有工作集的列表。单击右边的 New 按钮定义一个新的工作集。Eclipse 将显示新建工作集向导,从适用于所选元素的可用工作集类型列表开始。在图 4-2 中的例子中,我们选择了一个 Java 源文件,可用的工作集类型是基于这个源文件填充的。选择工作集的类型,然后单击 Next 进入下一步。

Image

图 4-2。 启动新工作集向导

在下一步中,为这个新的工作集命名。你也可以给它添加其他元素,如图图 4-3 所示。

Image

图 4-3。 命名并添加元素到工作集

当选择完成时,工作集分配对话框将再次显示,这一次新定义的工作集在列表中并被选中,如图图 4-4 所示。

Image

图 4-4。 工作集分配对话框显示新的工作集

这个新的工作集可以在多个地方用作过滤标准。作为一个例子,让我们用新定义的工作集来过滤 Package Explorer 视图的内容。点击 Package Explorer 工具栏上的展开箭头图标,选择视图的下拉菜单,如图图 4-5 所示。选择Select Working Set...设置要使用的工作集。最近使用的工作集被添加到上下文菜单中,以便于访问。

Image

图 4-5。 包浏览器查看菜单

现在,Package Explorer 视图将过滤其内容,只反映所选工作集成员的元素,如图 4-6 所示。

Image

图 4-6。 被工作集过滤的包资源管理器视图

大纲视图

Outline 视图提供了编辑器中当前打开文件的结构视图。它允许快速浏览编辑器的内容。大纲视图工具栏提供了对视图内容进行过滤和排序的选项。

Outline 视图的内容是特定于编辑器的。一些编辑器,例如纯文本文件编辑器,不支持大纲视图。使用 Java 编辑器时,Outline 视图将当前 Java 文件中的类、变量和方法显示为结构化元素,如图图 4-7 所示。

Image

图 4-7。Java 文件的概要视图

默认情况下,Outline 视图在 Java 透视图中是可见的。要将 Outline 视图添加到另一个透视图,请选择窗口 Image 显示视图 Image Outline

除了大纲视图,还有一种快速视图,称为快速大纲视图。默认情况下,此视图不可见。要显示它,在 Windows 和 Linux 上按 Ctrl +O,或者在 Mac OS X 上按 Command+O,它就会出现在编辑器区域,如图图 4-8 所示。

Image

图 4-8。****快速大纲视图显示在编辑器的顶部。

*默认情况下,快速大纲视图显示类字段和方法。第二次按 Ctrl +O会扩展这个列表,覆盖继承的字段、方法和类型。继承的元素以灰色显示,以便于区分。

快速大纲视图还支持自动过滤,允许用户键入元素的首字母来缩小其内容。像其他视图一样,Quick Outline 视图有自己的下拉菜单,允许进一步定制。

类型层次视图

类型层次结构视图是特定于 Java 的,显示所选 Java 对象的子类型和超类型。它允许您快速发现类型层次结构并在类型间导航。

为了启动 Type Hierarchy 视图,您需要首先从 Package Explorer 视图或编辑器中选择一个 Java 对象。选择对象后,可以用三种方式打开类型层次结构视图:

  • 按 F4。
  • 点击右键,从右键菜单中选择打开类型层次,如图图 4-9 所示。
  • 在顶部菜单栏选择窗口 Image 显示视图 Image 类型层次

Image

图 4-9。 从上下文菜单中选择开放式层级

类型层次视图分为两个窗格,如图图 4-10 所示。顶部窗格显示所选 Java 对象的类型层次结构。底部窗格显示成员列表。

Image

图 4-10。 类型层次视图分为两个窗格。

Type Hierarchy 视图有自己的菜单,可以通过单击右上角的展开箭头来激活它。从此菜单中,您可以通过工作集进一步过滤类型层次结构。

除了菜单之外,类型层次结构视图还有两个工具栏:每个窗格一个。顶部窗格的工具栏提供了在子类型层次、超类型层次和完整类型层次之间切换的图标。底部窗格的工具栏提供了对成员列表进行过滤和排序的图标。

双击该视图中的任何元素都允许您在编辑器区域中自动打开它。

与大纲视图一样,快速查看也是可用的。要打开快速类型层次视图,在 Windows 和 Linux 上按 Ctrl+T,或在 Mac OS X 上按 Command+T。它出现在编辑器区域的顶部,如图 4-11 所示。

Image

图 4-11。 快速类型层次视图编辑器中显示的

调用层次视图

另一个特定于 Java 的视图是 Call Hierarchy 视图,它显示所选 Java 成员对象的调用者和被调用者。它允许您快速发现代码中的调用层次结构,并在调用中导航。

要启动调用层次视图,首先选择一个 Java 成员对象,然后使用以下方法之一:

  • 在 Windows 和 Linux 上按 Ctrl+Alt+H,或者在 Mac OS X 上按 Control+Alt+H。
  • 右键单击并从上下文菜单中选择打开调用层次,如图图 4-12 所示。
  • 在顶部菜单栏中,选择窗口 Image 显示视图 Image 其他Image 调用层次结构

Image

图 4-12。 从上下文菜单中选择开放呼叫层级

“呼叫层次结构”视图也有自己的菜单。与其他视图一样,您可以通过单击展开箭头来激活此菜单。此下拉菜单允许您在呼叫者和被呼叫者层次结构之间更改呼叫层次结构模式。它还提供了筛选功能,例如在探索字段访问调用层次结构时按字段访问类型进行筛选。

如图 4-13 所示,调用层次结构以树形方式显示在左侧。视图的右侧用于显示行号和被调用的函数。当您单击树项目左侧的加号图标时,呼叫层次结构发现会继续前进一步。当到达调用层次结构中的最后一个方法时,加号图标会消失。

Image

图 4-13。 调用层次视图

标记

标记是可以与工作台资源相关联的元数据。标记显示在编辑器区域左边界的标记栏上。Eclipse 支持不同的标记类型。在这一部分,我们将回顾三种标记风格:书签、问题和任务。

书签视图

书签提供了一种方法来标记经常使用的资源,以便于以后访问。当在一个复杂的项目中工作时,代码的某些部分,比如主 API,可能是书签的很好的候选。您可以为文件中的特定行或整个资源添加书签。

书签视图以表格形式提供了这些书签的列表,如图图 4-14 所示。

Image

图 4-14。 书签视图

如果书签视图不可见,可以通过选择窗口 Image 显示视图 Image **其他… ** Image 书签将其添加到当前透视图中。

要添加新的书签,在编辑器区域的标记栏上单击鼠标右键,从快捷菜单中选择添加书签… ,如图图 4-15 所示。蓝色书签图标将出现在选定行的标记栏中,表示该行已被书签标记。然后,您可以使用书签视图管理书签。

Image

图 4-15。 添加新书签

问题视图

Problems 视图为各种 Eclipse 组件记录问题、错误和警告提供了一个中心位置。问题视图以表格的形式呈现这些信息,如图 4-16 所示。

Image

图 4-16。 问题视图显示存在的问题

例如,在编译期间,任何错误首先通过一个标记与相应的资源相关联,然后通过 Problems 视图报告给用户。双击错误消息可以快速跳转到相应的资源。

默认情况下,Problems 视图显示所有问题,并根据它们的严重性对它们进行分组。使用“视图”菜单(通过窗口的展开箭头访问),您可以过滤列表并更改分组和排序。问题解决后,它们会自动从 problems 视图中删除。

通过快速修复功能,问题视图还提供了修复报告问题的帮助。要启动快速修复,在 Windows 和 Linux 上按 Ctrl+1,或在 Mac OS X 上按 Command+1,或从所选问题项的上下文菜单中选择快速修复。快速修复提供了一套解决问题的建议,如图图 4-17 所示。

Image

图 4-17。 快速修复提供修复问题的建议。

任务视图

Tasks 视图允许您将任务与工作台资源相关联。例如,需要解决的缺失代码段或已知错误可以通过将任务与相关资源相关联来表达。任务视图以表格的形式显示这些信息,如图 4-18 所示。

Image

图 4-18。 任务视图

使用 Tasks 视图的下拉菜单,您可以组织这个列表。例如,您可以根据任务优先级对列表重新排序,或者过滤列表以仅显示特定类型的任务。

与大多数标记一样,可以通过右键单击相应行上的标记栏来定义新任务。您也可以在资源中使用某些关键字,如图图 4-19 所示。开发人员更经常使用后一种方法。

Image

图 4-19。 任务由待办事项关键字自动定义

以下是用于自动定义任务的最常用关键字:

  • TODO:该关键字用于记录任何需要以后实现的缺失代码部分。开发人员大多使用TODO来记录他们当前推迟并计划以后解决的任务。
  • FIXME:这个关键字主要用于记录代码中任何已知的需要解决的 bug。

向资源中添加任务时,并不局限于这些关键字。其他关键字可以通过任务标签首选项对话框定义,如图图 4-20 所示。要打开这个对话框,在 Windows 和 Linux 上选择窗口Image首选项,或者在 Mac OS X 上选择 Eclipse Image 首选项,导航到 Java,然后是编译器,然后是任务标签。

Image

图 4-20。 任务标签首选项对话框

搜索

有效的搜索是在开发环境中轻松导航的关键。Eclipse 提供了多层搜索功能,这些功能专门针对某些用例进行了优化。

Eclipse 提供的最基本的搜索特性,也称为文件搜索,是在工作台中搜索文本字符串。要打开搜索对话框,在 Windows 和 Linux 上按 Ctrl+H,或者在 Mac OS X 上按 Control+H,或者从顶部菜单中选择搜索 Image 搜索…

如图 4-21 所示,搜索对话框为搜索提供了广泛的定制。虽然这是一个非常强大的功能,但它仅针对在通用文件中搜索文本进行了优化。不推荐以这种方式搜索 Java 资源,因为已经有了专门针对 Java 资源的最佳解决方案。

Image

图 4-21。 使用搜索对话框进行文件搜索

对于 Java 资源,Java 搜索比文件搜索快得多,因为它依赖于现有的代码索引。您可以通过从顶部菜单中选择搜索 Image Java … 或单击搜索对话框中的 Java 搜索选项卡来启动 Java 搜索。如图 4-22 所示,Java 搜索选项卡提供了特定于 Java 的附加参数,您可以使用这些参数来进一步定制搜索。

Image

图 4-22。??【Java 搜索】选项卡的搜索对话框

搜索结果通过搜索视图呈现,如图图 4-23 所示。搜索视图下拉菜单和工具栏提供了进一步的过滤功能,以根据用户偏好组织搜索结果。

Image

图 4-23。 搜索视图

搜索菜单也提供了一些样板搜索,如图图 4-24 所示。当前选择的 Java 资源可用于快速开始新的参考、减速和实施者搜索。

Image

图 4-24。??【样板文件搜索】??

能够在项目中轻松导航无疑加快了编码过程,但这还不够。开发者在开发应用的同时,仍然需要编写相当数量的代码。在下一节中,我们将探索 Eclipse 为快速编码提供的高级特性。

快速编码

在大多数软件项目中,开发人员的大部分时间并不用于开发实际的应用逻辑。开发人员花费大量时间处理简单但劳动密集型的编码任务,例如实现 getters 和 setters,或者在进行代码重构后更新源代码中的所有引用。Eclipse 提供了一组高级特性,如模板和代码生成器,以自动化部分编码,并减少开发人员需要生成的代码量。在这一节中,我们将回顾一些方便的 Eclipse 特性。

模板

在开发任何类型的应用时,我们每天都在使用许多编码模式和代码结构。大多数时候,我们发现自己在复制和粘贴代码段,并试图通过操作它们的参数名来适应它们的新家。例如,日志记录是每个项目的必备条件之一。在开发应用时,开发人员通常会在很多地方复制日志记录器的启动代码。

大多数文本编辑器中的复制粘贴功能无疑使任务变得简单;但是,它要求您能够立即访问原始代码段,以便首先复制它们。因此,开发人员可能会花费大量时间搜索以前的项目,以便提取那些宝贵的代码段。

通过对代码模板的支持,Eclipse 为这个问题提供了一个更加优雅的解决方案。代码模板允许您在 Eclipse 中存储常用的代码模式和代码片段。Eclipse 处理这些模板的存储和索引,并使它们易于使用。

Eclipse 支持多种代码模板类型。用户可以定义自己的模板,也可以使用插件附带的预定义模板(可以由用户自定义)。

为了更好地了解 Eclipse 中模板支持的程度,在顶部菜单栏中,选择 Windows 和 Linux 上的窗口 Image 首选项,或者在 Mac OS X 上的 Eclipse 首选项来启动 Eclipse 首选项对话框。开始输入模板来过滤广泛的首选项列表,只过滤模板,如图图 4-25 所示。

Image

图 4-25。 为模板过滤的 Eclipse 首选项对话框

您可能已经注意到,在首选项对话框的 Java 部分列出了两组模板:代码样式下的代码模板和编辑器下的模板。我们将在本节中研究这两种类型。

代码模板

代码模板主要在自动代码生成过程中使用。最基本的代码模板用于放置在新文件顶部的注释行。在许多公司,你会被要求在你开发的每个源文件的顶部包含一个版权声明和一个许可证。要使用代码模板轻松实现这一点,在 Windows 和 Linux 上选择窗口 Image 首选项,或者在 Mac OS X 上选择 Eclipse Image 首选项,导航到 Java,然后是代码样式,然后是代码模板。您将看到一个可用代码模板的列表,如图图 4-26 所示。

Image

图 4-26。 Java 代码模板列表

代码模板以树状方式呈现在两个主要组下:注释和代码。单击 Comments 组左侧的三角形图标,展开可用注释代码模板的列表。我们将为这个示例修改的代码模板是一个名为 Files 的模板。从列表中选择该模板后,对话框的底部窗格将立即显示所选代码模板的当前模式,如图 4-27 所示。

Image

图 4-27。 文件注释代码模板

如您所见,目前它不包含任何文本,而只包含注释装饰。要对其进行修改,请单击右侧的编辑按钮。这将弹出编辑模板对话框,如图图 4-28 所示。

Image

图 4-28。 编辑模板对话框

您现在可以修改文件注释,如下所示:

/**  * Copyright © 2012 Apress Media LLC. All Rights Reserved.  */

代码模板都是关于可重用性的,开发人员喜欢使它们尽可能通用,以避免需要保持它们最新。在我们的例子中,模板中有一个硬编码的年份 2012。最好让这个版权行反映当前年份,而不是总是显示 2012 年。这可以通过添加一个变量来实现,利用 Eclipse 的模板支持很容易做到这一点。要用正确的变量替换 2012,请单击模式文本区域下方的插入变量…按钮。您将看到一个可用变量列表,这些变量可以在模板中使用,如图图 4-29 所示。

Image

图 4-29。 代码模板可用的变量

对于本例,从变量列表中选择year来替换 2012。我们的文件注释现在将如下所示:

/**  * Copyright © ${year} Apress Media LLC. All Rights Reserved.  */

从现在开始,你添加到你的项目中的任何新的 Java 文件都将在文件注释中生成版权行,如图图 4-30 所示。

Image

图 4-30。 新的 Java 文件用其文件中的版权注释

编辑器模板

因为代码模板只能通过代码生成器使用,所以 Eclipse 不允许用户向列表中添加新模板。然而,第二种模板类型,编辑器模板,主要供用户定义新模板,并在开发应用时使用它们。如前所述打开 Eclipse 首选项对话框,导航到 Java,然后是编辑器,然后是模板,如图 4-31 所示。

Image

图 4-31。 编辑模板

要定义新的编辑器模板,请单击右侧的新建按钮。新建模板对话框将被启动,如图图 4-32 所示。

Image

图 4-32。 新建模板对话框用于编辑模板

对于代码模板,该对话框比编辑模板对话框多两个字段:一个用于新模板的名称,另一个用于上下文。该名称主要用于在编辑器中使用该模板时引用它,它更像是一个关键字。Eclipse 使用上下文根据当前上下文过滤模板,以便只提供适用的模板。

例如,我们将为记录器启动代码定义一个新的编辑器模板。将新模板命名为 Logger ,并选择 Java 上下文。我们首先将一个现有的日志初始化行复制到模板的模式编辑器中。

private static final Logger logger = Logger.getLogger(Author.class.getName());

为了使这个编辑器模板更加通用,对类文件的引用应该被转换成一个变量。单击 Insert Variable…按钮,您将看到一个比代码模板可用的变量更大的变量列表。从变量列表中选择enclosing_type来替换模板中的Author。新模板将如下所示:

private static final Logger logger = Logger.getLogger(${enclosing_type}.class.getName());

Logger类是在java.util.logging包中定义的,它不是自动导入的 Java 包集的一部分。为了使编辑器模板更加通用,让我们指示 Eclipse 在将模板插入代码时导入Logger类。为此,从变量列表中选择import,并添加参数java.util.logging.Logging。如图 4-33 所示,修改后的模板如下图所示:

private static final Logger logger = Logger.getLogger(${enclosing_type}.class.getName()); ${:import(java.util.logging.Logger)} Image

图 4-33。 对话框中编辑模板完全定义

编辑器模板现在可以使用了。要将其插入到代码中,开始键入 logger ,然后在 Windows 和 Linux 上按 Ctrl+空格键,或者在 Mac OS X 上按 Control+空格键来启动内容辅助功能(下一节讨论),如图图 4-34 所示。

Image

图 4-34。 内容辅助提示编辑模板

您将看到一个建议列表,包括我们在本例中定义的编辑器模板。从列表中选择logging,将测井初始化代码模板插入编辑器,如图 4-35 中的所示。

Image

图 4-35。 编辑模板插入代码

内容辅助

如果您需要记住每一个类型和方法的名称,那么使用第三方 API 或处理复杂的项目将会非常困难。大多数时候,开发人员确实记得一个方法的存在,但不记得它的完整签名。在这些时刻,Eclipse 的内容辅助特性变得非常方便。

触发内容辅助最简单的方法是通过点字符。例如,开始输入 System.out. 并等待一秒钟。点字符调出内容帮助,显示一个建议列表来完成当前代码行,如图图 4-36 所示。

Image

图 4-36。 内容协助制作建议完成行

内容辅助通过使用光标左侧的第一个单词来准备建议列表,该列表可能很长。为了缩小建议的范围,请继续键入更多字符,内容助手将相应地过滤列表。在我们的例子中,输入 p ,列表将只包含以字母p开头的建议,如图 4-37 中的所示。

Image

图 4-37。 内容协助建议进一步筛选

您还可以浏览建议列表。当您选择一个建议时,建议的代码将自动插入该行。如果这些建议都不适用,您可以按 Esc 键关闭内容帮助列表。

虽然点字符会自动触发内容辅助,但在 Windows 和 Linux 上也可以通过使用组合键 Ctrl+空格键或在 Mac OS X 上使用 Control+空格键随时手动启动内容辅助。Content Assist 是一个非常强大和方便的工具,用于简化日常 Eclipse 开发。

代码生成器

为了便于编码,Eclipse 提供了一组代码生成器,可以为常用的编码模式自动生成代码。这些代码生成器选项可通过顶部菜单栏上的源代码菜单获得:

  • 覆盖/实现方法:提供了超类和实现接口的方法列表,用于覆盖和实现。
  • 生成 getter 和 setter:为选中的字段生成 getter 和 setter 方法。
  • 生成委托方法:为当前类型的字段生成方法委托。
  • 生成 toString(): 使用所选字段和方法的内容生成toString()方法。
  • 生成 hashCode()和 equals(): 根据选择的字段生成hashCode()equals()方法。
  • 使用字段生成构造函数:添加一个初始化所选字段的构造函数。
  • 从超类生成构造函数:添加一个在当前类的超类中定义的构造函数。

Eclipse 代码生成器最好的例子是 getter 和 setter 生成器。在面向对象编程中,mutator 方法是用于控制变量变化的方法。像 getters 和 setters 这样的方法就是 mutator 方法的例子。类变量总是被声明为私有的,而 getter 和 setter 方法是被定义来操作这些字段的公共方法。在大多数开发项目中,getter 和 setter 方法占据了源代码的很大一部分,开发人员可能会花费相当多的时间为这些简单但耗时的方法编写代码。

Eclipse 的 getter 和 setter 代码生成器为这个问题提供了一个优雅的解决方案。定义完类中的字段后,从顶部菜单栏中选择SourceImageGenerate Getters and Setters…,弹出生成 Getters and Setters 对话框,如图图 4-38 所示。

Image

图 4-38。 生成 Getters 和 Setters 对话框

“生成 Getters 和 Setters”对话框以树状方式提供了成员字段列表。每个成员字段左侧的复选框允许您为 getter 和 setter 生成标记一个字段。复选框的左边是一个三角形图标,用于展开选择,进一步显示将要生成的各个方法。默认情况下,getter 和 setter 都会生成,除非你指定生成哪个 mutator 方法,如图 4-39 中的例子所示。

Image

图 4-39。 选择要生成的个别方法

“生成 Getters 和 Setters”对话框还提供了额外的可配置参数来指定自动生成的方法的插入点、排序和访问修饰符。

图 4-40 显示了自动生成的 getters 和 setters。getters 和 setters 的格式基于 Java 代码模板,可以通过 Eclipse Preferences 对话框进行定制,方法是导航到 Java,然后是代码样式,然后是代码模板,如本章前面所讨论的。

Image

图 4-40。 自动生成 getters 和 setter

重构

重构是指在不改变代码功能的情况下,对代码进行转换的过程。重构经常在开发周期中进行,以便根据新的需求改进设计和代码的效率。

重命名是最简单的重构操作。然而,在重命名之后,确实需要大量的手工工作来调整代码。为了能够使用代码,每个对重命名对象的引用都需要修改。现有的搜索和替换功能不适用于此操作,因为它可能会导致代码的其他部分发生意外更改。由于需要大量的手动操作,这个过程也容易出现用户错误。

Eclipse 为这个问题提供了一个更加优雅的解决方案。要重命名 Java 对象,在 Windows 和 Linux 上按 Alt+Shift+R,或者在 Mac OS X 上按 Alt+Command+R,或者从顶部菜单栏中选择重构 Image 重命名。如图图 4-41 所示,Eclipse 将允许您重命名对象,并且它会相应地自动重构应用代码。

Image

图 4-41。 重命名一个 Java 对象

重命名并不是 Eclipse 支持的唯一重构操作,你可以在Refactor菜单中看到,如图 4-42 中的所示。

Image

图 4-42。Eclipse 支持的重构操作

此菜单提供了对许多重构操作的简单访问:

  • 重命名:重命名选中的 Java 对象,并修正所有引用。
  • 移动:移动选中的 Java 对象,修正所有引用。
  • 更改方法签名:更改参数名称和类型,并相应地更新所有引用。
  • 提取方法:将当前选中的代码段提取为一个新的模块,并用对新定义方法的引用替换选中的代码段。
  • 提取局部变量:提取当前选择的变量作为新的局部变量,并用新定义的局部变量的引用替换选择。
  • 提取常量:提取当前选择的表达式作为新的常量,并用新常量的引用替换选择。
  • 内联:内联局部变量、方法或常量。
  • 将匿名类转换为嵌套类:将选定的匿名类转换为成员类。
  • 将类型移动到新文件:将所选类型移动到它自己的 Java 源文件,并相应地更新引用。
  • 将局部变量转换为字段:将选定的局部变量转换为字段,并相应地更新引用和初始化。
  • 提取超类:从一组兄弟中提取一个超类,并将兄弟更改为新定义的超类的直接子类。
  • 提取接口:用一组选中的方法提取一个接口,让选中的类实现这个新接口。
  • 尽可能使用超类型:尽可能用超类型替换一个类型的出现。
  • 下推:在超类和类之间移动方法。
  • Pull Up: 在类和它的超类之间移动方法。
  • 提取类:提取一组字段作为新的类,并用新的类替换对这些字段的引用。
  • 引入参数对象:用一个新类替换一组参数,更新方法的所有调用方,用参数传递这个新类的一个实例。
  • 引入间接方法:生成一个静态间接方法,委托给选中的方法。
  • 引入工厂:为所选类型生成新的工厂方法。
  • 引入参数:用对新方法参数的引用替换表达式,并更新所有调用方。
  • 封装字段:用 getters 和 setters 替换所有对字段的引用。
  • 泛化声明的类型:允许用户选择引用当前类型的超类型,如果引用可以安全地更改为该超类型的话。
  • 推断泛型类型参数:尽可能用参数化类型替换泛型类型的原始类型。

一些重构任务可能涉及这些重构操作的组合。Eclipse 保存了重构任务的历史,允许您撤销特定的重构步骤。要查看重构历史,从顶部菜单栏中选择重构 Image 历史

通过选择重构 Image 创建脚本… ,还可以将选定的重构任务保存到脚本文件中以备后用。然后,您可以选择重构 Image 应用脚本… 来再次应用这些重构步骤。

剪贴簿

剪贴簿功能允许用户轻松地试验代码片段,而无需处理编写完整 Java 代码的额外负担。剪贴簿就像一个代码解释器。它允许您只键入一段代码进行实验,然后它可以快速执行代码并显示结果。在剪贴簿页面中,您可以使用项目中定义的类以及 Java 系统类。

要启动剪贴簿页面,从顶部菜单栏选择文件 Image 新建 Image 其他…ImageJavaImageJava 运行/调试 Image 剪贴簿页面。一个空白的剪贴簿页面将被添加到编辑区,如图图 4-43 所示。

Image

图 4-43。【scrapbook page】

开始键入以下示例表达式:

java.util.Date date = new java.util.Date(); date

剪贴簿提供了以下三种执行类型:

  • 显示:评估表达式,并将其值直接打印到剪贴簿页面。
  • Inspect :对表达式求值,并显示一个检查窗口,显示对象提供的所有信息。
  • 执行:将表达式作为普通 Java 代码进行求值。

为了使用这些执行类型,首先突出显示表达式。然后在 Windows 和 Linux 上按 Ctrl + Shift + D,或者在 Mac OS X 上按 Shift+Command+D,或者从顶部菜单中选择运行Image显示,开始显示执行。表达式将被求值,其值将显示在剪贴簿页面中,如图图 4-44 所示。

Image

图 4-44。 剪贴簿展示

保持表达式高亮显示,在 Windows 和 Linux 上按 Ctrl + Shift + I,或者在 Mac OS X 上按 Shift+Command+I,或者从顶部菜单中选择运行Image检查,开始检查执行。表达式将被计算,检查窗口将出现在剪贴簿页面的顶部,如图图 4-45 所示。

Image

图 4-45。 剪贴簿查看功能

将表达式更改如下:

java.util.Date date = new java.util.Date(); System.out.println(date.toString());

高亮显示表达式,在 Windows 和 Linux 上按 Ctrl + U,在 Mac OS X 上按 Command+U,或者选择运行 Image 执行来执行。表达式将像普通 Java 代码一样执行,输出将显示在控制台视图中,如图图 4-46 所示。

Image

图 4-46。 剪贴簿执行功能

总结

在这一章中,我们讨论了可以加速开发周期的强大的 Eclipse 特性。本章一开始,我们深入探讨了 Eclipse 提供的高级导航特性,包括视图和不同类型的标记,以便轻松定位代码部分。然后,我们研究了 Eclipse 的快速编码特性。这些包括代码和编辑器模板,可以用来维护代码的一致性,以及 Eclipse 提供的代码生成器和重构功能,用来处理耗时的开发任务。后面的章节将演示这些功能的实际应用。*

五、Eclipse 的 Android 开发工具

在前四章中,我们已经非常详细地研究了 Android 框架和 Eclipse 集成开发环境。在这一章中,我们将使用 Eclipse 的 Android 开发工具(ADT)插件把这两个世界粘在一起。我们将从安装 ADT 和 Android 软件开发工具包(SDK)开始我们的旅程。然后我们将开始探索它们提供的视图和工具。在下一章,在开发我们的第一个 Android 项目时,我们将开始把这些视图和工具付诸实践。

准备 Eclipse

尽管 Eclipse 附带了用于 Java 开发的工具,但是为了使用 Eclipse 开发 Android 应用,还需要特定于 Android 的平台 API 和应用打包工具。

安装 Android 开发工具

正如在第三章中所解释的,Eclipse 平台是围绕插件的概念构建的。ADT 是一组用于 Eclipse 平台上 Android 应用开发的插件。

ADT 扩展了 Eclipse 集成开发环境的功能,允许应用开发人员执行以下任务:

  • 快速建立新的 Android 项目
  • 可视化设计高级用户界面
  • 访问和使用 Android 框架组件
  • 调试、单元测试和发布 Android 应用

ADT 是在开源 Apache 许可下提供的免费软件。关于最新 ADT 版本和最新安装步骤的更多信息可以在 Eclipse 的 ADT 插件页面([developer.android.com/sdk/eclipse-adt.html](http://developer.android.com/sdk/eclipse-adt.html))上找到。

我们将使用 Eclipse 的安装新软件向导来安装 ADT。从顶部菜单栏选择帮助Image安装新软件启动向导,如图图 5-1 所示。

Image

图 5-1。 选择安装新软件

向导将启动并显示可用插件的列表。由于 ADT 不是官方 Eclipse 软件存储库的一部分,您需要首先添加 Android 的 Eclipse 软件存储库作为一个新的软件站点。为此,点击添加按钮,如图图 5-2 所示。

Image

图 5-2。 开始添加新软件

将出现“添加存储库”对话框。在“名称”字段中,输入引用该存储库的唯一名称。在位置字段中,输入 Android 的 Eclipse 软件仓库的 URL:[dl-ssl.google.com/android/eclipse/](https://dl-ssl.google.com/android/eclipse/),如图图 5-3 所示。

Image

图 5-3。 添加存储库对话框完成,带有 ADT 信息

添加新软件站点后,安装新软件向导会显示可用的 ADT 插件列表,如图图 5-4 所示。这些插件中的每一个对 Android 应用开发都至关重要,强烈建议您安装所有的插件。(我们将在本章后面的“探索 ADT”一节中讨论这些插件。)单击 Select All 按钮选择所有 ADT 插件,然后单击 Next 按钮进入下一步。

Image

图 5-4。 安装 ADT 开发工具

Eclipse 将检查所选插件的列表,将所有依赖项添加到列表中,然后提交最终的下载列表以供审查。单击“下一步”按钮进入下一步。

ADT 还包含一组具有不同许可条款的其他第三方组件。在安装过程中,Eclipse 会显示每个软件许可,并要求用户接受许可协议的条款,以便继续安装。查看许可协议,选择接受其条款,然后单击完成按钮开始安装过程。Eclipse 将报告安装的进度,如图 5-5 中的所示。

Image

图 5-5 。ADT 安装进度

ADT 插件来自未签名的 JAR 文件,这可能会触发安全警告,如图图 5-6 所示。单击“确定”按钮消除警告并继续安装。当 ADT 插件安装完成后,Eclipse 将需要重启以应用更改。

Image

图 5-6。 由于未签名的 ADT 插件导致的安全警告

安装 Android SDK

ADT 是一组插件,将 Android 开发工具融入 Eclipse 集成开发环境;它不是 Android SDK 的替代品。

Android SDK 是一套全面的开发工具,包括 Android 平台 Java 库、应用打包程序、调试器、仿真器和大量文档。为了使用 ADT 做任何有用的事情,需要在机器上安装 Android SDK。重新启动后,ADT 会用 SDK 配置向导欢迎您,如图图 5-7 所示。

Image

图 5-7。 Android SDK 配置向导

SDK 配置向导允许您将 ADT 指向现有的 Android SDK(如果之前已安装),或者指示 ADT 为您下载并安装 Android SDK。单击“完成”按钮继续 SDK 配置过程。SDK 配置向导将指导您完成将 Android SDK 安装到您的主机上的过程。请注意 Android SDK 的安装目录,因为您将需要它来更新系统变量Path,如下所述。

更新路径

在 Android SDK 安装期间,Path变量不会自动添加到系统中。ADT 不要求在系统变量Path中包含 SDK 二进制文件,但是为了使这些文件易于访问,强烈建议您添加它们。

更新 Microsoft Windows 上的路径

正如我们在第三章中所做的那样,将 JDK 添加到系统Path变量中,打开控制面板并选择系统以启动系统属性对话框。切换到高级选项卡,并单击环境变量按钮。从系统变量窗格中选择变量Path,并点击编辑按钮。将;<*sdk-dir*>\tools;<*sdk-dir*>\platform-tools追加到Path变量值,将<*sdk-dir*>替换为 Android SDK 安装目录,如图图 5-8 所示。单击确定按钮保存更改。

Image

图 5-8。 添加 Android SDK 目录到 Windows 系统路径变量

更新 Mac OS X 和 Linux 上的路径

要将 Android SDK 二进制目录附加到您的系统Path变量中,请在 Mac OS X 上打开一个终端窗口,或者在 Linux 上打开一个 shell 窗口,然后输入以下命令(将<*sdk-dir*>替换为 Android SDK 安装目录):

export PATH=$PATH:<sdk-dir>/tools:<sdk-dir>/platform-tools >> ~/.bashrc

图 5-9 显示了终端窗口中的命令。

Image

图 5-9。 添加 Android SDK 目录到 Mac OS X 系统路径变量

安装平台 API

默认情况下,SDK 配置向导将安装最新版本的 Android APIs 但是,您可以随时使用 Android SDK 管理器安装不同版本的 Android APIs。要启动 SDK 管理器,从顶部菜单栏选择窗口 Image ** Android SDK 管理器**,如图图 5-10 所示。

Image

图 5-10。 打开 Android SDK 管理器

如图 5-11 所示,Android SDK 管理器显示了一个 Android SDK 组件列表,如工具、API 和插件,可以下载。该列表以树状方式构建。列表中的第一项是工具。这些是 Android SDK 的通用和必需组件。强烈建议您使用最新版本的工具组件。列表中的其他组件按照 Android 版本和 API 级别分组,它们是可选的。

Image

图 5-11。 使用 Android SDK 管理器

单击这些 Android 版本旁边的加号,查看可用组件列表。根据所选的版本,您将看到一个核心组件列表,以及可用的附加组件。尽管此列表会因所选版本而异,但以下是最常见的组件:

  • SDK 平台:这是为所选 Android 版本开发应用必须安装的核心组件。Android SDK 管理器将 SDK 平台安装在<*sdk-dir*>/platforms/android-<*api-level*>目录下,仿真器系统镜像安装在<*sdk-dir*>/system-img/android-<*api-level*>目录子文件夹下。Android SDK 基于您的应用的目标平台为您的应用提供这些资源。Android 开发人员不希望直接与这些文件交互。
  • Android SDK文档:这提供了在[developer.android.com](http://developer.android.com)网站上可获得的 Android 资源的离线版本。如果选择安装,Android SDK 管理器会将文档安装在<*sdk-dir*>/docs目录下。您可以通过将 web 浏览器指向file:///<*sdk-dir*>/docs/index.html来访问文档的主页。为了快速和脱机访问,您可以考虑安装此组件。
  • SDK的样例:这些样例应用演示了 Android APIs 的使用。Android SDK 管理器将示例应用安装在<*sdk-dir*>/samples/android-<*api-level*>目录子文件夹下。强烈建议您安装示例应用,因为它们是了解 Android API 特性并进行实验的绝佳资源。
  • 【Android SDK 的源代码:这些提供了 Android 框架的源代码。Android SDK 管理器将源代码安装在<*sdk-dir*>/sources/android-<*api-level*>目录下。在对 Android 应用进行故障诊断时,这些源文件非常方便,因为它们允许开发人员深入 Android 框架,以快速确定许多模糊问题的根本原因。
  • 谷歌公司的谷歌 API:这不是核心组件的一部分,是作为一个附加组件发布的。Android SDK 管理器将这些 API 安装在<*sdk-dir*>/add-ons目录下。这个插件允许你使用谷歌的 API 和服务开发应用,比如谷歌地图。它还附带了一个扩展的模拟器系统映像,其中包含默认模拟器系统映像中没有的 Google 系统组件。

你可能还记得第一章的内容,Android 市场高度分散,Android 的新版本传播非常缓慢。为了覆盖更大的用户群,在最广泛支持的 API 级别之上构建应用是一种常见的做法。在撰写本文时,使用最广泛的 Android 版本是 2.3.3,它支持 API 级。

要安装 API level 10,单击列表中 Android 2.3.3 (API 10)旁边的加号将其展开。选择 SDK 平台组件和您想要安装的任何其他组件,然后单击“安装软件包”按钮。Android SDK 管理器将要求您接受所选组件的许可条款。选择 Accept All,然后单击 Install 按钮继续安装软件包。

某些组件可能要求您在制造商的网站上注册并提供下载凭据。在这些情况下,Android SDK 管理器会显示相应的对话框来指导您完成整个过程。Android SDK 管理器将选定的组件安装在 SDK 目录下相应的目录中。SDK 目录的位置显示在 Android SDK 管理器对话框的顶部,标记为“SDK 路径”(参见图 5-11 )。

探索 ADT

ADT 提供了从 Eclipse 内部对 Android SDK 组件的访问。在本节中,我们将探索这些组件:Android 虚拟设备管理器、Dalvik 调试监视器、Traceview、层次结构查看器和 Android Lint。

安卓虚拟设备管理

Android SDK 带有一个全功能模拟器,一个在你的机器上运行的虚拟设备。Android 模拟器允许您在本地机器上开发和测试 Android 应用,而无需使用物理设备。

Android 模拟器运行完整的 Android 系统堆栈,包括 Linux 内核。这是一个完全虚拟化的设备,可以模仿真实设备的所有硬件和软件功能。用户可以使用 Android 虚拟设备(AVD)管理器定制这些功能。要启动 AVD 管理器,从顶部菜单栏选择窗口 Image ** AVD 管理器**,如图图 5-12 所示。

Image

图 5-12。启动 AVD 管理器

AVD 管理器允许您定义多个虚拟设备配置。AVD 管理器对话框列出了之前定义的配置,如图图 5-13 所示。

Image

图 5-13。AVD 管理器对话框中列出的现有虚拟设备

配置新的虚拟设备

要定义新的虚拟机实例,请单击 AVD 管理器对话框右侧的新建按钮。这将打开创建新的 Android 虚拟设备(AVD)对话框,如图图 5-14 所示。

Image

图 5-14。 配置新的虚拟设备

该对话框包含以下字段:

  • 名称:这是新虚拟设备配置的唯一名称。
  • 目标:这是虚拟设备的 Android 版本号和 API 级别。下拉列表仅显示使用 Android SDK 管理器安装的 Android 版本。如果首选版本不可用,您将需要使用 Android SDK 管理器安装它。
  • CPU/ABI :这是新虚拟设备的机器架构。目前只支持 ARM 机器架构。
  • SD 卡:这或者是 SD 卡的大小,或者是现有磁盘镜像的位置。如果此虚拟设备配置不需要 SD 卡,此字段可以为空。
  • 快照:这是为了允许在会话之间保持虚拟设备的状态。
  • 皮肤:这是虚拟设备的皮肤和屏幕尺寸。下拉列表是根据已安装的版本和附件填充的。也可以定义自定义屏幕尺寸。
  • 硬件:这是虚拟设备支持的硬件特性列表,比如 GPS 和摄像头。您可以通过点击新建按钮并选择单个项目来启用功能,如图图 5-15 所示。

Image

图 5-15。 添加硬件特性

在接下来的章节中,我们将使用 Android 模拟器。建议使用以下虚拟机配置来执行这些章节中的示例代码片段:

  • Name 参数应该设置为 Android_10
  • 目标参数应该设置为Android 2 . 3 . 3–API Level 10。如果此目标在下拉列表中不可用,请使用 Android SDK 管理器下载它。
  • SD 卡的大小至少应设置为 128MB。

其他设置可以保持不变。

设置参数后,单击创建 AVD 按钮存储虚拟设备配置。

启动模拟器

虚拟设备配置可用于随时启动模拟器实例。选择虚拟设备配置后,单击 Start 按钮使用所选的虚拟设备配置启动一个新的模拟器实例。在启动仿真器之前,AVD 管理器显示启动选项对话框,如图图 5-16 所示。

Image

图 5-16。 模拟器启动选项对话框

模拟器屏幕可能看起来太大,这取决于您的屏幕大小和分辨率。使用“启动选项”对话框,选中“按实际大小缩放显示”框,并设置监视器大小和分辨率来缩放模拟器。

“启动选项”对话框还允许您擦除用户数据,以将模拟器恢复到初始状态。如果在配置期间将快照设置为 Enabled,则 Launch Options 对话框还允许您从现有快照启动模拟器,并决定模拟器状态是应该存储在快照中还是在终止时丢弃。

点击启动选项对话框中的启动按钮,启动模拟器,如图图 5-17 所示。Android 模拟器可能需要一些时间来启动,这取决于您的主机平台的 CPU 能力。

Image

图 5-17。 仿真器实例

控制仿真器

模拟器窗口的左窗格显示模拟器显示,右窗格包含软键。可以使用鼠标模拟触摸事件。此外,在表 5-1 中列出的组合键可用于控制硬件功能。

Android 控制台

基于鼠标和键盘的控制方法允许用户与仿真器交互并执行常见任务。但是,通过这种方法无法直接控制硬件功能,如网络连接。Android 控制台提供了一个广泛的界面,允许用户控制仿真器和硬件功能。在一台机器上,多个模拟器实例可以并行运行。每个模拟器实例都被自动分配一个唯一的端口号,介于 5554 和 5584 之间。该编号出现在仿真器窗口标题栏上的配置名称之前(参见图 5-17)。

模拟器监听该端口号以提供对 Android 控制台的访问。可以使用 telnet 应用连接到该端口来访问 Android 控制台。Telnet 应用建立到给定端口的 TCP 连接,并允许用户与远程服务交互。在 Mac OS X 和 Linux 平台上,telnet 应用由操作系统提供。对于 Windows 系统,可以下载一个免费的 telnet 应用,比如 PuTTY ( [www.chiark.greenend.org.uk/~sgtatham/putty/download.html](http://www.chiark.greenend.org.uk/~sgtatham/putty/download.html))。

使用基于您的系统的 telnet 应用,连接到与模拟器实例相关联的地址localhost和端口号。连接到 Android 控制台后,基于文本的界面允许您控制模拟器和硬件功能。输入 help ,可以得到可用命令列表,如图图 5-18 所示。

Image

图 5-18。 仿真器控制端口命令列表

达尔维克调试监控服务器

Android SDK 附带了一个名为 Dalvik Debug Monitor Server (DDMS)的调试工具。DDMS 允许开发人员监控连接的设备和仿真器并与之交互。它提供端口转发、屏幕捕获、访问进程和线程状态、堆信息、文件浏览器、日志和许多其他功能。

DDMS 还充当运行在设备或仿真器上的 Dalvik 虚拟机与 Eclipse 调试器之间的桥梁。它处理底层通信设置,以允许 Eclipse 调试器与 Dalvik 虚拟机通信。这使得开发人员可以轻松调试 Android 应用,就像它们是运行在主机上的普通 Java 应用一样。

尽管 DDMS 是作为一个独立的应用与 Android SDK 一起提供的,但它被 ADT 分解成多个 Eclipse 视图,并作为一个结合了这些单独视图的 Eclipse 透视图提供。在这一章中,我们将重点介绍 DDMS 的 Eclipse 透视风味。

要启动 DDMS 透视图,从顶部菜单栏中选择窗口 Image 打开透视图 Image 其他… ,并从打开透视图对话框中选择 DDMS。 DDMS 透视图由多个特定于 Android 的视图组成,如图图 5-19 所示,并在以下章节中描述。

Image

图 5-19 。DDMS 视角

设备视图

Devices 视图提供了连接的设备和仿真器的列表。通过单击每个设备左侧的加号,可以展开每个设备以显示正在运行的应用列表。设备视图还提供了一个工具栏和下拉菜单来启动所选设备或应用的常用操作,如图图 5-20 所示。

Image

图 5-20。 设备视图下拉菜单

设备视图下拉菜单提供以下选项:

  • 调试过程:该选项为选定的应用启动一个调试会话。
  • 更新堆:该选项支持收集所选应用的堆信息。
  • 转储 HPROF 文件:该选项将所选应用的堆转储到 HPROF 格式的文件中,以便进行更深入的内存调查。
  • Cause GC :该选项触发所选应用的垃圾收集,以释放未使用的内存。
  • 更新线程:该选项支持跟踪所选应用的线程状态。
  • Start Method Profiling :该选项允许从所选的应用中收集方法调用的分析数据。
  • 停止进程:该选项停止选中的申请进程。
  • 截屏:该选项将设备的当前显示捕捉到一个文件中。
  • 重置 adb :该选项重置 Android 调试桥(adb),该调试桥提供主机和设备之间的连接。
仿真器控制视图

如果所选设备是仿真器,仿真器控制视图允许模拟语音和数据网络以及位置状态,用于调试和测试目的,如图图 5-21 所示。

Image

图 5-21。 仿真器控制视图

模拟器控件视图功能分为三个部分:

  • 电话状态:此部分允许更改设备网络状态的不同方面,如连接状态、网络速度和延迟。
  • 电话操作:该部分允许针对设备生成呼叫和 SMS 消息,以便测试应用与传入语音呼叫和 SMS 消息的交互。
  • 位置控制:该部分允许为设备设置一个模拟位置,以测试应用与位置变化的交互。可以将模拟位置指定为固定坐标,或者可以使用 GPX 或 KML 格式的坐标文件将多个位置注入到设备中。
日志猫视图

图 5-22 中的所示的 LogCat 视图提供了对设备日志信息的访问。它以表格的形式实时显示日志消息。该表分为多列,包括级别、时间、PID、应用、标签和消息。

Image

图 5-22。 显示日志消息的 LogCat 视图

LogCat 视图允许按日志级别和基于消息过滤标准过滤日志消息。常用的日志过滤器也可以存储和重用。您可以使用 LogCat 视图界面将显示的日志消息保存到文件中。

线程视图

Threads 视图提供对所选应用的线程状态和堆栈跟踪的访问。默认情况下,不启用线程跟踪。要访问线程信息,请使用设备视图选择应用,然后单击更新线程按钮。线程视图以表格的形式呈现现有线程的列表,如图图 5-23 所示。

Image

图 5-23。 选中应用的线程视图

线程视图列提供每个线程的以下信息:

  • ID :这是分配给线程实例的虚拟机。
  • TID :这是 Linux 操作系统分配的线程 ID。
  • 状态:线程的当前状态,可以是以下任意一种状态:
    • 运行中,当执行代码
    • 睡觉,当睡在 Thread.sleep()
    • 监视器,当等待监视器锁定
    • 等待,当在等待时 Object.wait() 呼叫
    • 本机,当执行本机代码
    • Vmwait,当等待虚拟机资源
  • Utime :这是以 jiffies 为单位运行用户代码所花费的时间。
  • Stime :这是以 jiffies 为单位运行系统代码所花费的时间。
  • 名称:这是应用赋予线程的名称。

注: A jiffy 是系统定时器中断一个滴答持续时间的时间单位。在 Android 系统中,一瞬间等于 4 毫秒。

堆视图

堆视图提供关于所选应用正在使用的内存量的信息。它是研究记忆问题的一个非常重要的工具。它以表格的形式显示堆分配的列表,如图图 5-24 所示。此表显示了每个堆分配的计数、总大小和统计信息,按类类型分组。堆视图的底部窗格包含一个直方图,展示了每个分配大小的分配计数。

Image

图 5-24。 堆视图

默认情况下,不会从每个应用收集堆信息。要开始收集堆分配信息,请在 Devices 视图中选择应用,然后单击 Update Heap 按钮。堆视图将开始从应用收集堆分配信息。堆分配信息是在虚拟机进行垃圾收集时收集的。要获得堆分配的快速快照,单击 Cause GC 按钮触发垃圾收集。

分配跟踪器视图

分配跟踪器视图允许跟踪所选应用的内存分配。对于研究复杂应用中的内存问题,这是一个非常有用的工具。该视图以表格形式提供分配列表,如图图 5-25 所示。这些列显示关于分配的信息,包括分配的类类型;分配大小;以及分配发生在哪个类、方法和线程中。

Image

图 5-25。 分配跟踪器视图

要开始从选定的应用收集分配数据,请单击开始跟踪按钮。使用该应用,执行作为内存调查主题的任何操作。在此过程中,您可以通过单击“获取分配”按钮来获取分配的快照。完成调查后,单击“停止跟踪”按钮停止分配跟踪器。

文件浏览器视图

文件资源管理器视图允许用户与选定设备上的文件系统进行交互。如图 5-26 所示,以树和表相结合的形式展示了设备的文件系统。你可以通过点击目录左边的加号来展开目录。该列表还显示了每个文件和目录的大小、权限、修改日期和时间。

Image

图 5-26。 文件浏览器查看所选设备上的列表文件

文件资源管理器视图还通过其工具栏和下拉菜单提供文件操作,下拉菜单有以下选项:

  • 拉文件:该选项将文件从设备下载到主机。
  • 推送文件:该选项将文件从主机上传到设备。
  • 删除:该选项从设备中删除所选文件。
  • 新文件夹:该选项向设备添加新文件夹。

这些文件操作在一个受限制的用户帐户下运行,该帐户称为 shell 用户。因此,可以在设备上执行的操作受到该用户帐户权限的限制。如果操作由于限制而无法完成,文件资源管理器视图将显示一个错误对话框来通知用户。

Traceview

由于堆和分配跟踪器视图允许您分析其应用的内存消耗,Traceview 允许您分析应用执行期间 CPU 时间消耗的细分。Traceview 附带了 Android SDK,既可以作为独立的应用,也可以作为 Eclipse 编辑器插件。

Traceview 对记录的跟踪文件进行操作。默认情况下,Dalvik 虚拟机不会生成这些跟踪文件。要创建跟踪文件,您可以使用通过android.os.Debug API 提供的跟踪方法,或者您可以通过单击 Devices 视图中的 Start Method Profiling 按钮启用 DDMS 跟踪。Traceview 分析跟踪文件并给出结果,如图图 5-27 所示。

Image

图 5-27。 Traceview 分析一个跟踪文件

Traceview 有两个面板:

  • 时间线面板:顶部面板在自己的行中显示每个线程的执行,时间向右增加。在这个线程中执行的每个方法都用颜色编码,并在时间轴上显示为一条细线。
  • Profile panel :底部面板显示每种方法所用时间的详细汇总。它显示了包容性和排他性的时代。独占时间是运行方法本身所花费的时间。Inclusive time 是运行该方法和从此方法调用的其他方法所花费的总时间。配置文件面板还显示了方法被调用的次数。该面板提供了大量信息,用于识别在应用执行期间消耗最多 CPU 时间的方法。

层级查看器

  • Android 用户界面构建在布局组件之上,布局组件根据可用的屏幕空间动态定位其子视图。当这些布局和视图的结构不正确时,它们很容易降低整个应用的速度,并且很难在复杂的应用中找到这些瓶颈。
  • Android SDK 附带了一个名为 Hierarchy Viewer 的工具,允许您调试和优化用户界面。它提供了布局和视图层次的可视化表示。它还决定了测量、布局和绘制视图所需的时间。瓶颈是用颜色标记的,这使得它们很容易被发现。
  • 层次查看器还提供了像素完美工具,它放大了用户界面。这允许您检查实际显示的像素属性,以便进行最后的处理。

尽管 Hierarchy Viewer 是 Android SDK 中的一个独立应用,但它被 ADT 分解成多个 Eclipse 视图,并作为一个合并了这些单独视图的 Eclipse 透视图提供。在这一章中,我们将关注 Eclipse 透视图风格的层次结构查看器。

要启动层次结构查看器透视图,从顶部菜单栏中选择窗口 Image 打开透视图 Image 其他… ,并从打开透视图对话框中选择层次结构查看器。层次结构查看器透视图由多个特定于 Android 的视图组成,如图 5-28 所示,并在以下章节中描述。

Image

图 5-28。 层级视图

Windows 视图

Windows 视图以树状格式列出连接的设备和仿真器。点击左边的加号可以展开每个设备以显示活动窗口,如图图 5-29 所示。

Image

图 5-29。 显示活动窗口的窗口视图

需要选择一个窗口才能使用层级查看器。如果您的应用在列表中不可见,请单击视图的刷新按钮以重新加载活动窗口列表。

树形视图

树形视图以树状方式显示所选窗口的布局结构(参见图 5-28 )。每个树项目都使用线条连接到其父项目,这使得更容易可视化视图层次结构。

每个视图项目都以其名称和资源 ID 作为标题。您可以拖动内容在视图中导航。在标题下面,测量、布局和绘制步骤所花费的时间用颜色编码,并显示为用绿色、黄色或红色填充的圆圈。红色表示视图组件在这些步骤中花费了太多时间。当您点击一个视图项目时,这些步骤的实际测量以毫秒为单位显示,如图图 5-30 所示。

Image

图 5-30。 树形视图显示查看项目详情

树形总览视图

根据视图层次结构的大小,可能无法显示树视图中的所有视图和组件。对于视图层次内的导航,树形总览视图提供了代表整个树形视图窗口的较小地图,如图图 5-31 所示。当前选择的视图在地图上高亮显示。

Image

图 5-31。 将整个树形视图表示为地图的树形视图

查看属性视图

“视图属性”视图提供了对选定视图组件的属性的访问。“视图属性”视图在左窗格中显示为一个选项卡。使用 View Properties 视图,您可以检查所有的属性,而不需要查看应用源代码。为了使导航更容易,属性以按属性类别组织的树形格式显示,如图图 5-32 所示。

Image

图 5-32。 视图属性视图列出活动视图的所有属性

布局视图

布局视图提供了整个窗口的框图表示,如图 5-33 所示。选择视图块时,将在树视图和视图属性视图中选择相应的视图。

Image

图 5-33。 显示块表示的布局视图

块的轮廓颜色也提供了关于视图的额外信息:

  • 红色粗体表示当前在树视图中选择的视图。
  • 浅红色代表当前选中视图的父视图。
  • 白色表示不是当前选定视图的父视图或子视图的可见视图。

Android Lint

Android Lint 是一个工具,用于扫描 Android 应用项目中的潜在错误和最常见的错误。它还会查找布局、资源和清单文件中的任何不一致之处。它是一个非常强大的工具,应该在开发周期中使用,以保持应用源代码的整洁和健壮。

Android Lint 工具可以检测以下问题:

  • 缺失和未使用的翻译
  • 未使用和不一致的资源
  • 字符串资源的排版建议
  • 可访问性和国际化问题,如硬编码字符串
  • 布局性能问题
  • 布局和输入框的可用性问题
  • 图标和图形问题,如重复的图标和错误的大小
  • 清单错误
  • 使用不推荐使用的 API

Android Lint 既作为独立的应用提供,用于快速集成到现有的构建系统中,也作为 Eclipse 插件集成到开发环境中。在这一节中,我们将关注 Lint Eclipse 插件。

要启动 Android Lint,选择一个项目,从顶部菜单栏选择窗口 Image 运行 Android Lint ,如图图 5-34 所示。

Image

图 5-34。 选择运行 Android Lint

Android Lint 遍历项目文件,并通过 Lint Warnings 视图显示其结果,如图图 5-35 所示。

Image

图 5-35。 安卓线头警告视图

Lint 警告以表格形式列出。这些列显示 Lint 警告消息以及相关的文件和行号。从表中选择一个警告项会在右窗格中显示已识别问题的详细描述。

通过其工具栏,Lint Warnings 视图还允许您对列出的警告启动以下操作:

  • 刷新:再次检查项目文件,刷新 Lint 警告列表。
  • 修复:如果解决方案已知,这将自动修复警告。
  • 忽略类型:忽略所有相同类型的警告。例如,您可以忽略所有与图像密度相关的警告。
  • 删除:从列表中删除选中的警告。
  • 删除所有:从列表中删除所有警告。

Lint 也可以通过其首选项对话框进行配置。在 Windows 和 Linux 上选择窗口 Image 首选项,或者在 Mac OS X 上选择 Eclipse Image 首选项,并从首选项类别列表中选择 Android,然后选择 Lint 错误检查以访问 Lint 属性。Lint 首选项对话框提供了可以通过 Lint 检测到的问题列表。使用此列表,您可以更改与这些问题相关的严重级别,如图图 5-36 所示。如果某个问题与项目无关,可以将其严重性级别设置为 Ignore,以便在 Lint 警告中隐藏这些问题。

Image

图 5-36。 设置线头偏好

发布申请

如前几章所述,Android 平台要求每个应用都要经过其作者的签名,才能在 Android 平台上部署。ADT 提供了一个向导来指导开发人员完成签名过程。

在开发阶段,Android SDK 透明地生成一个调试密钥来自动签署应用,以简化流程。但是当应用要向公众发布时,Android 要求用发布密钥对其进行签名。

与其他移动平台不同,Android 不依赖认证机构向开发者颁发数字证书。每个 Android 开发者都可以在其主机上生成一个密钥并签署一个 Android 应用。当一个应用被安装在 Android 上时,它的签名被用来检查应用更新的真实性。如果应用更新没有使用相同的密钥签名,Android 不允许新版本作为更新部署。ADT 插件提供了一组向导,用于在公开发布之前生成密钥和签署应用。

要对您的应用进行发布签名,使用包资源管理器,选择应用项目,右键单击,从上下文菜单中选择Android ToolsImageExport Signed Application Package…,启动导出 Android 应用向导,如图图 5-37 所示。

Image

图 5-37。 导出安卓应用向导

确认要导出的项目,然后单击“下一步”按钮继续。如图 5-38 所示,向导将询问要使用的密钥库的位置。如果这是您第一次签署应用,请选择“创建新的密钥库”单选按钮来生成新的密钥库。密钥库持有一个或多个私钥。使用浏览按钮,选择密钥库的位置和文件名。定义保护密钥库的密码,然后单击“下一步”按钮继续。

Image

图 5-38。 用于导出签名应用的密钥库选择

如果您选择创建一个新的密钥库,导出 Android 应用向导会显示一个表单,以获取足够的信息来正确地生成密钥,如图 5-39 所示。填写完必要的信息后,单击“下一步”按钮继续。

Image

图 5-39。 键创建信息表单

如果您已经有一个要使用的密钥库,向导将要求您从给定的密钥库中选择要使用的密钥,如图 5-40 所示。

Image

图 5-40。 从给定的密钥库中通过别名选择密钥

作为最后一步,导出 Android 应用向导将询问将要发布的已签名的 APK 文件的目标位置,如图图 5-41 所示。单击浏览按钮,并选择位置和文件名。然后单击“完成”按钮开始该过程。

Image

图 5-41。 设置已签名的 APK 文件的发布目的地

向导将在发布模式下编译 Android 应用,并用所选密钥对其进行签名。签署的 APK 文件可以向公众发布。

总结

本章介绍了 Eclipse 的 ADT 插件。我们通过安装 ADT 和 Android SDK 开始了我们的旅程。然后,我们配置了一个 Android 虚拟机,并探索了它的控制界面。接下来,我们看了 DDMS、Traceview、Hierarchy Viewer 和 Android Lint,探索如何在日常 Android 开发中使用这些工具。最后,我们讲述了如何使用 ADT 为 Android 应用签名以供发布。

资源

以下资源可用于本章涵盖的主题:

  • Android Lint,[tools.android.com/tips/lint](http://tools.android.com/tips/lint)
  • 调试和分析用户界面
  • 达尔维克调试监视器,[www.netmite.com/android/mydroid/dalvik/docs/debugmon.html](http://www.netmite.com/android/mydroid/dalvik/docs/debugmon.html)
  • Android 工具项目,[tools.android.com/](http://tools.android.com/)

六、项目:电影播放器

在前一章中,我们探索了 Eclipse 的 ADT。我们回顾了 ADT 视图和工具,以及如何在日常 Android 开发中使用它们。在本章中,我们将开始把我们在前面章节中讨论的所有工具和概念付诸实践。

我们的第一个 Android 项目是一个简单的电影播放器应用。因为这个实验的目的是看 Android 在 Eclipse 上的实际开发,所以我们不会太深入地研究 Android 框架 API。在接下来的章节中,我们将继续构建这个简单的项目。

电影播放器概述

我们的电影播放器应用将是一个简单的单活动应用,它将呈现一个电影文件列表,这些文件位于外部存储中。该列表将显示每个电影文件的缩略图、名称和持续时间。当您单击列表中的电影项目时,电影播放器应用将依赖 Android 平台启动相应的视频播放器活动来播放所选电影。尽管这是一个非常简单的项目,但它将允许我们试验我们在前面章节中讨论过的大多数工具和概念。

我们将从使用新的 Android 项目向导来生成框架项目开始。然后我们将使用 ADT 提供的编辑器来创建用户界面。通过清单编辑器,我们将根据项目的需求修改AndroidManifest.xml文件。使用布局编辑器,我们将定义电影列表的用户界面布局,以及电影列表项目的布局。我们将使用资源编辑器来正确定义我们用户界面中需要的字符串资源。在生成必要的布局和资源的同时,我们将使用 Android Lint 并行验证代码。应用将依靠媒体商店内容提供者来获取外部存储器中的电影文件列表。获取的信息将被保存到我们将在本章中定义的电影对象中。我们还将实现电影列表适配器,将信息输入列表视图进行显示。

为了播放选中的电影文件,我们将依靠 Android 平台,利用Activity类的startActivity方法启动相应的视频播放器。在做所有这些的时候,我们将非常依赖 Eclipse 的代码模板、自动代码生成器和重构特性,通过让 Eclipse 处理耗时的操作来简化开发过程。

开始电影播放器项目

要启动我们新的 Android 项目,从顶部菜单栏选择文件 Image 新建 Image 其他打开新建项目对话框,如图图 6-1 所示。

Image

图 6-1。 Eclipse 新项目对话框

“新建项目”对话框按项目类别组织。展开 Android 项目类别,选择 Android 项目作为项目类型,然后单击 Next 按钮。这将启动新建 Android 项目向导。作为第一步,您需要提供项目名称及其位置。通过选择相应的单选按钮,您还可以选择是从一个空项目开始,在现有项目的基础上构建一个项目,还是从一个 Android 示例应用开始。对于本例,将项目命名为MoviePlayer,如图图 6-2 所示,然后点击下一步按钮。

Image

图 6-2。 新建 Android 项目向导

接下来,新建 Android 项目向导询问新项目的 Android 平台目标,如图图 6-3 所示。列表将只显示已经安装的 SDK。如果您的目标平台不在列表中,您可能需要使用 Android SDK 管理器下载它。对于这个项目,选择 Android 2.3.3,API Level 10 作为目标平台。这意味着新项目将在任何支持 API 级别 10 及以上的 Android 设备上运行。单击“下一步”按钮继续。

Image

图 6-3。 为新项目选择目标平台

Android 应用被打包成具有唯一包名的包。包命名概念和命名约定是从 Java 编程语言中借鉴来的。包名通常使用分层命名模式来定义,层次结构的各个级别用点分隔。虽然 Android 应用代码可能包含多个包,但应该仍然有一个主包供 Android 引用该应用。

作为定义新项目的最后一步,新的 Android 项目向导将要求应用名和唯一的包名。在我们的例子中,包名是com.apress.movieplayer,如图 6-4 中的所示。除了包名之外,这个对话框还要求最小的 SDK。最低 SDK 确定了在 Android 设备上运行该应用所需的最低 API 级别。

Image

图 6-4。 输入应用名称和唯一的包名

在图 6-4 中可以看到,新的 Android 项目向导还可以生成大部分默认组件,比如主活动和单元测试项目,提供足够的骨架代码,使其更快地启动一个新项目。因为我们的电影播放器应用需要一个活动来与用户交互,所以选择 Create Activity 选项。

单击“完成”按钮。新建 Android 项目向导会自动生成项目布局以及所需的项目文件,如图图 6-5 所示。

Image

图 6-5 。项目布局和所需项目文件

将创建以下项目目录和文件:

  • src:该目录包含 Java 源文件。应用包是由新建 Android 项目向导在这个目录中自动生成的。
  • gen:该目录包含自动生成的项目文件,如资源索引的 R 类。用户不应修改该目录的内容。每次编译项目时,都会重新生成该目录的内容。
  • assets:该目录包含应用素材。
  • bin:这个目录包含了这个应用编译后的类文件和可安装的 Android 包文件。用户不应修改该目录的内容。
  • res:该目录包含不同类型应用资源的子目录。新的 Android 项目向导将自动在相应的资源目录中为主活动生成布局、字符串资源和图标。资源组织如下:
    • 动画资源保存在 anim 子目录中。
    • 颜色资源保存在颜色子目录中。
    • 根据目标屏幕分辨率,图像文件保存在相应的可绘制子目录中。
    • 用户界面资源保存在布局子目录中。
    • 菜单资源保存在菜单子目录中。
    • 其他资源(如字符串资源和用户界面样式)保存在 values 子目录中。
  • AndroidManifest.xml:这是应用清单文件。新的 Android 项目向导自动生成这个文件,其中的内容来自通过向导对话框收集的信息。
  • proguard.cfg:这是 ProGuard 配置文件,在对发布版本的应用包进行模糊处理时由 ProGuard 使用。
  • project.properties:这是一个 Android SDK 构建系统在编译和打包应用时使用的属性文件。

使用 ADT 编辑器

ADT 提供了各种编辑器来操作项目文件。在接下来的小节中,我们将使用这些编辑器根据我们的项目需求定制项目框架。

清单编辑器

  • 双击AndroidManifest.xml文件将其打开。ADT 附带了一个用于操作清单文件的定制编辑器。Eclipse 将检测文件的类型并用清单编辑器打开它,如图 6-6 所示。

Image

图 6-6。 安卓清单文件编辑器

清单编辑器提供了一组选项卡,允许操作 Android 清单文件的各个方面。由于用户界面提供了所有可能的值,这使得编辑清单文件变得更容易和更健壮。在任何时候,您都可以切换到 XML 选项卡(AndroidManifest.xml)来处理 XML 源文件。

布局编辑器

Android 应用用户界面是使用基于 XML 的布局文件定义的。对于复杂的用户界面,维护这些 XML 文件是一项非常具有挑战性的任务。ADT 附带了一个用于 Eclipse 的可视化用户界面编辑器插件,它允许您设计和维护布局 XML 文件。

要查看布局编辑器的运行,使用项目浏览器,导航到res目录,然后是layout目录,选择main.xml文件。main.xml文件是我们主活动的布局文件。Eclipse 会自动检测这个文件的类型,并在 ADT 的布局编辑器中打开它,如图图 6-7 所示。代码生成器已经用“Hello World”消息填充了这个布局文件。

Image

图 6-7。 安卓视觉布局编辑器

可视化布局编辑器有三个窗格:

  • 右边的窗格显示了当前的布局,就像在真实的 Android 设备上一样。
  • 顶部窗格提供了一组下拉菜单来更改显示的大小和方向,以便查看布局如何根据这些更改进行自我调整。
  • 左侧窗格包含可用小部件和布局组件的列表。您可以将此窗格中的任何视图组件拖放到右窗格中,以将视图组件添加到当前布局中。

右键单击视图组件显示可用参数列表,您可以更改这些参数。除了提供可视化设计功能,编辑器还允许您直接与底层 XML 格式的布局代码进行交互。要切换到 XML 编辑模式,选择编辑器底部的main.xml选项卡。

现在让我们使用布局编辑器来更改我们的电影播放器应用的布局。

电影列表布局

我们希望让我们的电影播放器应用以列表形式显示电影文件。通过选择main.xml选项卡切换到 XML 编辑器模式,并在清单 6-1 中键入代码。

清单 6-1。main . XML 文件

`


    

`

这个 XML 组件只包含一个全屏的android.widget.ListView。属性,我们将 ID movieListView分配给android.widget.ListView组件。任何视图对象都可以有一个与之相关联的 ID,以便在视图层次结构中唯一地标识它。IDs 允许您在应用代码中引用视图组件。ID 字符串开头的 at 符号(@)表示 XML 解析器应该将其扩展并标识为 ID 资源。at 符号后面的加号(+)表示这是一个新的资源名称,必须添加到 ID 资源中。

现在回到视觉设计模式,看看布局的效果,如图图 6-8 所示。

Image

图 6-8。??【列表视图】添加到布局

电影项目布局

默认情况下,ListView允许您快速将数据显示为文本项。然而,对于我们的电影播放器应用,我们还希望在左侧显示电影缩略图,以便用户更容易做出选择。

要定义这个自定义列表项布局,从顶部菜单栏选择文件 Image 新建 Image 其他,从列表中选择 Android XML 布局文件,如图图 6-9 所示。

Image

图 6-9。 选择新的 Android XML 布局文件

下一步,Android XML 布局文件向导将要求输入文件名和根元素。该布局的文件名将为movie_item.xml。我们希望列表项在左边有缩略图,在右边有电影标题,在标题下面有电影时长。我们能够描述布局的方式强烈表明android.widget.RelativeLayout是项目布局的正确根元素。从列表中选择RelativeLayout,如图图 6-10 所示,然后点击完成按钮。

Image

图 6-10。 选择新的布局根元素

在这个布局中,我们将使用一个android.widget.ImageView视图来显示电影缩略图,使用两个android.widget.TextView视图来显示电影标题和时长。切换到 XML 编辑器模式,在清单 6-2 中键入 XML 代码。

清单 6-2。movie _ item . XML 文件

`

    <TextView
        android:id="@+id/title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignTop="@+id/thumbnail"
        android:layout_toRightOf="@+id/thumbnail"
        android:text="Large Text"
        android:textAppearance="?android:attr/textAppearanceLarge" />

<TextView
        android:id="@+id/duration"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignLeft="@+id/title"
        android:layout_below="@+id/title"
        android:text="Small Text"
        android:textAppearance="?android:attr/textAppearanceSmall" />

`

你现在可以切换到可视化编辑器模式来查看布局的运行,如图图 6-11 所示。

Image

图 6-11: 电影项目添加到布局

您可能已经注意到,在可视化布局编辑器的右上角有一个小小的警告图标。如果你将鼠标悬停在这个图标上,你会看到 Android Lint 警告你这个布局可能存在的问题。点击警告图标,弹出 Android Lint 的警告对话框,如图图 6-12 所示。

Image

图 6-12。 Android Lint 警告对话框显示布局问题

对于前两个错误,Android Lint 告诉我们,我们在 XML 布局文件中使用的字符串是硬编码的,它们应该在字符串资源中。Lint 可以自动为我们修复这些错误,如第五章所述。

选择与硬编码的"Large Text"字符串相关的第一个问题,并单击 Fix 按钮。Lint 将显示提取 Android 字符串对话框以确认提议的更改,如图 6-13 所示。单击“确定”按钮继续。

Image

图 6-13。 Lint 用字符串引用替换硬编码字符串

对与"Small Text"字符串相关的第二个错误重复相同的程序。现在,布局 XML 文件的相关部分将如下所示:

`<TextView
        android:id="@+id/title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignTop="@+id/thumbnail"
        android:layout_toRightOf="@+id/thumbnail"
**        android:text="@string/large_text"**
        android:textAppearance="?android:attr/textAppearanceLarge" />

<TextView
        android:id="@+id/duration"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"         android:layout_alignLeft="@+id/title"
        android:layout_below="@+id/title"
**        android:text="@string/small_text"**
        android:textAppearance="?android:attr/textAppearanceSmall" />`

对于这两个错误,Lint 定义了一个字符串资源,并用相应的字符串资源 ID 替换布局文件中的android:text属性值。在布局文件中不使用任何硬编码的字符串是定义 Android 布局的正确方式。

与其让 Lint 为我们修复第三个错误,不如让我们手动修复它。在布局编辑器中,用字符串引用thumbnail_description定义缩略图的android:contentDescription属性。经过这一更改后,ImageView组件将如下所示:

    <ImageView         android:id="@+id/thumbnail"         android:layout_width="64dp"         android:layout_height="64dp"         android:layout_alignParentLeft="true"         android:layout_alignParentTop="true"         android:layout_marginRight="16dp" **        android:contentDescription=”@string/thumbnail_description”**         android:src="@drawable/ic_launcher"  />

由于字符串资源尚未定义,错误标记将显示在thumbnail_description旁边。我们将使用资源编辑器来定义这个字符串资源。

资源编辑器

Android 应用字符串资源存储在 XML 格式的文件中。ADT 提供了一个自定义编辑器来操作这些资源文件。导航到res目录,然后是values目录,选择strings.xml资源文件。Eclipse 将在自定义编辑器中打开资源文件,如图图 6-14 所示。

Image

图 6-14。 资源编辑

在编辑器的顶部窗格中,您将看到一组字母来过滤资源列表,使其只包含某些类型的元素。通过单击右边的按钮,您可以操作资源列表。在任何时候,通过切换到 XML 选项卡,您可以直接与资源 XML 源文件进行交互。

要定义thumbnail_description字符串资源,请单击添加按钮。在出现的对话框中,选择字符串作为资源类型,如图图 6-15 所示,然后点击确定按钮继续。

Image

图 6-15。 选择资源类型

使用右侧窗格,定义thumbnail_description字符串资源,如图图 6-16 所示。

Image

图 6-16。 定义字符串资源

定义类别

我们已经完成了用户界面和必要资源的定义。我们现在将开始实现必要的模型类来保存将在用户界面中显示的数据。

电影课

对于我们的电影播放器应用,我们需要一个名为Movie的模型类来存储每个电影项目的信息。从顶部菜单栏选择文件 Image 新建 Image 来定义一个新类。Eclipse 将询问类名及其包。将类名字段设置为Movie,将包名设置为com.apress.movieplayer。在编辑器区域,输入清单 6-3 中的 Java 代码(现在不用担心错误)。

清单 6-3。【Movie.java 档案】??

`package com.apress.movieplayer;

/**
 * Movie file meta data.
   * @author Onur Cinar
 /
class Movie {
    /
Movie title. */
    private final String title;

/** Movie file. */
    private final String moviePath;

/** MIME type. */
    private final String mimeType;

/** Movie duration in ms. */
    private final long duration;

/** Thumbnail file. */
    private final String thumbnailPath;

/**
     * Constructor.
     *
     * @param mediaCursor
     *            media cursor.
     * @param thumbnailCursor
     *            thumbnail cursor.
     */
    public Movie(Cursor mediaCursor, Cursor thumbnailCursor) {
        title = mediaCursor.getString(mediaCursor
                .getColumnIndexOrThrow(MediaStore.Video.Media.TITLE));

moviePath = mediaCursor.getString(mediaCursor
                .getColumnIndex(MediaStore.Video.Media.DATA));

mimeType = mediaCursor.getString(mediaCursor
                .getColumnIndex(MediaStore.Video.Media.MIME_TYPE));

duration = mediaCursor.getLong(mediaCursor
                .getColumnIndex(MediaStore.Video.Media.DURATION));

if ((thumbnailCursor != null) && thumbnailCursor.moveToFirst()) {
            thumbnailPath = thumbnailCursor.getString(thumbnailCursor
                    .getColumnIndex(MediaStore.Video.Thumbnails.DATA));
        } else {
            thumbnailPath = null;
        }
    }
}`

这定义了一个新的Movie类,它有五个成员字段:

  • 电影名称
  • 电影文件 URI
  • 电影文件的 MIME 类型
  • 持续时间(毫秒)
  • URI 电影

我们将从android.provider.MediaStore内容提供商那里获取信息,这是一个系统内容提供商,用于向应用提供有关设备上媒体文件的信息。当你在编辑器中输入代码时,你会开始看到来自 Eclipse 的错误标记,指示代码中的错误,如图 6-17 所示。

Image

图 6-17。 Eclipse 代码中指示错误

当您将鼠标悬停在代码中带红色下划线的错误上时,Eclipse 将自动显示 Quick Fix 视图,其中包含修复问题的可能操作的建议。在我们的应用中,问题是我们没有导入所有被引用的类。您可以使用快速修复来手动修复它们,或者在 Windows 和 Linux 上按 Ctrl+O,或者在 Mac OS X 上按 Command+O 来组织和修复所有导入。

为了访问成员字段,我们现在需要定义 getter 和 setter 方法。如第四章所述,我们可以让 Eclipse 自动生成这些 getters 和 setters,如图图 6-18 所示。

Image

图 6-18。 自动生成电影类的 getters 和 setters

现在,Movie类的源代码将看起来像清单 6-4 中的。

清单 6-4。Movie.java 产生后的

`package com.apress.movieplayer;

import android.database.Cursor;
import android.provider.MediaStore;

/**
 * Movie file meta data.
 *
 * @author Onur Cinar
 /
class Movie {
    /
* Movie title. */
    private final String title;

/** Movie file. /
    private final String moviePath; /
* MIME type. */
    private final String mimeType;

/** Movie duration in ms. */
    private final long duration;

/** Thumbnail file. */
    private final String thumbnailPath;

/**
     * Constructor.
     *
     * @param mediaCursor
     *            media cursor.
     * @param thumbnailCursor
     *            thumbnail cursor.
     */
    public Movie(Cursor mediaCursor, Cursor thumbnailCursor) {
        title = mediaCursor.getString(mediaCursor
                .getColumnIndexOrThrow(MediaStore.Video.Media.TITLE));

moviePath = mediaCursor.getString(mediaCursor
                .getColumnIndex(MediaStore.Video.Media.DATA));

mimeType = mediaCursor.getString(mediaCursor
                .getColumnIndex(MediaStore.Video.Media.MIME_TYPE));

duration = mediaCursor.getLong(mediaCursor
                .getColumnIndex(MediaStore.Video.Media.DURATION));

if (thumbnailCursor.moveToFirst()) {
            thumbnailPath = thumbnailCursor.getString(thumbnailCursor
                    .getColumnIndex(MediaStore.Video.Thumbnails.DATA));
        } else {
            thumbnailPath = null;
        }
    }

/**
     * Get the movie title.
     *
     * @return movie title.
     */
    public String getTitle() {
        return title;
    }

/**
     * Gets the movie path.      *
     * @return movie path.
     */
    public String getMoviePath() {
        return moviePath;
    }

/**
     * Gets the MIME type.
     *
     * @return MIME type.
     */
    public String getMimeType() {
        return mimeType;
    }

/**
     * Gets the movie duration.
     *
     * @return movie duration.
     */
    public long getDuration() {
        return duration;
    }

/**
     * Gets the thumbnail path.
     *
     * @return thumbnail path.
     */
    public String getThumbnailPath() {
        return thumbnailPath;
    }

/*
     * (non-Javadoc)
     *
     * @see java.lang.Object#toString()
     */
    @Override
    public String toString() {
        return "Movie [title=" + title + ", moviePath=" + moviePath
                + ", mimeType=" + mimeType + ", duration=" + duration
                + ", thumbnailPath=" + thumbnailPath + "]";
    }
}`

电影列表适配器类

用户界面组件需要一个适配器来使用它的数据。尽管 Android 框架提供了默认适配器,但是由于自定义的项目布局,这些默认适配器在电影播放器应用中是不可用的。

要定义新的适配器类,从顶部菜单栏中选择文件 Image 新建 Image 。将新的类文件命名为MovieListAdapter,同时将其超类设置为android.widget.BaseAdapter,如图图 6-19 所示。

Image

图 6-19。 将超类设置为 BaseAdapter

Eclipse 将自动为每个需要在MovieListAdapter类中实现的抽象方法生成空体。实现这些方法后,MovieListAdapter代码将看起来像清单 6-5 中的。

清单 6-5。【MovieListAdapter.java 档案】??

`package com.apress.movieplayer;

import java.util.ArrayList;

import android.content.Context;
import android.net.Uri;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.TextView;

/**
 * Movie list view adapter.
 *
 * @author Onur Cinar
 /
class MovieListAdapter extends BaseAdapter {
    /
* Context instance. */
    private final Context context;

/** Movie list. */
    private final ArrayList movieList;

/**
     * Constructor.
     *
     * @param context
     *            context instance.
     * @param movieList
     *            movie list.
     */
    public MovieListAdapter(Context context, ArrayList movieList) {
        this.context = context;
        this.movieList = movieList;
    }

/**
     * Gets the number of elements in movie list.
     *
     * @see BaseAdapter#getCount() */
    public int getCount() {
        return movieList.size();
    }

/**
     * Gets the movie item at given position.
     *
     * @param poisition
     *            item position
     * @see BaseAdapter#getItem(int)
     */
    public Object getItem(int position) {
        return movieList.get(position);
    }

/**
     * Gets the movie id at given position.
     *
     * @param position
     *            item position
     * @return movie id
     * @see BaseAdapter#getItemId(int)
     */
    public long getItemId(int position) {
        return position;
    }

/**
     * Gets the item view for given position.
     *
     * @param position
     *            item position.
     * @param convertView
     *            existing view to use.
     * @param parent
     *            parent view.
     */
    public View getView(int position, View convertView, ViewGroup parent) {
        // Check if convert view exists or inflate the layout
        if (convertView == null) {
            LayoutInflater layoutInflater = (LayoutInflater) context
                    .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            convertView = layoutInflater.inflate(R.layout.movie_item, null);
        }

// Get the movie at given position
        Movie movie = (Movie) getItem(position);

// Set thumbnail         ImageView thumbnail = (ImageView) convertView
                .findViewById(R.id.thumbnail);

if (movie.getThumbnailPath() != null) {
            thumbnail.setImageURI(Uri.parse(movie.getThumbnailPath()));
        } else {
            thumbnail.setImageResource(R.drawable.ic_launcher);
        }

// Set title
        TextView title = (TextView) convertView.findViewById(R.id.title);
        title.setText(movie.getTitle());

// Set duration
        TextView duration = (TextView) convertView.findViewById(R.id.duration);
        duration.setText(getDurationAsString(movie.getDuration()));

return convertView;
    }

/**
     * Gets the given duration as string.
     *
     * @param duration
     *            duration value.
     * @return duration string.
     */
    private static String getDurationAsString(long duration) {
        // Calculate milliseconds
        long milliseconds = duration % 1000;
        long seconds = duration / 1000;

// Calculate seconds
        long minutes = seconds / 60;
        seconds %= 60;

// Calculate hours and minutes
        long hours = minutes / 60;
        minutes %= 60;

// Build the duration string
        String durationString = String.format("%1$02d:%2$02d:%3$02d.%4$03d",
                hours, minutes, seconds, milliseconds);

return durationString;
    }
}`

MovieListAdapter构造函数获取一组Movie类,并在android.widget.ListView请求时提供给它们。MovieListAdaptergetView方法使用Movie对象的成员字段填充我们的定制列表项布局。

活动类

现在我们已经满足了所有的先决条件,我们可以开始为 activity 类编写代码了。MoviePlayerActivity将提供android.widget.ListView组件向用户显示电影列表。电影信息将来自android.provider.MediaStore内容提供商。

使用Activity类的managedQuery方法,我们将首先向android.provider.MediaStore查询一组电影信息。对于每部电影,我们将对android.widget.MediaStore进行第二次查询以获得电影缩略图。结果稍后将存储在Movie类实例中,并显示在列表视图中。当您选择一个电影项目时,它将由默认的视频播放器根据其类型播放。将清单 6-6 中的代码输入到MediaPlayerActivity的编辑区。

清单 6-6。【MediaPlayerActivity.java 档案】??

`package com.apress.movieplayer;

import java.util.ArrayList;

import android.app.Activity;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.MediaStore;
import android.util.Log;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ListView;

/**
 * Movie player.
 *
 * @author Onur Cinar
 /
public class MoviePlayerActivity extends Activity implements OnItemClickListener
{
    /
* Log tag. /
    private static final String LOG_TAG = "MoviePlayer"; /
*
     * On create lifecycle method.
     *
     * @param savedInstanceState saved state.
     * @see Activity#onCreate(Bundle)
     */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

ArrayList movieList = new ArrayList();

// Media columns to query
        String[] mediaColumns = { MediaStore.Video.Media._ID,
                MediaStore.Video.Media.TITLE, MediaStore.Video.Media.DURATION,
                MediaStore.Video.Media.DATA,
                MediaStore.Video.Media.MIME_TYPE };

// Thumbnail columns to query
        String[] thumbnailColumns = { MediaStore.Video.Thumbnails.DATA };

// Query external movie content for selected media columns
        Cursor mediaCursor = managedQuery(
                MediaStore.Video.Media.EXTERNAL_CONTENT_URI, mediaColumns,
                null, null, null);

// Loop through media results
        if ((mediaCursor != null) && mediaCursor.moveToFirst()) {
            do {
                // Get the video id
                int id = mediaCursor.getInt(mediaCursor
                        .getColumnIndex(MediaStore.Video.Media._ID));

// Get the thumbnail associated with the video
                Cursor thumbnailCursor = managedQuery(
                        MediaStore.Video.Thumbnails.EXTERNAL_CONTENT_URI,
                        thumbnailColumns, MediaStore.Video.Thumbnails.VIDEO_ID
                                + "=" + id, null, null);

// New movie object from the data
                Movie movie = new Movie(mediaCursor, thumbnailCursor);
                Log.d(LOG_TAG, movie.toString());

// Add to movie list
                movieList.add(movie);

} while (mediaCursor.moveToNext());
        }         // Define movie list adapter
        MovieListAdapter movieListAdapter = new MovieListAdapter(this,
movieList);

// Set list view adapter to movie list adapter
        ListView movieListView = (ListView) findViewById(R.id.movieListView);
        movieListView.setAdapter(movieListAdapter);

// Set  item click listener
        movieListView.setOnItemClickListener(this);
    }

/**
     * On item click listener.
     */
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        // Gets the selected movie
        Movie movie = (Movie) parent.getAdapter().getItem(position);

// Plays the selected movie
        Intent intent = new Intent(Intent.ACTION_VIEW);
        intent.setDataAndType(Uri.parse(movie.getMoviePath()),
movie.getMimeType());
        startActivity(intent);
    }
}`

运行应用

我们的示例应用现在已经可以试用了。你可以在 Android 设备或模拟器上运行它。如果您要在 Android 模拟器中运行电影播放器应用,请确保模拟器配置了第五章中讨论的设置。

电影播放器应用需要一组电影文件存在于外部存储器(SD 卡)中,以便显示任何内容。如果您没有任何电影文件,请在启动应用之前使用相机应用录制一些示例电影文件。

当您准备好测试应用时,从顶部菜单栏中选择运行 Image 运行。因为这是你第一次运行这个应用,Eclipse 会询问你想如何运行它,如图 6-20 所示。

注意:默认情况下,某些 Android 设备通过 USB 连接到主机时,会被配置为充当存储介质。这可能会阻止电影播放器应用访问 SD 卡。使用 USB 设置,将 USB 操作模式更改为仅充电,以防止 SD 卡被锁定。

Image

图 6-20。 运行方式对话框询问应用应该如何运行

从运行方式对话框中选择 Android 应用。如果当前连接了不止一个设备或模拟器,Eclipse 将要求您选择执行应用的目标设备,如图 6-21 所示。

Image

图 6-21。 安卓设备选择器对话框

在对话框中点击确定后,应用将在所选的 Android 设备或仿真器上启动,如图图 6-22 所示。

Image

图 6-22。 电影播放器应用列表电影

您可以从设备上的应用列表中选择电影播放器,再次启动应用。

总结

在本章中,我们开始开发一个电影播放器应用,以熟悉典型的 Android 项目开发周期。我们将前面章节中涉及的一些核心概念和组件付诸实践。我们使用了新的 Android 项目向导、清单编辑器、布局编辑器、Android Lint 和资源编辑器。我们还定义了一个 Android 活动,并从内容提供商那里获取数据。在整个章节中,我们使用了 Eclipse 的代码模板、代码生成器和重构特性来自动化一些开发过程。在接下来的章节中,我们将扩展这个项目来演示 Android 应用开发的其他方面。

七、使用 Eclipse 的 Android 原生开发

在前面的章节中,我们已经探索了使用 Java 开发 Android 应用。Android 软件开发并不局限于只使用 Java 技术。Android 允许应用开发人员通过 Android 原生开发工具包(NDK)使用 C 和 C++ 等原生代码语言实现部分应用。

在这一章中,我们将从介绍 Android NDK 开始,并介绍在主要操作系统上正确安装它的步骤。我们将简要回顾 Android NDK 的结构及其提供的组件。然后我们将讨论 NDK 期望如何构建带有本地组件的 Android 应用。

为了简化开发体验,我们将使用 Eclipse 插件的塞阔雅来将 Android NDK 集成到 Eclipse 平台中。

在为本地开发建立了合适的工作环境之后,我们将开始回顾通过 JDK 提供的用于集成本地组件的工具。我们将重点关注 Java 本地接口(JNI),本地组件使用它来与应用的 Java 部分进行交互。

Android 原生开发套件

Android NDK 是 Android SDK 的配套工具集,旨在允许开发人员使用本机代码实现和嵌入其应用的性能关键部分。尽管 Android 框架完全是为基于 Java 的应用而设计的,但 NDK 提供了必要的工具和组件来使用机器代码生成编程语言(如 C、C++ 和 Assembly)开发部分 Android 应用。通过 JNI 技术,这些本地组件在基于 Java 的应用中无缝运行和访问,同时它们的实现作为机器码运行,不被 Dalvik 虚拟机解释。

什么时候使用原生代码?

使用本机代码并不总是会自动提高性能。尽管众所周知早期版本的 Java 比本地代码慢得多,但最新的 Java 技术经过了高度优化,在许多情况下速度差异可以忽略不计。Java 虚拟机的 JIT 编译特性允许在应用启动时将解释的字节码翻译成机器码。然后,在应用的整个执行过程中使用翻译后的机器码,使 Java 应用的运行速度与本地应用一样快。

在 Java 应用中使用本地组件也会增加整个应用的复杂性。为了有效地与虚拟机并行执行,本机组件应该是好邻居,并以一种微妙的方式与 Java 组件交互。如果没有正确管理这种交互,本机组件可能会导致应用中难以跟踪的不稳定性,甚至会通过使虚拟机崩溃来关闭整个应用。

在 Android 应用中使用原生代码绝对不是一个坏习惯。在某些情况下,它变得非常有益,因为它可以提供重用并提高一些复杂应用的性能。

应用依赖一组模块和库来完成它们的任务。例如,用户界面包含图形和图标来改善用户体验。这些图形资源通常是 PNG 或 JPEG 图像文件。这些格式不是任何编程语言的一部分,因此应用不能直接使用它们。由于开发处理这些格式所需的代码不是一种有效的时间利用,应用依赖于现有的 PNG 或 JPEG 代码库。尽管 Java 很流行,但代码库生态系统仍然高度依赖于基于 C/C++ 的本地代码库。虽然大多数常见的库已经与 Java 或 Android 框架集成,但并不是所有的库都是现成的。

Android NDK 允许应用开发人员轻松地将任何本地库的使用与其基于 Java 的 Android 应用集成在一起。如果没有 NDK,这些本地库需要用 Java 重新编写,以便供 Android 应用使用。Android NDK 促进了 Android 应用中非基于 Java 的组件的重用,并简化了开发过程。

关于性能,作为一种独立于平台的编程语言,Java 不提供任何使用特定于 CPU 的特性来优化代码的机制。与桌面平台相比,移动设备资源非常稀缺。对于具有高性能要求的复杂应用,如 3D 游戏和多媒体应用,有效利用每一个可能的 CPU 功能是关键。ARM 处理器,如 ARM NEON 和 ARM VFPv3-D32,提供了额外的指令集,以允许移动应用对许多性能关键型操作进行硬件加速。Android NDK 允许将应用组件开发为本机代码,以便使用这些 CPU 功能。

NDK 提供了什么?

Android NDK 是一套全面的 API、交叉编译器、连接器、调试器、构建工具、文档和示例应用,允许开发原生 Android 应用组件。它通过提供原生开发特性来补充 Android SDK。以下是它提供的一些原生 Android APIs:

  • c 库
  • 最小标准 C++ 库
  • 数学图书馆
  • zlib 压缩库
  • Android 日志库
  • Android 像素缓冲库
  • Android 本地应用 API
  • OpenGL ES 3D 图形库
  • OpenSL ES 本地音频库
  • OpenMAX AL 最小支持

安装安卓 NDK

Android NDK 可用于主要的操作系统。安装包可从安卓 NDK 网站[developer.android.com/sdk/ndk/index.html](http://developer.android.com/sdk/ndk/index.html)获得。以下部分描述了如何在 Microsoft Windows、Mac OS X 和 Linux 系统上安装 Android NDK。

在微软视窗系统上安装 NDK

Android NDK 最初被设计用于类 UNIX 系统。一些 NDK 组件是外壳脚本,它们不能在 Microsoft Windows 操作系统上直接执行。尽管最新版本的 Android NDK 在使自己更加独立和自我打包方面取得了进展,但它仍然需要在主机上安装 Cygwin 才能完全运行。Cygwin 是一个类似 UNIX 的环境,是 Windows 操作系统的命令行界面。它附带了基本的 UNIX 应用,包括一个允许运行 Android NDK 构建系统的外壳。

在撰写本文时,Android NDK for Windows 的最新版本是 r7b,它要求主机上预装 Cygwin 1.7。

安装 Cygwin

要安装 Cygwin,导航到[www.cygwin.com](http://www.cygwin.com)并点击安装 Cygwin。安装页面将提供到 Cygwin 安装程序的链接,也称为setup.exe应用。Cygwin 不是一个单一的应用;它是一个包含多个应用的大型软件发行版。Cygwin 安装程序只允许将选定的应用安装到主机上。

当你运行 Cygwin 安装程序时,你会看到 Cygwin 设置对话框,如图图 7-1 所示。

Image

图 7-1。 运行 Cygwin 安装程序

点击下一步按钮进入下一步,您需要选择下载源,如图图 7-2 所示。

Image

图 7-2。 选择 Cygwin 下载来源

选择从互联网安装选项,然后单击下一步按钮,指示 Cygwin 安装程序从网络下载软件包。在下一个对话框中,安装程序会让你选择要安装 Cygwin 的目录,如图图 7-3 所示。

Image

图 7-3。 为 Cygwin 选择目标目录

默认情况下,Cygwin 将安装在C:\cygwin目录下,这是推荐的位置。单击“下一步”按钮进入下一步。

Cygwin 安装程序首先将选定的包下载到主机上,然后在下载完所有内容后立即开始安装它们。安装程序会在安装过程中询问该目录的位置,如图图 7-4 所示。

Image

图 7-4。?? 选择本地包目录

由于这个目录的内容在安装后不会被使用,您可以将它指向一个临时位置,比如DownloadsTemp目录。

下一步,安装程序将询问连接类型,如图图 7-5 所示。除非您的网络连接另有要求,否则请选择直接连接,然后单击下一步按钮继续。

Image

图 7-5。?? 选择配置类型

Cygwin 是一个开源项目,世界各地的多个组织通过为 Cygwin 包提供镜像站点来捐赠他们的带宽。根据您的地理位置,从列表中选择一个下载站点,如图图 7-6 所示。然后单击“下一步”按钮继续。

Image

图 7-6。 选择下载站点

安装程序将以树形格式向您显示可用应用列表,如图图 7-7 所示。默认选择适合我们的目的。

Image

图 7-7。 选择小天鹅套餐

Android NDK 需要 GNU Make 3.8.1 或更高版本。要安装 GNU Make,在搜索栏中键入 make ,然后按回车键。安装程序将相应地过滤应用列表。展开开发应用的 Devel 部分,并选择make应用。单击下一步按钮,安装将开始。

安装 Android NDK

Android NDK 是作为 Windows 平台的压缩 ZIP 存档文件提供的。从安卓 NDK 网站下载安装包([developer.android.com/sdk/ndk/index.html](http://developer.android.com/sdk/ndk/index.html))。然后右击它并从上下文菜单中选择提取所有… 。你会看到解压压缩文件夹对话框,如图图 7-8 所示。选择一个目标目录,然后点击解压按钮安装 Android NDK。

Image

图 7-8。??【提取压缩文件夹】对话框

更新路径变量

将 Cygwin 和 Android NDK 添加到Path环境变量中使得 Android NDK 易于访问。要修改Path环境变量,去控制面板选择系统,或者选择开始 Image 运行,然后键入sysdm.cpl

在“系统属性”对话框中,切换到“高级”选项卡,然后单击“环境变量”按钮。在“系统变量”窗格中,单击“编辑”按钮。编辑Path环境变量。Android NDK 和 Cygwin 二进制目录都应该附加到Path变量,如图图 7-9 所示。如果你在安装过程中使用了默认的目标目录,你可以在变量后面加上;c:\cygwin\bin\;c:\android-ndk-r7b\

Image

图 7-9。 添加安卓 NDK 和 Cygwin 目录到路径变量

在 Mac OS X 上安装 NDK

Android NDK 是为 Mac OS X 平台提供的 bzip2 压缩 TAR 文件。从安卓 NDK 网站下载存档文件([developer.android.com/sdk/ndk/index.html](http://developer.android.com/sdk/ndk/index.html))。然后在目的目录下,在终端窗口执行tar jxvf ~/Downloads/android-ndk-r7b-darwin-x86.tar.bz2提取安卓 NDK 文件,如图图 7-10 所示。

Image

图 7-10。 提取安卓 NDK 文件

将 Android NDK 目录添加到Path变量中使其更容易访问。为此,从你解压安卓 NDK 的同一个目录执行echo export PATH=\$PATH:$(pwd)/android-ndk-r7b >> ~/.bashrc,如图图 7-11 所示。

Image

图 7-11。 添加安卓 NDK 目录到路径变量

在 Linux 上安装 NDK

Android NDK 是作为 Linux 平台的 bzip2 压缩 TAR 文件提供的。从安卓 NDK 网站下载存档文件([developer.android.com/sdk/ndk/index.html](http://developer.android.com/sdk/ndk/index.html))。然后在目标目录下,执行 shell 中的tar jxvf android-ndk-r7b-linux-x86.tar.bz2来提取 Android NDK 文件,如图图 7-12 所示。

Image

图 7-12。 提取安卓 NDK 文件

将 Android NDK 目录添加到Path变量中使其更容易访问。为此,从您提取 Android NDK 的同一个目录中执行echo export PATH=$PATH:$(pwd)/android-ndk-r7b >> ~/.bashrc。如图图 7-13 所示。

Image

图 7-13。 添加安卓 NDK 目录到路径变量

安卓 NDK 是如何构造的

在安装过程中,所有的 Android NDK 组件都安装在目标目录下。以下是一些重要的文件和子目录:

  • ndk-build:这个 shell 脚本是 Android NDK 构建系统的起点。它在 Android 应用目录中执行,并管理 Android 应用本机部分的构建过程。
  • 这个 shell 脚本允许使用 GNU 调试器调试本地组件。启动时,它建立设备和 GNU 调试器之间的通信。
  • 这个 shell 脚本有助于分析本地组件崩溃时产生的堆栈跟踪。它分析给定的堆栈跟踪,并将地址映射到源代码文件和行号。我们将在本章的后面进行实验。
  • 这个目录包含了整个 Android NDK 构建系统的模块。开发人员不应该直接与这些文件进行交互。
  • platforms:这个目录包含每个 Android 目标版本的头文件和库。Android NDK 构建系统会自动使用这些文件。
  • 这个目录包含了演示 Android NDK 所提供功能的示例应用。这些示例项目对于学习如何使用 Android NDK 提供的功能非常有用。
  • 这个目录包含了开发者可以导入到他们现有的 Android NDK 项目中的附加模块。
  • 这个目录包含了 Android NDK 目前支持的不同目标机器架构的交叉编译器。Android NDK 构建系统使用基于所选目标架构的交叉编译器。

原生项目是如何构建的

本地组件与基于 Java 的 Android 应用共享相同的项目目录。以下是重要文件和目录的列表:

  • jni:这个子目录保存本地组件的 C/C++ 头文件和源文件。

  • jni/Android.mk:这是描述本地项目的构建文件。它包含要编译的源文件列表和要链接的库。它在构建过程中被导入到主Makefile中。内容如下:`# Stores the current directory
    LOCAL_PATH := $(call my-dir)

    Clears the build variables

    include $(CLEAR_VARS)

    Native components get compiled into modules

    LOCAL_MODULE := hello-jni

    Native code source files

    LOCAL_SRC_FILES := hello-jni.c
    LOCAL_SRC_FILES += test1.c test2.c

    Builds a shared library for this module

    include $(BUILD_SHARED_LIBRARY)`

  • jni/Application.mk:这是一个可选的全局构建文件,它指定了将要构建的本地模块以及所有应用模块的公共配置标志列表。内容如下:`# Defines which modules to build; otherwise

    all modules are built

    APP_MODULES := hello-jni

    Alters the optimization level for building

    either in release or debug mode

    APP_OPTIM := release

    Defines which target machine architectures

    to build for

    APP_ABI := armeabi armeabi-v7a

    Compiler flags for all modules

    APP_CFLAGS := -I/opt/module`

  • libs:这个子目录是构建过程的结果。它被分成一个或多个子目录,这取决于目标机器的体系结构。这些子目录包含编译后的共享库,其中包含本地组件。当 Android SDK 将应用打包成可安装的 APK 文件时,会自动创建libs子目录。

  • obj:这个子目录是构建过程的结果。它包含每个源文件的编译后的目标文件以及共享库的调试版本。

塞阔雅为月食

Eclipse 的 ADT 插件只处理 Android 应用的 Java 部分。它不会自动处理原生组件,而是依赖 Android 开发人员提前手动编译它们。Eclipse 的塞阔雅插件简化了这个过程。

塞阔雅是一个开源的 Eclipse 插件项目,旨在提供一个基于 Eclipse 平台的完整的移动开发环境。为了提供一个完整的环境,塞阔雅继承了许多其他 Eclipse 项目的组件,比如 Mobile Linux 工具(TmL)、Mobile Tools for Java (MTJ)和 Pulsar。塞阔雅最显著的特点是它能够在现有的 Android 项目中添加 Android 原生代码支持。

安装塞阔雅

塞阔雅可以通过 Eclipse 插件存储库获得。启动 Eclipse,从顶部菜单栏选择帮助 Image 安装新软件……,启动安装向导。对于“使用”字段,选择 Indigo 存储库。在 Work with 字段下面的过滤文本字段中键入塞阔雅,Eclipse 将过滤可用插件的列表。展开移动和设备开发类别,选择塞阔雅 Android 原生代码支持,如图 7-14 所示。单击“下一步”按钮继续。

Image

图 7-14。 安装塞阔雅插件

塞阔雅插件依赖于 C/C++ 开发工具(CDT)来运行。CDT 提供了基于 Eclipse 平台的全功能 C/C++ 集成开发环境。安装向导将显示依赖项列表,如图图 7-15 所示。单击“下一步”按钮继续安装。

Image

图 7-15。 安装 C/C++ 开发工具

Eclipse 将显示所选插件的许可协议。接受许可协议,然后单击“完成”按钮开始安装。安装完成后,您需要重启 Eclipse。

配置塞阔雅

塞阔雅需要知道安卓 NDK 的安装位置才能运行。在 Windows 和 Linux 上选择窗口 Image 首选项,或者在 Mac OS X 上选择 Eclipse Image 首选项,启动首选项对话框。在首选项对话框中,展开 Android 类别,然后选择原生开发。点击浏览按钮,选择 NDK 位置,如图图 7-16 所示。

Image

图 7-16。 设定 NDK 地点

增加本地代码支持

为了验证塞阔雅配置,我们将通过 Eclipse 构建一个 Android NDK 示例应用。我们将使用 Hello JNI,这是一个简单的 Android NDK 应用,它从一个共享库中实现的本地方法加载一个字符串,并将其显示在应用的用户界面中。

从顶部菜单栏选择文件 Image 新建 Image ** Android 项目**,启动新建 Android 项目向导。将项目命名为HelloJni,并选择“从现有源代码创建项目”选项。然后点击浏览按钮,选择<*NDK Directory*>\samples\hello-jni作为位置,如图图 7-17 所示。单击“下一步”按钮继续。

Image

图 7-17。 为 NDK 样本应用启动一个新项目

新 Android 项目向导将询问目标 Android 版本。Android NDK 支持 Android 1.5 及更高版本。由于 Android 2.3.3 是我们的首选平台,因此选择 Android 2.3.3 作为新项目的 SDK 目标,并单击 Finish 按钮将示例 Android 项目添加到 Eclipse 中。

在导入项目时,您可能会看到一条错误消息,提示“无法解析目标‘Android-8’。”这是由于 ADT 插件的当前版本存在缺陷。自 ADT 版本 14 起,项目属性文件已从default.properties重命名为project.properties。当项目通过 Eclipse 导入时,ADT 插件生成了project.properties文件,但是也保留了default.properties文件,混淆了构建系统。使用包浏览器,打开default.propertiesproject.properties文件,并将目标属性的值从default.properties文件复制到project.properties文件。使用 Package Explorer,右键单击default.properties文件并从上下文菜单中选择 Delete

虽然示例项目包含本机代码,但 ADT 将无法构建它。您需要首先向项目添加本机代码支持,以允许塞阔雅在 Android 应用构建过程中构建本机代码。右键点击项目,在右键菜单中选择 Android Tools Image 添加原生支持,如图图 7-18 所示。

Image

图 7-18。 选择给一个 Android 项目添加原生支持

Eclipse 显示添加 Android 原生支持对话框,如图图 7-19 所示。该对话框中最重要的字段是编译本机代码后将生成的共享对象库的名称。Android NDK 将本地代码打包在共享库中,由 Java 应用在运行时加载。虽然对话框只有一个共享库字段,但是一个 Android 应用可以定义多个共享库。我们将在本章的后面重新审视 Android NDK 构建系统的内部。单击“完成”按钮将本机支持添加到项目中。

Image

图 7-19。 添加 Android 原生支持对话框

用本地组件建造

使用原生组件构建 Android 应用的过程与构建普通的基于 Java 的 Android 应用的过程是相同的。塞阔雅自动将必要的构建步骤注入到流程中。Android 应用编译完成后,控制台视图会显示与 Android NDK 相关的日志消息,如图图 7-20 所示。如果出现错误,这些消息会被自动解析,并通过 Problems 视图呈现给开发人员。

Image

图 7-20。 控制台视图显示安卓 NDK 日志消息

在这个阶段,您可以在设备上或使用模拟器运行应用。

我们构建环境现在可以进行本地开发了。在接下来的小节中,我们将探索一些可以促进本地开发的 Java 工具。

Java 工具

两个 Java 工具常用于原生开发:javahjavap。这些工具是 JDK 的一部分,作为命令行可执行文件提供。在这一节中,我们将探索它们的功能,并将它们集成到 Eclipse 中,以便在开发过程中简化它们的使用。

首先,我们需要定义一个变量,该变量允许我们在定义外部工具时指向 Android 框架 JAR 文件。在 Eclipse 中,在 Windows 和 Linux 上选择窗口 Image 首选项,或者在 Mac OS X 上选择 Eclipse Image 首选项,打开首选项对话框。要过滤列表,输入字符串替换,如图图 7-21 所示。

Image

图 7-21。 设置字符串替换

单击“新建”按钮定义新变量。在“新字符串替换变量”对话框中,将变量名设置为android_jar。对于值设置,使用浏览按钮导航到 Android SDK(不是 NDK)安装目录中的platforms子目录。目录列表取决于您安装的平台。选择最高的平台。如果在 Windows 主机上运行,则在值前加上\android.jar,对于 Mac OS X 和 Linux 系统,则加上/android.jar,如图图 7-22 所示。单击“确定”按钮关闭对话框。

Image

图 7-22。 添加字符串替代变量

我们现在准备开始将javahjavap工具与 Eclipse 集成开发环境集成。

C 头文件和存根文件生成器:javah

javah工具生成实现本地方法所需的 C 头文件和源文件。它获取编译后的类文件,并对它们进行本地方法解析,然后生成必要的头文件和源文件。虽然这可以在不使用javah工具的情况下实现,但它使这个过程更加健壮和容易。它是本机开发中最常用的工具之一。

为了简化javah的使用,我们将使用 Eclipse 定义一个新的外部工具。从顶部菜单栏选择运行 Image 外部工具 Image 外部工具配置… 。在“外部工具配置”对话框中,选择“程序”,然后单击“新建启动配置”按钮。如下填写工具信息,如图 7-23 所示:

  • 名称:设置名称为javah
  • 位置:将位置设置为${system_path:javah},这样 Eclipse 就可以使用系统路径提取到javah工具的完整路径。
  • 工作目录:将工作目录设置为${project_loc},这是项目的根目录。
  • 参数:设置参数为-verbose -jni -classpath "${project_loc}/bin/classes;${android_jar}" -d "${project_loc}/jni" ${java_type_name}。在 Mac OS X 和 Linux 系统上,用冒号而不是分号来分隔类路径。

Image

图 7-23。 定义 javah 外部工具

单击应用保存外部工具定义。

要使用javah外部工具,选择一个具有本地方法的类文件,并从顶部菜单栏中选择运行 Image 外部工具 Image javah 。Eclipse 将首先构建项目,以确保类文件是最新的。然后,javah工具将在jni子目录中生成 C 头文件。如果您喜欢让javah也生成存根 C 源文件,请更改外部工具定义,并将–stub添加到参数中。

Java 类文件反汇编器:javap

javap工具为请求的信息反汇编给定的编译类文件。在本地开发过程中经常使用它来轻松提取适当的字段和方法签名。

javah一样,我们将使用 Eclipse 为javap定义一个新的外部工具。从顶部菜单栏选择运行 Image 外部工具 Image 外部工具配置。在外部工具配置对话框中,选择程序并单击新建启动配置按钮。如下填写工具信息,如图图 7-24 所示:

  • 名称:设置名称为javap
  • 位置:将位置设置为${system_path:javap},这样 Eclipse 就可以使用系统路径提取到javap工具的完整路径。
  • 工作目录:将工作目录设置为${project_loc},这是项目的根目录。
  • 参数:设置参数为-classpath "${project_loc}/bin/classes;${android_jar}" -p -s ${java_type_name}。在 Mac OS X 和 Linux 系统上,用冒号替换分号来分隔类路径。

Image

图 7-24。 定义 javap 外部工具

要使用javap外部工具,选择一个带有本地方法的类文件,并从顶部菜单栏中选择运行 Image 外部工具 Image javap 。Eclipse 将首先构建项目,以确保类文件是最新的。然后javap工具将解析编译后的 Java 类,并将字段和方法签名输出到控制台视图,如图图 7-25 所示。

Image

图 7-25。 一些显示字段和方法签名的 javap 输出

这两个 Java 工具通过自动生成本机文件的存根代码以及字段和方法签名来帮助开发人员。在下一节中,我们将开始探索 JNI,我们将在编码这些存根函数的实际实现时使用它。

Java 原生接口

JNI 是 Java 编程语言的一个强大特性。它允许 Java 类的某些方法在本地实现,并且仍然像普通的 Java 方法一样被调用和使用。Android NDK 提供特定于平台的功能,并依靠 JNI 技术将本机代码粘合到 Java 应用上。

一个简单的 JNI 例子

在深入研究 JNI 技术的细节之前,我们将浏览一个 JNI 的示例应用。我们将从一个简单的 Hello World 应用开始。

`public class HelloWorldActivity extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

TextView textView = new TextView(this);
        textView.setText(sayHelloWorld());
        setContentView(textView);
    }     private String sayHelloWorld() {
        return "Hello World!";
    }
}`

HelloWorldActivity类包含一个方法sayHelloWorld,该方法在被调用时返回"Hello World!"消息。对于这个例子,我们将使用 C/C++ 在本地实现sayHelloWorld方法。为此,我们需要首先移除方法体,然后将native关键字添加到方法签名中。

    private native String sayHelloWorld();

native关键字表明该方法是本机实现的。虽然虚拟机现在知道该方法是本机实现的,但它仍然不知道在哪里可以找到该实现。

如前所述,本地方法被编译到一个共享库中。需要首先加载这个共享库,以便虚拟机找到本机方法实现。java.lang.System类为 Java 应用提供了在运行时加载共享库的loadLibrary方法。假设本地方法被编译成一个名为libHelloWorld.so的共享库,下面的方法调用应该被添加到代码中。

    static {         System.loadLibrary(“HelloWorld”);     }

static上下文中调用loadLibrary方法,因为我们希望在虚拟机的生命周期中只加载一次。在完成这一更改后,示例应用的 Java 部分就完成了。

`public class HelloWorldActivity extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

TextView textView = new TextView(this);
        textView.setText(sayHelloWorld());
        setContentView(textView);
    }

private native String sayHelloWorld();

static {
        System.loadLibrary("HelloWorld");
    }
}`

为了开始编写 C/C++ 代码,我们首先需要为sayHelloWorld方法生成函数签名。我们将使用本章前面介绍的javah工具来生成 C/C++ 头文件和源文件。调用javah工具产生头文件com_apress_HelloWorldActivity.h,内容如下。

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

include <jni.h>

/* Header for class com_apress_HelloWorldActivity */

ifndef _Included_com_apress_HelloWorldActivity

define _Included_com_apress_HelloWorldActivity

ifdef __cplusplus

extern "C" {

endif

/*
 * Class:     com_apress_HelloWorldActivity
 * Method:    sayHelloWorld
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_apress_HelloWorldActivity_sayHelloWorld
  (JNIEnv *, jobject);

ifdef __cplusplus

}

endif

endif`

头文件首先包括jni.h头文件。该头文件包含 JNI 数据类型和函数的定义。

头文件还将HelloWorldActivity类的sayHelloWorld方法映射到Java_com_apress_HelloWorldActivity_sayHelloWorld本地函数。这种显式函数命名允许虚拟机自动在加载的共享库中找到本机函数。虽然 Java 方法sayHelloWorld不带任何参数,但是本地函数带两个参数。第一个参数JNIEnv是一个接口指针,指向可用 JNI 函数的函数表。第二个参数是对HelloWorldActivity类实例的 Java 对象引用。每个本地函数调用总是提供有JNIEnv接口指针。第二个参数可以是成员方法的对象引用,也可以是静态方法的类引用。

使用自动生成的头文件,我们将在 C/C++ 源文件中提供本机实现。

#include "com_apress_HelloWorldActivity.h" jstring Java_com_apress_HelloWorldActivity_sayHelloWorld(JNIEnv* pEnv, jobject thiz) {     return (*env)->NewStringUTF(env, “Hello World!”); }

从代码中可以看出,我们不能直接原样返回 C 字符串"Hello World!",因为 Java 不知道如何处理它。使用来自JNIEnv接口的NewStringUTF函数,C 字符串被转换成 Java String引用。

C/C++ 源代码编译成共享库后,应用就准备好了。我们不会深入编译的细节,因为这将由 Android NDK 通过 Eclipse 自动处理。

数据类型

Java 中有两种数据类型:

  • 原始类型,如booleanbytecharshortintlongfloatdouble
  • 引用类型,如String、数组和其他类

让我们仔细看看这些数据类型。

原始类型

基元类型直接映射到 C/C++ 的等价类型。JNI 使用类型定义使这种映射对开发人员透明。例如,Java int类型被映射到jni.h头文件中的jint,如下所示:

typedef    int    jint;    /* signed 32 bits */

表 7-1 显示了原始类型映射和类型大小。

Image

Image

参考类型

JNI 以不同的方式处理引用类型。它们作为对本机方法的不透明引用传递。本机代码只能通过JNIEnv接口提供的函数集来交互和操作引用类型。它们的内部数据结构不会直接暴露给本机代码。参考类型映射如表 7-2 所示。

Image

Image

字符串操作

Java 字符串由 JNI 作为引用类型来处理。Java 字符串不能直接转换为本机 C 字符串。JNI 提供了在 Java 和原生字符串之间转换的必要函数。这些函数可以处理 Unicode 和 UTF 8 编码的字符串。在内存溢出的情况下,这些函数返回NULL通知本机代码,虚拟机中已经抛出异常,本机代码不应继续运行。

const jbyte* str; str = (*env)->GetStringUTFChars(env, javaString, NULL); if (0 != str) {     printf(“Java string: %s”, str); }

通过 JNI 函数获得的字符串需要在本机代码使用完之后被正确释放,否则会发生内存泄漏。要使用的正确释放函数取决于用来获取字符串的函数。

(*env)->ReleaseStringUTFChars(env, javaString, str);

还可以使用新的字符串函数从本机代码构造新的字符串实例。

jstring javaString; javaString = (*env)->NewStringUTF(env, "Hello World!");

阵列操作

Java 数组由 JNI 作为引用类型来处理。JNI 提供了访问和操作 Java 数组的必要函数。提供两种类型的数组函数:Get<*Type*>ArrayRegionGet<*Type*>ArrayElements

Get<*Type*>ArrayRegion函数将给定的原始 Java 数组复制到给定的 C 数组。

jint nativeArray[10]; (*env)->GetIntArrayRegion(env, javaArray, 0, 10, nativeArray);

Get<*Type*>ArrayElements函数允许本机代码获得指向数组元素的直接指针,但它要求本机代码在结束时释放这些指针。

`jint* nativeDirectArray;
nativeDirectArray = (*env)->GetIntArrayElements(env, javaArray, NULL);
if (0 != nativeDirectArray) {

(*env)->ReleaseIntArrayElements(env, javaArray, nativeDirectArray, 0);
}`

还可以使用New<*Type*>Array函数从本机代码构建新的数组实例。

jintArray javaArray; javaArray = (*env)->NewIntArray(env, 10); if (0 != javaArray) {     (*env)->SetIntArrayRegion(env, javaArray, 0, 10, nativeArray); }

访问字段

Java 有两种类型的字段:实例字段和静态字段。一个类的每个实例拥有它的实例字段的副本,而一个类的所有实例共享相同的静态字段。

JNI 提供了访问这两种字段类型的函数。以下是具有一个静态字段和一个实例字段的 Java 类的示例:

`public class JavaClass {
    /** Instance field */
    private String instanceField = "Instance Field";

/** Static field */
    private static String staticField = "Static Field";

/**
     * Access fields native method.
     */
    private native void accessFields();

...
}`

accessFields方法是一个本地方法,对于本例,它将具有以下签名:

void Java_com_apress_JavaClass_accessFields(JNIEnv* env, jobject instance) {

JNIEnv接口指针和对象实例由虚拟机提供给被调用的本地函数。JNI 通过字段 id 提供对这两种类型字段的访问。您可以通过给定实例的类对象获得字段 id。类对象是通过GetObjectClass函数获得的。

jclass clazz; … clazz = (*env)->GetObjectClass(env, instance);

根据字段类型,有两个函数可以从类中获取字段 ID:GetFieldId函数用于实例字段,而GetStaticFieldId用于静态字段。这两个函数都以jfieldID类型返回字段 ID。

`jfieldID instanceFieldId;
jfieldID staticFieldId;

instanceFieldId = (*env)->GetFieldID(env, clazz, “instanceField”,
“Ljava/lang/String;”);

staticFieldId = (*env)->GetStaticFieldID(env, clazz, “staticField”,
“Ljava/lang/String;”);`

这两个函数的最后一个参数采用表示 Java 中字段类型的字段描述符。在示例代码中,"Ljava/lang/String"表示字段类型是一个String

JNI 遵循字段描述符的特定格式。从现有的类文件中提取字段描述符最简单的方法是通过本章前面介绍的javap工具。来自javap的输出将显示类文件中每个字段和方法的签名。

`public class com.apress.JavaClass {
  private static java.lang.String staticField;
    Signature: Ljava/lang/String;
  private java.lang.String instanceField;
    Signature: Ljava/lang/String;
  static {};
    Signature: ()V

public com.apress.JavaClass();
    Signature: ()V

private native void accessFields();
    Signature: ()V
}`

获取字段 ID 后,可以通过Get<*Type*>Field函数获取实例字段的实际字段,或者通过GetStatic<*Type*>Field函数获取静态字段的实际字段。

jstring instanceField; jstring staticField; … instanceField = (*env)->GetObjectField(env, instance, instanceFieldId); staticField = (*env)->GetStaticObjectField(env, clazz, staticFieldId);

在内存溢出的情况下,这两个函数都可以返回NULL,本机代码应该不会继续执行。为了提高应用性能,可以缓存字段 id。

调用方法

与字段一样,Java 中有两种类型的方法:实例方法和静态方法。JNI 提供了访问这两种类型的函数。下面是一个包含一个静态方法和一个实例方法的 Java 类。

`public class JavaClass {
    /**
     * Instance method.
     */
    private String instanceMethod() {
        return "Instance Method";
    }

/**
     * Static method.
     */
    private static String staticMethod() {
        return "Static Method";
    }

/**
     * Access methods native method.
     */
    private native void accessMethods();

...
}`

accessMethods方法是一个本地方法,在这个例子中有如下签名:

void Java_com_apress_JavaClass_accessMethods(JNIEnv* env, jobject instance) {

JNIEnv接口指针和对象实例由虚拟机提供给被调用的原生函数。JNI 通过方法 id 提供对这两种方法的访问。

您可以通过给定实例的类对象获得方法 id。使用GetMethodID函数获取实例方法的方法 ID,或者使用GetStaticMethodID函数获取静态字段的方法 ID。这两个函数都以jmethodID类型返回方法 ID。

jmethodID instanceMethodId; jmethodID staticMethodId; … instanceMethodId = (*env)->GetMethodID(env, clazz, “instanceMethod”, “()Ljava/lang/String;”); staticMethodId = (*env)->GetStaticMethodID(env, clazz, “staticMethod”, “()Ljava/lang/String;”);

与字段函数一样,这两个函数的最后一个参数采用表示 Java 方法签名的方法描述符。方法签名可以通过javap工具获得。来自javap的输出将显示类文件中每个字段和方法的签名。

`public class com.apress.JavaClass {
  public com.apress.JavaClass();
    Signature: ()V

private java.lang.String instanceMethod();
    Signature: ()Ljava/lang/String;

private static java.lang.String staticMethod();
    Signature: ()Ljava/lang/String;

private native void accessMethods();
    Signature: ()V
}`

使用方法 ID,您可以通过实例方法的Call<*Type*>Method函数或静态方法的CallStatic<*Type*>Field函数调用实际的方法。

jstring instanceMethodResult; jstring staticMethodResult; … instanceMethodResult = (*env)->CallStringMethod(env, instance, instanceMethodId); staticMethodResult = (*env)->CallStaticStringMethod(env, clazz, staticMethodId);

在内存溢出的情况下,这两个函数都可以返回NULL,并且本机代码不应该继续执行。为了提高应用性能,可以缓存方法 id。

异常处理

异常处理是 Java 编程语言的一个重要方面。异常在 JNI 中的行为与在 Java 中不同。

当虚拟机中出现异常时,控制权会自动转移到与异常类型匹配的最近的try/catch语句。然后,虚拟机清除异常并执行异常处理程序。相反,JNI 要求开发人员在异常发生后显式实现异常处理流程。

JNIEnv接口提供了一组与异常相关的函数。为了查看这些函数的运行情况,我们将使用下面的 Java 类作为示例。

`public class JavaClass {
    /**
     * Throwing method.
     */
    private void throwingMethod() throws NullPointerException {
        throw new NullPointerException("Null pointer");
    }

/**
     * Access methods native method.
     */
    private native void accessMethods();
}`

在调用throwingMethod方法时,accessMethods本地方法需要显式地进行异常处理。JNI 提供了ExceptionOccurred函数来查询虚拟机是否有挂起的异常。异常处理程序需要在结束后使用ExceptionClear函数显式清除异常。

`jthrowable ex;

(env)->CallVoidMethod(env, instance, throwingMethodId);
ex = (
env)->ExceptionOccurred(env);
if (0 != ex) {
    (*env)->ExceptionClear(env);

/* Exception handler. */
}`

JNI 也允许本机代码抛出异常。由于异常是 Java 类,所以应该首先使用FindClass函数获得异常类,并且可以使用ThrowNew函数来发起并抛出新的异常。

jclass clazz; … clazz = (*env)->FindClass(env, “java/lang/NullPointerException”); if (0 != clazz) {     (*env)->ThrowNew(env, clazz, “Exception message.”); }

本地和全球参考

引用在 Java 编程中起着重要的作用。虚拟机通过跟踪类实例的引用并对不再被引用的实例进行垃圾收集来管理类实例的生命周期。由于本机代码不是托管环境,JNI 提供了一组函数来允许本机代码显式管理对象引用和生存期。JNI 支持三种类型的引用:局部引用、全局引用和弱全局引用,如以下部分所述。

本地参考

大多数 JNI 函数返回局部引用。本地引用不能被缓存并在后续调用中重用,因为它们的生存期仅限于本机方法。一旦本地函数返回,本地引用就会被释放。例如,FindClass函数返回一个本地引用;当本机方法返回时,它会自动释放。本机代码也可以通过DeleteLocalRef函数显式释放。

jclass clazz; … clazz = (*env)->FindClass(env, “java/lang/String”); … (*env)->DeleteLocalRef(env, clazz); …

当在单个方法调用中执行多个内存密集型操作时,这变得非常方便。

全局引用和弱全局引用

全局引用在本机方法的后续调用中保持有效,直到它们被本机代码显式释放。可通过NewGlobalRef功能从局部参考启动全局参考。

jclass localClazz; jclass globalClazz; … localClazz = (*env)->FindClass(env, “java/lang/String”); globalClazz = (*env)->NewGlobalRef(env, localClazz); … (*env)->DeleteLocalRef(env, localClazz);

当本地代码不再需要全局引用时,您可以通过DeleteGlobalRef函数随时释放它:

(*env)->DeleteGlobalRef(env, globalClazz);

全局引用的另一种形式是弱全局引用。像全局引用一样,弱全局引用在本地方法的后续调用中仍然有效。与全局引用不同,弱全局引用不会阻止底层对象被垃圾回收。弱全局引用可以使用NewWeakGlobalRef函数启动。

jclass weakGlobalClazz; … weakGlobalClazz = (*env)->NewWeakGlobalRef(env, localClazz);

要确定弱全局引用是否仍然指向一个活动的类实例,可以使用IsSameObject函数:

if (JNI_FALSE == (*env)->IsSameObject(env, weakGlobalClazz, NULL)) {     /* Object is still live and can be used. */ } else {     /* Object is garbage collected and cannot be used. */ }

使用DeleteWeakGlobalRef函数可以随时释放弱全局引用。

(*env)->DeleteWeakGlobalRef(env, weakGlobalClazz);

穿线

虚拟机支持将本机代码作为多线程环境的一部分运行。在开发本地组件时,需要记住 JNI 技术的某些限制:

  • 局部引用仅在执行本机方法期间和正在执行本机方法的线程上下文中有效。本地引用不能在多个线程之间共享。只有全局引用可以被多个线程共享。
  • 传入每个本机方法调用的JNIEnv接口指针在与方法调用相关的线程中也是有效的。它不能被其他线程缓存和使用。
同步

同步是多线程编程的一个重要方面。类似于 Java 的同步块,JNI 的监视器允许本机代码使用 Java 对象进行同步。虚拟机保证获得监视器的线程安全执行,而其他线程等待,直到被监视的对象变得可用。Java 应用中的同步块如下所示:

synchronized(obj) {     /* Synchronized thread-safe code block. */ }

使用 JNI 监控方法也可以达到同样的效果:

`if (JNI_OK == (env)->MonitorEnter(env, obj)) {
    /
Error handling. */
}

/* Synchronized thread-safe code block. */

if (JNI_OK == (env)->MonitorExit(env, obj)) {
    /
Error handling. */
}`

MonitorEnter函数的调用应该与对MonitorExit的调用相匹配,以防止代码中的死锁。

本机线程

正如本章前面提到的,JNI 主要用于将本地库和模块集成到 Java 应用中。这些本地组件可能已经在使用本地线程来并行执行某些任务。因为虚拟机不知道这些本地线程,所以它们不能直接与 Java 组件通信。本机线程应该首先连接到虚拟机,以便与应用的其余部分进行交互。

JNI 提供了AttachCurrentThread函数,允许本机代码将本机线程附加到虚拟机。

`JavaVM* cachedJvm;

JNIEnv* env;

/* Attach the current thread to virtual machine. /
(
cachedJvm)->AttachCurrentThread(cachedJvm, &env, NULL);

/* Thread can communicate with the Java application using the JNIEnv interface.
*/

/* Detach the current thread from virtual machine. /
(
cachedJvm)->DetachCurrentThread(cachedJvm);`

故障排除

运行在设备上的本机代码比 Java 代码更难排除故障。在本节中,我们将回顾可用于简化故障诊断过程的 Android NDK 工具。

从本机代码记录日志

排除本机代码故障的最简单方法是正确记录应用状态和事件。Android NDK 支持两种类型的日志记录机制:特定于 Android 的日志记录和控制台日志记录。

Android 专用日志记录

NDK 提供了两个 Android 特有的日志功能,允许原生组件在 Android 系统日志中记录消息:__android_log_print__android_log_write。然后可以通过 DDMS 的 LogCat 视图查看这些消息。要使用这些日志功能,源文件中应该包含android/log.h头文件。

#include <android/log.h>

本机组件可以通过调用这些函数随时将消息记录到系统日志中。

__android_log_write(ANDROID_LOG_INFO, “NativeCode”, “Info message.”);

除了头文件之外,在构建共享库时也应该链接日志库。这需要更新 jni/ Android.mk文件。

LOCAL_LDLIBS := -llog

应用会将消息记录到 Android 系统日志中,这些消息会出现在 LogCat 视图中。

控制台日志记录

当将现有的库和模块集成到一个 Android 应用项目中时,将它们的日志记录机制更改为 Android 特定的日志记录可能是不可能的。大多数日志记录机制要么将消息记录到文件中,要么直接记录到控制台中。

默认情况下,控制台文件描述符stdoutstderr在 Android 平台上不可见。要将这些日志消息重定向到 Android 系统日志,请在 Windows 上打开命令提示符,或者在 Linux 和 Mac OS X 上打开终端窗口,并执行以下命令:

$ adb shell stop $ adb shell setprop log.redirect-stdio true $ adb shell start

系统会保留此设置,直到设备重新启动。如果您想将这些设置设为默认设置,请将它们添加到设备或仿真器上的/data/local.prop文件中。

调试本机代码

本机组件可以使用 GNU 调试器(GDB)进行调试。Android NDK 提供了一个名为ndk-gdb的 shell 脚本来建立应用和 GDB 之间的通信。

GDB 在文本模式下提供了一个广泛的调试环境。在这一节中,我们将把ndk-gdb粘合到 Eclipse 平台上,以便简化调试过程。

在建立ndk-gdb调试会话之前,应用本身应该在其AndroidManifest.xml文件中被定义为可调试的。为此,使用包浏览器,打开AndroidManifest.xml文件,并在清单编辑器中,切换到应用选项卡。清单编辑器提供了一个基于表单的界面来操作 Android 清单文件。使用下拉菜单,将可调试属性设置为真,如图图 7-26 所示。

Image

图 7-26。在 AndroidManifest.xml 文件中设置可调试属性

进行此更改后,重新构建应用,并将其再次部署到目标设备或模拟器。ndk-gdb工具期望应用已经部署在平台上。您可以通过 Eclipse 启动应用,让它自动部署,或者您可以依靠adb命令行工具来手动安装 APK 文件。

使用 ndk-gdb 进行文本模式调试

要配置 Eclipse 直接从集成开发环境调用ndk-gdb工具,从 Eclipse 中,从顶部菜单栏选择运行外部工具 外部工具配置。在外部工具配置对话框中,选择程序并单击新建启动配置按钮。如下填写工具信息,如图 7-27 所示:

  • 名称:设置名称为ndk-gdb
  • 位置:在基于 Windows 的主机上,将位置设置为c:\cygwin\bin\bash.exe。在基于 Mac OS X 和 Linux 的主机上,将位置设置为/bin/bash
  • 工作目录:将工作目录设置为${project_loc},这是项目的根目录。
  • 参数:如果使用的是 Windows 主机,将参数设置为-c "/cygdrive/c/android-ndk-r7b/ndk-gdb --start"。默认情况下,ndk-gdb工具试图连接到应用的一个现有运行实例。--start参数在建立调试会话之前显式启动应用。它将启动应用包中第一个可启动的活动。要启动特定的活动,还需要添加--launch=<*name*>参数。

Image

图 7-27。 定义 ndk-gdb 外部工具

单击应用按钮保存外部工具定义。

在使用ndk-gdb外部工具之前,确保应用被设置为可调试的,并被正确地部署到目标设备上,如前所述。

要运行ndk-gdb,使用包浏览器选择项目,然后选择运行 Image 外部工具 Image ndk-gdbndk-gdb工具将在控制台视图中启动。它会进行一系列检查,以确保可以正确建立调试会话。如果您在使用ndk-gdb时遇到任何问题,请将--verbose添加到参数列表中,以打开详细的日志记录,这将有助于故障排除。

有关ndk-gdb的更多信息,包括它支持的其他命令行参数,执行带有--help参数的ndk-gdb。也可以参考 NDKdoc目录下的NDK-GDB.html文档文件。

使用 Eclipse 进行图形模式调试

文本模式调试是官方支持的在 Android 应用中调试本机组件的方法。但是,您可以通过调整基于 Android NDK 版本 R7 的某些 Android NDK 文件来设置图形模式调试。由于这不是调试原生 Android 应用的官方方式,这些步骤可能会随着 Android NDK 的新版本而改变,但总体流程应该是相同的。

图形模式调试需要一组使用文本模式ndk-gdb工具预生成的文件。在第一次运行应用的图形模式调试之前,如前一节所述执行ndk-gdb外部工具。如前所述,确保应用是可调试的,并正确部署到设备上。运行ndk-gdb工具后,它将生成一组定义图形化调试配置所必需的文件。我们将稍微修改这些文件,并用它们来建立一个使用 Eclipse 的调试会话。

由于 Eclipse 将使用其内部的 GDB 调试器客户端,我们需要阻止ndk-gdb在客户端会话中启动。进入安卓 NDK 安装目录,复制一份ndk-gdb脚本,命名为ndk-gdb-eclipse。打开ndk-gdb-eclipse脚本,删除最后一行:

$GDBCLIENT -x native_path $GDBSETUP``

ndk-gdb工具还在项目目录的obj/local/<*target architecture*>下准备了一个名为gdb.setup的配置设置脚本。我们需要修改这个脚本文件,但是由于在构建过程中它会被ndk-build覆盖,我们将修改它的一个副本。制作脚本文件的副本,并将其命名为gdb-eclipse.setup。右键单击gdb-eclipse.setup并选择 Image 文本编辑器打开 Eclipse 中的文件。删除最后一行:

target remote :5039

按照上一节描述的相同步骤,为ndk-gdb-eclipse脚本定义一个新的外部工具配置。启动 Eclipse,从顶部菜单栏选择运行 Image 调试配置。在“调试配置”对话框中,选择 C/C++ 应用,并单击“新建”图标来定义新的调试配置。如图 7-28 所示,填写工具信息如下:

  • C/C++ 应用:使用浏览按钮,导航到项目目录下的obj/local/<*target architecture*>目录,选择app_process应用。如果没有app_process应用,您需要首先运行默认的ndk-gdb会话来生成它。
  • 流程启动器:点击对话框底部的选择其他…链接,选择“用户配置特定设置”选项,并选择标准创建流程启动器。

Image

图 7-28。调试配置对话框主选项卡上的 配置

选择调试器页签,填写调试器信息,如下图图 7-29 所示:

  • 调试器:选择gdbserver作为调试器。
  • 在以下位置启动时停止:这可以检查并设置为您的主本机函数,或者设置为JNI_OnLoad,如果它已经实现。
  • GDB 调试器:使用浏览按钮,导航到安卓 NDK 目录下的toolchains子目录。根据您的目标机器架构,找到相应的gdb.exe风格。在 Windows 平台上,它位于<*NDK Directory*>\toolchains\arm-linux-androideabi-4.4.3\prebuilt\windows\bin\arm-linux-androideabi-gdb.exe
  • GDB 命令文件:使用浏览按钮,选择您之前生成的gdb-eclipse.setup文件。
  • GDB 命令集:在基于 Windows 的主机上,从下拉菜单中选择 Cygwin。在其他平台上,将此设置作为标准设置。

Image

图 7-29。 调试配置对话框的调试器标签上的配置

在调试器页签中选择连接页签,填写如下信息,如图 7-30 所示:

  • 类型:选择 TCP 作为连接类型。
  • 主机名或 IP 地址:将此设置为localhost,因为 Android 调试桥(ADB)将在设备和主机之间进行转发。
  • 端口号:将端口号设置为 5039。

Image

图 7-30。 调试器连接配置

Eclipse 现在可以调试本机组件了。要成功建立调试会话,请按照下列步骤操作:

  1. 启动您的 Android 模拟器实例或将您的 Android 设备连接到您的主机。
  2. 通过引入一个虚拟调用,比如System.out.println(),在loadLibrary调用之后,在com.example.hellojni.HelloJni Java 类中设置一个断点;以及在虚拟调用上启用断点。这将在加载共享库后立即停止 Java 调试器。
  3. 启动 Java 调试会话。
  4. 当调试器到达断点时,选择项目,然后通过选择运行Image外部工具 Image ndk-gdb-eclipse 启动您之前定义的ndk-gdb-eclipse外部工具。
  5. ndk-gdb-eclipse工具建立到 GDB 的连接时,选择您之前定义的 C/C++ 调试会话。
  6. Eclipse 将要求切换到调试透视图。现在可以开始调试本机组件了。

正如本节开始时提到的,由于这种图形调试设置没有得到 Android NDK 的官方支持,它可能不会以与 NDK 的更高版本完全相同的方式工作。

分析堆栈痕迹

如果本机组件崩溃,堆栈跟踪会记录到系统日志中。该堆栈跟踪可以通过 LogCat 视图访问,如图 7-31 所示。

Image

图 7-31。 显示堆栈跟踪的 LogCat 视图

下面几行显示了带有函数名和地址的堆栈跟踪。

I/DEBUG   (  114):          #00  pc 00000c38 /data/data/com.example.hellojni/lib/libhello-jni.so (Java_com_example_hellojni_HelloJni_stringFromJNI) I/DEBUG   (  114):          #01  pc 0001ec70  /system/lib/libdvm.so (dvmPlatformInvoke) I/DEBUG   (  114):          #02  pc 0005906a  /system/lib/libdvm.so (_Z16dvmCall JNIMethodPKjP6JValuePK6MethodP6Thread)

从堆栈跟踪中可以看出,本机代码在Java_com_example_hellojni_HelloJni_stringFromJNI函数中的地址00000c38处崩溃。在对复杂的本机组件进行故障排除时,这些信息可能还不够。Android NDK 带有ndk-stack工具,可以将堆栈跟踪解码成文件名和行号。从项目目录中,您可以在命令行上调用ndk-stack工具,如下所示:

adb logcat | ndk-stack –sym obj\local\armeabi

该工具分析日志行中的崩溃转储,并解码堆栈跟踪以显示文件名和行号。如图图 7-32 所示,地址00000c38被翻译到文件hello-jni.c中的第 31 行。

Image

图 7-32。ndk-Stack 工具解码的堆栈跟踪

ndk-stack工具也可以作为外部工具直接从 Eclipse 平台使用,以便简化故障排除过程。再次从 Eclipse 中,从顶部菜单栏选择运行 外部工具 外部工具配置… 。在外部工具配置对话框中,选择程序并单击新建启动配置按钮。如下完成外部工具信息,如图 7-33 所示:

  • 名称:命名新的外部工具配置ndk-stack
  • 位置:在 Windows 上,使用${system_path:cmd}作为位置。在 Linux 和 Mac OS X 上,使用${system_path:bash}作为位置。
  • 工作目录:在所有平台上,工作目录都是${project_loc}
  • 参数:在 Windows 上,输入参数/C "adb logcat –d | ndk-stack –sym obj\local\armeabi"。在 Linux 和 Mac OS X 上,输入-C "adb logcat –d | ndk-stack –sym obj/local/armeabi"作为参数。

Image

图 7-33。定义 ndk-stack 外部工具

为了防止 Eclipse 在每次工具启动时都重新构建应用,请转到 Build 选项卡并取消选中 Build before launch。

现在你可以使用ndk-stack工具了。一个应用崩溃后,选择运行 Image 外部工具 Image ndk-stack 。工具将被执行,输出将显示在控制台视图中,如图图 7-34 所示。

Image

图 7-34。??【控制台视图】中显示的 ndk-stack 输出

总结

在这一章中,我们探讨了 Android NDK,包括它的用途和它提供的功能。我们在大多数流行的主机平台上完成了安卓 NDK 的安装过程。我们通过塞阔雅插件将 Android NDK 粘贴到 Eclipse 平台上。然后我们看了 Android NDK 的核心——JNI 技术,并讨论了开发混合应用的重要方面。我们还讨论了最常见的故障排除任务,以及如何通过 Eclipse 平台简化这些任务。

资源

以下资源可用于本章涵盖的主题:

  • 塞阔雅项目,[www.eclipse.org/sequoyah/](http://www.eclipse.org/sequoyah/)
  • 安卓调试桥(ADB),[developer.android.com/guide/developing/tools/adb.html](http://developer.android.com/guide/developing/tools/adb.html)
  • Java 本地接口:程序员指南和规范[java.sun.com/docs/books/jni/](http://java.sun.com/docs/books/jni/)

八、项目:为 AVI 电影扩展电影播放器

在第六章中,我们在 Android 上开发了一个电影播放器应用。应用通过内容提供者获得现有电影文件的名称,并以列表格式呈现给用户。用户选择电影后,应用会间接启动默认的视频播放器活动来播放电影。

由于我们的电影播放器依赖于默认的视频播放器,它只能播放 Android 平台支持的视频格式。在本章中,我们将扩展电影播放器应用,以支持音频视频交错(AVI)电影文件。AVI 更像是一种容器格式,可以包装许多不同的媒体类型。为了简单起见,我们的 AVI 播放器将只支持 RGB565 色彩空间中未压缩视频的 AVI 文件。

处理依赖性

尽管 AVI 格式并不太复杂,但要用 Java 从头开始实现,支持它确实需要相当大的努力。在网上快速搜索 AVI 图书馆,会出现一个开源解决方案列表,大部分都是用 C/C++ 实现的。为了利用这些现有的库,Android NDK 是合适的工具。

我们将在整个项目中使用的 AVI 实现是 AVILib。它是一个更大的开源项目 Transcode 的一部分。

要下载转码,请使用浏览器导航至[tcforge.berlios.de/](http://tcforge.berlios.de/),然后点击下载链接。在撰写本文时,Transcode 的最新版本是 1.1.5。代码转换以 bzip2 压缩 TAR 存档文件的形式出现。要提取它的内容,如果你在基于 Windows 的平台上,打开 Cygwin,或者在 Mac OS 和 Linux 上打开终端窗口,转到你下载代码转换的目录,输入tar jxvf transcode-1.1.5.tar.bz2

如图图 8-1 所示,Transcode 附带了很多其他组件,AVILib 提供在它自己的名为avilib的目录中。

Image

图 8-1。 转码库的内容

有关 Transcode 的更多信息,请访问 Transcode Forge 网站,网址为[tcforge.berlios.de/](http://tcforge.berlios.de/)

添加原生支持

在我们开始将 AVILib 集成到我们现有的MoviePlayer项目之前,我们需要首先向它添加原生支持。右键单击MoviePlayer项目并从上下文菜单中选择Android ToolsImageAdd Native Support…以启动添加原生支持对话框。单击“完成”按钮使用默认参数。塞阔雅(我们在上一章安装的)将修改项目配置,它还将包括一组样板文件,如图图 8-2 所示。

Image

图 8-2。 为电影播放器添加原生支持

整合 AVILib

最新版本的安卓 NDK 支持模块。这使得第三方模块可以部署在一个中心位置,其他平台可以从这个位置快速地将它们引入到构建过程中。你可以在 Android NDK 文档中的IMPORT-MODULE.html文件中了解更多信息。为了简单起见,我们将直接在项目中包含 AVILib 库。

向项目添加 AVILib

avilib目录从 Transcode 复制到MoviePlayer项目的jni目录下作为子目录,如图图 8-3 所示。

Image

图 8-3。 将 AVILib 目录复制到 jni 目录

使用 Eclipse,从jni目录中打开platform.h头文件,并从文件中删除下面的include语句:

#include "config.h"

我们删除了这一行,因为我们不打算使用 AVILib 附带的Makefile;相反,我们将使用 Android NDK 的构建系统。

修改 Android.mk 文件

Android NDK 要求 AVILib 模块及其源文件在Android.mk文件中定义,以便对其进行编译。使用 Eclipse,打开Android.mk文件。第一行,LOCAL_PATH := $(call my-dir),为 Android NDK 项目设置当前工作目录,它需要是Android.mk文件中的第一个命令。在这一行之后,我们将开始为 AVILib 定义一个新的模块。

`#

AVILib

include $(CLEAR_VARS)`

include $(CLEAR_VARS)行允许 Android NDK 首先清除特定于模块的变量,以防止任何冲突。文件中的每个模块都有一个名字。当从其他模块或从Application.mk文件引用该特定模块时,使用该名称。

# Module name is avilib LOCAL_MODULE := avilib

AVILib 的源文件在名为avilib的子目录中。为了编译 AVILib,我们只需要两个源文件。

# Source files LOCAL_SRC_FILES := avilib/avilib.c avilib/platform_posix.c

如您所见,avilib/前缀是重复的,因为所有源文件都在avilib子目录下。这个Android.mk文件实际上是一个Makefile,里面有很多 Android 特有的宏。Android.mk文件由 GNU Make 工具处理,它支持 GNU Make 提供的所有功能。我们可以使用 GNU Make 的addprefix函数将该行分成两行。

`# Temporary variable to hold list of source files
MY_SRC_FILES := avilib.c platform_posix.c

Prefix them with the sub-directory name

LOCAL_SRC_FILES := $(addprefix avilib/, $(MY_SRC_FILES))`

第一行只包含源文件名。第二行通过在源文件名前面加上前缀avilib/来设置LOCAL_SRC_FILES

# Export the includes directory for dependent modules LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/avilib

模块通常有它们的包含路径层次结构;例如,AVILib 头文件在avilib子目录中。模块还可以依赖于具有其他包含路径的其他模块,跟踪组合的包含路径可能会成为一项耗时的任务。Android NDK 提供了LOCAL_EXPORT_C_INCLUDES变量,允许模块将它们的包含路径导出到它们的从属模块。从属模块自动继承包含路径,不需要手动处理。

# Build it as static library include $(BUILD_STATIC_LIBRARY)

我们将通过告诉 Android NDK 我们希望将 AVILib 构建为静态库来最终确定模块定义。AVILib 不必是一个共享的库,因为我们不会从 AVILib 直接向 Java 公开任何函数。清单 8-1 显示了Android.mk文件现在的样子。

清单 8-1。JNI/Android . MK 文件

`LOCAL_PATH := $(call my-dir)

AVILib

include $(CLEAR_VARS)

Module name is avilib

LOCAL_MODULE := avilib

Temporary variable to hold list of source files

MY_SRC_FILES := avilib.c platform_posix.c

Prefix them with the sub-directory name

LOCAL_SRC_FILES := $(addprefix avilib/, $(MY_SRC_FILES))

Export the includes directory for dependent modules

LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/avilib

Build it as static library

include $(BUILD_STATIC_LIBRARY)

Movie Player

include $(CLEAR_VARS)

Module name

LOCAL_MODULE := MoviePlayer

Source files

LOCAL_SRC_FILES := MoviePlayer.cpp

Build as shared library

include $(BUILD_SHARED_LIBRARY)`

为了确保我们现在可以构建 AVILib,从顶部菜单栏选择项目 Image 构建项目来构建MoviePlayer项目。如果一切顺利,你会看到“构建完成”的提示信息,如图 *** 图 8-4 *** 所示。

Image

图 8-4。 控制台视图显示构建完成

实现 AVI 播放器

我们现在将开始实现 AVI 播放器类。AVI 播放器类将依靠 AVILib 来正确读取 AVI 视频文件,并且它将依靠 Android APIs 来呈现视频帧。由于其依赖性,AVI 播放器类将是一个混合类,一部分在 Java 中,另一部分在本地空间中。我们将从定义冰山一角开始,即 AVI 播放器的 Java 部分。

Java 部分

AVI 播放器的 Java 部分将采用 AVI 视频文件的名称,并通过其本地方法将其传递给 AVILib。当帧开始出现时,它将设置一个 Android surface 来呈现这些帧。

要开始实现 AVI 播放器的 Java 部分,通过项目浏览器选择com.apress.movieplayer Java 包,从顶部菜单栏选择文件 Image 新建 Image ,将新类命名为AviPlayer。清单 8-2 显示了AviPlayer类文件的内容。

清单 8-2。【AviPlayer.java 档案】??

`package com.apress.movieplayer;

import android.graphics.Bitmap;
import android.util.Log;
import android.view.SurfaceHolder;

/**
 * AVI player. *
 * @author Onur Cinar
 /
class AviPlayer implements Runnable {
    /
* Log tag. */
    private static final String LOG_TAG = "AviPlayer";

/** Surface holder. */
    private SurfaceHolder surfaceHolder;

/** Movie file. */
    private String movieFile;

/** Playing flag. */
    private boolean isPlaying;

/** Thread instance. */
    private Thread thread;

/**
     * Sets the surface holder.
     *
     * @param surfaceHolder
     *            surface holder.
     */
    public void setSurfaceHolder(SurfaceHolder surfaceHolder) {
        this.surfaceHolder = surfaceHolder;
    }

/**
     * Sets the movie file.
     *
     * @param movieFile
     *            movie file.
     */
    public void setMovieFile(String movieFile) {
        this.movieFile = movieFile;
    }

/**
     * Start playing.
     */
    public synchronized void play() {
        if (thread == null) {
            isPlaying = true;

thread = new Thread(this);
            thread.start();
        }
    }     /**
     * Stop playing.
     */
    public synchronized void stop() {
        isPlaying = false;
    }

/**
     * Runs in its thread.
     */
    public void run() {
        try {
            render(surfaceHolder, movieFile);
        } catch (Exception e) {
            Log.e(LOG_TAG, "render", e);
        }

thread = null;
    }

/**
     * New bitmap using given width and height.
     *
     * @param width
     *            bitmap width.
     * @param height
     *            bitmap height.
     * @return bitmap instance.
     */
    private static Bitmap newBitmap(int width, int height) {
        return Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565);
    }

/**
     * Renders the frames from the AVI file.
     *
     * @param surfaceHolder surface holder.
     * @param movieFile movie file.
     * @throws Exception
     */
    private native void render(SurfaceHolder surfaceHolder, String movieFile)
            throws Exception;

/** Loads the native library. */
    static {
        System.loadLibrary("MoviePlayer");
    }
}`

AviPlayer类是非常简单的播放器实现。它提供了 setter 方法来定义电影文件,并提供了一个android.view.Surface实例来呈现帧。控制玩家有两种方法,playstop。由于 AVI 电影文件可能需要很长时间才能播放,AVI 播放器有自己的线程来执行渲染任务。正如您在代码中看到的,Java 部分不包含任何与处理 AVI 文件相关的代码。本地渲染方法将负责处理和渲染 AVI 电影文件。

本土部分

AVI 播放器的本机部分将充当 AVILib 模块和 AVI 播放器的 Java 部分之间的桥梁。本机部分将从 Java 部分获得 AVI 视频文件名,并开始通过 AVILib 模块读取它。当读取帧时,它将它们交给 Java 部分进行渲染。

要开始实现播放器的本地部分,我们需要为 AVI 播放器生成 C 头文件。和上一章一样,我们将使用javah工具来完成这项任务。选择AviPlayer类,从外部工具菜单启动javah,如图图 8-5 所示。

Image

图 8-5。 使用 javah 生成头文件

该工具将处理 AVI 播放器类,并在jni子目录中生成com_apress_movieplayer_AviPlayer.h C 头文件。清单 8-3 显示了头文件的内容。

清单 8-3。com _ a press _ movie player _ avi player . h 文件

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

include <jni.h>

/* Header for class com_apress_movieplayer_AviPlayer */ #ifndef _Included_com_apress_movieplayer_AviPlayer

define _Included_com_apress_movieplayer_AviPlayer

ifdef __cplusplus

extern "C" {

endif

/*
 * Class:     com_apress_movieplayer_AviPlayer
 * Method:    render
 * Signature: (Landroid/view/SurfaceHolder;Ljava/lang/String;)V
 */
JNIEXPORT void JNICALL Java_com_apress_movieplayer_AviPlayer_render
  (JNIEnv *, jobject, jobject, jstring);

ifdef __cplusplus

}

endif

endif`

基于生成的头文件,我们现在需要实现Java_com_apress_movieplayer_AviPlayer_render函数。选择jni目录,从顶部菜单栏选择文件 Image 新建 Image 其他…ImageC/c++Image源文件。将 C 源文件命名为com_apress_movieplayer_AviPlayer.c

如清单 8-4 所示,C 源文件的第一部分简单地包含了必要的库。为了让这个例子更容易理解,我们将使用安卓 NDK 的位图库来绘制 AVI 帧,并使用它的日志库来记录大量的操作。您可能已经注意到,有一组用于不同级别日志记录的宏。这些宏使 Android NDK 的日志机制更容易使用,并添加更多的日志信息来帮助故障排除。

清单 8-4。com _ a press _ movie player _ avi player . c 文件

`#include "com_apress_movieplayer_AviPlayer.h"

include <limits.h>

include <android/bitmap.h>

include <android/log.h>

include <avilib.h>

define LOG_TAG "AviPlayer"

define LOG_PRINT(level,fmt,...) \

__android_log_print(level, LOG_TAG, "%s: " fmt, PRETTY_FUNCTION, ##VA_ARGS)

define LOG_DEBUG(fmt,...) \

LOG_PRINT(ANDROID_LOG_DEBUG, fmt, ##VA_ARGS) #define LOG_WARNING(fmt,...)
        LOG_PRINT(ANDROID_LOG_WARN, fmt, ##VA_ARGS)

define LOG_ERROR(fmt,...) \

LOG_PRINT(ANDROID_LOG_ERROR, fmt, ##VA_ARGS)

define LOG_INFO(fmt,...) \

LOG_PRINT(ANDROID_LOG_INFO, fmt, ##VA_ARGS)`

清单 8-5 提供了一个struct,用于在本地函数之间缓存和共享常用的方法和字段 id,以及像表面容器和位图这样的对象。

清单 8-5。avi _ player 结构

`/**
 * AVI player instance fields.
 /
typedef struct avi_player {
    JNIEnv
env;
    jobject obj;
    jclass clazz;

/* SurfaceHolder */
    jmethodID lockCanvasMethodId;
    jmethodID unlockCanvasAndPostMethodId;

/* Canvas */
    jmethodID drawBitmapMethodId;

jfieldID isPlayingFieldId;
    jobject surfaceHolder;
    jobject bitmap;

} avi_player_t;`

定义位图助手函数

清单 8-6 ,中的newBitmap函数是一个助手函数,它调用 Java 中的静态newBitmap函数来生成给定尺寸的位图。它使用了前一章讨论的一些 JNI 函数和错误处理操作。

清单 8-6。new bitmap 函数

/**  * Calls the new bitmap method with the given width and height.  * ` * @param p avi player.
 * @param width bitmap width.
 * @param height bitmap height.
 * @return bitmap instance.
 /
jobject newBitmap(avi_player_t
p, int width, int height) {
    jobject bitmap = 0;
    jmethodID newBitmapMethodId = 0;

LOG_DEBUG("BEGIN p=%p width=%d height=%d", p, width, height);

newBitmapMethodId = (*p->env)->GetStaticMethodID(p->env, p->clazz,
            "newBitmap", "(II)Landroid/graphics/Bitmap;");
    if (0 == newBitmapMethodId) {
        LOG_ERROR("Unable to find newBitmap method");
        goto exit;
    }

bitmap = (*p->env)->CallStaticObjectMethod(p->env, p->clazz,
            newBitmapMethodId, width, height);

exit:
    LOG_DEBUG("END bitmap=%p", bitmap);
    return bitmap;
}`

缓存 AVI 球员 id 和参考

newBitmap函数(清单 8-6 )通过使用签名(II)Landroid/graphics/Bitmap;来查找 Java 方法。正如上一章所讨论的,为了快速找到这些签名,我们可以使用javap工具。选择AviPlayer Java 类,并选择使用javap外部工具。处理完 Java 类文件后,javap会输出以下内容:

Compiled from "AviPlayer.java" class com.apress.movieplayer.AviPlayer implements java.lang.Runnable {   private static final java.lang.String LOG_TAG;     Signature: Ljava/lang/String;   private android.view.SurfaceHolder surfaceHolder;     Signature: Landroid/view/SurfaceHolder;   private java.lang.String movieFile;     Signature: Ljava/lang/String;   private boolean isPlaying;     Signature: Z   private java.lang.Thread thread;     Signature: Ljava/lang/Thread;   static {};     Signature: ()V `  com.apress.movieplayer.AviPlayer();
    Signature: ()V

public void setSurfaceHolder(android.view.SurfaceHolder);
    Signature: (Landroid/view/SurfaceHolder;)V

public void setMovieFile(java.lang.String);
    Signature: (Ljava/lang/String;)V

public synchronized void play();
    Signature: ()V

public synchronized void stop();
    Signature: ()V

public void run();
    Signature: ()V

private static android.graphics.Bitmap newBitmap(int, int);
    Signature: (II)Landroid/graphics/Bitmap;

private native void render(android.view.SurfaceHolder, java.lang.String) throws java.lang.Exception;
    Signature: (Landroid/view/SurfaceHolder;Ljava/lang/String;)V
}`

通过查看输出,我们可以很容易地提取成员字段和方法的签名。为了加快访问速度,可以缓存这些字段和方法 id。清单 8-7 提供了一段代码用于缓存AviPlayer的类引用,以及isPlaying成员字段的字段 ID。

清单 8-7。cache aviplayer 函数

`/**

* Caches the AVI player.
 *
 * @param p avi player.
 * @return result code.
 /
int cacheAviPlayer(avi_player_t
p) {
    int result = 0;
    jclass clazz = 0;
    jfieldID isPlayingFieldId = 0;

/* Get object class instance. /
    clazz = (
p->env)->GetObjectClass(p->env, p->obj);
    if (0 == clazz) {
        LOG_ERROR("Unable to get class");         goto exit;
    }

/* Get is playing field id. /
    isPlayingFieldId = (
p->env)->GetFieldID(p->env, clazz, "isPlaying", "Z");
    if (0 == isPlayingFieldId) {
        LOG_ERROR("Unable to get isPlaying field id");
        (*p->env)->DeleteLocalRef(p->env, clazz);
        goto exit;
    }

result = 1;

p->clazz = clazz;
    p->isPlayingFieldId = isPlayingFieldId;

exit:
    return result;
}`

在代码的后面,isPlaying成员字段的值将用于控制播放器的生命周期。

缓存曲面夹具 id 和参照

Android 框架允许用户界面(UI)只能在 UI 线程中修改。电影文件通常需要很长时间才能播放,并且在整个播放过程中会涉及许多 UI 操作。直接在 UI 线程中执行这些操作并不是一个好的做法。

Android 框架提供了 surface 对象,允许应用从非 UI 线程绘制到 UI。表面是媒体应用的最佳工具。Android 应用可以通过SurfaceHolder对象访问 surface 对象。SurfaceHolder对象提供了两个主要的方法,lockCanvasunlockCanvasAndPost,允许应用操作表面对象。因为我们正在实现一个电影播放器应用,所以在整个播放过程中会大量调用这些方法。清单 8-8 显示了cacheSurfaceHolderMethods函数来缓存这些常用方法的方法 id。

清单 8-8。cacheSurfaceHolderMethods 函数

/**  * Cache surface holder method ids.  *  * @param p avi player.  * @return result code./
int cacheSurfaceHolderMethods(avi_player_t
p) {
    int result = 0;

jclass surfaceHolderClazz = 0;
    jmethodID lockCanvasMethodId = 0;
    jmethodID unlockCanvasAndPostMethodId = 0;

surfaceHolderClazz = (*p->env)->FindClass(p->env,
            "android/view/SurfaceHolder");
    if (0 == surfaceHolderClazz) {
        LOG_ERROR("Unable to find surfaceHolder class");
        goto exit;
    }

lockCanvasMethodId = (*p->env)->GetMethodID(p->env, surfaceHolderClazz,
            "lockCanvas", "()Landroid/graphics/Canvas;");
    if (0 == lockCanvasMethodId) {
        LOG_ERROR("Unable to find lockCanvas method");
        goto exit;
    }

unlockCanvasAndPostMethodId = (*p->env)->GetMethodID(p->env,
            surfaceHolderClazz, "unlockCanvasAndPost",
            "(Landroid/graphics/Canvas;)V");
    if (0 == unlockCanvasAndPostMethodId) {
        LOG_ERROR("Unable to find unlockCanvasAndPost method");
        goto exit;
    }

p->lockCanvasMethodId = lockCanvasMethodId;
    p->unlockCanvasAndPostMethodId = unlockCanvasAndPostMethodId;

result = 1;

exit:
    return result;
}`

缓存画布 id 和引用

SurfaceHolder对象提供了一个画布来绘画。AVI 播放器代码将把每个 AVI 帧转换成一个位图对象,该对象将被绘制在画布上。这个操作在播放过程中也会出现很多次。如清单 8-9 所示的cacheCanvasMethods函数缓存了Canvas类的drawBitmap方法的方法 ID。

清单 8-9。??【cacheCanvasMethods】函数

`/**
 * Cache canvas method ids.
 *
 * @param p avi player.
 * @return result code
 /
int cacheCanvasMethods(avi_player_t
p) {
    int result = 0;

jclass canvasClazz = 0;
    jmethodID drawBitmapMethodId = 0;

canvasClazz = (*p->env)->FindClass(p->env, "android/graphics/Canvas");
    if (0 == canvasClazz) {
        LOG_ERROR("Unable to find canvas class");
        goto exit;
    }

drawBitmapMethodId = (*p->env)->GetMethodID(p->env, canvasClazz,
            "drawBitmap",
            "(Landroid/graphics/Bitmap;FFLandroid/graphics/Paint;)V");
    if (0 == drawBitmapMethodId) {
        LOG_ERROR("Unable to get drawBitmap method");
        goto exit;
    }

p->drawBitmapMethodId = drawBitmapMethodId;

result = 1;

exit:
    return result;
}`

将位图绘制到画布上

清单 8-10 中的drawBitmap本地函数使用之前缓存的方法 id 将给定的位图对象绘制到表面对象上。您可能已经注意到,在函数的末尾,对Canvas对象的本地引用被删除。由于 JNI 函数返回的本地引用在本地方法调用的整个生命周期内都是有效的,所以在回放结束之前,Canvas对象不会被自动清除。由于在回放过程中会多次调用drawBitmap函数,它可以用不再使用的Canvas实例快速填充内存。为了防止这种情况,函数在终止之前删除了对Canvas对象的本地引用。

清单 8-10。draw bitmap 函数

`/**
 * Draw bitmap.
 *
 * @param p avi player.
 * @return result code
 /
int drawBitmap(avi_player_t
p) {
    int result = 0;
    jobject canvas = 0;

/* Lock and get canvas /
    canvas = (
p->env)->CallObjectMethod(p->env, p->surfaceHolder,
            p->lockCanvasMethodId);
    if (0 == canvas) {
        LOG_ERROR("Unable to lock canvas");
        goto exit;
    }

/* Draw bitmap /
    (
p->env)->CallVoidMethod(p->env, canvas, p->drawBitmapMethodId, p->bitmap,
            0.0, 0.0, 0);

/* Unlock and post canvas /
    (
p->env)->CallVoidMethod(p->env, p->surfaceHolder,
            p->unlockCanvasAndPostMethodId, canvas);

result = 1;

exit:
    (*p->env)->DeleteLocalRef(p->env, canvas);

return result;
}`

打开 AVI 文件

openAvi函数,如清单 8-11 所示,提供了对 AVILib 打开 AVI 文件的必要调用。这个函数接受一个 Java 字符串作为参数。它首先将其转换为 C 字符串,以便能够将其用作AVI_open_input_file函数的参数。它在终止前释放 C 字符串。

清单 8-11。open avi 函数

/**  * Opens the given AVI movie file.  * ` * @param movieFile movie file.
 * @return avi file.
 /
avi_t
openAvi(avi_player_t* p, jstring movieFile) {
    avi_t* avi = 0;
    const char* fileName = 0;

/* Get movie file as chars. /
    fileName = (
p->env)->GetStringUTFChars(p->env, movieFile, 0);
    if (0 == fileName) {
        LOG_ERROR("Unable to get movieFile as chars");
        goto exit;
    }

/* Open AVI input file. */
    avi = AVI_open_input_file(fileName, 1);

/* No need to have the file name. /
    (
p->env)->ReleaseStringUTFChars(p->env, movieFile, fileName);

exit:
    return avi;
}`

渲染 AVI 文件

最后一段代码是本机 render 方法的实际实现。如清单 8-12 所示,它使用前面讨论的助手函数初始化环境。在一个循环中,它遍历 AVI 帧,用帧数据填充位图对象,并使用drawBitmap函数在表面上绘制位图。在每一帧之后,它检查isPlaying标志的值,如果播放器已经停止,则终止。因为可以从两个线程访问isPlaying标志,所以在检查isPlaying标志的值之前,它使用 JNI 的监控函数来同步对象实例。根据 AVI 电影的帧速率,它会在每一帧后休眠,以匹配实际播放时间。

清单 8-12。Java _ com _ a press _ movie player _ avi player _ render 函数

void Java_com_apress_movieplayer_AviPlayer_render(JNIEnv* env, jobject obj,         jobject surfaceHolder, jstring movieFile) {     avi_player_t ap;     avi_t* avi = 0;     jboolean isPlaying = 0;     double frameRate = 0;     long frameDelay = 0;     long frameSize = 0; `char* frame = 0;
    int keyFrame = 0;

/* Cache environment and object. */
    memset(&ap, 0, sizeof(avi_player_t));
    ap.env = env;
    ap.obj = obj;
    ap.surfaceHolder = surfaceHolder;

/* Cache surface holder and canvas method ids. */
    if (!cacheAviPlayer(&ap) || !cacheSurfaceHolderMethods(&ap)
            || !cacheCanvasMethods(&ap)) {
        LOG_ERROR("Unable to cache the method ids");
        goto exit;
    }

/* Open AVI input file. */
    avi = openAvi(&ap, movieFile);
    if (0 == avi) {
        LOG_ERROR("Unable to open AVI file.");
        goto exit;
    }

/* New bitmap. */
    ap.bitmap = newBitmap(&ap, AVI_video_width(avi), AVI_video_height(avi));
    if (0 == ap.bitmap) {
        LOG_ERROR("Unable to generate a bitmap");
        goto exit;
    }

/* Frame rate. */
    frameRate = AVI_frame_rate(avi);
    LOG_DEBUG("frameRate=%f", frameRate);

frameDelay = (long) (1000 / frameRate);
    LOG_DEBUG("frameDelay=%ld", frameDelay);

/* Play file. /
    while (1) {
        /
Lock the bitmap and get access to raw data. */
        AndroidBitmap_lockPixels(env, ap.bitmap, (void**) &frame);

/* Copy the next frame from AVI file to bitmap data. */
        frameSize = AVI_read_frame(avi, frame, &keyFrame);
        LOG_DEBUG("frame=%p keyFrame=%d frameSize=%d error=%s",
                frame, keyFrame, frameSize, AVI_strerror());

/* Unlock bitmap. /
        AndroidBitmap_unlockPixels(env, ap.bitmap);         /
Synchronize on the current object. /
        if (0 != (
env)->MonitorEnter(env, obj)) {
            LOG_ERROR("Unable to monitor enter");
            goto exit;
        }

isPlaying = (*env)->GetBooleanField(env, obj, ap.isPlayingFieldId);

/* Done synchronizing. /
        if (0 != (
env)->MonitorExit(env, obj)) {
            LOG_ERROR("Unable to monitor exit");
            goto exit;
        }

/* If there is no frame or player stopped. */
        if ((-1 == frameSize) || (0 == isPlaying)) {
            break;
        }

/* Draw bitmap. */
        drawBitmap(&ap);

/* Wait for the next frame. */
        usleep(frameDelay);
    }

exit:
    if (0 != avi) {
        AVI_close(avi);
    }
}`

更新 Android.mk

Android.mk文件中的MoviePlayer模块需要用位图和日志功能所需的 C 源文件名和系统库进行更新。更新Android.mk文件中的MoviePlayer模块,如下所示:

`#

Movie Player

include $(CLEAR_VARS)

Module name

LOCAL_MODULE := MoviePlayer

Source files LOCAL_SRC_FILES := com_apress_movieplayer_AviPlayer.c

Static libraries

LOCAL_STATIC_LIBRARIES := avilib

System libraries

LOCAL_LDLIBS := -llog -ljnigraphics

Build as shared library

include $(BUILD_SHARED_LIBRARY)`

在这个版本的文件中,LOCAL_LDLIBS变量定义了两个额外的系统库,以便在构建过程中进行链接。

定义 AVI 玩家活动

AVI 播放器类需要一个surface来回放 AVI 文件。AVI 播放器活动将包装AviPlayer对象,并提供一个表面和电影路径。

定义布局

从顶部菜单栏选择文件 Image 新建 Image 其他…ImageAndroidImageAndroid XML 布局文件将一个新的布局文件添加到项目中作为avi_player.xml。布局将只包含一个填充整个屏幕的SurfaceView对象。清单 8-13 显示了布局文件的内容。

清单 8-13。avi _ player . XML 布局文件

`

`

定义活动

从顶部菜单栏选择文件 Image 新建 Image 以启动新建 Java 类对话框。新类将扩展Activity类,并实现SurfaceHolder.Callback接口,如图图 8-6 所示。

Image

图 8-6。AVI 玩家活动的新 Java 类对话框

这个类文件的实现如清单 8-14 所示。正如在onCreate方法中看到的,AVI 玩家活动将接收 AVI 电影文件名作为启动意图的一部分。它会用给定的电影文件配置 AVI 播放器,并依靠SurfaceHolder回调来启动和停止 AVI 播放器。

清单 8-14。【AviPlayerActivity.java 档案】??

`package com.apress.movieplayer;

import android.app.Activity;
import android.os.Bundle;
import android.view.SurfaceHolder;
import android.view.SurfaceHolder.Callback;
import android.view.SurfaceView;

/**
 * AVI movie player activity.
 *
 * @author Onur Cinar
 /
public class AviPlayerActivity extends Activity implements Callback {
    /
* AVI player. */
    private AviPlayer aviPlayer;

/**
     * On create.
     *
     * @see Activity#onCreate(Bundle)
     */
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

setContentView(R.layout.avi_player);

SurfaceView surfaceView = (SurfaceView) findViewById(R.id.surface_view);
        surfaceView.getHolder().addCallback(this);

aviPlayer = new AviPlayer();
        aviPlayer.setMovieFile(getIntent().getData().getPath());
    }

/**
     * Surface changed.
     *
     * @see Callback#surfaceChanged(SurfaceHolder, int, int, int)
     */
    public void surfaceChanged(SurfaceHolder holder, int format, int width,
            int height) {
    }

/**
     * Surface created.      *
     * @see Callback#surfaceCreated(SurfaceHolder)
     */
    public void surfaceCreated(SurfaceHolder holder) {
        aviPlayer.setSurfaceHolder(holder);
        aviPlayer.play();
    }

/**
     * Surface destroyed.
     *
     * @see Callback#surfaceDestroyed(SurfaceHolder)
     */
    public void surfaceDestroyed(SurfaceHolder holder) {
        aviPlayer.stop();
    }
}`

修改 AndroidManifest.xml

我们想让AviPlayerActivity成为 AVI 电影文件的系统默认播放器。我们将使用适当的意图过滤器修改AndroidManifest.xml文件来实现这一点。在定义了MoviePlayerActivity之后,立即定义一个新活动,如下所示:

<activity android:name=".AviPlayerActivity" android:label="@string/avi_player_label">     <intent-filter>         <action android:name="android.intent.action.VIEW" />         <category android:name="android.intent.category.DEFAULT" />         <category android:name="android.intent.category.BROWSABLE" />         <data android:mimeType="video/avi" />     </intent-filter> </activity>

意图过滤器为具有video/avi MIME 类型的电影文件提供VIEW动作。现在,当用户选择 AVI 电影文件时,Android 会将 AVI 播放器作为一个选项。

更新字符串资源

res/values/strings.xml中,AVI 玩家活动的标签也应该被添加到字符串资源中。

<?xml version="1.0" encoding="utf-8"?> <resources>     ...     <string name="avi_player_label">AVI Player</string> </resources>

将 AVI 文件扫描到媒体商店

Android 平台有一个默认的媒体扫描器,它用音频、视频和图片文件的列表更新媒体商店。由于 Android 平台不直接支持 AVI 文件,因此媒体扫描仪不会自动将它们添加到媒体商店。我们将在前面创建的MoviePlayerActivity类中添加扫描功能。

`/**
 * Goes through the external movies directory and scans the AVI files into
 * the movies.
 */
private void scanAviFiles() {
    LinkedList queue = new LinkedList();
    queue.add(Environment.getExternalStorageDirectory());

while (!queue.isEmpty()) {
        File dir = queue.poll();
        Log.i(LOG_TAG, "Scanning " + dir.getPath());

File[] files = dir.listFiles();
        if (files != null) {
            for (File file : files) {
                if (file.isDirectory()) {
                    queue.add(file);
                } else if (file.getName().endsWith(".avi")) {
                    scanAviFile(file);
                }
           }
        }
    }
}`

scanAviFiles方法遍历外部存储目录并搜索 AVI 文件。

/**  * Scans the given AVI files into movies.  *  * @param file  *            AVI file.  */ private void scanAviFile(File file) {     ContentValues contentValues = new ContentValues(); `    String data = file.getPath();
    Log.i(LOG_TAG, "scanAviFile " + data);

contentValues.put(MediaStore.Video.Media.TITLE, file.getName());
    contentValues.put(MediaStore.Video.Media.DATA, data);
    contentValues.put(MediaStore.Video.Media.MIME_TYPE, "video/avi");

ContentResolver contentResolver = getContentResolver();

if (0 >= contentResolver.update(
            MediaStore.Video.Media.EXTERNAL_CONTENT_URI, contentValues,
            MediaStore.Video.Media.DATA + "=?", new String[] { data })) {

contentResolver.insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
                contentValues);
    }
}`

scanAviFiles方法找到一个 AVI 文件时,它调用scanAviFile方法。scanAviFile方法填充 AVI 文件的媒体信息,并更新它或将其插入媒体存储。我们将在填充电影列表之前调用scanAviFiles方法。

`public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

scanAviFiles();


}`

为了简单起见,我们在 UI 线程内调用scanAviFiles方法;然而,在现实生活中,应该使用一个AsyncTask作为这个操作的包装器,以防止大量 IO 操作阻塞 UI 线程。

运行应用

在启动应用之前,您需要将一个 AVI 文件部署到设备上。导航到[zdo.com/galleon.zip](http://zdo.com/galleon.zip),提取galleon.avi文件。galleon.avi文件是一个未压缩的原始 AVI 文件,其帧在 RGB565 色彩空间中。使用 Windows 上的命令提示符,或 Linux 和 Mac OS X 上的终端窗口,使用以下命令将 AVI 文件上传到设备上的 SD 卡:

adb push galleon.avi /sdcard/

启动电影播放器

使用 Eclipse 在设备上启动电影播放器应用。电影播放器会先扫描 SD 卡,然后列出 AVI 文件,如图图 8-7 所示。

Image

图 8-7。 电影玩家活动列表 AVI 电影

单击 AVI 电影文件,电影播放器将在系统上启动默认的 AVI 电影播放器,这是我们在本章中开发的 AVI 播放器活动。AVI 播放器将立即开始播放 AVI 文件,如图图 8-8 所示。

Image

图 8-8。 AVI 玩家活动播放 AVI 电影

查看图库

AVI 播放器现在是 AVI 文件的默认电影播放器应用。如果您从应用列表中选择图库,您也会在列表中看到 AVI 文件。点击 AVI 文件,Android 会呈现一个可用玩家列表,如图图 8-9 所示。

Image

图 8-9。 画廊展示 AVI 球员名单

检查日志

在电影回放期间,LogCat 视图将显示来自 Java 应用和本地函数的日志消息,如图 8-10 所示。

Image

图 8-10。 显示 AVI 玩家日志的日志视图

作业

为了进一步探索前一章中介绍的概念和工具,您可能希望试验项目代码来添加这些功能:

  • 缩略图支持:正如你在《??》第六章中所记得的,电影播放器列表可以显示电影文件的缩略图。您需要实现一个本地方法来从 AVI 电影文件中提取缩略图,还需要修改 AVI 媒体扫描仪来将缩略图添加到媒体商店。您可以修改逻辑,使 Java 代码能够逐个请求帧,而不是让本机代码读取整个 AVI 文件并对每个帧进行回调。这可以允许 Java 代码只提取第一帧作为缩略图。在获得缩略图后,位图可以被转换成适当的图像格式,并且可以通过媒体商店内容提供商接口保存。
  • 播放器控制:AVI 播放器自动开始播放 AVI 电影文件,整部电影播放完毕后停止播放。它不为用户提供暂停、停止或重新开始播放的任何控制。实现这些特性将允许您探索 Java 和本机层之间的交互。通过重构本机代码并将阅读器代码分成多个函数,您可以允许 Java 代码控制实现这些特性的流程。

总结

在本章中,为了更好地理解 Android NDK 和 JNI 技术,我们扩展了在第六章中构建的电影播放器应用,以支持 AVI 格式的电影文件。我们将一个开源的 AVI 库集成到我们的项目中。在实现本机代码时,我们回顾了方法和字段访问、静态和实例方法执行,以及使用 JNI 函数从本机代码使用监视器的线程同步。

九、使用 Eclipse 的 Android 脚本

使用 Java 编程语言开发一个完整的 Android 应用可能会成为自动化和原型制作等简单任务的巨大开销。对于这样简单的任务,Android 脚本成为一个非常方便的工具。脚本语言支持动态类型、自动内存管理和多种编程范例,并且它们提供了一个简单的编程环境。与 Java 相比,脚本语言是解释型编程,执行时不需要编译、链接、打包和部署。在控制台上键入脚本或从脚本文件中读取脚本时,脚本会被即时解释和执行。

基于不同的脚本语言,Android 平台有多种脚本解决方案。在这一章中,我们将探索 Android 脚本层(SL4A)开源项目的 R5 版本。SL4A 为 Android 脚本提供了更通用的解决方案。它允许直接在 Android 设备上编辑和执行脚本,也可以从主机远程编辑和执行。它通过一组外观提供对 Android APIs 的访问,并依赖脚本解释器来处理实际的脚本文件。SL4A 支持大多数流行的脚本语言。

【Android 脚本层

SL4A 有三个主要组件:

  • 脚本解释器:它们在实际的 Android 设备或主机上的沙箱中执行脚本。
  • Android RPC 客户端:客户端允许沙箱内解释器正在执行的脚本与 SL4A 通信。
  • 外观:这些通过 Android RPC 客户端暴露给脚本。它们是一组扩展的 API,用于脚本与 Android 平台进行交互。

在本节中,我们将详细探讨这些组件。

脚本解释器

SL4A 提供了一个脚本宿主,并依赖脚本解释器来执行实际的脚本。SL4A 为大多数流行的脚本语言提供解释器,比如 Python、Perl、Ruby、Lua、BeanShell、JavaScript 和 Tcl。新的脚本语言也可以通过为该脚本语言开发新的 SL4A 解释器来动态地合并到 SL4A 中。

SL4A 在自己的解释器实例中运行沙箱中的每个脚本。这允许多个脚本同时运行,而不会相互影响。

Android RPC 代理客户端

在其解释器实例中运行的脚本通过 Android 代理 RPC 客户端与 SL4A 应用通信。客户端建立到 SL4A 的远程过程调用(RPC)连接,并允许脚本通过使用 SL4A 外观与 Android 框架进行交互。SL4A 通过在获得对外观的访问之前发送共享握手秘密,要求所有脚本向 SL4A RPC 服务器进行身份验证,从而加强了每个脚本的安全性。这个握手秘密通过AP_HANDSHAKE环境变量提供给 RPC 客户端。

Android RPC 客户端为每一种支持的脚本语言提供。当直接在 Android 设备上执行脚本时,这些客户端模块已经存在于脚本解释器的路径中。当脚本被远程执行时,客户机模块需要出现在主机解释器的路径上。客户端模块可以从 SL4A 网站[code.google.com/p/android-scripting/wiki/AndroidFacadeAPI](http://code.google.com/p/android-scripting/wiki/AndroidFacadeAPI)获得。

尽管它们的实现不同,但是 RPC 客户端在每种脚本语言中都提供了相同的接口。RPC 客户端模块为脚本环境提供了一个 Android 对象,并封装了 RPC 内部组件。这个对象上的实例方法调用被翻译成 RPC 方法调用,并通过 SL4A 在 Android 设备上远程执行。这允许引入新的 API 方法,而无需修改 RPC 客户端模块。

门面

SL4A 通过大量的外观将 Android 框架 API 暴露给脚本:

  • ActivityResultFacade:允许脚本在被startActivityForResult调用触发时返回结果。产生的意图在SCRIPT_RESULT属性中包含脚本结果。
  • AndroidFacade:提供通用的 Android 例程,如开始活动、广播意图、toast 通知、振动设备、通过对话框查询用户输入、发送电子邮件、日志记录和剪贴板操作。
  • ApplicationManagerFacade:允许管理 Android 应用,比如获取可启动的活动类名列表,按类名启动一个活动,获取当前运行的活动和服务列表,强制一个应用包停止。
  • BatteryManagerFacade:暴露电池管理器,允许跟踪电池状态、健康、类型、级别、电压、温度和技术。
  • BluetoothFacade:暴露蓝牙 API,允许控制蓝牙连接,使设备可被发现,查询蓝牙设备及其信息,连接到另一个蓝牙设备,以及通过蓝牙交换数据。该立面要求至少 API 级。
  • CameraFacade:允许使用设备的摄像头拍摄照片,并将其保存到指定的路径。
  • CommonIntentsFacade:展示了对常见 Android 意图的轻松访问,例如打开联系人列表、进行地图搜索、将浏览器指向本地 HTML 页面、启动条形码扫描仪、通过一个动作启动一项活动,以及显示由 URI 选择的内容。
  • ContactsFacade:允许访问联系人,如提供联系人列表用于选择联系人,按属性查询联系人列表,获取所有联系人及其 id 和属性的列表。
  • EventFacade:允许管理事件队列,如清除现有事件、删除旧事件、发布新事件、等待事件、阻止脚本执行直到特定事件。它还允许列出、注册和取消注册广播信号。
  • EyesFreeFacade:API 4 级以下设备可用。它允许脚本使用文本到语音转换技术说话。现已被TextToSpeechFacade弃用。
  • LocationFacade:展示位置管理器,可以收集位置数据,查询当前位置和当前位置的地址。
  • MediaPlayerFacade:允许播放媒体文件,播放时控制播放器,获取媒体文件信息。
  • MediaRecorderFacade:允许将音频和视频录制到指定位置的媒体文件中,并在录制时控制录像机。
  • PhoneFacade:显示手机功能,并允许跟踪手机状态、漫游状态、发起呼叫、SIM 卡信息、手机定位以及读取电话号码和语音邮件号码。
  • PreferencesFacade:允许访问共享偏好设置,例如获取现有偏好设置列表,以及读取、修改和添加新偏好设置。
  • SensorManagerFacade:允许跟踪传感器数据,如光线、加速度、磁场和方向。
  • SettingsFacade:暴露设备设置,并允许脚本修改设置,如屏幕超时、亮度、飞行模式、铃声音量、媒体音量和振动。
  • SignalStrengthFacade:允许监控手机信号强度。它至少需要 API 级。
  • SmsFacade:公开短信功能,允许脚本访问现有短信,标记为已读,删除,发送新短信。
  • SpeechRecognitionFacade:公开语音识别功能,允许脚本识别用户语音。
  • TextToSpeechFacade:API 4 级以上设备可用。它不赞成EyesFreeFacade。它允许脚本使用文本到语音转换技术说话。
  • ToneGeneratorFacade:生成给定数字的 DTMF 音。
  • UiFacade:通过各种对话框和菜单将 Android UI 组件暴露给脚本,以呈现内容并查询用户输入。它还允许交互式使用 HTML 页面。
  • WakeLockFacade:允许脚本保持唤醒锁,以在脚本执行期间保持 CPU 和屏幕打开。
  • WebCamFacade:允许从设备摄像头向网络传输 MJPEG 流。它至少需要 API 级。
  • WifiFacade:暴露 Wi-Fi 管理器,允许脚本查询 Wi-Fi 连接状态,搜索接入点,连接和断开 Wi-Fi 网络,并在脚本执行期间持有 Wi-Fi 锁。

有关这些外观提供的方法的完整列表,请参考位于[code.google.com/p/android-scripting/wiki/ApiReference](http://code.google.com/p/android-scripting/wiki/ApiReference)的 SL4A API 参考文档。

安装 SL4A

要下载 SL4A,请导航至 SL4A 主页[code.google.com/p/android-scripting/](http://code.google.com/p/android-scripting/)。左边的特性部分列出了 SL4A 的最新版本和解释器。在撰写本文时,SL4A 的最新版本是 R5。从特色下载中选择sl4a_r5.apk将 Android 包下载到您的机器上。SL4A 的旧版本可以在[code.google.com/p/android-scripting/downloads/list](http://code.google.com/p/android-scripting/downloads/list)的 SL4A 下载页面通过浏览废弃下载下载。

然后可以通过 ADB 安装 APK 文件。在基于 Windows 的主机上,打开命令提示符窗口。在基于 Mac OS X 和 Linux 的主机上,打开一个终端窗口。导航到下载sl4a_r5.apk文件的目录,输入adb install sl4a_r5.apk将其部署到仿真器或设备,如图图 9-1 所示。

Image

图 9-1。 从命令行使用 ADB 安装 SL4A】

你可能已经注意到了,SL4A Android 包并不太大——只有 857KB。SL4A 包只包括脚本主机和外观。除了 Android 平台提供的解释器之外,解释器不包括在内,因为 SL4A 不知道您在这一点上首选的脚本语言。根据需要,SL4A 从 SL4A 网站下载并安装解释器。

由于解释器不是从 Android Market 下载的,为了允许 SL4A 正确部署解释器,应该在设备设置中选择允许安装非市场应用”选项。在运行 Gingerbread 及以下版本的 Android 设备上,按下 Menu 键,选择设置 Image 应用,即可找到该设置。在较新的设备上,按菜单键,选择设置 Image 安全即可找到,如图图 9-2 所示。

Image

图 9-2。 设置设备允许安装非市场应用

提示:如果使用 ADB 手动部署解释器,您不需要将设备设置为允许安装非市场应用。解释器的安装分两个阶段进行。首先,SL4A 将解释器安装程序 Android 包安装到设备上,然后解释器安装程序根据目标机器架构下载一组包含实际解释器的压缩 ZIP 档案。您可以从 SL4A 网站的特色下载部分下载解释器安装程序,并使用 ADB 手动安装它们。

接下来,启动 SL4A 应用。第一次启动 SL4A 时,它会询问你是否允许收集使用统计数据,如图图 9-3 所示。您可以拒绝此请求;但是,强烈建议接受使用情况跟踪。收集使用统计数据允许 SL4A 项目将其路线图与 SL4A 用户经常使用的区域和功能相匹配。

Image

图 9-3。 SL4A 请求收集使用统计数据的许可

SL4A UI 提供了一组用于使用该应用的菜单。按菜单键展开菜单栏,如图图 9-4 所示。

Image

图 9-4。 SL4A 应用菜单栏

添加解释器

在开始编写脚本之前,您需要安装一个脚本解释器。从应用菜单栏中选择视图 Image 解释器。SL4A 将显示已安装脚本解释器的列表。SL4A 安装中只捆绑了 Shell 解释器。这个解释器提供了对 Android 设备的控制台访问。使用 Shell 解释器,您可以浏览文件系统并执行本机应用。

从应用菜单栏中,选择添加。SL4A 会显示一个可安装的解释器列表,如图图 9-5 所示。

Image

图 9-5。 SL4A 可安装解释器

从解释器列表中,选择一个您计划用于脚本的解释器。在本章中,为了演示 SL4A 的功能,我们将使用 Python 编程语言。选择 Python 解释器,SL4A 会下载 Python 解释器安装程序。在 Android 通知栏中,展开下载图标查看状态,如图图 9-6 所示。

Image

图 9-6。 Python 解释器安装程序下载通知

下载完成后,点击安装按钮开始安装 Android 包,如图图 9-7 所示。

Image

图 9-7。 为 Android 安装 Python

Android 包只包含解释器安装程序,不包含实际的脚本解释器。安装完软件包后,单击 Open 按钮启动解释器安装程序。然后点击安装按钮,部署实际的解释器应用,如图图 9-8 所示。

Image

图 9-8。 安装解释器应用

安装程序将根据设备架构下载 Python 解释器,然后将其部署在设备上。为了高效地使用设备内存,SL4A 解释器的资源部分安装在外部存储中,实际的二进制文件进入设备的内存。如果您在仿真器上运行 SL4A,请确保遵循第五章中提供的配置设置。如果安装失败,请检查以确保两个存储位置都有足够的空间。

成功安装后,“安装”按钮会变成“卸载”按钮。回到 SL4A 应用,您会看到 Python 2.6.2 被添加到可用解释器列表中。

执行脚本

SL4A 支持多种开发和执行脚本的方式。在本节中,我们将详细回顾这些方法。

在设备上本地执行脚本

安装了您喜欢的脚本语言的必要解释器后,SL4A 就可以直接在设备上执行脚本了。SL4A 为在本地开发和运行脚本提供了两个选项:交互式控制台模式和脚本编辑器。

使用交互式控制台

当您从可用解释器列表中选择一个解释器时,将启动交互式控制台模式。该模式占据整个屏幕,并提供对实际脚本解释器的控制台访问,如图图 9-9 所示。解释器以横向模式运行,以便更好地利用可用的显示区域。如果在模拟器上运行 SL4A,可以使用组合键 Ctrl+F11 或数字键盘上的键 7 将模拟器显示旋转到横向模式。使用设备的键盘,您可以开始输入命令,这些命令将以交互方式执行。

Image

图 9-9。 在虚拟控制台上交互运行的 Python 解释器

交互模式的优势在于,您可以逐步执行脚本命令,而无需准备一个完整的脚本。这是一个很好的 API 实验工具。

使用脚本编辑器

除了交互式控制台模式,SL4A 还提供了一个用于编辑和存储脚本的脚本编辑器。从交互式控制台模式,使用返回键,返回到脚本视图,并从应用菜单栏中选择添加。SL4A 将根据已安装的解释器显示可用的脚本类型列表,如图图 9-10 所示。

Image

图 9-10。 启动 Python 脚本编辑器

从列表中选择 Python 2.6.2,启动 Python 脚本编辑器,如图图 9-11 所示。编辑器的顶部窗格允许您命名脚本文件。底部窗格是脚本编辑器。脚本编辑器区域将根据所选的脚本语言自动填充样板脚本代码。

Image

图 9-11。 Python 脚本编辑

使用编辑器,您可以开发和测试您的脚本,将它们存储在您的设备上,并通过电子邮件共享它们。这些任务可通过脚本编辑器应用菜单栏访问,如图图 9-12 所示。

Image

图 9-12。 脚本编辑器应用菜单栏

API 浏览器是脚本编辑器提供的最强大的功能之一。如图 9-13 所示,它允许您浏览 SL4A 外观提供的方法,以简化脚本开发。

Image

图 9-13。 脚本编辑器 API 浏览器列表可用方法

远程执行脚本

SL4A 不仅仅是一个基于设备的脚本环境。它还支持远程执行在主机上开发的脚本。这允许您利用集成开发环境,同时仍然在 Android 设备上执行您的脚本。

通过 ADB 启动脚本

在主机上开发的脚本可以被复制到 Android 设备上,然后通过命令行上的 ADB 直接执行。这允许自动化脚本的部署和执行。

要将脚本复制到 Android 设备上的 SD 卡,在基于 Windows 的主机上,打开命令提示符窗口,或者在基于 Mac OS X 和 Linux 的主机上,打开终端窗口,并输入以下 ADB 命令:

adb push script.py /sdcard/sl4a/scripts

这将把script.py文件放到 SL4A 的scripts目录中。

将脚本复制到设备上后,您可以根据需要在前台或后台执行它们。要在后台执行脚本,请在主机的命令提示符下发出以下 ADB 命令,所有命令都在一行中:

adb shell am start -a com.googlecode.android_scripting.action.LAUNCH_BACKGROUND_SCRIPT -n com.googlecode.android_scripting/.activity.ScriptingLayerServiceLauncher -e com.googlecode.android_scripting.extra.SCRIPT_PATH /sdcard/sl4a/scripts/script.py

活动管理器(am)使用LAUNCH_BACKGROUND_SCRIPT意图和脚本路径启动 SL4A 应用。

为了在前台执行该脚本,意图发生了变化,ADB 命令变成了以下内容,全部在一行中:

adb shell am start -a com.googlecode.android_scripting.action.LAUNCH_FOREGROUND_SCRIPT -n com.googlecode.android_scripting/.activity.ScriptingLayerServiceLauncher -e com.googlecode.android_scripting.extra.SCRIPT_PATH /sdcard/sl4a/scripts/script.py

使用远程程序调用

使用在主机上物理运行的脚本解释器,可以在主机上托管和执行脚本。这允许脚本受益于主机的高 CPU 能力及其广泛的调试环境,同时仍然能够远程执行 Android 特定的操作。Android 相关的 API 调用是通过 Android 设备上的 RPC 执行的。

默认情况下,SL4A 不侦听远程 RPC 连接。SL4A RPC 服务器需要首先启动,以便允许主机上运行的脚本与 SL4A 通信。要启动服务器,从应用菜单栏中选择视图 Image 口译员以查看口译员列表。然后在应用菜单栏中选择启动服务器,如图图 9-14 所示。

Image

图 9-14。 启动解释器视图中的服务器菜单项

接下来,SL4A 将要求您选择您想要启动的服务器类型,如图图 9-15 所示。SL4A 支持两种服务器类型:

  • Private: 此服务器监听环回网络适配器,并且只能从设备内部或通过 USB 连接到设备的主机到达。
  • 公共:此服务器监听 Wi-Fi 或数据网络适配器,并且可以通过公共网络访问。

当您只从一台与 Android 设备物理连接的主机上执行脚本时,建议使用私有服务器。

Image

图 9-15。 选择脚本服务器类型

SL4A 将启动服务器,并将其图标放在通知栏上,以指示脚本服务器处于活动状态。如果设备上运行多台服务器,SL4A 将在一个圆圈内的图标旁显示活动服务器的数量,如图图 9-16 所示。

Image

图 9-16。 SL4A 活动服务器显示在通知栏上

您可以选择 SL4A 图标并拖动它来展开通知。点击通知将带您进入脚本监视器活动,其中 SL4A 列出了正在运行的服务器,如图 9-17 所示。服务器列表提供了所有活动服务器的地址、端口号、解释器进程 ID 和持续时间。您可以使用地址和端口号远程连接到服务器。

Image

图 9-17。 脚本监控列出活动服务器的活动

使用连接的设备

要从通过 USB 电缆直接连接到 Android 设备的主机远程执行脚本,请启动一个私有服务器。展开通知图标以启动脚本监视器来查找端口号。环回设备上的端口是开放的,只能从设备本身到达。为了从主机连接到这个端口,需要通过 ADB 转发。

要设置端口转发,在基于 Windows 的主机上,打开命令提示符窗口,或者在基于 Mac OS X 和 Linux 的主机上,打开终端窗口,发出以下命令(用服务器端口号替换<*server port*>):

adb forward tcp:9999 tcp:<*server port*>

ADB 开始监听主机上的 TCP 端口 9999,并将通信转发给设备上运行的脚本服务器。

在主机上运行的 SL4A RPC 模块需要知道脚本服务器端口号,以便与服务器通信。在脚本中启动 Android 对象时提供端口号:

# Connect to port 9999 droid = android.Android((‘localhost’, 9999))

如果不想更改脚本文件,可以通过环境变量注入端口号。如果没有隐式提供目的主机和端口号,SL4A RPC 模块将读取AP_HOSTAP_PORT环境变量。在启动解释器之前,可以在命令行上或通过系统环境变量列表设置这些环境变量。

使用网络设备

要从可以通过网络到达 Android 设备的主机远程执行脚本,请启动一个公共服务器。展开通知图标以启动脚本监视器来查找公共 IP 地址和端口号。在主机上运行的 SL4A RPC 模块需要知道服务器端口号,以便与之通信。在脚本中启动 Android 对象时,提供 IP 地址和端口号:

# Connect to IP adress 10.0.2.15 and port number 47176 droid = android.Android((’10.0.2.15’, 47176))

如果您不想更改脚本文件,可以通过环境变量注入端口号,如前一节所述。

添加用户界面

根据它们的功能,脚本有时可能需要与用户交互。尽管脚本语言支持控制台上基于文本的输入和输出,但移动用户更熟悉基于图形和触摸的 UI。SL4A 提供了一组 UI 外观,允许开发人员从他们的脚本中使用 Android GUI。SL4A 提供了基于对话框、基于 web 和全屏的原生 UI 选项。在这一节中,我们将使用每一种 UI 类型实现一个简单的计算器应用,来演示如何从脚本中使用这些外观。

基于对话框的用户界面

与用户互动最简单的方式是通过对话框和菜单。UI facade 附带了一组用于最常见任务的预定义对话框。我们的计算器应用的第一个版本将使用这个外观和用 Python 脚本语言编写的脚本。我们从初始化 SL4A Android RPC 客户端开始。

注意:如果您是 Python 新手,请确保您复制了所示代码的缩进。Python 依靠代码缩进来定义代码段的边界。

`#

SL4A Dialog based UI

@author Onur Cinar #

import android

Initialize the SL4A Android RCP Client

droid = android.Android()`

得到这两个数字

我们希望只要用户想要进行计算,我们的脚本就会一直执行,因此我们将它包含在一个无限循环中:

`# Title of our dialogs
title = "Calculator"

We will calculate recursively

while True:`

该脚本首先通过输入对话框要求输入第一个数字:

`    # Get the first number from the user
    result = droid.dialogGetInput(title, "Enter the first number:").result

# Check if user answered it
    if result is None:
        break

# Convert the text input to an integer
    first = int(result)`

正如你在例子中看到的,检查对话框的结果总是好的,因为用户可能不提供任何输入就取消对话框。

执行脚本时,显示输入对话框,如图图 9-18 所示。只要用户提供一个数字,应用就会循环,并在用户选择 Cancel 按钮时停止。

Image

图 9-18。 对话框为第一个数字

然后,脚本使用以下方法要求输入第二个数字:

`    # Get the second number from the user
    result = droid.dialogGetInput(title, "Enter the second number:").result

# Check if user answered it
    if result is None:
        break

# Convert the text input to an integer
    second = int(result)`

做手术

现在这两个数字都可用了,脚本接下来使用列表对话框询问操作:

`# List of possible operations
    operations = [ "+", "-", "*", "/" ]

# Open a generic dialog
    droid.dialogCreateInput(title, "Select operation")

# Set the items to make it a list
    droid.dialogSetItems(operations)

# Make the dialog visible
    droid.dialogShow()

# Get the user's response
    result = droid.dialogGetResponse().result

# Check if user answered it
    if (result is None) or (result.has_key("canceled")):
        break # Get the index of selected operation
    index = result["item"]

# Find the operation at that index
    operation = operations[index]`

我们在这里使用的获取数字的dialogGetInput方法是一种获取用户输入的便利方法。UI 外观提供了多种方法来塑造对话框。为了呈现可用选项的列表,我们从通过dialogCreateInput方法调用创建的通用输入对话框开始,然后我们通过dialogSetItems方法调用提供条目列表以生成列表对话框。自定义对话框后,我们调用dialogShow方法使其可见。dialogGetResponse方法阻塞,直到用户响应对话框,并返回响应。执行脚本时,出现操作列表对话框,如图图 9-19 所示。

Image

图 9-19。 显示计算器操作的对话框

根据用户的选择,脚本使用给定的两个数字执行必要的操作。

`# Do the calculation
    solution = {
      "+" : first + second,
      "-" : first - second,
      "*" : first * second,
      "/" : first / second
    }[operation]

# Show the solution and ask if user wants
    # to do more calculations
    droid.dialogCreateAlert(title, "The solution is %d. New calculation?" %
solution)

# Set the answer options
    droid.dialogSetPositiveButtonText("Yes")
    droid.dialogSetNegativeButtonText("No") # Show dialog
    droid.dialogShow()

# Get the user's response
    result = droid.dialogGetResponse().result

# Check if user answered it
    if (result is None) or (result.has_key("canceled")):
        break

# If user answer saying no
    option = result["which"]
    if option == "negative":
        break`

显示结果

结果以警告对话框的形式呈现给用户,底部有 Yes 和 No 按钮,如图图 9-20 所示。用户可以选择这些按钮中的任何一个来控制脚本的流程。该脚本首先检查对话框是否被关闭或取消。否则,根据所点击的按钮,结果指示用户的响应是肯定的还是否定的。如果用户想继续计算,脚本会重复相同的流程。

Image

图 9-20。 对话框显示计算结果

在终止之前,脚本显示了一个使用makeToast方法调用的 toast:

# Terminating script droid.makeToast("Thank you")

结束语“谢谢”祝酒词如图 9-21 所示。

Image

图 9-21。 脚本终止上的祝酒辞

基于对话框的界面需要最少的编程。然而,从用户的角度来看,在这个 UI 中输入信息和导航比在原生 Android 应用中更困难。SL4A 没有提供任何功能来设计这些对话框的样式,以定制应用的外观。基于对话框的界面是不需要太多用户交互的快速自动化任务的最佳选择。

基于网络的用户界面

SL4A 提供了基于 web 的 UI 支持,作为基于对话框的界面的替代。基于 Web 的 ui 在嵌入式 web 浏览器中运行,并且可以使用浏览器已经提供的 CSS 支持来设计样式。与原生 Android 应用类似,使用基于 web 的 UI,多个 UI 组件可以共享同一个屏幕。从用户的角度来看,导航比基于对话框的界面要容易得多。

使用基于 web 的界面并不意味着所有的脚本都必须使用 JavaScript 和 HTML 编写。SL4A 提供了广泛的事件队列机制,使得开发人员可以将基于 JavaScript 的 web 界面与任何脚本语言混合使用,作为应用的后端。这种架构非常类似于目前开发基于 web 的应用的方式。例如,您可以使用 SL4A,使用基于 JavaScript/HTML 的 UI 和基于 Ruby 的后端代码开发您的 Android 应用。

通过 HTML 和 CSS 的用户界面布局

在本节中,我们将使用基于 web 的 UI 重做计算器示例。整个 UI 将使用通用 HTML 代码实现。清单 9-1 显示了webview.html UI 文件的源代码。

清单 9-1。【webview.html 档案】??

`

<head>     <title>Calculator</title>

    


        Calculator


        

<label for"second">Second number:
        


        


        


    

`

你可能已经注意到了,在代码的顶部,我们使用 CSS 来设计 UI 的样式。应用将要访问或更新的每个 HTML 元素都有一个惟一的 ID。

通过 JavaScript 操作用户界面

UI 的动态部分是通过 JavaScript 实现的。SL4A 也为 JavaScript 提供了一个 Android RPC 代理客户端。这个 RPC 客户端允许基于 web 的 UI 与 SL4A 和平台进行通信。

`    

`
使用 Python 的应用逻辑

应用的后端使用 Python 脚本语言作为webview.py来实现。与基于对话框的界面示例类似,Python 代码从初始化 Android RPC 代理客户端开始。

`#

SL4A WebView based UI

@author Onur Cinar

import android

Initialize the SL4A Android RCP Client

droid = android.Android()`

JavaScript/HTML 代码通过使用webViewShow方法调用来加载:

# Show the HTML page droid.webViewShow("file:///sdcard/sl4a/scripts/webview.html")

在进行此方法调用之前,HTML 文件应该在设备上可用。使用 ADB,将文件推送到 Android 设备。

adb push webview.html /sdcard/sl4a/scripts/

注意:由于 Android 模拟器中的一个已知错误,您只能在 Android 设备上运行基于 web 的 UI 示例。不支持 Android 模拟器。

在这个例子中,HTML 文件位于 SL4A 默认脚本目录/sdcard/sl4a/scripts;但是,HTML 文件可以位于设备上的任何位置。SL4A 启动嵌入式 web 浏览器并加载基于 web 的 UI,如图图 9-22 所示。

Image

图 9-22。嵌入式浏览器内的计算器界面

如 JavaScript 代码所示,当用户单击 Calculate 按钮时,应用的 web 部分会发布一个带有数字和操作的calculate事件。Python 代码通过 SL4A 事件队列接收该事件:

`# We will calculate recursively
while True:
    # Wait for calculate event
    result = droid.eventWaitFor("calculate").result

# Make sure that event has data
    if result is not None:
        # Data comes as a comma separated list of values
        request = result["data"].split(",")

# Extract parameters from request array
        first = int(request[0])
        second = int(request[1])
        operation = request[2]         # Calculate solution
        solution = {
            "+" : first + second,
            "-" : first - second,
            "*" : first * second,
            "/" : first / second
        }[operation]`

该脚本可以在设备或主机上运行。SL4A RPC 客户端允许脚本访问主 SL4A 事件队列来接收这个请求。收到请求后,它首先提取参数并进行请求的计算。计算的解也通过 SL4A 事件队列发送到基于网络的接口:

        # Post the solution to event queue         droid.eventPost("solution", str(solution))

已经注册处理solution事件的 JavaScript 代码接收解决方案并更新 UI,如图图 9-23 所示。

Image

图 9-23。web 界面中显示的解决方案

与基于对话框的用户界面相比,基于网络的用户界面更加灵活。因为 Android RPC 代理客户端也可以通过 web 代码访问,所以整个应用也可以用 JavaScript 开发。

全屏幕用户界面

全屏 UI 允许在脚本中使用基于 XML 的 Android 布局文件。在本节中,我们将使用全屏 UI 重做计算器示例。

该脚本首先初始化 Android RPC 代理客户端,然后通过一个字符串变量定义 Android 布局 XML。

`#

SL4A Full screen UI #

@author Onur Cinar

import android

Initialize the SL4A Android RCP Client

droid = android.Android()

XML layout

layout = """

<Spinner
        android:id="@+id/operation"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content" /

droid.fullSetList("operation", operations)`

该脚本等待click事件,然后检查被点击组件的 ID 以确保它是 Calculate 按钮。

`# We will calculate recursively
while True:
    # Wait for click event
    event = droid.eventWaitFor("click").result

# Check if it is the calculate button
    if event["data"]["id"] == "calculate":`

该脚本首先使用fullQueryDetail方法找到 UI 组件,然后获取第一个和第二个数字的值。如果没有提供数字,脚本不会进行计算。

`        # Get the first number
        field = droid.fullQueryDetail("first").result["text"]

# Check if field is empty
        if field == "":
            continue

# Convert field to integer
        first = int(field)

# Get the first number
        field = droid.fullQueryDetail("second").result["text"]

# Check if field is empty
        if field == "":
            continue

# Convert field to integer
        second = int(field)`

该脚本调用fullQueryDetail方法来查找操作微调器,然后使用selectedItemPosition属性来确定所选择的操作。

`        # Get the operation index
        index =
int(droid.fullQueryDetail("operation").result["selectedItemPosition"])

# Get operation
        operation = operations[index]

# Do the calculation
        solution = {
          "+" : first + second,
          "-" : first - second,
          "*" : first * second,
          "/" : first / second
        }[operation]`

然后使用fullSetPropery方法设置解决方案文本视图的text属性来显示结果。

        # Show solution         droid.fullSetProperty("solution", "text", "Solution is %d" % solution)

图 9-25 显示了结果显示的一个例子。与基于对话框和基于 web 的替代方式相比,全屏 UI 是唯一一种提供平台外观和感觉的方式。它允许脚本受益于所有现有的 Android UI 组件。

Image

图 9-25。 更新显示显示结果

将脚本打包成 apk

SL4A 还可以用来将一个脚本打包成一个可安装的 Android 包。这允许像普通的 Android 应用一样分发脚本。可安装的 Android 包只包含脚本和资源;脚本解释器仍然应该单独安装在目标设备上。在本节中,我们将介绍设置这样一个 Android 项目的步骤,集成脚本文件并将其部署到 Android 设备上。

下载项目模板

为了便于将脚本打包成可安装的 Android 包,SL4A 项目网站提供了一个 Android 项目模板。将浏览器指向[android-scripting.googlecode.com/hg/android/script_for_android_template.zip](http://android-scripting.googlecode.com/hg/android/script_for_android_template.zip)并下载模板项目作为压缩的 ZIP 存档文件。

在将模板项目导入 Eclipse 之前,您需要确保安装了 Android API level 4。如果没有,使用 Android SDK 管理器下载它,如第五章所述。或者,您可以根据目标平台的 API 级别来更改项目的构建目标。为此,右键单击项目并选择属性来启动属性对话框。从左侧列表中选择 Android,将项目构建目标更改为大于等于 4 的 API 级别。

现在我们准备将模板项目导入 Eclipse。打开 Eclipse,从顶部菜单栏选择文件 Image 导入… ,启动导入向导,如图图 9-26 所示。

Image

图 9-26。 Eclipse 项目导入向导

从源代码列表中,选择 Existing Projects into Workspace,然后单击 Next 按钮。Eclipse 将询问项目文件的位置。选择选择存档文件单选按钮,使用浏览按钮,指向您下载的script_for_android_template.zip文件,如图图 9-27 所示。然后单击 Finish 按钮开始导入项目。

Image

图 9-27。 从 ZIP 存档文件导入项目

配置项目

Eclipse 将以名称ScriptForAndroidTemplate将模板项目导入到您的工作区中。要重命名项目,右键单击其名称,然后从上下文菜单中选择重构 Image 重命名,启动重命名 Java 项目向导,如图 9-28 所示。

Image

图 9-28。重命名模板项目

除了重命名项目之外,我们还应该更改包名,以防止在部署应用时出现任何命名冲突。在重命名 Java 包本身之前,我们需要重命名AndroidManifest.xml文件中的包。打开AndroidManifest.xml文件,相应修改 Android 包名。

<?xml version="1.0" encoding="utf-8"?> <manifest     package="com.apress.chapter9"     android:versionCode="1"     android:versionName="1.0"     xmlns:android="http://schemas.android.com/apk/res/android">

接下来,要重命名 Java 包,右击包名com.dummy.fooforandroid并从上下文菜单中选择重构Image重命名来启动重命名包向导。在新名称字段中,输入新的包名com.apress.chapter9,如图图 9-29 所示。

Image

图 9-29。 改名包

确保选中“更新引用”选项,然后单击“预览”按钮。Eclipse 将重命名这个包,然后相应地更新所有的引用。在对文件进行任何更改之前,Eclipse 将显示所需的更改,如图 9-30 所示。验证更改后,单击“确定”按钮将更改应用到项目文件。

Image

图 9-30。 修改需要重命名的包

尽管模板项目附带了预配置的一切,但是出于安全原因,除了 Internet 访问权限之外,AndroidManifest.xml文件中的所有权限请求都被有意地注释掉了。取消注释正确执行脚本所需的权限。

合并脚本文件

使用 Package Explorer 视图,展开res目录,然后展开raw目录以显示原始项目资源。模板项目将实际的脚本文件作为原始项目资源保存。

尽管模板项目提供的脚本文件是 Python 脚本,但模板项目实际上可以包含 SL4A 支持的任何类型的脚本。例如,如果您正在使用 Ruby 脚本语言,删除script.py引用并将您的脚本作为script.rb插入到原始资源中。这里的要点是,脚本文件应该被命名为script,并带有适合所用脚本语言的文件扩展名。如果您喜欢用不同的方式重命名它,您将需要相应地修改Script类中的ID静态字段:

`package com.apress.chapter9;

public class Script {
    …
    public final static int ID = R.raw.script;
    …
}`

SL4A 引擎将这个脚本文件用作主入口点。Android 应用可能包含多个脚本文件,作为主脚本文件的模块或依赖项。这些额外的脚本文件也应该添加到原始资源中。当应用启动时,在执行脚本文件之前,它将原始资源中的所有脚本文件提取到files目录中。

部署和运行应用

该应用可以像普通的 Android 应用一样部署和运行,如第五章所述。当它第一次启动时,SL4A 根据所使用的脚本语言在平台上寻找脚本解释器,如果它不可用,就自动安装一个。这仍然需要将 Android 设备配置为允许安装非市场应用。

在撰写本文时,SL4A 不直接支持将解释器与独立的 Android 应用捆绑在一起。尽管它没有得到官方支持,但是可以通过组合 SL4A 源代码中的必要包来修改模板应用,以包含必要的解释器。

总结

本章提供了使用 SL4A 开源库的 Android 脚本的快速介绍。我们查看了 SL4A 应用的架构,并研究了 SL4A 提供的解释器和外观。我们还探索了在 Android 设备上本地和远程执行脚本的不同方法。然后,我们介绍了 SL4A 提供的 UI 选项,并通过一个简单的计算器示例比较了它们的用法。最后,我们演示了如何将脚本打包成一个独立的应用,该应用可以作为一个普通的 Android 应用进行分发。

十、项目:使用 HTML 和 JavaScript 的电影播放器

在这一章中,我们将使用 SL4A 框架用 HTML 和 JavaScript 重新实现我们在第五章中开发的电影播放器应用。由于 SL4A 框架没有提供电影播放器应用所需的所有功能,我们将更进一步,将一个新的自定义外观集成到 SL4A 框架中。我们将把生成的应用打包成一个独立的 Android 脚本应用,可以像普通的 Android 应用一样进行分发和部署。

获取 SL4A 源代码

我们的电影播放器应用需要访问媒体商店内容提供商,以便查询设备上的电影文件列表。现有的 SL4A 版本 R5 外观都不提供对媒体商店的访问。作为这个例子的一部分,我们将开发一个新的外观,使脚本应用能够从平台获取必要的信息。SL4A 框架目前不支持新外观的动态发现,并且仅限于使用内置外观。

T 要添加新的外观,SL4A 框架源代码需要稍微修改一下。为了实现这一点,在本节中,我们将从 SL4A 源代码库中检查 SL4A 源代码。

准备工作空间

SL4A R5 源代码要求在主机上安装 Android API level 3、4、7 和 8 平台 SDK。如第五章所述,从顶部菜单栏选择窗口 Image ** Android SDK 管理器**启动 SDK 管理器,并将这些 SDK 下载到主机上。

SL4A 项目依赖于 Eclipse 中预定义的ANDROID_SDK classpath 变量来编译。要定义这个变量,打开 Eclipse Preferences 对话框,如前面章节所述。使用搜索框,过滤类路径变量的首选项列表。然后单击“新建”按钮启动“新变量输入”对话框。如下定义新的变量条目,如图 10-1 所示:

  • 名称:设置名称为ANDROID_SDK
  • Path: 变量应该指向 Android SDK 在主机上的位置。使用右边的文件夹按钮,选择 Android SDK 目录。

Image

图 10-1。 为 SL4A 源代码添加 ANDROID_SDK 类路径变量

设置 Java 编译器合规级别

SL4A 源代码基于 Java 源码 1.6 版。如果您已经按照第五章中的建议安装了 JDK 版本 6,则不需要额外的配置。否则,为了编译 SL4A 源代码,需要将 workspace 编译器兼容级别更改为 1.6。要更改符合性级别,请打开 Eclipse Preferences 对话框,并使用搜索框过滤编译器的首选项列表。选择 Java 编译器首选项,将兼容级别改为 1.6,如图图 10-2 所示。

Image

图 10-2。 改变工作区 Java 编译器兼容级别

安装水星

SL4A 源代码通过 Google Code 网站作为 Mercurial 源代码库。要将 SL4A 源代码签出到主机,需要安装 Mercurial 和 Mercurial Eclipse 插件。在 Mac OS X 和 Linux 平台上,需要在下载 Mercurial Eclipse 插件之前安装 Mercurial 二进制文件。在本节中,我们将介绍在这些平台上安装 Mercurial 二进制文件的过程。

在 mac os x 上安装 mercurial

使用您的 web 浏览器,导航到位于[mercurial.selenic.com](http://mercurial.selenic.com)的 Mercurial 下载站点,下载 Mac OS X 的二进制文件。如图 10-3 所示,Mercurial 网站会自动检测您的操作系统,并为 Mercurial 安装程序提供一个下载按钮。

Image

图 10-3。Mac OS X 平台的 Mercurial 下载页面

单击下载按钮,将 Mercurial 安装程序 ZIP 存档文件下载到您的主机上。接下来,进入你的Downloads文件夹。根据 Mac OS X 的版本,ZIP 文件可能会自动解压缩,或者您可能需要手动解压缩。此 ZIP 文件包含 Mercurial 二进制文件的 Mac OS X 安装包。

双击可安装软件包文件以启动 Mercurial 安装程序,该程序将引导您完成安装过程。安装完成后,可以在/usr/local/bin/hg找到 Mercurial 二进制文件。要验证 Mercurial 安装,请打开一个终端窗口,并在命令提示符下输入hg。如果你能看到如图图 10-4 所示的 Mercurial 基本命令列表,你的 Mercurial 安装就成功了。现在可以继续安装 Mercurial Eclipse 插件了。

Image

图 10-4。 验证 on Mac 上的 Mercurial 安装

在 Linux 上安装 Mecurial

Mercurial 二进制文件可以通过大多数 Linux 发行版上的应用库获得。打开一个终端窗口,根据您的 Linux 发行版,执行相应的安装命令:

  • debian/Ubuntu:??]
  • 开口:??sudo zipper in mercurial
  • Fedora: sudo yum install mercurial
  • Gentoo:

根据您的 Linux 发行版,Mecurial 安装目录可能会有所不同。要找到 Mercurial 二进制文件的位置,请打开终端窗口,并在命令提示符下输入which hg。如果你能看到 Mercurial 的安装目录,如图图 10-5 所示,你的 Mercurial 安装成功了。

Image

图 10-5。 验证 Linux 上的 Mercurial 安装

安装水星 Eclipse 插件

要安装 Mecurial Eclipse 插件,在 Eclipse 中,从顶部菜单栏选择帮助 Image 安装新软件… 来启动安装向导。Mercurial 不是官方 Eclipse 软件站点的一部分。点击添加按钮,定义位置为[cbes.javaforge.com/update](http://cbes.javaforge.com/update)的 Mercurial Eclipse 软件站点,如图图 10-6 所示。

Image

图 10-6。 给 Eclipse 添加 Mercurial 软件站点

添加新的软件站点后,Eclipse 将获取 Mercurial 插件列表,并在安装向导中显示它们。此过程可能需要一些时间,具体取决于您的网络连接。从这个插件列表中,选择 Mercurial Eclipse。对于 Windows 平台,同样选择 Mercurial 的 Windows 二进制,如图图 10-7 所示。

Image

图 10-7。 选择 Mercurial 插件

单击“下一步”按钮继续安装。Eclipse 将列出将要安装的插件。单击“完成”按钮开始安装。

检查 SL4A 源代码

安装好 Mecurial 和 Mecurial Eclipse 插件后,我们就可以检查 SL4A 源代码了。从顶部菜单栏选择文件 Image 新建 Image 其他… ,展开 Mercurial 类别,选择克隆已有的 Mercurial 库,如图 10-8 所示。

Image

图 10-8。 选择克隆现有的 Mercurial 库

在向导的 URL 字段中,输入[code.google.com/p/android-scripting/](https://code.google.com/p/android-scripting/)作为存储库位置,如图图 10-9 所示,然后点击 Next 按钮。Mercurial 是一个分布式源代码控制系统,这意味着它会将整个存储库克隆到主机上。此过程可能需要几分钟,具体取决于您的网络连接。

Image

图 10-9。 设置用于克隆的存储库位置

SL4A 的最新官方版本是 R5。在撰写本文时,SL4A 版本 R5 在源代码库中还没有标记。为了将代码库签出到 R5 版本,切换到修订选项卡,输入 1214 作为修订号,如图图 10-10 所示。单击“下一步”按钮继续。

Image

图 10-10。 选择 SL4A R5 改版

Mercurial 克隆库向导将显示 SL4A 库中所有项目的列表,如图 10-11 所示。虽然示例项目不会使用所有的 SL4A 项目,但是选择将除了DocumentationGenerator之外的所有项目导入 Eclipse。

Image

图 10-11。?? 选择要导入的项目

Eclipse 将自动开始构建所有的 SL4A 项目。检查问题视图并解决任何报告的问题。

电影播放器脚本项目

正如上一章所讨论的,SL4A 提供了一个模板项目,用于将脚本打包成独立的 Android 包。模板项目的源代码称为ScriptForAndroidTemplate。示例项目将使用模板项目作为其基础。

克隆模板项目

我们将在不同的项目名称下克隆模板项目,而不是直接修改模板项目。选择ScriptForAndroidTemplate项目,右键单击,从上下文菜单中选择复制。再次右击并从上下文菜单中选择粘贴来启动复制项目向导。将新项目命名为MoviePlayerScript,如图图 10-12 所示。

Image

图 10-12。 将模板项目克隆为电影播放器脚本

Eclipse 克隆整个项目设置,包括 Mercurial 元数据。因为新项目不是 SL4A 源代码库的一部分,所以右键单击项目名称并选择TeamImageDisconnect将其从 Mercurial 中分离。

链接到 SL4A 框架代码

MoviePlayerScript项目是 SL4A 模板的一个完全相同的克隆。SL4A 模板项目是一个独立的项目,除了解释器之外,没有任何外部依赖性。

SL4A 框架代码被预编译,并作为一个 JAR 文件与 SL4A 模板项目一起提供。因为我们将在这个示例项目中修改 SL4A 框架,所以我们需要删除这个 JAR 文件,并使该项目直接依赖于 SL4A 框架项目。使用 Package Explorer 视图,展开MoviePlayerScript项目下的libs目录,并删除script.jar文件。在删除文件之前,Eclipse 会显示一个确认对话框。

接下来,右键单击项目并选择首选项来启动项目首选项对话框。从左窗格中选择 Java 构建路径,然后切换到库选项卡。使用移除按钮,将script.jar从项目构建路径中移除,如图图 10-13 所示。

Image

图 10-13。 从项目中移除预编译的 SL4A 库

为了使MoviePlayerScript项目直接依赖于 SL4A 框架,切换到 Projects 选项卡并单击 Add 按钮。如图 10-14 所示,选择添加BluetoothFacadeCommonInterpreferForAndroidScriptingLayerSignalStrengthFacadeTextToSpeechFacadeUtilsWebCamFacade。然后单击确定按钮保存选择。

Image

图 10-14。 为 SL4A 框架选择所需项目

除了依赖这些项目之外,这些项目的输出还应该与MoviePlayerScript项目打包在一起,以便在 Android 设备上执行。为此,切换到订单和导出选项卡,并选择相同的导出项目列表,如图图 10-15 所示。Eclipse 将重新构建项目。此时,尝试在您的 Android 设备上运行项目,以确保项目配置成功。

Image

图 10-15。 标注 SL4A 项目出口

重命名项目包

由于MoviePlayerScript项目是 SL4A 模板项目的克隆,它共享相同的 Android 包名。为了防止在部署MoviePlayerScript项目时出现任何可能的冲突,打开AndroidManifest.xml文件并将包名改为com.apress.movieplayerscript。要重命名 Java 包,右击com.dummy.fooforandroid包并选择重构 Image 重命名

添加电影外观

为了提供对媒体商店的访问,需要开发新的外观并将其添加到 SL4A 框架中。为了最小化实际 SL4A 框架代码的变化量,我们将为外观实现创建一个单独的项目。

从顶部菜单栏选择文件 Image 新建 Image ** Java 项目**,并将 Java 项目命名为MovieFacade,如图图 10-16 所示。点击Next按钮继续。

Image

图 10-16。 创建电影艺术学院项目

在下一个屏幕上,选择“项目”选项卡,并添加 Common 和 Utils 项目作为项目依赖项,如图 10-17 所示。MovieFacade将使用这些项目中的组件作为 SL4A 框架的一部分。

Image

图 10-17。 添加 MovieFacade 项目依赖

MovieFacade也将使用来自 Android 框架的组件;但是,它不是一个 Android 项目。我们需要将 Android 框架库添加到项目中。切换到库选项卡,并单击添加变量…按钮。在列表中选择 ANDROID_SDK,点击编辑按钮将其值改为ANDROID_SDK/platforms/android-7/android.jar,如图图 10-18 所示。根据您将在外观中使用的 Android 功能,您可以用所需的适当 API 级别替换android-7。点击 OK 按钮保存变量,然后点击 Finish 按钮将库更改应用到项目中。

Image

图 10-18。 将 Android 框架作为库添加到 movie cade

创建 MovieFacade 类

为了在 SL4A 框架中充当门面,MovieFacade项目需要扩展com.google.android_scripting.jsonrpc.RpcReceiver类。选择MovieFacade项目,从顶部菜单栏中选择文件 Image 新建 Image 。在com.apress.movieplayerscript包中定义MovieFacade类,如图图 10-19 所示。

Image

图 10-19。 定义电影学院类

MovieFacade将包含一个暴露给脚本的方法moviesGet。SL4A 希望使用必要的 RPC 属性对公开的方法进行注释。SL4A 通过com.googlecode.android_scripting.rpc包提供了以下 RPC 属性:

  • Rpc:该属性用于将方法标记为通过 RPC 公开。它还提供了该方法的简要文档,包括其返回值。
  • RpcParameter:该属性用于记录方法的参数。
  • RpcOptional:该属性用于将参数标记为可选。
  • RpcDefault:该属性用于标记有默认值的参数。
  • RpcMinSdk:该属性用于指定执行该方法所需的最低 Android SDK 级别。
  • RpcStartEvent:该属性用于标记启动事件生成的方法。
  • RpcStopEvent:该属性用于标记终止事件生成的方法。

MovieFacade类的源代码显示在清单 10-1 中。

清单 10-1。MovieFacade.java档案

`package com.apress.movieplayerscript;

import java.util.LinkedList;
import java.util.List;

import org.json.JSONException;
import org.json.JSONObject;

import android.app.Service;
import android.content.ContentResolver;
import android.database.Cursor;
import android.provider.MediaStore;
import android.util.Log;

import com.googlecode.android_scripting.facade.FacadeManager;
import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
import com.googlecode.android_scripting.rpc.Rpc;

/**
 * Movie facade.
 *
 * @author Onur Cinar
 /
public class MovieFacade extends RpcReceiver {
    /
* Log tag. */
    private static final String LOG_TAG = "MovieFacade";

/** Service instance. */
    private final Service service;

/**
     * Constructor.
     *
     * @param manager
     *            facade manager.
     */
    public MovieFacade(FacadeManager manager) {
        super(manager);

// Save the server instance for using it as a context
        service = manager.getService();
    }

@Override
    public void shutdown() {

} /**
     * Gets a list of all movies.
     *
     * @return movie list.
     * @throws JSONException
     */
    @Rpc(description = "Returns a list of all movies.", returns = "a List of
movies as Maps")
    public List moviesGet() throws JSONException {
        List movies = new LinkedList();

// Media columns to query
        String[] mediaColumns = { MediaStore.Video.Media._ID,
                MediaStore.Video.Media.TITLE, MediaStore.Video.Media.DURATION,
                MediaStore.Video.Media.DATA, MediaStore.Video.Media.MIME_TYPE };

// Thumbnail columns to query
        String[] thumbnailColumns = { MediaStore.Video.Thumbnails.DATA };

// Content resolver
        ContentResolver contentResolver = service.getContentResolver();

// Query external movie content for selected media columns
        Cursor mediaCursor = contentResolver.query(
                MediaStore.Video.Media.EXTERNAL_CONTENT_URI, mediaColumns,
                null, null, null);

// Loop through media results
        if (mediaCursor.moveToFirst()) {
            do {
                // Get the video id
                int id = mediaCursor.getInt(mediaCursor
                        .getColumnIndex(MediaStore.Video.Media._ID));

// Get the thumbnail associated with the video
                Cursor thumbnailCursor = contentResolver.query(
                        MediaStore.Video.Thumbnails.EXTERNAL_CONTENT_URI,
                        thumbnailColumns, MediaStore.Video.Thumbnails.VIDEO_ID
                                + "=" + id, null, null);

// New movie object from the data
                JSONObject movie = new JSONObject();

movie.put("title", mediaCursor.getString(mediaCursor
                        .getColumnIndexOrThrow(MediaStore.Video.Media.TITLE)));
                movie.put("moviePath", "file://" + mediaCursor.getString(mediaCursor
                        .getColumnIndex(MediaStore.Video.Media.DATA)));
                movie.put("mimeType", mediaCursor.getString(mediaCursor
                        .getColumnIndex(MediaStore.Video.Media.MIME_TYPE))); long duration = mediaCursor.getLong(mediaCursor
                        .getColumnIndex(MediaStore.Video.Media.DURATION));
                movie.put("duration", getDurationAsString(duration));

if (thumbnailCursor.moveToFirst()) {
                    movie.put(
                            "thumbnailPath",
                            "file://" +
thumbnailCursor.getString(thumbnailCursor

.getColumnIndex(MediaStore.Video.Thumbnails.DATA)));
                } else {
                    movie.put("thumbnailPath", "");
                }

Log.d(LOG_TAG, movie.toString());

// Close cursor
                thumbnailCursor.close();

// Add to movie list
                movies.add(movie);

} while (mediaCursor.moveToNext());

// Close cursor
            mediaCursor.close();
        }

return movies;
    }

/**
     * Gets the given duration as string.
     *
     * @param duration
     *            duration value.
     * @return duration string.
     */
    private static String getDurationAsString(long duration) {
        // Calculate milliseconds
        long milliseconds = duration % 1000;
        long seconds = duration / 1000;

// Calculate seconds
        long minutes = seconds / 60;
        seconds %= 60;

// Calculate hours and minutes
        long hours = minutes / 60;         minutes %= 60;

// Build the duration string
        String durationString = String.format("%1$02d:%2$02d:%3$02d.%4$03d",
                hours, minutes, seconds, milliseconds);

return durationString;
    }
}`

MovieFacade在初始化时获取FacadeManager实例。FacadeManager允许访问 SL4A Android 服务实例。在与 Android 框架交互时,服务实例可以被外观用作 Android 上下文。moviesGet方法的实现部分借用了第五章示例项目,并修改为作为 RPC 方法运行。因为脚本不能直接使用 Java 类,所以moviesGet方法的返回类型被改为JSONObject列表。

注册外观

虽然现在已经正确定义了外观,但是 SL4A 框架还不知道它。MovieFacade需要向FacadeConfiguration类注册。

右键单击ScriptingLayer项目并选择 Properties。在属性对话框中,选择 Java Build Path,切换到 Projects 页签,添加MovieFacade作为依赖项,如图图 10-20 所示。

Image

图 10-20。 将 MovieFacade 作为依赖添加到脚本层

在同一个项目下,使用 Package Explorer 视图,展开 Sources 下的com.googlecode.android_scripting.facade包,并打开FacadeConfiguration类。

FacadeConfiguration类充当 SL4A 外观的注册表。SL4A 目前只允许在此手动注册外观。在类文件的顶部,在静态上下文中,外观被添加到sFacadeClassList集合中。如下面的代码所示,将标记在CHANGES BEGINCHANGES END注释之间的部分添加到FacadeConfiguration类中。

`    if (sSdkLevel >= 8) {
      sFacadeClassList.add(WebCamFacade.class);
    }

// **** CHANGES BEGIN ****

// Movie facade
    sFacadeClassList.add(MovieFacade.class);

// **** CHANGES END ****

for (Class<? extends RpcReceiver> recieverClass : sFacadeClassList) {
      for (MethodDescriptor rpcMethod :
MethodDescriptor.collectFrom(recieverClass)) {
        sRpcs.put(rpcMethod.getName(), rpcMethod);
      }
    }`

现在MovieFacade是 SL4A 框架的一部分,可以从脚本中使用。

导出电影外观

虽然MovieFacade已经在 SL4A 框架中正确注册,但是它仍然没有在MoviePlayerScript项目的导出列表中声明。右键单击MoviePlayerScript项目,选择 Java Build Path,将MovieFacade项目添加到项目列表中,并在 Order and Export 选项卡中将其标记为 Export。

添加脚本

项目的实际 UI 和应用逻辑将使用 HTML 和 JavaScript 实现。SL4A 框架依赖 Android 框架来呈现 HTML 和解释嵌入的 JavaScript 代码,它不需要下载解释器。

SL4A 模板项目附带了一个示例 Python 脚本。打开MoviePlayerScript项目,展开资源,从原始资源目录/res/raw中删除script.py Python 脚本文件。从顶部菜单栏选择文件 Image 新建 Image 文件,并添加一个script.html文件。您可以通过右击该脚本文件并从上下文菜单中选择打开 Image 文本编辑器来打开该脚本文件进行编辑。在运行时,SL4A 框架将使用其扩展名检测文件类型,并自动启动嵌入式 web 浏览器来执行脚本。

HTML 部分

脚本的 HTML 部分非常简单。如下面的代码所示,它只定义了一个 HTML div元素,用moviesid来保存电影列表。CSS 定义了浏览器将如何呈现电影项目。

`

<head>     <style type="text/css">         .movie {             border: 1px solid #000;             padding: 0.4em;         }

.thumbnail {
            width: 4em;
            height: 4em;
            float: left;
            margin-right: 0.4em;
        }

.title {
            font-size: x-large;
        }

.clear {
            clear: both;
        }
    

`

JavaScript 部分

该脚本将使用 JavaScript 通过 SL4A 框架与MovieFacade通信,以获取电影列表和相关信息。与所有其他脚本语言一样,脚本从初始化 Android 代理 RPC 客户端开始。

`    

`

运行应用

电影播放器脚本应用现在可以部署了,如第五章所述。由于 Android 模拟器中的一个已知错误,示例代码目前只能在 Android 设备上运行。因为示例应用将在外部存储中查找视频文件,所以请确保 Android 设备包含一个带有视频文件的 SD 卡,并断开 Android 设备与您的主机的连接以释放 SD 卡。当你运行应用时,你会看到电影列表,如图图 10-21 所示。

Image

图 10-21。 显示电影列表的电影播放器脚本应用

总结

在这一章中,我们深入到 SL4A 框架并探索了它的内部,包括外观注册和项目结构。SL4A 是一个开源项目,具有很强的可扩展性。您可以遵循本章示例中描述的相同步骤来扩展主 SL4A 应用ScriptingLayerForAndroid项目,以包含新的外观,并在以后通过任何脚本语言在本地或远程使用它们。

在本书中,我们探讨了 Android 平台的基础,以更好地理解其基础。我们研究了 Android 应用架构,并将这些新概念应用于我们的第一个 Android 应用,一个电影播放器。然后,我们通过集成本地代码库来扩展电影播放器应用,以支持其他视频格式。在开发的每个阶段,我们都采用了 Eclipse 提供的高级开发特性,例如快速导航、内容辅助、代码生成器以及调试和故障排除特性,以简化开发过程。

资源

以下资源可用于本章涵盖的主题:

  • Android 脚本层(SL4A),[code.google.com/p/android-scripting/](http://code.google.com/p/android-scripting/)
  • 水星月食,[javaforge.com/project/HGE](http://javaforge.com/project/HGE)

十一、附录一:测试 Android 应用

测试是应用开发周期中最重要的阶段之一。Android SDK 提供了一个强大的测试框架,用于定义和运行各种测试来验证 Android 应用的不同方面。Android 测试框架建立在流行的 JUnit 测试框架之上。Android 测试框架扩展了 JUnit,加入了 Android 特有的工具功能,允许测试控制 Android 应用周围的环境。这使得测试所有可能的用例变得容易。

JUnit 基础知识

JUnit 是 Java 编程语言的测试框架。JUnit 提供了一组类来定义、组织和运行测试用例。JUnit 提供的最重要的类是junit.framework.TestCase,它是所有测试用例的基类。Android 测试类也构建在这个类之上,它们遵循相同的代码结构和流程。在列出 A-1 的中显示了一个基本的测试用例类。

A-1 上市。 基本测试用例类

public class MyTest extends AndroidTestCase {     /**      * Sets up the text fixture before each test is executed.      */     protected void setUp() { `    }

/**
     * Test method.
     */
    protected void testSomething() {
    }

/**
     * Tears down the text fixture after each test is executed.
     */
    protected void tearDown() {
    }
}`

以下是测试用例类的关键部分:

  • setUp:该方法在每次测试前设置测试夹具。开发人员应该重写该类,以正确初始化测试设备,从而确保新的测试运行与之前的测试运行相隔离。
  • tearDown:这个方法拆除文本 fixture 并释放所有为测试分配的资源。JUnit 测试框架在整个测试用例的执行过程中保留测试用例类。开发人员应该释放tearDown方法中的任何资源,以防止耗尽平台资源。
  • 测试用例类可以包含一个或多个测试。测试方法有前缀test。JUnit 框架在处理测试用例类时运行所有带有该前缀的方法。

断言

在计算机科学中,断言是一个谓词,用于表明开发者对给定阶段的应用状态的假设。断言用于测试应用的正确性。

JUnit 框架通过junit.framework.Assert类提供了一组常用的断言方法,用于测试用例。基类junit.framework.TestCase扩展了Assert类,并提供了对这些断言方法的直接访问。

JUnit 公开的断言方法是高度通用的。它们没有涵盖 Android 测试用例所涵盖的频繁断言操作。Android 测试框架在这里发挥了作用,它提供了额外的断言类,这些断言类具有专门为解决 Android 测试需求而设计的大量方法。尽管 JUnit 有Assert类,这些额外的类并不是测试用例的基类。开发人员需要将这些类导入到 Java 代码中,并使用它们的静态断言方法。

以下附加断言类是作为android.test Java 包的一部分提供的:

  • 这个类提供了一组通用的断言方法,JUnit 没有提供这些方法来测试 Java 类型、数组和值。
  • ViewAsserts:这个类为 Android 视图提供了一套断言方法。这些方法可用于断言用户界面(UI)组件的可见性,以及它们在显示器上的位置。

单元测试

单元测试允许开发者孤立地测试应用组件。Android 测试框架在android.test Java 包下提供了一组组件测试类,方便了特定于组件的测试需求——比如夹具设置、拆卸和生命周期控制。测试用例可以扩展这些类,并提供建立在所提供功能之上的实际测试方法。

下面是提供的一些测试框架类:

  • 这是一个通用的测试用例类,具有访问上下文和资源以及测试应用权限的方法。
  • ApplicationTestCase:这个类提供了一个在受控环境中测试Application类的环境。它允许测试代码控制应用的生命周期,以及注入依赖项,如独立的上下文。它延迟应用的启动,直到执行createApplication方法,以允许开发人员进行夹具设置。
  • ActivityUnitTestCase:这是对Activity类进行隔离测试的测试类。在测试中,一个活动在与 Android 平台最小连接的情况下启动。它允许在测试之前将模拟上下文和应用实例注入到活动中。为了提供真正的单元测试环境,它覆盖了一组 Android 方法,以防止该活动与其他活动和平台进行交互。
  • ServiceTestCase:这是一个测试类,用于在受控环境中测试Service类。它为服务生命周期管理提供了基本支持,也允许开发人员通过测试代码注入依赖项和控制环境。
  • ProviderTestCase2:这是一个单独的ContentProvider类的测试类,用于测试独立内容提供者的应用代码。它没有为提供者使用系统映射,而是维护其内部列表,并将这些内容提供者只暴露给测试用例。它反对使用ProviderTestCase类来打破对检测的依赖。

模拟物体

单元测试是一个具有已知输入和输出的可重复过程。组件的所有依赖都通过模拟对象来实现,以消除影响测试结果的外部依赖。

为了方便依赖注入,Android 框架在android.test.mock Java 包下为 Android 框架的核心部分提供了模拟对象。这些模拟类通过覆盖和存根它们的正常操作将测试与运行系统隔离开来。除了开发人员定义的部分之外,它们没有任何功能。所有没有被覆盖的方法抛出一个java.lang.UnsupportedOperationException来通知开发人员测试代码正在试图与环境通信。提供了以下模拟类:

  • MockApplication:这个类扩展了Application类,并保留了它的方法。开发人员可以扩展这个模拟类来实现依赖注入所必需的方法。所有其他方法都会提高UnsupportedOperationException
  • MockContext:这个类扩展了Context类,并保留了它的方法。开发人员可以使用模拟上下文将其他依赖项注入应用。
  • MockContentResolver:这个类扩展了ContentResolver类,覆盖了 Android 通过权限解析内容提供商的正常方式。模拟内容解析器保留了一个内部映射,而不是使用系统的内容提供者映射。开发人员应该在 fixture 设置期间将他们的模拟内容提供者注册到模拟内容解析器中。模拟内容解析器通过只解析直接注册的模拟内容提供者来隔离被测试的应用。
  • MockContentProvider:这个类扩展了ContentProvider类,并保留了它的方法。开发人员应该重写必要的内容提供者方法,以便向该内容提供者的使用者提供静态数据。稍后,通过模拟内容解析器,可以将模拟内容提供者注入到被测试的应用中。
  • MockCursor:这个类扩展了Cursor类,并保留了它的方法。它通常与模拟内容提供者一起使用,为被测试的应用提供静态数据。
  • MockDialogInterface:这个类用存根方法实现了DialogInterface。开发人员可以重写它的方法来验证对话框的 UI 输入。
  • MockPackageManager:这个类扩展了PackageManager类,并保留了它的方法。开发人员可以覆盖必要的方法来模拟正在测试的应用和 Android 系统之间的交互。
  • MockResources:这个类扩展了Resources类,并保留了它的方法。它使开发人员能够通过覆盖模拟方法在被测试的应用中进行资源注入。

功能测试

功能测试是一种黑盒测试。它根据软件组件的规格来测试它们。功能测试包括输入和检查输出;很少考虑内部程序结构。

Android 测试框架允许通过工具对 Android 应用进行功能测试。Android instrumentation 是一组控制方法和挂钩,用于将用户事件和请求注入到应用中,同时管理其生命周期。插装方法通过android.app.Instrumentation类提供。该类在任何应用代码运行之前被实例化。

与单元测试类不同,功能测试类使用实际的系统上下文加载应用,并使用应用的 UI 或向系统公开的 Android 服务将事件提供给应用。功能测试类扩展了InstrumentationTestCase类,并通过getInstrumentation方法提供对插装实例的访问。在android.test Java 包中提供了以下插装类:

  • 这个类提供了对单个活动进行功能测试的方法。被测试的活动是使用系统基础设施启动的,然后可以使用插装方法进行操作。它通过为被测试的活动提供更细粒度的配置选项而摒弃了ActivityInstrumentationTestCase类。
  • SingleLaunchActivityTestCase:这个类启动正在用其setUp方法测试的Activity类,并在其tearDown方法中终止它。与其他测试类不同,这个类在现有的活动实例上运行所有的测试方法,而不是为每个测试设置和拆除活动实例。

用户界面操作

Android 框架要求所有与 UI 组件的交互都发生在应用的主线程中,也称为 UI 线程InstrumentationTestCase类为在 UI 线程中运行测试代码提供了以下选项:

  • TouchUtils:该类提供了从仪器测试中生成触摸事件的方法,该测试被分类为模拟用户通过触摸屏与应用的交互。
  • UiThreadTest : Annotation 可以用来标记测试类中应该在应用的 UI 线程中执行的测试用例。在这种模式下,可能无法使用检测方法。
  • runTestOnUiThread:这个方法可以用来调度 UI 线程中的Runnable对象。这允许测试用例将测试的一部分注入到应用的 UI 线程中。

测试项目

测试项目与一般的 Android 项目没有什么不同。它们是作为独立于实际应用的项目生成的。尽管它们是一个独立的项目,但是最佳实践是将测试项目存储在主项目根目录下的tests目录中。

本书第五章中介绍的 Android 开发工具(ADT)为生成测试项目提供了两个选项。一种方法是在创建实际项目的同时创建测试项目。新的 Android 项目向导,在它的第三步,询问你是否也应该生成一个测试项目,如图图 A-1 所示。您可以先标记“创建测试项目”选项,然后配置测试项目。

Image

图 A-1。 用新的 Android 项目向导配置一个测试项目

在应用开发的最开始拥有一个测试项目是测试驱动编程的良好实践。然而,如果测试项目没有在开始时创建,那么创建一个也不迟。ADT 提供了一个新的项目向导,专门用于为工作区中现有的 Android 项目生成一个新的测试项目。

创建新的测试项目,从顶部菜单栏选择文件 Image 新建 Image 项目… ,展开 Android,选择 Android 测试项目,如图 A-2 所示。

Image

图一-2。 选择创建一个新的 Android 测试项目

ADT New Android Test Project 向导将首先询问您这个测试项目的名称。作为新项目的位置,建议使用项目内部的tests子目录进行测试。单击“下一步”按钮继续下一步。

每个测试项目都需要与现有的 Android 项目相关联。下一步,向导会要求您选择目标项目,如图图 A-3 所示。选择目标项目,然后单击“下一步”按钮继续。

Image

图 A-3。 为测试项目选择目标 Android 项目

作为最后一步,New Android Test Project 向导将询问新测试项目的目标 Android SDK 版本。选择适合目标 Android 项目的 SDK 目标,点击 Finish 按钮。ADT 将生成测试项目。

运行测试

要运行测试用例,请选择测试项目并单击 run 按钮。如图 A-4 所示,Eclipse 将询问项目应该如何执行。从列表中选择 Android JUnit Test,然后单击 OK 按钮继续。

Image

图 A-4。 测试项目第一次运行时的运行方式对话框

ADT 首先构建和部署实际的 Android 项目,然后对测试项目本身进行同样的操作。当测试在目标设备或模拟器上运行时,您可以使用 Eclipse 中的 JUnit 视图来监控它们,如图图 A-5 所示。

Image

图一-5。 JUnit 视图显示测试进度

JUnit 视图有两个窗格:

  • 顶部的窗格提供了正在执行的测试列表,以及关于通过和失败的测试用例数量的统计数据。
  • 如果一个测试用例失败,底部窗格提供显示错误位置的失败跟踪,如图图 A-6 所示。

Image

图 A-6。 显示测试失败的失败痕迹

在对大型项目中失败的测试用例进行故障排除时,最好只运行那个测试用例,而不是整个测试套件。若要仅运行一个测试,请使用包资源管理器选择测试用例类,然后单击“运行”按钮。Eclipse 将显示运行方式对话框,如图图 A-7 所示。选择 Android JUnit 测试,然后单击确定按钮继续。JUnit 将只运行所选测试用例类中的测试。

Image

图 A-7。 执行选中的测试用例类

测量测试代码覆盖率

多少测试就够了?这是测试中最常被问到的问题之一。测试用例的数量并不是测试覆盖率的良好衡量标准。Android SDK 附带了 EMMA,用于测量和报告测试用例的代码覆盖率。

注意:代码覆盖率目前仅在 Android 模拟器和根设备上受支持。

尽管 EMMA 是 Android 测试框架的一个重要组成部分,但它在 Android 应用中的使用并没有明确的文档记录。在这一节中,我们将经历从测试项目中生成代码覆盖报告的步骤。

设置艾玛权限

在撰写本文时,Eclipse 的 ADT 插件没有提供对 EMMA 的直接访问。EMMA 代码覆盖工具只能通过基于 Ant 的构建脚本来调用。创建 Android 项目时,Eclipse 不会生成 Ant 构建脚本。应该为应用项目和测试项目手动创建构建脚本。

要创建构建脚本,如果在 Windows 主机上运行,请打开命令提示符,如果使用基于 Mac OS X 或 Linux 的主机,请打开终端窗口,并调用以下命令:

`cd
android update project --path .

cd
android update test-project --main --path .`

这些命令将在应用和测试目录中生成 Ant 构建脚本、build.xml和其他必要的属性文件。

为测试运行启用 EMMA

要启用 EMMA,请使用包资源管理器,展开测试项目。如果build.xml文件不可见,按 F5 刷新项目目录。右键单击build.xml文件并从上下文菜单中选择运行为 Ant Build… 。将出现“编辑配置”对话框。切换到主选项卡,将参数设置为all clean emma debug install test,如图 A-8 所示。

Image

图一-8。 配置蚂蚁构建脚本

确保 Android 设备连接到主机,并单击 Run 按钮执行 Ant 构建脚本。构建脚本将应用和测试部署到目标设备或启用 EMMA 的仿真器,如图图 A-9 所示。

Image

图一-9。 控制台视图显示艾玛被启用

在测试用例完成后,脚本将提取 EMMA 结果文件,并在测试项目下的coverage目录中生成一个 HTML 格式的报告。使用包资源管理器选择测试项目后,按 F5 键刷新项目目录。展开coverage目录,打开coverage.html报表文件,如图 A-10 所示。

Image

图 A-10。 艾玛代码覆盖报告文件

EMMA 报告 HTML 文件提供了关于测试用例代码覆盖率的大量信息。通过点击包和类,您可以导航到源文件,并查看没有被任何测试用例执行的代码部分。使用这些信息,您可以扩展测试用例来覆盖更大部分的应用代码。

压力测试

压力测试是一种测试形式,通过引入超出应用运行能力的负载来确定应用的稳定性。Android SDK 提供了 Monkey 工具来向应用发送伪随机的击键、触摸和手势流。压力测试不是一个可重复的过程;然而,Monkey 工具允许重复事件流来重现错误情况。

要启动 Monkey 工具,首先将目标设备连接到主机或启动模拟器。如果您使用的是基于 Windows 的主机,请打开命令提示符,或者在基于 Mac OS X 和 Linux 的主机上打开终端窗口,并调用以下命令:

adb shell monkey -p <your application package> -v 500

这个命令启动 Monkey 工具,并用给定的包名向 Android 应用发送 500 个伪随机事件。关于 Monkey 工具命令行参数的更多信息,见[developer.android.com/guide/developing/tools/monkey.html](http://developer.android.com/guide/developing/tools/monkey.html)

posted @   绝不原创的飞龙  阅读(73)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· DeepSeek R1 简明指南:架构、训练、本地部署及硬件要求
· NetPad:一个.NET开源、跨平台的C#编辑器
· PowerShell开发游戏 · 打蜜蜂
点击右上角即可分享
微信分享提示