英特尔平台上的安卓应用开发教程-全-

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

原文:Android Application Development for the Intel Platform

协议:CC BY-NC-SA 4.0

一、面向英特尔架构的嵌入式应用开发概述

嵌入式系统是计算机技术的一个新兴领域,它结合了多种技术,如计算机、半导体、微电子和互联网,因此在我们的现代世界中得到越来越多的应用。随着计算机和通信技术的快速发展以及互联网的日益普及,嵌入式系统在后 PC 时代获得了巨大成功和广泛应用,尤其是作为物联网的核心组成部分。它们渗透到现代生活的每一个角落,从日常生活,如自动家庭恒温器,到工业生产,如制造业中的机器人自动化。从移动电话、MP3 播放器和 PDA 到汽车、飞机和导弹,嵌入式系统可以在军事和国防、医疗保健、科学、教育和商业服务中找到。

本章提供了嵌入式系统的概念、结构和其他基本信息,为嵌入式应用开发奠定了理论基础,其中 Android 操作系统的应用开发成为开发人员的最大兴趣。

嵌入式系统简介

自 1946 年第一台计算机 ENIAC 问世以来,计算机制造工艺已经从真空管、晶体管、集成电路和大规模集成电路(LSI)发展到超大规模集成电路(VLSI),从而产生了更紧凑、更强大、更节能但更便宜(每单位计算能力)的计算机。

20 世纪 70 年代微处理器问世后,计算机使用领域发生了革命性的变化。微处理器是微型计算机的基础,个人电脑使它们变得更加便宜和实用,允许许多私人用户拥有它们。在这个阶段,计算机满足了各种各样的需求:它们足够多才多艺,能够满足各种各样的需求,例如计算、娱乐、信息共享和办公自动化。随着微型计算机的采用,越来越多的人希望将它们嵌入到特定的系统中,以智能地控制环境。例如,微型计算机被用在工厂的机床上。它们用于通过配置外围传感器来控制信号和监控运行状态。当微型计算机被嵌入到这样的环境中时,它们就是嵌入式系统的原型。

随着技术的进步,更多的行业需要特殊的计算机系统。结果,用于特定环境的专用计算机系统和通用计算机系统的发展方向和目标产生了分歧。通用计算机系统的技术要求是快速、大规模和多样化的计算,而技术发展的目标是更快的计算速度和更大的存储容量。然而,嵌入式计算机系统的技术要求更多地是针对目标的智能控制,而技术发展的目标是与目标系统密切相关的嵌入式性能、控制和可靠性。

嵌入式计算系统以完全不同的方式发展。通过强调特定处理器的特性,他们将传统电子系统转变为现代智能电子系统。图 1-1 展示了一款嵌入式电脑处理器,英特尔凌动 N2600 处理器,2.2 × 2.2 cm,旁边是一枚硬币。

A978-1-4842-0100-8_1_Fig1_HTML.jpg

图 1-1。

Comparison of an embedded computer chip to a US penny. This chip is an Intel Atom processor

嵌入式计算机系统和通用计算机系统的出现是现代计算机技术的一个里程碑。通用计算机和嵌入式系统的对比见表 1-1 。

表 1-1。

Comparison of General-Purpose Computers and Embedded Systems

| 项目 | 通用计算机系统 | 嵌入式系统 | | --- | --- | --- | | 五金器具 | 高性能硬件、大容量存储介质 | 多样化的硬件、单处理器解决方案 | | 软件 | 大型复杂的操作系统 | 精简、可靠的实时系统 | | 发展 | 高速、专业化的开发团队 | 广泛的发展部门 |

如今,嵌入式系统因其移动性而成为人们生活中不可或缺的一部分。如前所述,它们在现代生活中无处不在。智能手机是嵌入式系统的一个很好的例子。

移动电话

移动设备,尤其是智能手机,是近年来增长最快的嵌入式领域。手机软件开发衍生出了很多新名词,比如泛嵌入式开发、移动开发等。手机不仅无处不在,而且功能强大,价格实惠,应用多样化。除了基本的电话功能,它们还包括但不限于集成 PDA、数码相机、游戏控制台、音乐播放器和可穿戴设备。

消费电子和信息设备

消费电子和信息家电是嵌入式系统的附加大应用领域。属于这一类别的设备包括个人移动设备和家庭/娱乐/视听设备。

个人移动设备通常包括 PDA 等智能手机,以及移动互联网设备(mid)等无线互联网接入设备。理论上,智能手机也属于这一类;但由于它们数量庞大,因此被列为一个部门。

家庭/娱乐/视听设备主要包括类似互动电视的网络电视;数字成像设备,例如数码相机、数码相框和视频播放器;数字音频和视频设备,例如 MP3 播放器和其他便携式音频播放器;以及诸如手持游戏控制台、PS2 控制台等电子娱乐设备。平板电脑(tablets)是一种新型嵌入式设备,自苹果公司 2010 年发布 iPad 以来,已经成为消费者的最爱。

消费电子的可负担性真正体现了嵌入式系统设计的性价比。

嵌入式系统的定义

到目前为止,通过给出的例子,您已经对嵌入式系统有了大致的了解。但是什么是嵌入式系统呢?目前,业界对嵌入式系统有不同的概念。

根据 IET 工程技术协会的定义,嵌入式系统是用于控制、监控或辅助设备、机器或工厂运行的装置。智能手机作为嵌入式系统的重要组成部分,具有以下特点:

有限的资源

大多数嵌入式系统的资源极其有限。一方面,这里所指的资源是硬件资源,包括 CPU 的计算速度和处理能力、可用物理存储器的大小以及存储代码和数据的 ROM 或闪存的容量。另一方面,资源也是软件提供的功能。与通用操作系统相比,嵌入式操作系统具有相对简单的功能和结构。嵌入式系统的资源限制导致设计不够充分,而不是功能强大。

实时性能

嵌入式系统的实时性意味着任务通常必须在某个可预测的时间内执行,并且必须确保最大执行时间限制。

实时分为软实时和硬实时。软实时具有不太严格的要求;即使在某些情况下无法满足时间限制,也不会对系统造成致命影响。例如,媒体播放器系统是软实时的。系统应该在一秒钟内播放 24 帧,但是当系统在一些过载情况下出现故障时,这也是可以接受的。硬实时有严格的要求。在所有情况下,必须绝对确保任务的执行;否则后果将是灾难性的。例如,飞机自动驾驶仪和导航系统是硬实时系统。他们必须在一定的时限内完成特定的任务;否则可能会发生重大事故、碰撞或撞车。

许多嵌入式系统(手机、游戏机等等)不需要实时保证。但是实时性是一些嵌入式系统的关键,例如大型钢厂的轧钢系统和大型变电站的实时报警系统。在这些应用中,系统必须在给定时间对特定信号做出响应。

稳健性

一些嵌入式系统要求高可靠性。可靠性也称为稳健性,即在异常或危险情况下持续运行的能力。例如,当嵌入式系统遇到输入错误、网络过载或故意攻击时,系统必须足够健壮,不会挂起或崩溃,而是照常运行。

集成硬件和软件

通用计算机动态安装软件。该软件可以根据用户的需求进行安装和卸载。但是对于嵌入式系统来说,软件和硬件通常被集成在一起,打包出售。对于总是通过互联网连接的设备,如智能手机和物联网(例如可穿戴设备),这一趋势正在发生变化。在这些情况下,原始设备制造商(ODM)可以定期进行软件更新。

嵌入式软件通常内置于硬件 ROM 中,并在系统启动时自动运行。在正常情况下,如果没有特殊工具的帮助,用户不能轻易修改或删除软件,以确保嵌入式系统的完整性。由于硬件和软件的集成,嵌入式系统通常没有一般计算机系统必须解决的知识产权问题。例如,由于软件的安装方式,在手机和数码相机等消费电子产品上进行软件盗版几乎是不可能的。但是,这一特性也导致系统软件升级缓慢,因为很难做到。

功率限制

通用计算机通常直接连接到交流电源。因此,通用计算机硬件和软件设计者可以假设电源是取之不尽的。但是对于不能直接连接到交流电源的嵌入式系统,例如手机、电动玩具和相机,唯一的电源是电池。这意味着它们的功耗受到限制,因此能效非常重要。冷却是另一个关键因素。一般来说,在特定时间段内更多的功耗会导致产生更多的热量,这在某些情况下会导致问题,例如电池着火、由于过热导致的组件故障以及电力的快速损失。

开发调试困难

相对于通用计算机的软硬件开发,嵌入式系统开发具有更高的技术要求。例如,嵌入式软件的开发者在开发阶段通常必须理解硬件和硬件层的工作原理和机制。为了调试代码,这些开发人员通常必须使用在线模拟、ROM 监视器和 ROM 编程工具,这些在桌面开发中是不会出现的。

嵌入式系统的典型架构

图 1-2 显示了一个典型的嵌入式系统的配置图,由两个主要部分组成:嵌入式硬件和嵌入式软件。嵌入式硬件主要包括处理器、内存、总线、外围设备、I/O 端口和各种控制器。嵌入式软件通常包含嵌入式操作系统和各种应用。

A978-1-4842-0100-8_1_Fig2_HTML.jpg

图 1-2。

Basic architecture of an embedded system

输入和输出是任何开放系统的特征,嵌入式系统也不例外。在嵌入式系统中,硬件和软件经常协作处理来自外部的各种输入信号,并通过某种形式输出处理结果。输入信号可以是人体工程学设备(如键盘、鼠标或触摸屏)或另一个嵌入式系统中传感器电路的输出。输出可以是声音、光、电或其他模拟信号的形式,或者是数据库的记录或文件。

典型硬件架构

基本的计算机系统组件——微处理器、内存和输入输出模块——通过系统总线相互连接,以便所有部件进行通信和执行程序(见图 1-3 )。

A978-1-4842-0100-8_1_Fig3_HTML.jpg

图 1-3。

Computer architecture

在嵌入式系统中,微处理器的角色和功能通常与通用计算机中 CPU 的角色和功能相同:控制计算机操作、执行指令和处理数据。在许多情况下,嵌入式系统中的微处理器也称为 CPU。内存用于存储指令和数据。I/O 模块负责处理器、内存和外部设备之间的数据交换。外部设备包括辅助存储设备(如闪存和硬盘)、通信设备和终端设备。系统总线为处理器、内存和 I/O 模块提供数据并控制信号通信和传输。

基本上有两种类型的架构适用于嵌入式系统:冯诺依曼架构和哈佛架构。

冯·诺依曼建筑

冯·诺依曼建筑(也称为普林斯顿建筑)是由约翰·冯·诺依曼首先提出的。这种架构最重要的特点就是软件和数据使用同一个内存:即“程序是数据,数据是程序”(如图 1-4 )。

A978-1-4842-0100-8_1_Fig4_HTML.jpg

图 1-4。

Von Neumann architecture

在冯·诺依曼体系结构中,指令和数据共享同一总线。在这种体系结构中,信息的传输成为计算机性能的瓶颈,影响数据处理的速度;因此,它通常被称为冯·诺依曼瓶颈。实际上,缓存和分支预测技术可以有效地解决这个问题。

哈佛建筑

哈佛建筑最初是以哈佛 Mark I 计算机命名的。与冯诺依曼架构相比,哈佛架构处理器有两个突出的特点。首先,指令和数据存储在两个独立的内存模块中;指令和数据不能在同一个模块中共存。第二,两条独立的总线被用作 CPU 和存储器之间的专用通信路径;两辆公共汽车之间没有联系。哈佛架构如图 1-5 所示。

A978-1-4842-0100-8_1_Fig5_HTML.jpg

图 1-5。

Harvard architecture

由于 Harvard 架构具有独立的程序存储器和数据存储器,因此可以提供更大的数据存储器带宽,是数字信号处理的理想选择。大多数为数字信号处理(DSP)设计的系统都采用哈佛架构。冯诺依曼架构具有简单的硬件设计和灵活的程序和数据存储,通常是通用和大多数嵌入式系统的选择。

为了有效地执行存储器读/写,处理器不直接连接到主存储器,而是连接到高速缓存。通常,哈佛架构和冯诺依曼架构之间的唯一区别是单或双 L1 高速缓存。在哈佛架构中,L1 缓存通常分为指令缓存(I 缓存)和数据缓存(D 缓存),但冯诺依曼架构只有一个缓存。

嵌入式系统的微处理器体系结构

微处理器是嵌入式系统的核心。通过将微处理器安装到特殊的电路板上,并添加必要的外围电路和扩展电路,可以创建一个实用的嵌入式系统。微处理器结构决定指令、支持外围电路和扩展电路。微处理器种类繁多:4 位、8 位、16 位、32 位和 64 位,性能从 MHz 到 GHz,从几个引脚到几千个引脚不等。

一般来说,有两种类型的嵌入式微处理器体系结构:精简指令集计算机(RISC)和复杂指令集计算机(CISC)。RISC 处理器使用一个小的、有限的、简单的指令集。每条指令使用标准字长,执行时间短,有利于指令流水线的优化。为了补偿命令功能,CPU 通常配备大量通用寄存器。CISC 处理器具有强大的指令集和不同的指令长度,这有利于指令的流水线执行。RISC 和 CISC 的比较在表 1-2 中给出。

表 1-2。

Comparison of RISC and CISC

|   | 精简指令集计算 | 复杂指令集电脑(complex instruction set computer) | | --- | --- | --- | | 指令系统 | 简单高效的指令。通过组合指令实现不常见的功能。 | 丰富的指令系统。通过特殊指令执行特定功能;高效处理特殊任务。 | | 存储瀑作 | 限制存储器操作并简化控制功能。 | 具有多个存储器操作指令并执行直接操作。 | | 程序 | 汇编程序需要大量的内存空间,并且具有特殊功能的复杂程序。 | 具有相对简单的汇编器,并具有对科学计算和复杂运算进行简单有效编程的特点。 | | 中断 | 只在指令执行的适当位置响应中断。 | 仅在执行结束时响应中断。 | | 中央处理器 | 单元电路少,体积小,功耗低。 | 电路单元功能丰富,功能强大,面积大,功耗高。 | | 设计周期 | 特点是结构简单,布局紧凑,设计周期短,易于应用新技术。 | 结构复杂,设计周期长。 | | 使用 | 特点是结构简单,指令规则,控制简单,易学易用。 | 特点是结构复杂,功能强大,特殊功能容易实现。 | | 适用范围 | 确定每个特定区域的指令系统,这更适用于特殊机器。 | 变得更适合通用机器。 |

RISC 和 CISC 有不同的特点和优势,但在微处理器领域,RISC 和 CISC 之间的界限开始模糊。许多传统的 CISC 吸收了 RISC 的优点,并使用类似 RISC 的设计。英特尔 x86 处理器就是其中的典型。它们被认为是 CISC 建筑。这些处理器通过解码器将 x86 指令翻译成类似 RISC 的指令,并遵守 RISC 设计和操作,以获得 RISC 架构的好处并提高内部操作效率。处理器的内部指令执行称为微操作,表示为微操作,缩写为μ-op(或写为μ-OP 或μop)。相比之下,x86 指令被称为宏操作(macro-op),整个机制如图 1-6 所示。

A978-1-4842-0100-8_1_Fig6_HTML.jpg

图 1-6。

Micro and macro operations of an Intel processor

通常情况下,一个宏操作可以被解码成一个或多个微操作来执行,但有时解码器可以将几个宏操作组合起来生成一个微操作来执行。这个过程被称为 x86 指令融合(宏操作融合)。例如,处理器可以组合 x86 CMP(比较)指令和 x86 JMP(跳转)指令来产生单个微操作——比较和跳转指令。这种组合有明显的好处:指令更少,间接增强了处理器执行的性能。这种融合使得处理器能够最大化指令之间的并行性,从而提高处理器的执行效率。

目前,大多数嵌入式系统中使用的微处理器有五种体系结构:RISC、CISC、MIPS、PowerPC 和 SuperH。细节如下。

RISC:高级 RISC 机器(ARM)架构

高级 RISC 机器(ARM)是一种 RISC 微处理器的通称。ARM 由英国公司 ARM Holdings 设计。该公司专门从事 RISC 芯片的设计和开发。作为知识产权的供应商,该公司本身并不制造其芯片,而是将其设计许可给其他合作伙伴生产。世界各大半导体厂商购买 ARM 设计的 ARM 微处理器内核,根据不同的应用领域添加合适的外部电路,打造自己的 ARM 微处理器芯片。

CISC: x86 体系结构

x86 系列 CPU 是台式机中最受欢迎的 CPU。x86 架构被认为是 CISC。该指令集是英特尔为其第一个 16 位 CPU (i8086)专门开发的,IBM 在 1981 年推出世界上第一台 PC 时采用了该指令集。随着英特尔推出 i80286、i80386、i80486、奔腾和其他产品,它继续使用 x86 指令集来确保传统应用可以运行,并保护和集成多样化的软件资源。因此,这些 CPU 被称为 x86 架构。

除了 Intel 之外,AMD、Cyrix 和其他制造商也生产了基于 x86 指令集的 CPU。那些 CPU 可以运行各种针对 Intel 处理器开发的软件,所以在业界被称为 x86 兼容产品,属于 x86 架构。英特尔专门推出了面向嵌入式系统的英特尔凌动 x86 32 位处理器。第二章描述并展示代号为 Bay Trail 的 64 位英特尔凌动处理器的优势。

Note

IA-32、IA-64、英特尔 64、IA-32、IA-64 和英特尔 64 是英特尔的架构类型,适用于其处理器以及兼容的 CPU。

IA-32(英特尔架构-32)是指英特尔的 32 位架构处理器。数字 32 是处理器的工作宽度;它一次可以处理 32 位二进制数据。如果其他处理器(例如,AMD 32 位 CPU)与此架构兼容,则它们属于 IA-32 架构。

IA-64(英特尔架构-64)是英特尔的 64 位架构。凭借 64 位的工作宽度,其微架构与 x86 架构完全不同。IA-64 与 x86 软件不兼容,因此 x86 软件必须使用各种形式的仿真才能在 IA-64 上运行,这往往导致效率低下。该架构由惠普创建,由惠普和英特尔共同开发。英特尔安腾是典型的 IA-64 处理器。

Intel64 是一种 64 位 x86 架构,工作宽度为 64 位。在 AMD 推出之后,英特尔推出了一款名为 EM64T 的兼容处理器,正式更名为 Intel64。几乎所有的英特尔 CPU 现在都是英特尔 64:至强、酷睿、赛扬、奔腾和凌动。与 IA-64 架构相反,它也可以运行 x86 指令。

MIPS 架构

没有互锁管道级(MIPS)的微处理器也是 RISC 处理器。其机制是充分利用软件来避免管道中的数据问题。它最初是由斯坦福大学的约翰·汉尼斯教授领导的研究小组在 20 世纪 80 年代初开发的,后来由 MIPS Technologies 公司商业化。

与 ARM 一样,MIPS Technologies 通过智能产权(IP)内核向半导体公司提供 MIPS 微处理器内核,并允许他们进一步开发 RISC 架构中的嵌入式微处理器。核心技术是多发布能力:将处理器中的闲置处理单元拆分出来,虚拟化为另一个核心,提高处理单元的利用率。

PowerPC 体系结构

PowerPC 是 RISC 体系结构中的 CPU。它来源于 POWER 架构,其基本设计来自于 IBM PowerPC 601 微处理器性能优化增强的 RISC (POWER)。20 世纪 90 年代,IBM、苹果和摩托罗拉成功开发了 PowerPC 芯片,并创造了基于 PowerPC 的多处理器计算机。PowerPC 体系结构具有可伸缩性、便利性、灵活性和开放性:它定义了指令集体系结构(ISA),允许任何人设计和制造 PowerPC 兼容的处理器,并自由使用为 PowerPC 开发的软件模块的源代码。PowerPC 具有从移动电话到游戏控制台的广泛应用,广泛应用于通信和网络领域,如交换机、路由器等。苹果 Mac 系列使用 PowerPC 处理器长达十年,直到苹果改用 x86 架构。

超级

SuperH (SH)是一款性价比极高的紧凑型嵌入式 RISC 处理器。SH 架构最早由日立开发,归日立和 st 微电子所有。现在已经被瑞萨接手了。SuperH 包括 SH-1、SH-2、SH-DSP、SH-3、SH-3-DSP、SH-4、SH-5 和 SH-X 系列,广泛用于打印机、传真机、多媒体终端、电视游戏机、机顶盒、CD-ROM、家用电器和其他嵌入式系统。

嵌入式系统的典型结构

嵌入式系统的典型硬件结构如图 1-7 所示。微处理器是系统的中心,有存储设备、输入输出外设、电源、人机交互设备和其他必要的支持设施。在实际的嵌入式系统中,硬件通常是为应用量身定制的。为了节省成本,外围设备可以非常紧凑,并且只保留基本的外围电路用于处理器和应用。

A978-1-4842-0100-8_1_Fig7_HTML.jpg

图 1-7。

Typical hardware structure of an embedded system

随着集成电路设计和制造技术的发展,集成电路设计已经从晶体管集成,到逻辑门集成,再到现在的 IP 集成或片上系统(SoC)。SoC 设计技术将流行的电路模块集成在单个芯片上。SoC 通常包含大量外围功能模块,如微处理器/微控制器、存储器、USB 控制器、通用异步收发器(UART)控制器、A/D 和 D/A 转换、I2C 和串行外设接口(SPI)。图 1-8 是基于 SoC 的嵌入式系统硬件结构示例。

A978-1-4842-0100-8_1_Fig8_HTML.jpg

图 1-8。

Example of an SoC-based hardware system structure

可编程芯片上系统(SoPC)提倡用可编程逻辑技术将电子系统集成到硅芯片上。因此,SoPC 是一种特殊类型的 SoC,因为整个系统的主要逻辑功能是由单个芯片实现的。因为它是一个可编程系统,它的功能可以通过软件改变。可以说,SoPC 结合了 SoC、可编程逻辑器件(PLD)和现场可编程门阵列(FPGA)的优点。

嵌入式系统硬件的发展方向之一是以 SoC/SoPC 为中心,通过最少的外部元件和连接器构建一个硬件应用系统,以满足应用的功能需求。

典型软件架构

像嵌入式硬件一样,嵌入式软件架构高度灵活。简单的嵌入式软件(如电子玩具、计算器等)可能只有几千行代码,执行简单的输入和输出功能。另一方面,复杂的嵌入式系统(如智能手机、机器人等)需要更复杂的软件架构,类似于台式电脑和服务器。简单的嵌入式软件适用于低性能的芯片硬件,功能非常有限,需要繁琐的二次开发。复杂的嵌入式系统提供更强大的功能,需要为用户提供更方便的界面,需要更强大的硬件支持。随着硬件集成和处理能力的提高,硬件瓶颈已经逐渐松动甚至被打破,所以嵌入式系统软件现在趋于功能齐全和多样化。典型的、完整的嵌入式系统软件具有如图 1-9 所示的架构。

A978-1-4842-0100-8_1_Fig9_HTML.jpg

图 1-9。

Software architecture of an embedded system

嵌入式软件系统自下而上由四层组成:

  1. 硬件抽象层
  2. 操作系统层
  3. 系统服务层
  4. 应用层

硬件抽象层

硬件抽象层是嵌入式系统硬件和操作系统之间的软件抽象层,是操作系统的一部分。一般来说,HAL 包括引导程序、板支持包(BSP)、设备驱动程序和其他组件。类似于个人电脑中的 BIOS,引导装载程序是在操作系统内核执行之前运行的程序。它完成硬件的初始化,建立存储空间的映像,从而使硬件和软件环境达到一个合适的状态,以便最终调度系统内核。从最终用户的角度来看,引导加载程序用于加载操作系统。BSP 实现了硬件操作的抽象,使操作系统能够独立于硬件,并使操作系统能够在不同的硬件架构上运行。

必须为每个操作系统创建唯一的 BSP。例如,Wind River VxWorks BSP 和 Microsoft Windows CE BSP 对于嵌入式硬件开发板来说具有类似的功能,但它们具有完全不同的架构和接口。在讨论各种桌面 Windows 或 Linux 操作系统时,很少提到 BSP 的概念,因为所有的 PC 都采用统一的 Intel 架构;操作系统可以轻松地移植到各种基于英特尔架构的设备上,无需任何更改。BSP 是嵌入式系统中一个独特的软件模块。此外,设备驱动程序使操作系统能够屏蔽硬件组件和外围设备之间的差异,并为操作硬件提供统一的软件接口。

操作系统层

OS 是用于统一管理硬件资源的软件系统。它抽象出许多硬件功能,并以服务的形式提供给应用。调度、文件同步和联网是操作系统提供的最常见的服务。操作系统广泛应用于大多数桌面和嵌入式系统。在嵌入式系统中,操作系统有其独特的特征:稳定性、定制性、模块化和实时处理。

常见的嵌入式 OS 有嵌入式 Linux,Windows CE,VxWorks,MeeGo,Tizen,Android,Ubuntu,以及一些特定领域使用的操作系统。嵌入式 Linux 是为移动和嵌入式产品量身定制和修改的通用 Linux 内核。Windows CE 是微软为各种嵌入式系统和产品推出的可定制嵌入式操作系统。来自 Wind River 的嵌入式实时操作系统(RTOS)VxWorks 支持 PowerPC、68K、CPU32、SPARC、I960、x86、ARM 和 MIPS。它具有出色的实时性和可靠性,广泛应用于通信、军事、航天、航空和其他需要高度复杂的实时技术的领域。尤其是 VxWorks 被 NASA 用在火星探测器上。

系统服务层

系统服务层是操作系统提供给应用的服务接口。使用这个接口,应用可以访问操作系统提供的各种服务。在某种程度上,它起到了连接操作系统和应用的作用。这一层通常包括文件系统、图形用户界面(GUI)、任务管理器等等。GUI 库为应用提供了各种 GUI 编程接口,这使得应用能够通过应用窗口、菜单、对话框和其他图形形式而不是命令行与用户进行交互。

应用层

应用位于软件层次结构的顶层,实现系统功能和业务逻辑。从功能角度来看,应用中所有级别的模块都旨在执行系统功能。从系统的角度来看,每个应用都是一个独立的操作系统进程。通常,应用在低特权处理器模式下运行,并使用操作系统提供的 API 系统调度与操作系统进行交互。

嵌入式应用开发的特殊困难

正如本章前面提到的,嵌入式系统通常是资源受限的、实时的和健壮的。这些特点使得在嵌入式系统上开发应用比在通用计算机上开发更困难。

嵌入式系统的资源受限特性意味着它们比通用系统拥有更少的资源、更低的 CPU 运行速度和处理能力以及更少的 RAM。嵌入式系统将代码和数据存储在 ROM 或闪存中,而不是硬盘上,容量比硬盘小。与通用计算机相比,大多数专用嵌入式系统,尤其是嵌入式操作系统,也具有非常简单的功能。这些资源限制要求嵌入式硬件开发人员为芯片和外设选择更合理的配置。他们必须比开发桌面环境时更仔细地考虑资源利用。

嵌入式交互对应用开发提出了特殊的要求。一般桌面计算机使用 GUI 窗口、图标、菜单和指针(WIMP),包括常见的交互元素,如按钮、工具栏和对话框。WIMP 对交互硬件要求严格;例如,它要求显示器具有一定的分辨率和尺寸,鼠标或类似设备必须支持指点操作。然而,许多嵌入式系统的交互硬件并不符合 WIMP 的要求。例如,MP3 播放器的显示屏太小,分辨率不够;ABS 没有显示屏;而且大多数嵌入式系统都没有鼠标或者触摸屏来完成指点操作(比如基本的手机都没有触摸屏)。因为嵌入式应用的交互非常特殊,我们不能完全采用 WIMP 接口。

嵌入式系统特殊的用户体验和可靠性特征增加了应用开发的难度。例如,用户期望嵌入式系统的启动时间比通用计算机短得多。与通用计算机系统相比,嵌入式系统也更难保证可靠性。当任务出现问题时,嵌入式系统没有任务管理器、Kill 命令或类似的工具来终止出错的进程。显然,嵌入式系统对错误的容忍度低于一般系统。

嵌入式系统一般不支持本机代码开发。通用计算机上的软件开发通常有原生开发、编译和运行。它不适合嵌入式系统,因为它们没有足够的资源来运行开发和调试工具。所以嵌入式系统软件通常采用交叉编译开发,在另一个硬件平台上生成执行代码。

交叉编译开发环境构建在主机上,而嵌入式系统被称为目标机器。主机上的交叉编译、汇编和链接工具创建可执行的二进制代码,这些代码在主机上是不可执行的:只能在目标机器上执行。可执行文件被下载到目标机器。主机上的开发环境并不完全反映目标机器上的环境,因此对目标机器的调试和故障诊断可能很耗时。嵌入式系统的非本地开发模型给应用开发带来了一定的挑战。

摘要

本章讨论了嵌入式系统的原理、SoC 的架构以及 ARM 和 x86/x64 等平台的优缺点。个人电脑的应用开发者经常忽略硬件,而完全专注于他们的软件,因为这两个实体是相当独立的。然而,开发者不能忽视嵌入式系统硬件。由于 SoC 的独特功能、有限的资源以及硬件和软件的集成,开发人员需要了解硬件和硬件层的工作原理和机制,以便为 SoC 设计高效的应用(例如,ARM 和 x86 具有不同的硬件)。下一章将详细讨论英特尔嵌入式硬件平台,包括英特尔凌动处理器、英特尔嵌入式芯片组、SoC 和参考平台。

Creative Commons Open Access This chapter is licensed under the terms of the Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License ( http://​creativecommons.​org/​licenses/​by-nc-nd/​4.​0/​ ), which permits any noncommercial use, sharing, distribution and reproduction in any medium or format, as long as you give appropriate credit to the original author(s) and the source, provide a link to the Creative Commons licence and indicate if you modified the licensed material. You do not have permission under this licence to share adapted material derived from this chapter or parts of it. The images or other third party material in this chapter are included in the chapter’s Creative Commons licence, unless indicated otherwise in a credit line to the material. If material is not included in the chapter’s Creative Commons licence and your intended use is not permitted by statutory regulation or exceeds the permitted use, you will need to obtain permission directly from the copyright holder.

二、英特尔嵌入式硬件平台

Keywords Embed System Trust Platform Module Arithmetic Logic Unit Stream SIMD Extension Intel Atom

通用计算机上的应用开发人员通常可以忽略硬件,而完全专注于他们的软件,因为这两个实体已经变得相当独立。然而,开发者不能忽视嵌入式系统硬件。由于嵌入式系统的特殊功能、有限的资源以及硬件和软件的集成,您需要了解硬件和硬件层的工作原理和机制,以便为嵌入式环境设计高效的应用。

作为芯片创新的全球领导者,英特尔一直在为通用计算机和嵌入式系统设计高性能处理器和相关硬件。本章重点介绍面向嵌入式系统的英特尔技术,为后续应用开发铺平道路。

英特尔凌动处理器

从 2008 年开始,英特尔专门为嵌入式和移动设备设计了英特尔凌动处理器。作为最小和最低功耗的处理器,它使用全新的嵌入式设备微架构来降低功耗,同时保持与英特尔酷睿 2 处理器的指令集兼容性。

英特尔凌动处理器是当前基于英特尔的嵌入式系统架构。它与英特尔架构指令软件兼容。与面向台式机系统的英特尔处理器相比,其尺寸、功耗和其他特性更适合嵌入式应用。

当今一代英特尔凌动处理器可提供高能效性能,为一系列计算设备提供动力。轻薄的智能手机和平板电脑。智能汽车。创新的医疗保健设备。智能城市基础设施监控。面向云的高性能微服务器。这些只是英特尔凌动处理器创新以超低功耗推动更高性能的部分方式——连接人们、丰富生活、推动物联网。

英特尔凌动处理器 E3800 产品家族(前身为 Bay Trail)提供了一系列多核片上系统(SoC)选项。基于业界领先的 22 纳米制程技术,这些 SOC 将英特尔架构内核、图形、内存和 I/O 接口集成到一个单芯片解决方案中,可提供出色的计算、图形和媒体性能。

英特尔凌动处理器架构

在英特尔凌动 Clover Trail 平台之前,英特尔凌动处理器基于一个代号为 Saltwell 的微架构,该架构应用了两个发布宽度和有序流水线;它还支持英特尔超线程技术。微架构如图 2-1 所示。

A978-1-4842-0100-8_2_Fig1_HTML.jpg

图 2-1。

Intel Atom architecture

前端区域是优化的管道,包括

  • 32 KB,8 路组关联,一级高速缓存
  • 分支预测单元和即时翻译后备缓冲器(ITLB)
  • 两个指令解码器,每个解码器每个周期最多解码两条指令

在每个周期内,前端最多可以向指令队列发送两条指令进行调度。同样,在每个周期中,调度器最多可以通过双向端口向整数或 SIMD/浮点执行区发送两条指令。(单指令,多数据[SIMD])将在下一节介绍。)

整数或 SIMD/浮点区域的端口具有以下绑定功能:

整数执行区

  1. 端口 0:算术逻辑单元 0 (ALU0)、移位/旋转单元和加载/存储单元。
  2. 端口 1:算术逻辑单元 1、位处理单元、跳转单元和 LEA。
  3. 循环 0 中“装载使用”的有效等待时间。

SIMD/浮点执行区

  1. 端口 0: SIMD 算术逻辑单元、混洗单元、SIMD/浮点乘法单元和除法单元。
  2. 端口 1: SIMD 算术逻辑单元和浮点加法器。
  3. 在 wide 浮点执行区,SIMD 算术逻辑单元和洗牌单元是 128 位宽,但 64 位整数 SIMD 计算仅限于端口 0。
  4. 浮点加法器可以在 128 位数据路径中执行加法压缩单精度(ADDPS)/减法压缩单精度(SUBPS),而其他浮点加法运算在 64 位数据路径中执行。
  5. 浮点/SIMD 运算的安全指令识别算法可以直接执行新的、更短的整数算术指令,而无需等待旧的浮点/SIMD 指令(这可能会导致一些异常)。
  6. 浮点乘法流水线也支持存储负载。
  7. 具有加载/存储引用的浮点加法指令通过两个端口分配。

指令队列执行静态分区,以便调度来自两个线程的执行指令。调度器可以从两个线程中选择一条指令,并将它们分配给端口 0 或端口 1 执行。硬件在两个线程上选择预取/解码/分派,并根据每个线程的准备情况执行下一次执行。

Silvermont:下一代微体系结构

英特尔的 Silvermont 微体系结构采用 3D 三栅极晶体管设计,并与英特尔的 22 纳米 SoC 工艺共同优化。通过利用这一行业领先的技术,Silvermont 微体系结构包括

  • 一个新的无序执行引擎,可实现同类最佳的单线程性能。
  • 一种全新的多核和系统结构体系结构,可扩展至多达八个内核,可实现更高的性能、更低的延迟和更高效的无序支持,从而使系统更加平衡,响应速度更快。
  • 新的英特尔架构指令和技术带来增强的性能、虚拟化和安全管理能力,以支持广泛的产品。这些指令基于英特尔现有的 64 位支持和英特尔架构软件安装基础的广度。
  • 增强的电源管理功能,包括新的智能突发技术、低功耗 C 状态和利用英特尔 3D 晶体管的更宽动态工作范围。英特尔突发技术 2.0 支持单核和多核,可提供出色的响应能力,并可提高能效。

微架构如图 2-2 所示。

A978-1-4842-0100-8_2_Fig2a_HTML.jpg A978-1-4842-0100-8_2_Fig2b_HTML.jpg

图 2-2。

Silvermont microarchitecture

Silvermont 提供了以下优势和功能:

  • 高性能而不牺牲能效:无序执行流水线、改进了指令延迟和吞吐量的宏操作执行流水线,以及智能流水线资源管理
  • 功率和性能:高效的分支处理、精确的分支预测器和快速恢复流水线
  • 更快、更高效地访问内存:低延迟、高带宽高速缓存、无序内存事务以及多个高级硬件预取器、平衡内核和内存子系统

英特尔凌动处理器的特性

如本节所述,英特尔凌动处理器具备面向移动互联网设备(MID)、上网本、上网机和嵌入式系统的特性。

小外形尺寸

最新的英特尔凌动处理器 Z3740(代号 Bay Trail)封装尺寸仅为 17 mm × 17 mm,是一款多核 SoC,将下一代英特尔处理器内核、显卡、内存和 I/O 接口集成到一个解决方案中。它也是英特尔首款基于 22 纳米处理器技术的 SoC(见图 2-3 )。

A978-1-4842-0100-8_2_Fig3_HTML.jpg

图 2-3。

Intel Atom processor Z3xxx Series

低功耗

如前所述,嵌入式系统的功耗有限。英特尔凌动处理器采用节能技术,如增强型英特尔 SpeedStep 技术(EIST)、 1 低热设计功耗、动态高速缓存调整和深度睡眠。采用英特尔凌动处理器的设备散热非常有限,远低于常见的“全功率”设备。

需要注意的是,不同的英特尔凌动处理器系列具有不同的低功耗处理策略。例如,N 系列不支持 EIST,也不在待机状态下进行自动降频。

面向移动和嵌入式设备的动态低压技术

许多移动和嵌入式系统由电池供电;因此,电压不具有交流电源系统的稳定性,交流电源系统的电压保持在一定范围内。英特尔凌动处理器还采用了动态调整每个处理器活动状态的工作电压的技术,并支持面向移动和嵌入式系统的英特尔移动电压定位(IMVP)-6 标准。

高性能

英特尔凌动处理器是一款嵌入式微处理器,能够提供传统通用处理器的性能,并提供类似于英特尔奔腾 4 处理器的性能。高性能主要体现在以下几个方面:

  • 四核支持四核/四线程乱序处理和 2mb L2 缓存,通过允许多个应用和服务同时运行,使设备运行更快,响应更灵敏。
  • 英特尔突发技术 2.0 允许系统在必要时挖掘额外的内核,从而使 CPU 密集型应用运行得更快、更流畅
  • 使用 22 纳米处理器技术提高了性能:
    • 在导通状态下最大化电流,以获得更好的性能
    • 最大限度地减少关闭状态下的泄漏,从而提高能效
  • 支持 64 位操作系统
  • 支持 CPU 和 IP(显卡)之间的动态功率共享,允许更高的峰值频率
  • 总 SoC 能量预算根据应用需求动态分配
  • 支持细粒度的低功耗状态,这提供了更好的电源管理并延长了电池寿命
  • 支持深度睡眠状态下的高速缓存保留,从而降低闲置功耗,缩短唤醒时间
  • 提供超过 10 小时的有效电池寿命
SSE3 指令集增强了数字媒体的处理能力

CAD 工具、3D/2D 建模、视频编辑、数字音乐、数字摄影和游戏等软件应用都需要大规模浮点并行计算。它们被称为浮点密集型应用。比如视频处理经常需要两个 n 长的数据集相乘,所以常用的算术指令要运算 n 次(n 个周期)。为此,SIMD 建筑被创造出来。与传统处理器相比,SIMD 处理器有更多的算术单元,由一个控制器控制,同时在每个数据集(也称为矢量数据)中进行相同的数据操作,以实现空间并行。在图 2-4 所示的例子中,如果 CPU 使用 8 个处理单元,n /8 条 SIMD 指令就可以完成计算,运算时间缩短为原来的 1/8,速度提高了 8 倍。SIMD 的本质是从一个数据过程转移到一个数据集过程。

A978-1-4842-0100-8_2_Fig4_HTML.jpg

图 2-4。

Realization procedure of SIMD instructions

英特尔处理器中的流式 SIMD 扩展(SSE)加速了流式浮点计算,并大大提高了浮点密集型应用的性能。英特尔凌动处理器支持 SSE3 和 SSSE3(补充流式 SIMD 扩展 3;补充上证 3)。SSE 指令集的版本历史如表 2-1 所示。

表 2-1。

Development History of the SSE Instruction Set

| 版本 | 同SOUTH-SOUTH-EAST | SSE2 | SSE3 | SSSE3 | SSE4 | 钽电容 | | 日期 | One thousand nine hundred and ninety-nine | Two thousand | Two thousand and four | Two thousand and six | Two thousand and seven | Two thousand and eight | | 说明 | Seventy | One hundred and forty-four | Thirteen | Thirty-two | Forty-seven | Two hundred and fifty-six | | 提高 | 单精度矢量流量操作 | 双精度向量 128 位向量整数 | 复数运算 | 解码 | 视频加速图形模块协处理器加速 | SSE 扩展浮点运算 |
英特尔虚拟化技术(英特尔 VT)

英特尔凌动处理器支持英特尔 VT,这是一种 CPU 虚拟化技术。英特尔 VT 允许一个 CPU 模拟多个 CPU 的并行运行,让一个平台运行多个操作系统,并支持应用在独立的空间中独立运行,从而提高应用效率。

英特尔超线程技术(英特尔 HT 技术)和多核技术

全新英特尔 Z3xxx 凌动处理器支持英特尔超线程技术,产生的额外功耗不到 10%。同时,N 系列采用了双核架构。英特尔超线程技术和多核技术使处理器能够并行执行两个指令线程,并提供线程级并发应用,从而在当今的多任务环境中提高性能和系统响应。英特尔凌动处理器中的英特尔超线程技术和多核技术比单线程微处理器具有更高的执行效率。

英特尔凌动处理器使用的其他技术

此外,英特尔凌动处理器还采用了其他一些通常不为人所知的技术,这些技术可以提高处理器的性能:

  • 智能高速缓存:英特尔凌动处理器使用更智能、更高效的高速缓存和总线技术来有效支持数据共享,并提供增强的性能、响应和节能能力。
  • 功耗优化的前端总线:英特尔凌动处理器支持高达 1910 MHz 的频率(E3845 ),可满足苛刻应用的需求。此外,英特尔架构指令(宏操作)融合技术允许在低功耗状态下更快地执行指令。
  • 增强的数据预取技术:该技术可以有效地预测哪些数据将被具体使用,并提前自动加载到 L2 缓存中。
  • 突发模式:突发模式作为增强的硬件技术,在 Z5xx 系列之后的英特尔凌动处理器中使用。它会根据系统负载自动设置处理器性能水平,而不会影响散热设计,因此用户可以按需选择处理器性能。
  • 低成本:为了满足嵌入式系统的需求,英特尔凌动处理器采用了低成本的设计策略,其中之一就是应用英特尔架构的有序执行。与通用台式机处理器的无序执行相比,英特尔凌动处理器中的有序执行设计可以减少晶体管数量和制造成本,但会导致性能下降。为了弥补较低的性能,英特尔凌动处理器使用了更高的工作频率。

除了这些特性之外,与其他嵌入式处理器相比,英特尔凌动处理器还具有一些独特的优势。由于基于英特尔架构,英特尔凌动处理器拥有大量兼容的基于英特尔架构的软件应用。其中许多应用可以轻松无缝地迁移到基于英特尔凌动处理器的设备上。

总的来说,低功耗、小尺寸、低成本、低散热系数和高性能使得英特尔凌动处理器更适合嵌入式系统应用。由于采用低功耗、无铅、无卤素制造工艺,英特尔凌动处理器也非常环保。

英特尔嵌入式芯片组

芯片组是计算机主板的核心组件之一,它最大限度地将复杂的电路和组件集成在几个芯片内。芯片组决定了主板的功能、级别、档次。如果它不能与 CPU 一起正常工作,芯片组会严重影响整体性能,甚至会导致硬件故障。如果说 CPU 或微处理器是大脑,芯片组就是设备的神经系统。

计算机系统结构的典型示例如图 2-5 所示。CPU 通过具有高频率的 FSB 连接到主存储器 RAM、图形和其他组件。网络适配器和其他组件连接到中速总线(PCI 总线的频率比 FSB 低得多)。北桥(主机桥芯片)实现高速前端总线和中速总线的连接。COM、LPT 和 USB 等低速设备以及低速 ISA 总线通过南桥(标准总线桥芯片)连接到低速总线。

A978-1-4842-0100-8_2_Fig5_HTML.jpg

图 2-5。

Example of computer system architecture

这种结构的变体包括,例如,没有 ISA 总线的计算机。北桥和南桥集成在一些英特尔凌动系列处理器中,如后续章节所述。图 2-5 中的系统架构可以帮助你了解芯片组的主要组件及其功能。

Tip

PCI 和 ISA 两种类型的 PC 总线标准是 PCI 和 ISA。外围组件互连(PCI)是本地总线的标准,由英特尔于 1992 年推出。PCI 总线有 32 位或 64 位,速度有 33 MHz 或 66 MHz。32 位、33 MHz PCI 总线的带宽为 32/8 × 33 MHz = 132 MB/s。工业标准体系结构(ISA)基于 IBM PC 总线,是 20 世纪 80 年代初开发的总线标准。总线宽度为 8/16 位,工作频率为 8 MHz,远低于 PCI。大多数新计算机不支持 ISA 总线。

芯片组中的主要芯片及其功能如下:

  • 北桥芯片:决定主板系统的 CPU 类型、时钟速度、总线频率、内存类型、最大容量、性能、显卡插槽规格(ISA/PCI/AGP 插槽)、ECC 纠错支持等等。北桥在芯片组中起主导作用,所以也被称为主机桥。
  • 南桥芯片:南桥芯片提供对键盘控制器(KBC)、实时时钟控制器(RTC)、通用串行总线(USB)、Ultra DMA/33 (66) EIDE 数据传输模式、高级能源管理(ACPI)等的支持。它决定了扩展槽和扩展接口的类型和数量(如笔记本的 USB2.0/1.1、IEEE1394、串口、并口、VGA 输出接口)。南桥也被称为标准公交桥。
  • 其他芯片:一些芯片组结合了 3D 加速显示(集成图形芯片)、AC'97 音频解码和其他功能,并决定了计算机系统的显示性能和音频播放性能。

最新的英特尔凌动处理器包括采用突发技术的第七代英特尔 GPU,可提供改进的图形和媒体体验。新处理器支持高达 2,560 × 1,600 的 60 Hz 高分辨率显示器,并通过 Miracast 支持英特尔无线(英特尔 WiDi)技术。媒体编码和解码的高性能、低功耗硬件加速支持无缝视频播放。

英特尔片上系统(SoC)

与桌面设备不同,由于体积和空间的限制,嵌入式系统中的处理器、芯片组、显卡、主板和其他组件不能独立制造、配置和组装;否则,它们会太大,消耗太多的能量,具有不切实际的复杂设计,并且具有类似于台式机的不稳定布局。因此,目前大多数嵌入式系统都采用 SoC 设计。通过在单个芯片上集成微处理器/微控制器、存储器、总线、频率发生器和 A/D 或 D/A 转换的外围功能模块,SoC 提供了小尺寸、高能效、高可靠性和简单外围电路设计的优势。英特尔已经逐步将 SoC 作为英特尔凌动处理器的发展方向。最近的设计描述如下。

梅菲尔德

2012 年发布的 Medfield 是英特尔首款面向智能手机的 SoC 处理器。Medfield 平台的核心是 SoC 芯片(代号 Penwell)。事实上,以前的 Moorestown 平台需要双芯片解决方案来实现相同的功能。作为一个真正的 SoC,Medfield 不同于英特尔凌动处理器的单芯片布局,但与以前的芯片组相当。因此,它变成了一个更紧凑、更节能的处理器。Medfield SoC 处理器采用层叠封装(POP),整个芯片面积约为 12 × 12 mm,Medfield SoC 的内部架构如图 2-6 所示。

A978-1-4842-0100-8_2_Fig6_HTML.jpg

图 2-6。

Internal architecture of Penwell SoC

首款专为智能手机打造的 Medfield SoC 采用英特尔凌动处理器 Z2460。计划是在未来的 Medfield SoCs 中使用最新的英特尔凌动处理器。例如,第二个 Medfield SoC 的计划是采用英特尔凌动处理器 Z2610,并具有主流平板电脑的应用。Medfield SoC 采用 32 nm 处理器;集成了单核英特尔凌动处理器、512 KB L2 高速缓存、Imagination Technologies 的 PowerVR SGX540 GPU 和双通道 LPDDR2 内存控制器;并且支持 30 fps 1080p 视频解码。英特尔凌动处理器的最高频率被限制在 1.6 GHz。Z2460 可以将最低频率降低到 100 MHz,具有 1.3 GHz 的标准工作频率,在加速模式下仅工作在 1.6 GHz。作为第二个 Medfield SoC 内核,Z2610 保持 1.6 GHz 时钟速度运行。

英特尔凌动处理器 Z2460 在 100 MHz 时钟速度(最低频率)下功耗为 50 mW600 MHz 时钟速度时为 175 mW 在 1.3 GHz 时钟速度(标准频率)下为 500 mW 在 1.6 GHz 时钟速度(最高频率)下为 750 mW。与台式机处理器相比,Z2460 的功耗非常低。

如今,Android OS 完全支持 Medfield。英特尔与谷歌合作开发用于编译 ARM 和英特尔架构应用的软件。

海湾小径

Bay Trail 是基于 Silvermont 架构构建的全新英特尔多核 SoC,来自英特尔面向移动和桌面设备的强大处理器家族。Bay Trail 采用英特尔行业领先的三栅极 22 纳米工艺技术制造。

Bay Trail 是一款多核 SoC,将下一代英特尔处理器内核、显卡、内存和 I/O 接口集成到一个解决方案中。它也是英特尔首款基于 22 纳米处理器技术的 SoC。这款多核英特尔凌动处理器提供了卓越的计算能力,与前代产品相比更加节能。除了最新的英特尔架构核心技术,它还提供了广泛的平台特性,如图形、连接、安全性和传感器,使开发人员能够创建具有无限用户体验的软件。

基于英特尔架构的 64 位 Android 操作系统

一般来说,64 位和 32 位处理器之间没有太大的区别。但是计算密集型应用(稍后,本章将讨论在 64 位处理器上运行更快的软件工作负载)在从 32 位迁移到 64 位时可以看到显著的改进。几乎在所有情况下,64 位应用在 64 位环境中的运行速度都比 32 位应用在 64 位环境中的运行速度快,这是开发人员关心它的一个足够好的理由。利用平台功能可以提高执行大量计算的应用的速度。

64 位与 32 位 Android

64 位架构意味着整数寄存器和指针的宽度是 64 位。64 位操作系统的三个主要优势如下:

  • 寄存器数量增加
  • 扩展地址空间
  • 增加的内存

不难想象在不太遥远的将来,64 位芯片的 Android 手机。因为 Android 内核是基于 Linux 内核的,而 Linux 多年来一直支持 64 位技术,所以 Android 要完全支持 64 位处理,唯一需要做的就是让 Dalvik VM 兼容 64 位。Dalvik 应用(仅用 Java 编写)无需任何更改就可以在 64 位设备上运行,因为字节码是独立于平台的。

本地应用开发人员可以充分利用底层处理器提供的功能。例如,英特尔高级矢量扩展指令集(英特尔 AVX)已经扩展到在 64 位处理器上支持 256 位指令大小。

内存和 CPU 寄存器大小

与 CPU 相比,内存非常慢,与 CPU 处理一条指令所需的时间相比,读取和写入内存可能需要很长时间。CPU 试图通过多层缓存来隐藏这一点,但是即使是最快的缓存层与内部 CPU 寄存器相比也很慢。更多的寄存器意味着更多的数据可以完全保存在 CPU 内部,从而减少内存访问并提高性能。

这有多大的区别取决于所讨论的特定代码,以及编译器在优化代码以充分利用可用寄存器方面的能力。当英特尔架构从 32 位迁移到 64 位时,寄存器数量翻了一番,从 8 个增加到 16 个,这极大地提高了性能。

64 位指针允许应用寻址更大的 RAM 地址空间:通常,在 32 位处理器上,程序可用的可寻址内存空间在 1 到 3 GB 之间,因为只有 4 GB 是可寻址的。即使有 1–3gb 可用,单个程序也无法使用所有可寻址的内存,除非它采用将程序分成多个进程的技术,这需要大量的编程工作。在 64 位操作系统上,这没有关系,因为可寻址的内存空间非常大。

内存映射文件越来越难以在 32 位架构上实现,因为超过 4 GB 的文件越来越常见。如此大的文件不容易被内存映射到 32 位体系结构—一次只能将文件的一部分映射到地址空间。为了访问这样的文件,映射部分必须根据需要换入和换出地址空间。这是一个问题,因为如果由操作系统正确实现,内存映射是最有效的磁盘到内存的方法之一。

64 位指针也有一个很大的缺点:大多数程序使用更多的内存,因为指针需要被存储,它们消耗两倍的内存。在 64 位 CPU 上运行的相同程序比在 32 位 CPU 上运行的程序占用更多的内存。因为指针在程序中非常常见,这可能会增加缓存大小并对性能产生影响。

寄存器数量会极大地影响应用的性能。与 CPU 上的寄存器相比,RAM 速度较慢。CPU 缓存有助于提高应用的速度,但是访问缓存会导致性能下降。

性能提升的程度取决于编译器对 64 位环境的优化程度。能够在少量内存中完成大部分处理的计算密集型应用的性能会显著提高,因为大部分应用可以存储在 CPU 寄存器中。

相比之下,未经优化的应用的计算机性能可能会下降,因为 64 位指针需要两倍的带宽。但是,在移动环境中,操作系统和安装的应用应该经过精心设计以避免这种情况。在 64 位环境中运行较慢的大型程序的一个著名例子是 Oracle JVM。

ARM 和 Intel 64 位 CPU 都有 32 位兼容模式。尽管 32 位应用可以在 64 位处理器上运行,但使用 64 位优化编译器进行编译可以让它们利用 64 位环境的架构优势。

英特尔嵌入式系统参考平台

所谓的英特尔嵌入式系统参考平台是一系列使用英特尔凌动处理器和 SoC 的硬件设备。这种硬件组合具有紧凑的尺寸、低功耗、高性能、低成本以及适用于图形处理和其他专业领域的出色芯片组。因此,它可以广泛应用于各种嵌入式设备,如上网本、上网本、平板电脑、手机和 mid。

物联网(IoT)和下一代计算单元(NUC)

物联网(IoT)和下一代计算单元(NUC)是英特尔移动处理器的最新应用领域。如图 2-7 所示,英特尔 NUC 套件 DE3815TYKHE 采用面向智能系统的英特尔凌动处理器,是面向注重价值的企业和组织的小型设备。这种低成本、低功耗的解决方案为 NUC 外形带来了许多第一次:无风扇散热解决方案,实现了极致的静音和可靠性;板载闪存存储,实现了小尺寸软件解决方案;内置屏幕的内部平板显示器连接;视频图形阵列(VGA)端口,实现了传统安装中的显示器兼容性;串行端口接头,用于需要硬件握手稳定性的外设;看门狗定时器,实现了弹性系统可用性;内部集成电路(I2C)和脉宽调制(PWM)信号,用于与传感器和其他支持物联网的嵌入式设备接口。凭借其三年的供货能力,英特尔 NUC 套件 DE3815TYKHE 将为长时间的开发和生产周期提供支持。

A978-1-4842-0100-8_2_Fig7_HTML.jpg

图 2-7。

Intel NUC Kit DE3815TYKHE

凭借其垂直工业设计以及对 Linux 和 Windows 嵌入式操作系统的支持,这款英特尔 NUC 被设计为推动瘦客户机市场发展的基本构建模块。这款 NUC 是一款内置闪存、USB3 支持和音频耳机支持的无风扇套件,非常适合在学校、呼叫中心和其他安装了大量 VGA 显示器的地方使用。

采用英特尔凌动处理器 E3815 的英特尔 NUC 套件 DE3815TYKHE 还提供了功耗、性能、经济性和软件兼容性的理想组合,以驱动轻型数字标牌、销售点和信息亭解决方案等应用。凭借内置的 4 GB 嵌入式多媒体卡(eMMC)存储,许多嵌入式应用将受益于更低的整体系统级 BOM 成本。通过内置的看门狗定时器,还可以为这些和其他无人值守解决方案启用高可用性弹性,从而防止停机。这款 NUC 为基于硬件的数据加密提供了独立的可信平台模块设备,是机密信息受到威胁的应用的必备设备。

面向物联网的英特尔伽利略开发套件

如图 2-8 所示,英特尔 Galileo 开发板是采用英特尔架构的 Arduino 兼容开发板新系列中的首款产品。对于新设计师和希望将设计提升到更高水平的人来说,该平台易于使用。

A978-1-4842-0100-8_2_Fig8_HTML.jpg

图 2-8。

The Intel Galileo board

英特尔 Galileo 主板是一款基于英特尔 Quark SoC X1000 应用处理器(一款 32 位英特尔奔腾品牌的 SoC)的微控制器主板。这是首款基于英特尔架构的主板,其硬件和软件引脚与 Arduino Uno R3 的屏蔽板兼容。

该平台通过支持 Microsoft Windows、Mac OS 和 Linux 主机操作系统,简化了英特尔架构的开发。它还带来了 Arduino 集成开发环境(IDE)软件的简单性。

英特尔 Galileo 主板还与 Arduino 软件开发环境软件兼容,这使得易用性和介绍变得轻而易举。除了 Arduino 硬件和软件兼容性之外,英特尔 Galileo 主板还具有多个 PC 行业标准 I/O 端口和特性,可将本机使用和功能扩展到 Arduino shield 生态系统之外。一个全尺寸 mini-PCI Express 插槽、一个 100 Mb 以太网端口、一个 Micro-SD 插槽、一个 RS-232 串行端口、一个 USB 主机端口、一个 USB 客户端端口和 8 MB NOR 闪存是主板上的标准配置。

真正的英特尔处理器和 SoC 周围的本机 I/O 功能为制造商社区和学生提供了功能齐全的产品。对于寻求简单且经济高效的开发环境来开发基于英特尔凌动处理器和英特尔酷睿处理器的复杂设计的专业开发人员来说,它也非常有用。

智能手机

随着智能手机变得无处不在,客户对顶级设备的需求增加了,设计和可用性变得越来越重要。

Lenovo K900

如图 2-9 所示,联想 K900 是首款采用英特尔凌动处理器的大屏幕智能手机。K900 是世界上首批将 5.5 英寸 IPS 显示屏与每英寸 400+像素的 1080 像素全高清分辨率性能相结合的智能手机之一,所有这些都是在最新的触摸电容大猩猩玻璃 2 下实现的。

A978-1-4842-0100-8_2_Fig9_HTML.jpg

图 2-9。

Lenovo K900 smartphone

联想 K900 运行在英特尔凌动 Z2580 处理器上,这是一款双核芯片,运行频率高达 2.0 GHz,并利用英特尔超线程技术来提高性能效率。这款采用英特尔技术的设备还配备了运行 PowerVR SGX 544MP2 GPU 的英特尔图形媒体加速器引擎。联想为 K900 配备了大光圈 f1.8 镜头,使其成为第一款在其相机上提供如此大光圈的智能手机。结合其他规格,K900 现在是智能手机中数码相机的合法替代品。

Vexia 拉链手机

Vexia Zippers 手机,如图 2-10 所示,运行在 Android 4 操作系统上,由英特尔凌动处理器提供支持。它还配备了双 SIM 卡和一个 500 万像素的摄像头,因此您可以拍摄高清照片和视频。它的拉链界面使这款智能手机独一无二,你可以根据自己的生活方式进行个性化设置。

A978-1-4842-0100-8_2_Fig10_HTML.jpg

图 2-10。

Vexia Zippers phone

中兴大 X2*

中兴大 X2 如图 2-11 所示,其先进的双核英特尔凌动处理器 Z2580 在 Android 操作系统上运行超线程技术,可提供即时性能。用户可以享受更快的网页加载速度、应用启动时间和内容下载时间,以及图形功能和快速响应的多任务处理。

A978-1-4842-0100-8_2_Fig11_HTML.jpg

图 2-11。

ZTE Grand X2 Smartphone

中兴新的旗舰智能手机还配备了一个 8 MP 社交智能相机,是市场上拍摄时间最短的相机之一,每秒可拍摄 24 帧,没有快门延迟。它通过实时 2x 轴稳定以及人脸和智能场景识别,在充满挑战的环境中确保高图像质量。

药片

平板电脑是英特尔凌动处理器的主要应用领域之一。作为一台配备平板触摸屏的完整计算机,平板电脑没有常见的键盘和鼠标输入设备,而是在触摸屏上使用手写笔、数字笔和手指输入。自 2010 年苹果发布 iPad 以来,平板电脑占据了巨大的市场份额。接下来详细介绍几款采用英特尔凌动处理器的著名平板电脑。

三星 Galaxy Tab 3 10.1

如图 2-12 所示,三星 Galaxy Tab 3 10.1 由三星在 Computex 2013 上发布,并于 2013 年 7 月在美国上市。它的屏幕为 10.1 英寸,分辨率为 1280×800。Tab 3 10.1 采用了 1.6 GHz 的英特尔凌动 Z2560 双核处理器和 1 GB RAM,并配有 16 GB 的存储空间。这款平板电脑有一个 3.2 MP 的后置摄像头和一个 1.3 MP 的前置摄像头。它还有一个 SD 卡插槽。在撰写本文时,该设备运行的是 Android 4.2.2。

A978-1-4842-0100-8_2_Fig12_HTML.jpg

图 2-12。

Samsung Galaxy Tab 3 10.1

戴尔 Venue 7/8 英寸平板电脑

如图 2-13 所示,戴尔 Venue 7 于 2014 年初推出,采用 7 英寸屏幕,分辨率为 1280×800。它运行在英特尔凌动 Z2560 双核处理器上,主频为 1.6 GHz,包括 2 GB RAM 和 16 GB 内部存储。该设备包括一个用于可扩展存储的 SD 卡插槽,并有一个 300 万像素的后置摄像头和一个 VGA 前置摄像头。在撰写本文时,这款平板电脑运行的是 Android 4.3。

A978-1-4842-0100-8_2_Fig13_HTML.jpg

图 2-13。

Dell Venue 7/8" Tablet

钢 Iconia A1-830*

宏碁 Iconia A1-830 拥有 7.9 英寸显示屏,分辨率为 1024×768,于 2014 年初上市。它采用英特尔凌动 Z2560 双核处理器,主频为 1.6 GHz。该设备有 1 GB 的内存和 16 GB 的内部存储空间。它包括一个用于可扩展存储的 SD 卡插槽。前置摄像头 5 MP,后置摄像头 2 MP。在撰写本文时,这款平板电脑运行的是 Android 4.4.2。

asus memo pad fhd 10 *

如图 2-14 所示,华硕 MeMO Pad FHD 10 采用最新的英特尔凌动 Z2560 处理器(1.6 GHz)、2 GB 内存、178°宽视角、1920 × 1200 全高清 IPS 显示屏和 10 点多点触控显示屏,可提供生动的视觉效果,从而改善游戏体验。华硕 MeMO Pad FHD 10 重 580 克,薄 9.5 毫米。它有一个 microSD 扩展槽。

A978-1-4842-0100-8_2_Fig14_HTML.jpg

图 2-14。

ASUS Memo Pad

随着英特尔和谷歌的合作,每年都有越来越多采用英特尔凌动处理器的 Android 平板电脑发布。

车载信息娱乐系统

车载信息娱乐(IVI)系统是在汽车、卡车和飞机等交通工具中提供导航、娱乐和网络计算服务的设备。尤其是汽车制造商越来越将 IVI 系统视为其产品的一个关键区别点。司机和乘客开始期望在他们的汽车上看到他们在其他设备上看到的同样类型的创新,如移动电脑和手机。宝马、英菲尼迪、日产,当然还有其他已经宣布使用英特尔凌动处理器的平台。毫无疑问,英特尔凌动处理器将在这一充满希望的领域扮演重要角色。

其他应用平台和领域

除了上述领域,英特尔凌动处理器和相应的芯片还可应用于广泛的领域、平台和设备。

云计算

在云计算模式中,内容和基础设施驻留在云(网络)中。云内容消费者只需要一个轻量级的瘦客户端浏览器设备就可以参与其中。云计算的本质是,如果基础设施成本分摊给足够多的人,更多的人可以参与并受益于云内容和服务。基于英特尔凌动处理器的设备价格相对较低,非常适合云计算客户。

由于采用英特尔凌动处理器的设备能够使用英特尔架构代码库,因此也将有助于扩大云内容消费者群体。第二代 64 位英特尔凌动 C2000 产品系列 SoC 专为微服务器和冷存储平台(代号为 Avoton)以及入门级网络平台(代号为 Rangeley)而设计。这些新 SoC 是该公司基于 Silvermont 微架构的首款产品,这是领先的 22 纳米三栅极 SoC 工艺中的新设计,可显著提高性能和能效。

英特尔还推出了英特尔以太网交换机 FM5224 芯片,该芯片与 Wind River 开放式网络软件套件相结合,为服务器带来软件定义网络(SDN)解决方案,以提高密度和降低功耗。

机器人学

机器人技术是改善生产流程、提升定制产品生产能力和确保产品质量的公认关键。机器人自动化在历史上一直非常昂贵,具有非常长期的成本效益回报和巨大的进入壁垒。幸运的是,高性能英特尔凌动处理器能够满足机器人的大部分自动化计算要求,并凭借其经济高效和低功耗特性在机器人应用领域占据领先地位。实验室测试表明,基于英特尔凌动处理器的设备在不连接的情况下电池续航时间长达 8 小时,可以轻松地为移动机器人设备供电。

威斯康辛州的 Smith Childs Farms,Inc .为机器人拖拉机配备了英特尔凌动处理器。几台这种更小、更灵活的拖拉机可以同时在一块地里工作,由坐在桌前的农民控制。他们使用 GPS 导航设备、车载传感器和一系列复杂的算法,同时对土壤进行采样,并分配所需的精确数量的种子和肥料。史密斯·查尔兹农场有限公司的老板丹尼斯·史密斯说:“在黄金种植季节,你甚至可以看到农民在夜间工作,而不用担心有没有光线,因为拖拉机上的传感器在黑暗中也能‘看’得很清楚。”

无线传感器网络

英特尔凌动处理器用于创建智能无线传感器网络,这是嵌入式系统的一个主要应用领域。

英特尔在其亚利桑那州钱德勒的工厂部署了无线传感器网络。许多电池供电的无线传感器节点监控功耗和环境参数,如温度、湿度、照明和实验室空间。大量的小型无线传感器形成了一个自配置的动态路由网络。这些传感器与智能处理和分析传感器数据的服务器路由器通信。每个服务器路由器都是一个小型无线计算平台,配有基于英特尔凌动处理器的处理器、内存、闪存、I/O 和无线电组件,可处理来自多达 40 个传感器的网络数据。

这种灵活的自我配置网络体系结构易于安装,并且成本效益高。它还可以很容易地扩展到办公楼、商业设施和工厂,或者根据楼层面积进行更改。该系统为英特尔设施经理提供了一个全新的视角,帮助他们最大限度地降低电力成本。

学问

英特尔凌动处理器和相关硬件有助于生产低功耗、经济高效且环保的迷你/微型计算设备,这些设备在发展中国家和过去服务不足的市场中被广泛采用。这些系统支持本地化和远程学习,为这些国家和地区的国民教育做出贡献。

背包新闻和便携式录像

基于英特尔凌动处理器的设备具有超长电池续航时间、充足的存储容量和便捷的互联网连接,是新一代业余和临时视频记者进行现场报道(如体育赛事、演讲和辩论比赛以及专题研讨会的在线网络直播)的理想设备。录制视频的编辑和处理不能直接在此类设备上实现,但可以通过英特尔凌动处理器出色的互联网连接轻松传输到后端台式机或服务器,从而使用分布式处理解决方案实现快速、经济的视频录制。

RFID 现场工具

射频识别(RFID)是一个很有前途的行业。这种标签用于库存控制和监管链跟踪,将在医疗保健、药品管理和交付以及运输行业发挥重要作用。RFID 标签优于传统条形码的一个优点是标签询问器可以从几十米或更远的距离读取和写入标签。迄今为止,标签询问器一直是定制设备,需要与后端库存系统进行昂贵的、特定于供应商的集成。采用英特尔凌动处理器的设备不仅能够提供手持式询问器的移动性和连接性,还能够与英特尔架构后端库存和管理系统无缝兼容,从而显著降低这些系统的拥有成本。

摘要

第一章和第二章讨论了嵌入式平台、SoC 架构和硬件平台的历史,为系统和应用开发人员提供了不同的类别。从下一章开始,本书介绍了英特尔硬件平台上的 Android 应用开发。开发 Android 系统应用需要一些特殊的开发、调试和性能分析工具。在开发 Android 应用之前,您需要了解 Android 系统应用的开发过程,这就是您开始的地方。

Creative Commons Open Access This chapter is licensed under the terms of the Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License ( http://​creativecommons.​org/​licenses/​by-nc-nd/​4.​0/​ ), which permits any noncommercial use, sharing, distribution and reproduction in any medium or format, as long as you give appropriate credit to the original author(s) and the source, provide a link to the Creative Commons licence and indicate if you modified the licensed material. You do not have permission under this licence to share adapted material derived from this chapter or parts of it. The images or other third party material in this chapter are included in the chapter’s Creative Commons licence, unless indicated otherwise in a credit line to the material. If material is not included in the chapter’s Creative Commons licence and your intended use is not permitted by statutory regulation or exceeds the permitted use, you will need to obtain permission directly from the copyright holder. Footnotes 1

请访问 ark 查看处理器规格查找器。英特尔。如欲了解更多信息,请联系您的英特尔代表。

三、面向英特尔架构的 Android 应用开发流程和工具链

Keywords Tool Chain Android Application Integrate Development Environment Host Machine Target Machine

本章介绍英特尔硬件平台上的 Android 应用开发。开发 Android 系统应用需要一些专门的开发、调试、性能分析工具,开发环境和对象格式也不同于通用桌面电脑。在开发 Android 应用之前,我们需要了解 Android 系统应用的开发流程。

Android 为应用开发提供了一整套工具链(工具集)。Android 操作系统的早期版本支持 ARM 硬件平台,并从 Android 2.3(姜饼)开始支持英特尔凌动硬件平台。为了支持基于英特尔凌动架构的应用开发,英特尔添加了重要的插件、库和其他辅助模块,以便与 Android 工具链协同工作。此外,为了帮助开发人员获得英特尔硬件的性能优势,英特尔提供了编译器和英特尔?? 图形性能分析器等开发工具。

本章描述了在英特尔凌动平台上开发 Android 应用的一般流程和方法。使用特殊的英特尔工具实现优化性能和低能耗的方法将在后续章节中介绍。

Android 应用开发

以下部分描述了 Android 系统的开发环境、开发过程、调试和模拟。

Android 应用的开发环境

正如我们前面提到的,通用计算机的软件开发总是通过本机编译或开发来实现的。一般来说,嵌入式系统与本地开发环境不兼容,因此软件开发通常采用交叉开发。

交叉发展

典型的交叉开发配置如图 3-1 所示。交叉开发环境建立在开发机器或主机上。通常主机是一台通用计算机,如 PC。相应的嵌入式系统称为目标机。目标机器可以是多种嵌入式设备中的任何一种,比如手机、平板电脑等等。它们也可能是嵌入式系统制造商提供的专用评估板或基于软件的开发仿真器。在开发期间,使用主机上的交叉编译、汇编和链接工具来产生可在目标机器上执行的二进制代码;然后,可执行文件被下载并在目标机器上运行。交叉开发方法不仅是编译所需要的,也是调试所需要的。

A978-1-4842-0100-8_3_Fig1_HTML.jpg

图 3-1。

Cross-development configuration of embedded systems

嵌入式系统采用交叉开发的主要原因是本地编译通常不能在目标机器上有效地完成。首先,在开发过程中,目标机器的硬件经常不可用或不稳定。第二,目标机器平台上缺乏完整的原生编译工具。第三,目标机性能不足,导致编译缓慢。嵌入式系统上的软件编译比桌面计算机上的更耗时,因为它不仅需要编译应用,还需要编译库依赖项和 OS 内核。例如,在基于英特尔奔腾 4 处理器的电脑上编译 Linux 内核需要 10 多分钟。决定编译速度的主要硬件因素包括 CPU 速度、内存容量和文件系统 I/O 速度。在这些因素上,嵌入式系统的表现通常不如个人电脑。这导致目标机器上本机编译的低效率。嵌入式系统通常采用交叉开发方法,如交叉编译(包括交叉链接)和交叉调试。

由于主机和目标机在配置、功能、系统结构和操作环境方面的差异,它们通常通过串行端口、并行端口、USB 或以太网连接电缆进行连接。包括编码器、编译器、连接器、调试工具和软件配置管理工具的工具集安装在主机上。

通常,主机和目标机在以下方面有所不同:

  • 不同的结构:通常,主机是 Intel 架构的系统,而目标机可能是 Intel 或非 Intel 架构的系统结构,如 ARM 或 MIPS。
  • 处理能力不同:通常情况下,主机的处理速度和存储能力都优于目标机。
  • 不同的操作系统:通常,通用操作系统运行在主机上,而 Android 操作系统运行在目标机上。
  • 输出方式不同:与主机相比,目标机的输入输出功能能力较差。

对于某些 Android 系统来说,这些特征可能不存在或者微不足道。以英特尔凌动系统的开发为例。主机和目标机使用相同的系统英特尔架构结构。当然,指令集可能有所不同。例如,主机(如英特尔酷睿2 双核处理器)可能兼容 SSE4,而英特尔凌动处理器仅支持 SSE3。我们应该在编译时考虑目标机器的指令集。考虑到大多数英特尔凌动系统的资源有限,我们推荐交叉开发方法。

编程语言

在过去的四十年中,已经为通用计算机应用开发了几十种编程语言。从 FORTRAN、C/C++、ADA 和 Java 到 C#NET。许多因素决定了编程语言的适用性。各有各的特点,综合比较是不可能的。每种语言的性能取决于执行环境。考虑到多重因素和实际开发状况,Android 系统常用的语言有 C/C++、Java、Python 等,偶尔会用到汇编语言。编程一个复杂的 Android 系统需要多种语言的结合。常用编程语言如表 3-1 所示。

表 3-1。

Commonly Selected Programming Languages

| 水平 | 常见编程语言 | | --- | --- | | 应用软件 | C/C++,Java,。网络,脚本,Python | | 普通程度 | C/C++,汇编 | | 驱动程序级别 | C/C++,汇编 | | 引导代码,硬件抽象层(HAL) | 汇编,C/C++ |

由 Sun Microsystems 于 1995 年 5 月推出的 Java 是一种跨平台的面向对象编程语言,包括 Java 编程语言和 Java 平台(JavaSe、JavaEE、JavaME)。Java 的风格与 C 和 C++ 非常相似。它是一种纯面向对象的编程语言,继承了面向对象 C++ 的核心内容,摒弃了 C++ 语言中的指针(被引用代替)、运算符重载、多重继承(被接口代替)等导致错误频发的问题。添加的垃圾收集器用于收集未被引用的对象占用的内存,因此程序员不需要担心内存管理。在 Java 1.5 版本中,Sun 增加了其他语言特性,如泛型编程、类型安全枚举类、可变长度扩充和自动装箱/自动取消装箱。

Java 不同于普通的编译和执行计算机语言,因为它是一种解释性的计算机语言。Java 编译器产生的是二进制字节码而不是机器码,可以直接在本地执行。编译后的 Java 程序通过 Java 虚拟机(JVM)被解释成可直接执行的机器代码。JVM 可以解释不同平台上的执行字节码,以实现“一次编译所有执行”的跨平台特性但是解释字节码需要一定的时间,这在一定程度上会降低 Java 程序的运行效率。为了减轻这一负担,谷歌在 2014 年推出了 Android Run Time (ART)作为 Dalvik 版本 2,该版本首次在 KitKat (Android 4.4)中作为预览功能提供。未来的 64 位 Android 将基于 ART。总的来说,Java 是简单的、面向对象的、分布式的、解释性的和健壮的。它是一种可移植的、高性能的、多线程的和动态的编程语言。考虑到 Java 的各种优势,它是 Android 应用开发的首选。

选择了一种语言,你不一定要使用它的所有功能。虽然我们选择了 Java 作为 Android 的开发工具,但是 Android 系统的开发过程不同于传统的(桌面)Java SDK。Android SDK 使用了 Java SDK 的大部分,但放弃了一些部分。比如接口,java.awt 包只有 java.awt.font 引用,如果一个 java 游戏迁移到 Android 平台,可能需要移植。

我们提到过 Java 是一种跨平台的解释性计算机语言。这一特性使高迁移能力不受平台限制,但它也有一些缺点,其中之一是开发人员不能使用与平台或架构相关的特性或潜力。但这可以通过编译 C/C++ 和汇编语言,由机器相关的目标代码来实现。这在性能优化期间更加明显。为了使用机器硬件的特性并挖掘其性能潜力,我们通常需要使用 C/C++ 和汇编语言来编写优化的应用。虽然这类代码在所有代码中所占比例很小,但编程复杂度却远高于 Java。因此,这种代码只在极少数情况下使用。我们会看到 Android 应用开发采用了以改进的 Java 为主,C 和汇编语言为支撑的混合编程模式。

我们将分两部分讨论这种编程方法。对于开发 Android 应用的一般功能,我们将使用 Java。但是为了优化性能,我们将使用混合语言编程方法。

Android 应用开发流程

一般来说,开发 Android 软件需要和通用软件一样的步骤:设计、编码、编译、链接、打包、部署、调试、优化。对于某些 Android 系统,还需要测试和验证步骤。就过程而言,它可以分为五个阶段:编码、构建、部署、调试和调优。典型的开发过程如图 3-2 所示。

A978-1-4842-0100-8_3_Fig2_HTML.jpg

图 3-2。

Development process for Android software

编码

编码是软件开发过程的第一步。可以使用各种编辑器编写软件源代码。Android 开发期间,这项工作主要是编辑。java 代码和。xml 源文件。

建筑

构建阶段的任务是将代码转换成 Android 硬件上的可执行程序。该阶段包括编译、链接和打包等子步骤,如图 3-3 所示。

A978-1-4842-0100-8_3_Fig3_HTML.jpg

图 3-3。

Software construction stage

构建的第一步是构建,这意味着将所有源代码文件翻译成目标文件。一些目标文件是与机器相关的,例如对应于机器的执行指令的 C/C++ 目标文件。但是有些不是特定于机器的,例如 Java 目标源代码不是机器可执行的指令。在 Android 应用开发期间,这些文件通常带有后缀。班级。在安卓系统上,。类被翻译成。德克斯文件。

第二步是包装。打包的目的是将所有目标文件和附属文件合并并安装到目标计算机上的一个文件夹中。至于安卓,。dex 文件和资源文件都打包到一个。可以存储在目标计算机外部的 apk 文件。包装操作通常用特殊的包装工具来完成。

部署

部署是软件开发的最后一个阶段,是从主机上复制安装包、解压缩并安装到 Android 设备的内存中。

Android 采用了基于 USB 线缆的 ISP 部署。如图 3-4 所示,主机通过 USB 线连接到目标机。Android 操作系统在目标机器上运行,而 Windows 或 Linux 操作系统在主机上运行。生成的文件包(。apk)被复制到目标机器中文件系统的一个目录中,然后被解压缩和安装以完成部署。这个过程可以使用命令行终端或 Eclipse 中的 DDMS 来完成。

A978-1-4842-0100-8_3_Fig4_HTML.jpg

图 3-4。

Android application deployment

在联机编程模式下,主机和目标机之间的文件复制方向是不同的。并且不同的术语用于文件复制。比如下载/上传在 Android 里叫做 push/pull。Push 表示将文件从主机复制到目标机,pull 表示将文件从目标机复制到主机。

调试和优化阶段

这个阶段主要是对软件进行调试和优化操作。

即使是最有经验的软件工程师也不能完全避免程序中的错误。掌握调试技术对于软件开发至关重要。调试 Android 软件代码的效率不是很高,因为即使你只需要更改一行代码,你仍然需要经历所有的构建、打包和部署过程。PC 用户可能会接受每天一次的崩溃。但试想一下,如果 ATM、医疗操作系统或卫星上的最终 Android 系统产品存在 bug,后果会有多严重。

后续章节中讨论了很多 Android 软件的调试技术和技巧。许多方法在通用计算机软件中很少使用。

软件产品的最低目标是保证其正常运行。但这个目标对 Android 软件来说还不够好,Android 软件资源有限,对空间和性能的要求比桌面系统更严格。为了满足这些要求,Android 软件必须以性能优化的方式确保正常运行。这些目标可能是矛盾的,开发者很难实现所有的目标。因此,他们做出妥协,通常强调性能要求。

提高应用的性能是一个耗时的过程。哪些函数消耗了大部分执行时间通常并不明显。因此,我们需要使用专门的工具来分析代码,以准确理解性能瓶颈,并向我们提出改进建议。这个过程通常称为代码剖析,所使用的工具称为剖析器或性能分析器。

使用分析器来提高性能的原则是优化软件中经常被调用的部分。例如,如果 50%的时间花在字符串函数上,我们将这些函数优化 10 %,那么我们可以将软件的执行时间减少大约 5%。通过使用概要分析器,您可以准确地测量执行过程中花费的不同时间部分,以了解哪些区域可以优化。一些评测器可以针对特定类型的处理器提出改进建议。例如,英特尔Vtune放大器识别代码中的热点,可以进一步优化以提高整体性能。

Android 系统的调试和仿真

调试 Android 软件有一些特殊的挑战,因此开发了一些方法和设备来帮助开发人员完成调试过程。最常见的调试方法包括以下各节中描述的方法。

系统模拟器

早期的系统模拟器是用指令集模拟器实现的,即使用软件模拟系统架构的技术。换句话说,软件是用来解释机器代码来模拟某个处理器的。现代系统模拟器包括除 CPU 模拟之外的模拟外设。模拟外设用于实现系统仿真结果。有些书称模拟器为虚拟机或模拟器。

指令集仿真包括同构仿真和异构仿真。同构模拟意味着使用一个处理器上的软件来模拟具有相同架构的虚拟机。目前,常见的微软虚拟 PC 或 VMware 模拟基于英特尔架构的处理器的执行,使其成为一种同构模拟。异构仿真意味着在一个处理器上仿真另一个处理器的执行。大多数指令集仿真器都是异构仿真类型。例如,设备仿真器在英特尔架构处理器上模拟 ARM 处理器的执行。一些常见的系统模拟器如表 3-2 所示。

表 3-2。

Common System Simulators

| 模拟器的名称 | 模拟目标平台 | 评论 | | --- | --- | --- | | 微软虚拟个人电脑/虚拟服务器 | 英特尔架构 |   | | 虚拟机 | 英特尔架构 | 兼容 Windows、Mac 和 Linux | | 博奇斯 | 英特尔架构 | 开源项目 | | 设备仿真器 | 手臂ˌ武器ˌ袖子ˌ装备 | SMDK2410 开发板的仿真 | | 天空之眼 | 手臂ˌ武器ˌ袖子ˌ装备 | 中国制造 | | VirtualBox Advance | 手臂ˌ武器ˌ袖子ˌ装备 | 模拟任天堂 GBA 游戏玩家 | | Oracle VM Virtualbox | X86 和 AMD64/Intel64 虚拟化 | GPL 许可,并可免费获得 |

在 Android 系统上调试程序时,主机(通常是 PC)运行系统模拟器,目标机的软件在系统模拟器中运行,因此不需要额外的硬件。主机和目标机在同一台机器上实现,被称为“一机两用”。还记得之前提到的交叉开发环境吗?我们说目标机器不一定是真实的设备,因为它可能是基于软件的模拟器。在交叉开发过程中,模拟器取代了实际的目标机器。仿真器不仅节省了硬件开销,而且使调试更加方便。

Android 开发工具捆绑了 Android 虚拟设备,这是一个用于创建 ARM 和 x86 模拟器的管理器。仿真器模仿目标设备的硬件和软件配置。图 3-5 显示了一个在 Windows 中运行的 AVD 的截图。

A978-1-4842-0100-8_3_Fig5_HTML.jpg

图 3-5。

AVD (Android Virtual Device) interface

安卓模拟器也叫金鱼。每个 AVD 模拟一组运行 Android 平台的移动设备,包括内核、系统映像、数据分区以及 SD 卡、用户数据和显示器。Android 模拟器基于 Qemu,这是一个流行的开源虚拟化项目。Android 模拟器的源代码在 external/qemu 目录下。

AVD 模拟目标机器的常见组件,如 CPU、屏幕、键盘、音频输出、摄像头,以及 GPS、触摸和重力加速度等传感器。例如,采用英特尔架构的 avd 包括对应于每个 API 级别的英特尔 x86 系统映像。当然,与真实设备相比,AVD 有某些缺点,包括:

  • 无法拨打或接听实际电话;但是它可以通过控制台模拟电话呼叫(拨入和拨出)
  • 无 USB 连接
  • 无法捕捉数码照片或视频
  • 无法捕捉音频输入,但支持输出(重放)
  • 缺少对扩展耳机的支持
  • 无法确定电池电量或交流电源的充电状态
  • 无法确定是否插入或取出了 SD 卡
  • 不支持蓝牙

此外,AVD 可以模拟主机和目标机之间的 USB 和网络连接。AVD 使用主机作为默认网关和 NAT(地址转换器)来连接网络。换句话说,如果您可以在主机上访问互联网,那么您也可以在模拟 AVD 的目标机上访问互联网。

其他调试工具

Android 系统除了系统模拟器还有其他调试工具。虽然这些工具在 Android 中没有用到,但是你应该对它们有一个基本的了解,才能有一个完整的了解。

交叉调试

当操作系统支持交叉调试 Android 应用时,您应该尝试使用这种方法。交叉调试类似于交叉编译:被调试的程序在目标机器上运行,而调试的显示、监视和控制在主机上完成。

交叉调试只能在联机模式下执行。主机需要通过 USB 电缆、网络或 JTAG-ICE 连接到目标机。调试服务器通常运行在目标机器上,在 GNU 工具链中被称为存根。在主机上运行调试过程的前端实际上是客户端。前端与向调试服务器发出请求的开发人员进行交互。调试服务器接收前端的命令,控制应用的执行,并将结果发送到前端显示,如图 3-6 所示。

A978-1-4842-0100-8_3_Fig6_HTML.jpg

图 3-6。

Software environment for cross-debugging

例如,如果您在前端设置一个断点来观察变量的值,调试服务器将接收断点设置请求,并在程序的相应位置插入一个中断。当应用到达断点时,调试服务器接管控制权,挂起应用,并将相应变量的值发送回前端,然后前端显示该值。

许多开发工具支持交叉调试,例如 GNU 调试器。Android Debug Bridge (adb)是一个常用的调试工具,也支持交叉调试。adb 调试器基于客户机/服务器模型。它的工作原理是本地工作平台充当调试客户端,而安装远程应用的机器充当调试服务器的角色。使用 adb 时,远程应用(在目标机器上)的调试过程可能与本地调试不同。Adb 管理设备,模拟状态,并执行以下操作:

  • 设备和仿真器中的快速代码更新,如应用或 Android 系统更新
  • 在设备上运行 shell 命令
  • 管理设备或仿真器的预定端口
  • 在设备或模拟器上复制或粘贴文件

亚行的一些常见业务包括:

命令行

这个命令允许您进入设备或仿真器的 Linux shell 环境,在这里您可以执行许多 Linux 命令。如果只想执行一个 shell 命令,可以输入:

亚行 shell[命令]

对于[command],输入您想要执行的特定命令,例如:adb shell dmesg,它输出内核的调试信息。注意:Android adb 的 Linux shell 已经被简化,所以它与许多常见的 Linux 命令不兼容。我们将在接下来的章节中讨论命令行。

Adb 可以命令行形式独立运行,也可以作为插件集成到您喜欢的 IDE(集成开发环境)中,如 Eclipse。图 3-7 是在 Eclipse 中调试一个 Android 应用的截图。Adb 提供了许多常见的调试工具,如断点设置、观察变量、单步执行和检查调试输出。调试过程与本地应用的调试过程相同。许多开发人员甚至不知道应用是在目标机器上运行还是在主机上运行。

A978-1-4842-0100-8_3_Fig7_HTML.jpg

图 3-7。

Android application debugging in Eclipse

在接下来的部分中,我们将展示使用 adb 命令和 Eclipse 调试的例子。

典型的开发工具链

Android 软件开发的各个阶段都有相应的工具来帮助开发者完成任务。开发工具组被称为工具链或工具集。表 3-3 中列出了典型的工具链。

表 3-3。

Typical Tool Chains for Android Software Development

| 发展阶段 | 功能描述 | 典型例子 | | --- | --- | --- | | 编辑 | 编写和编辑源代码 | vi、Emacs、Windows 记事本 | | 编译和链接 | 将源程序编译和链接成可执行的二进制文件 | gcc、icc(英特尔编译器) | | 遮雨板 | 将可执行的二进制程序刻录到 Android 系统的 ROM 或闪存中,以确保系统自动启动 | J-fFlash,Sjflash | | 排除故障 | 项目运行状态的动态跟踪;检查程序的执行并识别程序错误背后的原因 | Gdb、adb、内核调试器 | | 最佳化 | 分析程序性能,帮助开发人员创建占用空间少、速度更快、效率更高的程序 | gprof、英特尔 vtune〔??〕??〕放大器 | | 测试 | 帮助测试人员识别程序中的错误,降低人力资源成本 | 这是什么 | | 核查 | 验证程序的逻辑正确性和常见错误,尤其是在恶劣的测试和调试环境下 | 应用验证程序 | | 模拟/仿真 | 模拟和仿真 Android 硬件的运行环境,帮助开发者开发和调试 | Qemu、VirtualBox 和 VMware Player |

许多工具集是可用的,由不同的公司和组织提供,每个工具集都有自己的特点。Icc、Vtune 放大器和 idb 由 Intel 提供,而 gcc、gdb 和 gproof 由自由软件组织 GNU 提供;而 CETK、应用验证器、设备仿真器都是微软提供的。其中一些工具是免费的,比如 GNU 工具集。其他工具,如微软工具集,必须购买。这些工具运行在不同的平台上。例如,Jflash 运行在 Linux 平台上,而大多数微软工具都基于 Windows(包括桌面 Windows OS 和 Android OS-Windows CE/Mobile)。而且有的甚至是跨平台工具;例如,GNU 工具集可以在多个平台上运行,比如 Linux、Windows 和 Mac 操作系统。

这些工具集的使用方式分为两类:一类是命令行,另一类是集成开发环境(IDE)。命令行工具集由命令行中输入的单个命令执行。在 ide 的情况下,所有的功能都集成到一个工具中,包括编辑、编译、链接、部署和调试,因此整个开发过程可以在一个应用中执行。大多数 GNU 工具运行在命令行上。可能使用最广泛的 IDE 是 Microsoft Visual Studio。Anjuta DevStudio 是一个基于 Linux 的 IDE。Android 开发工具 Eclipse 是一个可以在包括 Windows 和 Linux 在内的多种操作系统上运行的 IDE。在本书中,我们将使用 Windows 版本。

GNU 工具集可以在多种平台上运行;它们的开放性、使用范围大、与其他工具的兼容性,使其成为 Android 应用开发的普遍选择。

Tip

GNU、GPL 和 LGPL GNU 是迄今为止最大、最著名和最有影响力的自由软件组织。它是由理查德·斯托尔曼在 1985 年创建的,他创立了自由软件基金会(FSF)来脱离商业软件。在使用 GNU 软件之前,您必须遵守 GNU 软件许可证。

GPL 是 GNU 通用公共许可证的缩写,是 GNU 软件许可证的一种。GPL 允许公众享受运行、复制和共享软件的自由,获得源代码,改进软件并与公众共享。GPL 还规定,只要被改动内容的一部分或全部来自 GPL 编译的程序,那么被改动软件的共享就必须符合 GPL 的要求,这意味着你需要发布被改动的源代码,并且不得对改进软件的共享增加限制。GPL 是开发和发布 Linux 操作系统和相关软件的催化剂。

LGPL,意思是宽松的 GPL,也是 GNU 软件许可证之一。它是 GPL 的变体。不同的是,用户在 LGPL 授权的自由软件上享受私人使用。开发的新软件可以是专有的,而不是免费的。在使用自由软件之前,用户必须获得 LGPL 或 GPL 的其他变体。LGPL 最初用于一些 GNU 程序库(软件库)。所以它被称为库 GPL。Mozilla 和 OpenOffice.org 是 LGPL 时代开发的软件的例子。

GNU 开发工具是免费的。任何同意 GPL 许可的人都可以下载它们。GNU 还为 Android 系统和英特尔架构系统上的软件开发提供了完整的工具链。这些工具包括编译器、汇编器、链接器和调试工具。它们可以独立于命令行运行,也可以集成到 Eclipse 之类的 IDE 中。GNU 工具链在表 3-4 中列出。

表 3-4。

GNU Tool Chains

| 功能 | 成分 | 描述 | | --- | --- | --- | | 编辑 | 六、Emacs、ed | 用于编辑源代码的文本编辑器 | | 编译和链接 | (同 groundcontrolcenter)地面控制中心 | 一套多编程语言编译器 | | 排除故障 | 基因组数据库 | 调试器 | | 最佳化 | 证明 | 用于分析程序性能并帮助开发人员创建运行速度更快的程序的优化工具 | | 项目管理 | 制造 | 软件编译自动化管理工具 | | 制度建立 | 汽车工具 | 构建项目所需的所有材料和文件 |

下面将进一步解释这些组件。

编者ˌ编辑

任何文本编辑工具都可以用来编写和编辑源代码。Linux 平台有两类编辑器:一类包括 ed、ex 等行编辑器;另一个包括全屏编辑器,如 vi、Emacs 和 gedit。行编辑器只能对一行进行操作,而全屏编辑器可以编辑整个屏幕的代码,编辑后的文件被显示出来,从而克服了行编辑的缺点,使用起来更加方便。全屏编辑器比行编辑器具有更大的功能集。

在 IDE 中,编辑器被集成到工具中,不需要单独用来编写源代码。

编译器和链接器

编辑过程包括语法、语义和词汇分析、中间代码的生成和优化、符号表管理和错误管理。GNU 编辑器是 gcc。Gcc 被认为是 Linux 的标准编译器。

Gcc 最初是 GNU 的 C 语言编辑器。现在它支持 C、C++、Object-C、FORTRAN、Java 和 ADA。在某种程度上,gcc 是所有 GNU 编辑器的组合。Gcc 编译源代码并完成链接过程。用户可以选择命令参数来编译、链接和生成可执行文件。

英特尔编译器还优化了代码路径,以提高英特尔平台上的应用性能。英特尔编译器与英特尔提供的名为英特尔集成本地开发人员体验的工具捆绑在一起。

调试器

调试器使程序员更容易调试程序。但它不一定是代码执行所需的工具。在编译过程中,花在调试上的时间比花在编码上的时间多。因此,易于使用的全功能调试器是必要的。

GNU 调试器是 gdb(GNU 调试器的缩写)。它也是开源代码,是一个基于命令行的调试器。所有调试命令都是通过控制站的命令实现的。

构建管理器

GNU 提供了一个名为 make 的构建管理器,这是一个控制多个软件文件编译的工具。它类似于 Windows 中的 Visual c+++ 项目。此外,它可以自动管理软件编译的内容、方法和时间,以帮助程序员专注于编码,而不是组织编译序列。

Make 可以根据开发者定义的 makefile 调用 gcc 将源代码编译链接成目标机器的可执行文件。

Makefile 自动生成工具

Makefile 可以帮助 make 执行目标文件生成任务。但是对 makefile 进行编码并不是一件容易的事情,尤其是对于大型项目。GNU 提供了一系列自动工具来制作 makefiles。这种工具知道系统配置问题,以帮助开发人员处理迁移问题。自动工具包括 aclocal、autoscan、autoconf、autoheader、automake 和 libtool。

从源代码生成目标文件有几种方法,如图 3-8 所示。

  • 方法 1:使用 gcc(或英特尔编译器 ICC)编译链接所有源代码文件,生成可执行的目标文件
  • 方法 2:使用 IDE(比如 Eclipse)编译 makefile 和其他配置文件,然后使用 make 生成可执行的目标文件
  • 方法三:使用系统构建工具——autotools 制作 makefile 等配置,然后使用 make 生成可执行的目标文件

A978-1-4842-0100-8_3_Fig8_HTML.jpg

图 3-8。

Methods for generating target files using GNU tool chain

优化工具- gprof

为了帮助开发者优化他们的程序,GNU 提供了一个性能分析器 gproof,GNU binutils 工具之一。

Gproof 可以测量程序的性能,并记录每个函数的调用次数和相应的执行时间,以便优化工作可以集中在最耗时的部分。此外,gproof 还可以在程序执行过程中生成函数调用关系,包括调用次数,帮助程序员分析程序是如何执行的。通过依赖函数调用关系,开发人员不需要经历程序执行的所有细节,提高了他们的工作效率。而且这个功能对于维护旧代码或者分析开源项目也很有帮助。通过调用图,你可以对程序的运行框架和“骨架”有一个基本的了解。那么分析它们就不那么困难了,尤其是对于你可能不熟悉的代码和开源项目。

英特尔架构上 Android 应用开发工具链的概述、安装和配置

Android 为应用开发提供了一整套工具链(或工具集)。最初,Android 只能在 ARM 架构的硬件平台上运行。但是现在,为了在英特尔凌动硬件平台上支持 Android 工具链,英特尔增加了重要的插件、库和其他辅助组件。此外,为了更好地发挥英特尔硬件的性能优势,英特尔增加了编译器和优化器等特殊的开发工具。

本章介绍了在英特尔凌动平台上开发 Android 应用的一般流程和方法。在接下来的章节中,我们将讨论使用特殊英特尔工具实现优化性能和低能耗的方法。

Android 和 GNU 开发工具链以及 Android 交叉开发阶段对应的功能如表 3-5 所示。

表 3-5。

Comparison between GNU and Android Tool Chains

| 交叉发展阶段 | GNU 工具链 | 面向英特尔架构的 Android 开发工具链 | 评论 | | --- | --- | --- | --- | | 编辑 | 六、Emacs、ed | Eclipse,Android SDK | Android 开发工具和英特尔相关插件 | | 编译和链接 | (同 groundcontrolcenter)地面控制中心 | | 项目管理 | 制造 | | 自动生成工具-makefile | 汽车工具 | | 部署 | \ | | 排除故障 | 基因组数据库 | | 模拟/仿真 | \ | Android 虚拟设备(AVD) | | 最佳化 | gprof(全球定位系统) | Vtune 分析器 | 英特尔系列工具 |

除了上面展示的与 GNU 工具的区别,英特尔还提供了一些特殊的性能库,包括英特尔集成性能原语(英特尔 IPP)、英特尔数学内核(英特尔 MKL)、英特尔线程构建模块(英特尔 TBB)。一些函数库已经提供了特殊的服务,例如英特尔 TBB 中基于 C++ 模板的线程服务 API。其中一些使用英特尔架构指令潜力来实现优化的性能,例如英特尔 IPP 中的快速傅立叶变换(FFT)。一些库仍然没有直接的 Java 接口。我们将在随后的章节中讨论它们。

表 3-5 显示,面向英特尔架构的 Android 开发工具链基本包括两部分:一部分是 Android 开发工具。这里的英特尔工具包括英特尔架构仿真器、开发库和其它插件。另一部分是独立的英特尔工具。Android 开发工具支持应用开发的大多数步骤,如编辑、构建、打包、部署和调试,而英特尔工具主要涉及优化。

Android 开发工具是指由 JDK (Java SE 开发工具包)、Android SDK(软件开发工具包)和 IDE(集成开发环境)——Eclipse 组成的软件环境。Android 开发工具可以在 Linux、OS X 和 Windows 系统上运行。在本书中,我们将讨论 Windows 场景。

Android 开发工具可以在命令行格式或 IDE 中运行。Android SDK 中 Android 命令行工具的一般开发流程如图 3-9 所示。Eclipse 是一个图形用户界面工具,通常是用于 IDE 模式的工具,集成了编辑、编译、链接、部署和调试功能。我们将讨论基于 IDE 的方法。

A978-1-4842-0100-8_3_Fig9_HTML.jpg

图 3-9。

Development process of the Android SDK command line

Android SDK 的目录结构如下所示。它可以通过从命令行运行 tree 命令来获得。

■插件

★★★★★★★★★★★★★★★★★★★★★★★

─医生

◆关于

■素材

──设计

■发展

■分配

■指南

■图象

◆θ∑intl

■活

■退出

──参考文献

■资源

■样品

├─sdk

──可分享

■工具

──训练

ο额外

├─android

──谷歌

─平台工具

──蜜蜂

├─lib

──渲染脚本

─平台

★★★★★★★Android-16

■样品

★★★★★★★Android-16

ο来源

★★★★★★★Android-16

ο系统映像

★★★★★★★Android-16

ο临时

ε工具

├─ant

ο应用

■喷气式飞机

├─lib

■progresp

ο支持

├─systrace

ε模板

您应该注意的主要文件是:

  • 附加组件:由 Google 提供的 API 包,比如 Google Maps APIs
  • 文档:帮助和说明文档
  • 平台:每个 SDK 版本的 API 包和一些示例文件
  • 工具:一些通用工具文件
  • usb_driver: AMD64 和英特尔架构驱动程序文件

主要文件及其功能描述如下。

安卓. jar

这个文件位于%android-sdk%\platforms目录下,每个版本的 Android 都有一个 android.jar。jar 文件你可以了解内部 API 包的结构和组织。这里的字符串%android-sdk%是 Android SDK 的安装目录,16 版对应的目录是android-16。例如,作者的 android.jar 位于:

C:\Documents and Settings>dir D:\Android\

......

2012-07-08 20:02 18,325,478 android.jar

android.jar 是一个标准的 zip 包,包含编译后的压缩文件和所有 API。您可以使用 WinRAR 或其他归档工具来查看其内部结构,如图 3-10 所示。它的 API 包又进一步分为 app、内容、数据库等等。

A978-1-4842-0100-8_3_Fig10a_HTML.jpg A978-1-4842-0100-8_3_Fig10b_HTML.jpg

图 3-10。

Content structure of android.jar

ddms.bat

图 3- 11 所示的调试监控服务 ddms.bat 集成在 dal vik(Android 平台的虚拟设备)中,用于管理仿真器或设备的进程,辅助调试工作。它可以消除一些进程,选择某个程序进行调试,生成后续数据,检查线程数据,或者拍摄仿真器或设备的快照。

A978-1-4842-0100-8_3_Fig11_HTML.jpg

图 3-11。

The debugging monitor service ddms.bat

adb.exe

Android Debug Bridge (adb)是一个多用途工具,可以帮助你管理设备或仿真器的状态。如前所述,该文件位于%android-sdk%\platform-tools下。例如,作者的 adb.exe 位于C:\android\adt-bundle-windows-x86_64-20131030\sdk\platform-tools目录中,如图 3-12 所示。

A978-1-4842-0100-8_3_Fig12_HTML.jpg

图 3-12。

File location of the adb.exe tool

aapt.exe

使用 Android 资源打包工具(aapt.exe),您可以创建。包含 Android 应用的二进制文件和资源文件的 apk 文件。文件位置与 adb.exe 相同。

aidl.exe

Android 接口描述语言(aidl.exe)用于生成进程间接口代码。文件位置与 adb.exe 相同。

sqlite3.exe

Android 可以创建和使用 SQLite3 数据库文件。开发人员和用户可以很容易地访问这样的 SQLite 数据文件。文件位置与 ddms.bat 相同。

dx.bat(消歧义)

将类字节码重写为 Android 字节码(保存在一个 dex 文件中)。文件位置与 adb.exe 相同。

安卓. bat

android.bat 文件与 ddms.bat 在同一个目录下,该命令用于显示和创建 AVD。

A978-1-4842-0100-8_3_Fig13a_HTML.jpg A978-1-4842-0100-8_3_Fig13b_HTML.jpg

图 3-13。

The command shows that two target machine development libraries are installed on the machine

Android 的英特尔环境设置(OS X 主机)

Android (OS X 主机)环境设置将常见的英特尔和第三方工具集成到您的首选 IDE 中,用于面向生产力的设计、编码和调试。支持的 ide 包括 Eclipse 和 Android Studio。该测试版以前称为 Beacon Mountain 测试版,将成为面向 OS X 主机的英特尔集成本地开发人员体验(英特尔 INDE)的一部分,可在 https://software.intel.com/en-us/inde/environment-setup-osx 下载。表 3-6 列出了 Android (OS-X 主机)环境设置中包含的内容。

表 3-6。

Environment Setup for Android (OS-X Host)

| 产品安装 | Android Studio beta 英特尔集成本地开发人员体验(英特尔INDE)Android Studio 本地项目模板 Android SDK Android NDK 英特尔硬件加速执行管理器(英特尔HAXM)Apache Ant 英特尔Eclipse INDE 插件 | | 5 月 | Eclipse Android Studio 测试版 | | 主机支持 | OS X | | 目标支持 | Android* 4.3 及以上版本(基于 ARM 和英特尔架构) |

基于 Linux 的主机上的 Android 开发

以下适用于基于 Linux 的主机的 Android 开发工具可从以下网站下载:

英特尔集成本地开发人员体验测试版

英特尔集成本地开发人员体验(英特尔 INDE)是英特尔跨平台开发套件的测试版,旨在快速轻松地创建面向 Android 和 Windows 设备的应用,具有本地性能、出色的电池续航时间和独特的平台功能。INDE 为基于英特尔架构的设备上的环境设置、代码创建、编译、调试和分析以及基于 ARM 的 Android 设备上的选择功能提供了一套完整、一致的 C++/Java 工具、库和样本。

作为一个原生的跨平台开发套件,英特尔 INDE 包括 Android 和 Microsoft Windows 的 C++/Java 原生工具和样本,将工具集成到流行的 ide 中,并自动更新到最新的工具和技术。

工具和库

媒体:轻松添加视觉上引人注目的原生视频和音频扩展,可在最新流行的 Android 手机和平板电脑上工作。面向 Android 的英特尔 INDE 媒体包提供了源代码和示例,可通过以下方式增强应用:

  • 摄像头和屏幕截图
  • 视频编辑
  • 视频流
  • 音频指纹识别
  • 支持运行 4.3 及以上版本的英特尔架构和基于 ARM 的 Android 设备。

线程化:使用英特尔线程构建模块(TBB),高效实施更高级别的基于任务的并行处理。TBB 是一个屡获殊荣的 C++ 模板库,用于开发更高性能、可扩展的应用。使用并行工具创建的应用可以在基于英特尔架构和 ARM 处理器的 Android 4.3 和更高版本设备以及 Microsoft Windows 7–8.1 客户端上运行。

编译:借助面向 Android 的英特尔 C++ 编译器,以性能为导向进行编译,为您的 Android 应用带来行业领先的性能。该编译器与 GCC 源代码兼容,易于使用。GNU C++ 编译器也通过 Android NDK 提供,它是英特尔 INDE 环境设置组件中的一个定制选项。使用英特尔 C++ 编译器创建的应用可以在运行 Android 4.3 及更高版本的基于英特尔架构的设备上运行。

计算代码构建器:利用可编程图形最大限度地提高性能-使用计算代码构建器开发可在 CPU 之外的计算设备上执行的代码。该工具有助于创建、编译、调试和分析 Google Renderscript 和 OpenCL 等计算 API。compute code builder 可以以独立模式使用,也可以与 Microsoft Visual Studio 或 Eclipse 集成使用。创建的应用可以在基于英特尔架构的 Android 4.4 设备以及 Microsoft Windows 7–8.1 客户端上运行。如欲了解更多信息,请访问英特尔的入门指南。

  • 分析和调试:使用分析和优化工具套件包括英特尔图形性能分析器(英特尔 GPA)系统分析器、英特尔 GPA 平台分析器、英特尔 GPA 帧分析器和英特尔帧调试器。您可以使用它们来执行以下操作:
  • 对代码执行、CPU/GPU 使用和任务数据等进行实时跟踪分析
  • 帧捕获分析和调试
  • 平台范围和应用特定的 GPU 指标分析和图形管道覆盖

使用分析和调试工具创建的应用可以在运行 Microsoft Windows 7–8.1 或 Android 4.4 的基于英特尔架构的设备上运行。

设置

为英特尔 INDE 设置环境非常简单。您可以在几分钟而不是几小时内构建您的自定义环境:

  • 有选择地选择要安装的工具,允许定制环境。
  • 从 Google Android SDK(包括 Eclipse)、用于 Microsoft Visual Studio 的 vs-Android 插件、Android NDK、Android Design、Apache Ant 和 Intel HAXM 中进行选择。

使用环境设置创建的应用可以在运行 Android 4.3 及更高版本的英特尔架构和基于 ARM 的目标上运行。

Intel 印度安装

以下部分描述了英特尔 INDE 安装过程。

下载英特尔印度

前往 https://software.intel.com/en-us/intel-inde ,点击下载链接,接受许可协议。您将收到一封带有下载链接的电子邮件,如图 3-14 所示。

A978-1-4842-0100-8_3_Fig14_HTML.jpg

图 3-14。

Download screen for INDE

正在安装 Intel 印度

运行下载的文件:IntelHubSetup.exe。英特尔 INDE 窗口显示许可条款和条件,如图 3-15 所示。

A978-1-4842-0100-8_3_Fig15_HTML.jpg

图 3-15。

INDE install window

选中复选框以同意许可条款和条件,然后单击安装。安装过程开始,几个命令行窗口闪烁。您的桌面上会创建一个英特尔 INDE 图标和一个 NDK.cmd 图标。当该过程完成时,您就可以开始了,如图 3-16 所示。

A978-1-4842-0100-8_3_Fig16_HTML.jpg

图 3-16。

INDE setup complete

英特尔印度产品发布

单击启动图标,英特尔 INDE 主窗口将启动,如图 3-17 所示。

A978-1-4842-0100-8_3_Fig17_HTML.jpg

图 3-17。

Main window for INDE

按照每个工具和应用下载必要的软件。您已经准备好开始跨平台开发了。

配置 Eclipse

A978-1-4842-0100-8_3_Fig19_HTML.jpg

图 3-19。

Directory Location Setting of Android SDK

  1. 将弹出一个首选项对话框。选择 Android 分支,然后在 SDK 位置框中输入正确的路径(通常这是自动填充的),如图 3-19 所示。注意:点击 Android 分支后,会弹出一个对话框。单击继续继续。

A978-1-4842-0100-8_3_Fig18_HTML.jpg

图 3-18。

Startup page for configuring Eclipse

  1. 启动 Eclipse,选择窗口菜单,然后选择首选项,如图 3-18 所示。

创建 AVD(模拟器)

A978-1-4842-0100-8_3_Fig23_HTML.jpg

图 3-23。

Display of created list of emulator

  1. 然后会显示 Android 虚拟设备管理器,如图 3-23 所示,你可以在列表中看到新添加的项目。单击关闭按钮(x)关闭对话框。

A978-1-4842-0100-8_3_Fig22_HTML.jpg

图 3-22。

Creation parameter setting for emulator

  1. 当创建新的 Android 虚拟设备(AVD)对话框出现时,如图 3-22 所示,输入适当的名称,并为目标选择您希望使用的 Android 版本。CPU/ABI 框将自动显示英特尔凌动处理器(x86)。SD 卡的大小字段是硬盘上为其分配的空间量(在本例中为 1024 MB)。如果您的目标设备有更大的 SD 卡,请输入正确的大小。设置正确后,单击创建 AVD 关闭对话框。

A978-1-4842-0100-8_3_Fig21_HTML.jpg

图 3-21。

Initial page of emulator list

  1. 会弹出 Android 虚拟设备管理器对话框,如图 3-21 所示。单击新建按钮。

A978-1-4842-0100-8_3_Fig20_HTML.jpg

图 3-20。

Start menu for creating emulator

  1. 在菜单栏上选择窗口,然后选择 AVD 管理器,如图 3-20 所示。

摘要

到目前为止,您已经完成了为模拟器目标机器安装开发环境工具。下一章讨论,如果你的目标机器是一个真实的设备(例如,智能手机或平板电脑),你需要如何安装和配置开发环境,以便在那个设备上开发和测试应用。

Creative Commons Open Access This chapter is licensed under the terms of the Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License ( http://​creativecommons.​org/​licenses/​by-nc-nd/​4.​0/​ ), which permits any noncommercial use, sharing, distribution and reproduction in any medium or format, as long as you give appropriate credit to the original author(s) and the source, provide a link to the Creative Commons licence and indicate if you modified the licensed material. You do not have permission under this licence to share adapted material derived from this chapter or parts of it. The images or other third party material in this chapter are included in the chapter’s Creative Commons licence, unless indicated otherwise in a credit line to the material. If material is not included in the chapter’s Creative Commons licence and your intended use is not permitted by statutory regulation or exceeds the permitted use, you will need to obtain permission directly from the copyright holder.

四、真实设备环境安装

Keywords Mobile Phone Real Device Android Application Host Machine Target Machine

到目前为止,您已经完成了模拟器目标机器的开发环境的安装。但是如果您的目标机器是一个真实的设备(例如,带有 Intel Inside 标志的手机或平板电脑),您需要为它安装和配置开发环境。本章讨论如何使用真实的 Android 设备构建应用开发,包括如何安装驱动程序并将设备连接到您的开发主机。稍后,您将看到如何创建一个应用,并在模拟器和真实设备上测试它。

手机设置

设置真实设备支持 Android SDK adb 连接的方法有很多种,设置因设备而异。例如,在联想 K900 智能手机的情况下,您可以通过选择设置➤应用➤开发并单击 USB 调试来打开 Android 设备的调试功能。对于其他一些设备,USB 调试选项不可用,因为默认情况下不启用开发人员选项。戴尔 Venue 8 Android 平板电脑就是一个例子。要在 Dell Venue Android 平板电脑上启用开发者选项,您需要转到设置➤关于➤内部版本号;轻按“Build Number”七次以启用“Dell Venue Developer”选项,该选项将出现在“System”类别下。

在主机上安装 USB 驱动程序

本章以联想 K900 智能手机为例,说明如何在主机上安装手机 USB 驱动程序:

  1. 通过 USB 电缆将手机连接到开发 PC。

  2. 手机在设备管理器中被命名为未知设备(见图 4-1 )。

  3. 安装驱动程序。当手机 USB 连接设置为驱动程序安装模式时,可以从手机制造商处或者有时从仿真 CD-ROM 设备处(对于 K900)找到驱动程序。

A978-1-4842-0100-8_4_Fig1_HTML.jpg

图 4-1。

Lenovo K900 ADB device in Device Manager (a yellow ? appears on top of the icon if Device Manager is unable to recognize the mobile phone when a USB driver is not installed)

当您使用 USB 电缆将联想手机连接到 Windows 笔记本电脑时,光盘会安装到如图 4-2 所示的目录中。联想英特尔手机 u 盘的文件结构是

E: \Lenovo Kxxx Mobile phone driver>dir

2011-09-21 09:08         30 Autorun.inf

2012-03-23 17:10         2,366,976 bootstrap.exe

2012-03-23 17:15         69 bootstrap.ini

2012-03-23 10:57         10,993,152 LeDrivers.msi

A978-1-4842-0100-8_4_Fig2_HTML.jpg

图 4-2。

Directory display on the Windows host machine

  1. 双击LeDrivers.msi,开始安装 USB 驱动(见图 4-3 )。

  2. 安装后重新启动主机。

A978-1-4842-0100-8_4_Fig3_HTML.jpg

图 4-3。

Lenovo K900 Device Drivers Setup dialog

可以看到已经安装了 ADB 接口。在软件列表中,联想 Racer-A 设备驱动已经成功安装(见图 4-4 )。

A978-1-4842-0100-8_4_Fig4_HTML.jpg

图 4-4。

Software list for the Lenovo K900 after installation

主机和目标机之间的交互

配置好环境后,主机和目标机现在可以使用 Android 开发环境来提供辅助工具,以便在应用开发期间,除了部署操作之外,进行更多的交互。您对目标机器有更多的控制权,包括模拟器类型的目标机器。下面几节将介绍这些工具的使用。

开发 Android 应用

本节解释了如何使用 Eclipse 和 Android SDK 来创建项目、编辑项目,以及使用模拟器和真实设备来运行应用。

创建项目

要创建项目,请按照下列步骤操作:

  1. 启动 Eclipse,并选择文件➤新➤项目。在新建项目对话框中,选择安卓➤安卓应用项目,点击下一步继续(见图 4-5 )。

  2. 在新建 Android 应用对话框中,在应用名称字段中输入应用名称,如图 4-6 所示。项目名和包名是自动填充的。请注意,设置的项目名称也是目标机器上的应用名称。

A978-1-4842-0100-8_4_Fig5_HTML.jpg

图 4-5。

Starting a new Android project

  1. 使用默认配置,然后单击下一步。出现“配置启动器图标”对话框(见图 4-7 )。

A978-1-4842-0100-8_4_Fig6_HTML.jpg

图 4-6。

New project (application) name

  1. 使用默认配置,然后单击下一步。创建活动对话框出现(见图 4-8 )。

A978-1-4842-0100-8_4_Fig7_HTML.jpg

图 4-7。

New project—application icon setting

  1. 使用默认配置,然后单击下一步。出现新的空白活动对话框(参见图 4-9 )。

A978-1-4842-0100-8_4_Fig8_HTML.jpg

图 4-8。

New project—activity setting (1)

A978-1-4842-0100-8_4_Fig9_HTML.jpg

图 4-9。

New project—activity setting (2)

文件结构和内容如图 4-10 所示。

A978-1-4842-0100-8_4_Fig10_HTML.jpg

图 4-10。

New project—directory structure

编辑和运行(在模拟器上)

要使用模拟器测试运行应用,请执行以下步骤:执行以下步骤。

  1. 右击项目名称,并在快捷菜单上选择“以➤运行配置身份运行”。或者,从菜单中选择运行➤运行配置。

  2. 在对话框中,右键单击 Android 应用(当前项目名称),然后选择新建。左键单击目标选项卡,然后单击自动选择兼容复选框。在列表中选择英特尔凌动相关 AVD。点击应用,然后点击关闭以关闭对话框(参见图 4-11 )。

  3. 右键单击项目名称,从快捷菜单中选择“作为➤ Android 应用运行”(见图 4-12 )。

A978-1-4842-0100-8_4_Fig11_HTML.jpg

图 4-11。

Runtime configuration of the emulator target machine

  1. 运行前,出现如图 4-13 所示的消息框。单击“确定”继续。模拟器窗口出现并显示运行结果。

A978-1-4842-0100-8_4_Fig12_HTML.jpg

图 4-12。

Editing and runtime for starting an Android application

A978-1-4842-0100-8_4_Fig13_HTML.jpg

图 4-13。

Message prompt before running Android

在主机上,Eclipse 的控制台窗口显示编辑、部署和运行的进度,如图 4-14 所示。

A978-1-4842-0100-8_4_Fig14_HTML.jpg

图 4-14。

Eclipse interface when running an Android application

当最后一句出现时,仿真器屏幕显示应用窗口,如图 4-15 所示。注:如果在您的主机上安装并运行了英特尔 HAXM,此过程大约需要 2 分钟。

  1. 点击 Eclipse 窗口右上角的 DDMS 按钮,进入 DDMS 界面。左边的窗格显示了当前在目标机器(模拟器)上运行的应用。在这个例子中,com.example.helloandroid 和 helloandroid 正在运行(见图 4-16 )。

A978-1-4842-0100-8_4_Fig15_HTML.jpg

图 4-15。

An application running an interface on the emulator

  1. 您可以看到目标机器上的菜单发生了变化。单击主页按钮

A978-1-4842-0100-8_4_Fig16_HTML.jpg

图 4-16。

DDMS interface of Eclipse

在键盘上看到如图 4-17 所示的界面。然后点击屏幕底部左起第三个按钮。A978-1-4842-0100-8_4_Figa_HTML.jpg

A978-1-4842-0100-8_4_Fig17_HTML.jpg

图 4-17。

Home page of the emulator

在图 4-18 所示的应用列表中,可以看到新的 MainActivity 应用。

  1. 要停止运行应用,单击 Eclipse 右上角的 DDMS 按钮进入 DDMS 界面。从左边的进度列表中选择调试软件(通常是 com . example .[项目名称])。点击停止进程(见图 4-19 )结束在目标机器上运行进程。

A978-1-4842-0100-8_4_Fig18_HTML.jpg

图 4-18。

The application list on the emulator

A978-1-4842-0100-8_4_Fig19_HTML.jpg

图 4-19。

Ending running the application in DDMS

然后你会看到模拟器默认应用的页面,如图 4-20 所示。

  1. 在 Eclipse 中点击左上角的 Java。IDE 界面回到原来的编辑状态(见图 4-21 )。

A978-1-4842-0100-8_4_Fig20_HTML.jpg

图 4-20。

Initial page of the emulator

  1. 关闭模拟器窗口。

A978-1-4842-0100-8_4_Fig21_HTML.jpg

图 4-21。

The Eclipse editing interface

在真实设备上运行

要在真实设备上运行应用,请遵循以下步骤:

  1. 将手机连接到 PC。

  2. 进入 Eclipse 窗口,右键单击项目名称。在快捷菜单中,选择“作为➤运行配置运行”;或者,在 Eclipse 菜单中,选择运行➤运行配置。

  3. 在对话框中,左键单击 Android 应用,[当前项目名称]。左键单击目标选项卡,然后单击在所有兼容设备/AVD 上启动,这将设置为活动设备和 AVD。点击应用并关闭以关闭对话框(参见图 4-22 )。

  4. 右键单击项目名称,并从快捷菜单中选择“作为➤ Android 应用运行”。

A978-1-4842-0100-8_4_Fig22_HTML.jpg

图 4-22。

Setup for running an application on the real device

在真实设备上,可以看到应用上运行的界面,如图 4-23 所示。

A978-1-4842-0100-8_4_Fig23_HTML.jpg

图 4-23。

Application interface on the real device

应用图标出现在移动电话菜单上。并且在图 4-24 中可以看到,手机菜单上已经安装了 MyMainActivity。

A978-1-4842-0100-8_4_Fig24a_HTML.jpg A978-1-4842-0100-8_4_Fig24b_HTML.jpg

图 4-24。

Application list on the real device

有意思的是,真实设备上的应用进程比虚拟设备上的小很多(见图 4-25 )。它只有一个应用进程。

A978-1-4842-0100-8_4_Fig25_HTML.jpg

图 4-25。

DDMS interface of Eclipse

与在模拟器上运行您的应用相比,Eclipse 控制台窗格(参见图 4-26 )不提供太多关于编辑和部署的信息。

  1. 按照与模拟器相同的步骤停止运行应用。

A978-1-4842-0100-8_4_Fig26_HTML.jpg

图 4-26。

Eclipse interface while the application is running

调试 Android 应用

调试是应用开发过程中的一个重要步骤。对于 x86 平台目标,您需要基于 x86 的设备或 x86 模拟器来测试和调试应用。借助市面上的 IA 手机和平板电脑,如 Lava Xolo 和 Lenovo K900,以及三星 Galaxy Tab 10.1 和戴尔 Venue 7/8 等平板电脑,您可以在真正基于 x86 的平板电脑和手机上测试和调试应用。

如果您没有用于测试的 x86 设备,x86 模拟器也能很好地工作。可以使用 Android SDK Manager 安装 x86 模拟器。

编辑源代码

在 Eclipse 项目文件窗格中,找到\XXX\src\com.example.XXX\***.java文件,其中XXX是项目名称。双击文件名,源代码显示在右边。通过添加如图 4-27 (阴影线)所示的代码行来编辑源代码。

A978-1-4842-0100-8_4_Fig27_HTML.jpg

图 4-27。

Modifying the source code in Eclipse

设置断点

将光标放在代码上,右键单击快捷菜单中的切换断点,如图 4-28 所示。

A978-1-4842-0100-8_4_Fig28_HTML.jpg

图 4-28。

Menu to set a breakpoint

设置了断点的代码左侧显示绿色图标,如图 4-29 所示。

A978-1-4842-0100-8_4_Fig29_HTML.jpg

图 4-29。

Display after a breakpoint is set

重复此过程以取消在一行代码上设置的断点。

开始调试

要开始调试,请按照下列步骤操作:

  1. 右键单击项目名称。在弹出菜单中选择调试为➤安卓应用(见图 4-30 )。

  2. 在警告对话框中(图 4-31 ,点击是继续。

A978-1-4842-0100-8_4_Fig30_HTML.jpg

图 4-30。

Entering the debugging state

  1. Eclipse IDE 进入调试界面,如图 4-32 所示。在可能的情况下,目标机器(真实设备或仿真器)的初始运行界面如图 4-33 所示。

A978-1-4842-0100-8_4_Fig31_HTML.jpg

图 4-31。

Message box after entering the debugging state

A978-1-4842-0100-8_4_Fig33_HTML.jpg

图 4-33。

The emulator interface during debugging

A978-1-4842-0100-8_4_Fig32_HTML.jpg

图 4-32。

Interface of the Eclipse IDE during debugging

程序执行技术

如果你想执行单步执行,点击代码窗口使其成为活动窗口(见图 4-34 )。您可以突出显示代码段,并单击鼠标右键进入如图 4-34 所示的菜单。从菜单中,选择“单步执行”、“单步执行”或“单步返回”来执行。

A978-1-4842-0100-8_4_Fig34_HTML.jpg

图 4-34。

Single-step execution

观察日志的调试输出。x 函数

Log.X函数相当于 MFC TRACE函数,用于在 Eclipse LogCat 窗口中输出信息。要观察Log.X的调试输出,请遵循以下步骤:

  1. 如果 LogCat 窗格没有显示,点击窗口➤显示查看➤ LogCat(见图 4-35 )。根据您使用的 ADT 版本,可能找不到 LogCat。如果找不到 LogCat,您可以选择“其他”来显示更多选项,并将 LogCat 添加到您的列表中。LogCat 属于 Android 类别。

  2. 单步执行两个Log.d句子:

    1. 单击并激活代码窗口。
    2. 按 F6 键浏览代码。您可能需要多次按下 F6,才能在 LogCat 窗口中到达这些句子的最新输出(参见图 4-36 )。

A978-1-4842-0100-8_4_Fig35_HTML.jpg

图 4-35。

Viewing LogCat

  1. 点击 LogCat 窗口右上角的添加新的 LogCat 过滤器按钮,为调试输出信息创建一个过滤器(参见图 4-37 )。在对话框中输入过滤器名称和日志标记。过滤器名称可以是你喜欢的任何名称,但是 By Log 标签必须是源代码中Log.d()函数的第一个参数(字符串)。然后单击确定关闭窗口。

A978-1-4842-0100-8_4_Fig36_HTML.jpg

图 4-36。

Viewing output in the LogCat window

A978-1-4842-0100-8_4_Fig37_HTML.jpg

图 4-37。

Creating a new LogCat filter

在图 4-38 中,可以看到 LogCat 窗口中显示的Log.X被调用的输出信息。

A978-1-4842-0100-8_4_Fig38_HTML.jpg

图 4-38。

The LogCat window displaying the filtered output

观察变量

要观察变量,选择运行➤手表,如图 4-39 所示。

A978-1-4842-0100-8_4_Fig39_HTML.jpg

图 4-39。

The Watch command

右键单击表达式选项卡,弹出如图所示的菜单。点击添加新表达式,如图 4-40 所示,添加观察变量。

A978-1-4842-0100-8_4_Fig40_HTML.jpg

图 4-40。

Adding variables for observation

结束调试

点击工具栏上的终止(图 4-41 )或选择运行菜单上的终止,结束调试。

A978-1-4842-0100-8_4_Fig41_HTML.jpg

图 4-41。

Ending debugging by clicking the Terminate button

可以看到仿真器显示的默认应用页面,如图 4-42 所示。

A978-1-4842-0100-8_4_Fig42_HTML.jpg

图 4-42。

The default application page displayed by the emulator

返回到编辑主页。在 Eclipse 中点击左上角的 JavaIDE 界面返回到原始编辑状态。

用于 Android 应用开发的英特尔辅助工具

英特尔为基于英特尔凌动处理器的系统上的软件开发提供了一系列工具。这些工具是 Android 开发工具链的辅助工具,进一步支持 Android 应用开发。在第三章中,你看到了如何获取苹果 OS X 和 Linux 主机系统的 Beacon Mountain 工具,兼容 Eclipse,支持包括安卓 NDK 在内的流行安卓 SDKs 英特尔集成本地开发人员体验(英特尔 INDE)提供扩展的工具、支持和更多功能,用于在 Microsoft Windows 7-8.1 主机系统上创建 Android 应用。以下是对其中一些工具的介绍。

英特尔 C++ 编译器(英特尔 ICC)

英特尔 C++ 编译器(Intel ICC)是一组 C/C++ 编码器,可以运行在多种平台上,包括 Windows、Linux 和 OS X,在 Linux 平台上,它可以代替 gcc 完成 C/C++ 代码编译和链接。

英特尔 ICC 编码器可以生成挖掘英特尔处理器潜力的指令。英特尔 ICC 编码的代码在英特尔处理器上具有相对更好的性能。运行在 IA-32 和英特尔 64 上的 ICC 可以为 SSE、SSE2、SSE3 和 SSE4 等 SIMD 指令生成自动矢量分量,并为英特尔无线 MMX 生成变量。英特尔 ICC 支持 OpenMP 和对称多处理器(SMP)的自动并行化。借助额外的集群 OpenMP,英特尔 ICC 编译的代码可以传递分布式内存多处理(DM-SMP)的接口调用,从而在 OpenMP 指令中生成消息。这在性能优化部分有详细说明。

Intel ICC 和 gcc 都有编辑和链接功能。英特尔 ICC 可以命令行格式运行,例如

icc [options] [@response_file] file1 [file2...]

在哪里

  • options表示零个或多个编码部分
  • response_file是一个文本文件,列出了要编译的文件的编码选项,可以包括 C 或 C++ 文件(后缀:.C, .c.cc.cpp.cxx.c++.i.ii)和汇编文件(后缀:.s.S)、目标文件(后缀:.o)和静态库(后缀:.a)

英特尔 ICC 的常用选项如表 4-1 所示。

表 4-1。

Common Intel C++ Compiler Encoding Options

| 选择 | 描述 | | --- | --- | | `-fast` | 几个选项的缩写:`-O3 -ipo -static -xHOST -no-prec-div`。注意:xhost 标签上的说明解释了优化基于哪个处理器。处理器标签可能会在实践中被重写。 | | `-g` | 为调试 gdd 和 idb 调试器生成调试信息版本。 | | `-help [CODE]` | 在命令行上显示帮助信息。`CODE`解释帮助组的类型和选项。 | | `-m32` | 告诉编码器产生 IA-32 代码。 | | `-m64` | 告诉编码器产生 IA-64 代码。 | | `-O0` | 告诉编码器不要执行优化。 | | `-O1` | 告诉编码器优化代码大小。 | | `-O2` | 优化运行速度和启动优化 | | `-O3` | 启动所有优化,包括 O2 和密集循环优化。 | | `-prof-gen` | 将程序编译成代码分析器的运行模式。 | | `-prof-use` | 在每个步骤中编译和处理代码分析器信息。该选项仅适用于已经应用了`prof_gen`编码的程序。 | | `-xO` | 为非英特尔 CPU 启动 SSE3、SSE2 和 SSE 指令集优化。 | | `-xS` | 生成 SSE 矢量编码器和媒体加速指令。 |

表格中列出的选项是英特尔 ICC 独有的。英特尔 ICC 与 gcc 的兼容性意味着 gcc 的编码选项也可以在英特尔 ICC 中使用。例如,-o选项可以用来命名目标文件;-S用于解释复合汇编代码;-c只编译文件,不链接成可执行文件(即抵制链接)。

面向 Android 操作系统的英特尔图形性能分析器

英特尔图形性能分析器(英特尔 GPA)套件是一套功能强大的图形和游戏分析工具,旨在以游戏开发人员的方式工作,通过快速提供可操作的数据来帮助您从系统级到单个绘图调用寻找性能机会,从而节省宝贵的优化时间。

英特尔 GPA 现在支持运行谷歌 Android 操作系统的基于英特尔凌动处理器的手机和平板电脑。这个版本的工具集使您能够使用您选择的开发系统(Windows、OS X 或 Ubuntu OS)来优化 OpenGL ES 工作负载。借助这一功能,作为 Android 开发人员,您可以执行以下操作:

  • 获得涵盖 CPU、GPU 和 OpenGL ES API 的二十多项关键系统指标的实时视图
  • 进行大量图形管道实验,以隔离图形瓶颈
  • 使用基于英特尔凌动处理器的平板电脑时,运行英特尔 GPA 帧分析器来执行详细的帧分析和优化
  • 使用基于英特尔凌动处理器和 PowerVR 显卡的 Android 设备时,运行英特尔 GPA 平台分析器来执行详细的平台分析

要下载英特尔 GPA 的免费版本,请浏览至英特尔 GPA 主页( https://software.intel.com/en-us/vcsource/tools/intel-gpa ),然后单击相应产品版本的下载按钮。要为 Android 操作系统平台开发游戏或应用,请根据您的开发系统选择英特尔 GPA 版本。

英特尔系统工作室

英特尔 System Studio 是一个全面的集成工具套件,提供先进的系统工具和技术,帮助加速交付下一代高能效、高性能、可靠的嵌入式和移动设备。

英特尔 System Studio 2014 现在允许您针对嵌入式和移动 Android 和 Tizen IVI 系统进行开发,增加了从 Windows 主机的交叉开发,并为所有 IA 平台提供扩展的 JTAG 调试支持。新的基于代理的 UEFI debug 可帮助您加快上市时间,并增强这些日益复杂的嵌入式和移动系统的可靠性。Eclipse 集成和交叉构建功能允许使用英特尔 System Studio 2014 进行更快的系统开发。

英特尔 System Studio 包括表 4-2 中列出的组件。

表 4-2。

Intel System Studio Components

| 成分 | 描述 | | --- | --- | | 面向系统的英特尔 VTune 放大器 | 高级 CPU 和片上系统(SoC)性能分析和调整。 | | 英特尔能源分析器 | 高级 GPGPU 和 SoC 功耗分析和调整。 | | 英特尔系统分析器 | 利用 Android 目标的 CPU 和 GPU 指标进行实时系统级性能分析。 | | intel jtag 除错程式 | 用于深入了解 SoC 平台的系统调试器,具有低开销事件跟踪、日志记录、通过 JTAG 和 EDKII 调试代理对 EFI/UEFI 固件进行源代码级调试、引导加载程序、操作系统内核和驱动程序等功能。 | | gdb 调试器 | 软件调试器,用于快速应用级缺陷分析,以提高系统稳定性、应用级指令跟踪和数据竞争检测。 | | 英特尔系统检测器 | 动态和静态分析器,识别难以发现的内存和线程错误,以确保功能可靠性。 | | 英特尔 C++ 编译器 | 业界领先的 C/C++ 编译器,包括用于高度优化性能的英特尔 Cilk Plus 并行模型。二进制和源代码与 gcc 编译器和交叉编译器兼容。 | | 英特尔集成性能原件 | 广泛的高性能软件构建模块库,用于信号、数据和多媒体处理。 | | 英特尔数学内核库 | 高度优化的线性代数、快速傅立叶变换(FFT)、矢量数学和统计函数。 | | 系统可见事件连接(SVEN) 1.0 技术 | 超低开销事件跟踪。 |

英特尔 System Studio 开发工具与英特尔 Quark、英特尔凌动、英特尔酷睿和英特尔至强处理器平台相结合,为在众多市场交付强大的嵌入式和移动平台解决方案提供了更多价值和竞争优势。

英特尔项目无政府状态:Havok 的免费手机游戏引擎

Project Anarchy 是一款适用于 iOS、Android(包括 X-86)、Tizen 的免费手机游戏引擎。它包括 Havok 的视觉引擎以及 Havok 物理学、Havok 动画工作室和 Havok AI。它拥有可扩展的 C++ 架构、优化的移动渲染、灵活的素材管理系统以及 Lua 脚本和调试。SDK 中包含完整的游戏示例,以及 Project Anarchy 网站上的大量课件,游戏开发人员可以使用这些课件快速熟悉引擎,并将其游戏创意付诸实践:

  • 基于可扩展 C++ 插件的架构
  • 全面的游戏样本与完整的源代码艺术和源代码
  • 关注社区,通过论坛获得支持、问答、反馈和实践培训
  • 对公司规模或收入没有商业限制
  • 针对其他平台和产品、资源以及支持的升级
  • 包括行业领先的音频工具 FMOD

英特尔性能库

特殊性能库包括英特尔集成性能基元(IPP)、英特尔数学内核(MKL)和英特尔线程构建模块(TBB)。

IPP 8.1 是一个庞大的软件函数库,用于 Windows、Linux、Android 和 OS X 环境下的多媒体处理、数据处理和通信应用。它包含了广泛的功能,包括通信和图像处理、计算机视觉、语音识别、数据压缩、加密和解密、字符串操作、语音处理、视频格式化、真实感渲染和 3D 数据处理。它还包括用于构建音频、视频和语音编码器/解码器的复杂原语,如 MP3、MPEG-4、H.264、H.263、JPEG、JPEG2000、GSM-AMR 和 G723。

通过支持所有数据类型和函数布局,数据结构类型的数量被最小化。在应用设计和优化过程中,英特尔 IPP 函数库提供了多种选项集。每个函数都支持各种数据类型和布局。IPP 软件的最小化数据结构为生成优化的应用、高级软件模块和库函数提供了最大的灵活性。IPP 的 Linux 版本提供支持 IA-32、英特尔 64、IA-64 和英特尔凌动处理器的独立软件包。

TBB 是一个广泛使用、屡获殊荣的 C 和 C++ 库,用于创建高性能、可扩展的并行应用。它通过一组丰富的组件来高效实施更高级别的基于任务的并行性,从而提高了工作效率和可靠性。您可以通过构建面向未来的应用来利用多核和众核能力,从而获得性能优势。高级线程库兼容多种编译器,可移植到各种操作系统。

英特尔 IPP 和 TBB 提供便利,并帮助优化程序运行时性能。您可以通过调用库中的函数来减少必须编写的代码量。英特尔性能库可以提供与第三方库相同或相似的服务和功能。它们充分利用英特尔和兼容处理器的指令能力;因此,相同或相似的服务比第三方库或操作系统提供的服务性能更好。这个话题在第八章和第九章的代码优化章节中有详细讨论。

摘要

在本章中,您讨论了如何在主机系统上设置和配置应用开发,为 Android real 设备安装 USB 驱动程序,以便在设备和主机系统之间建立连接,从而允许您测试和调试应用。您还讨论了如何使用英特尔仿真器、加速仿真器所需的所有步骤以及如何使用仿真器。在下一章中,您将讨论 Android 操作系统,并了解基于英特尔架构的 Android 操作系统的原理。

Creative Commons Open Access This chapter is licensed under the terms of the Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License ( http://​creativecommons.​org/​licenses/​by-nc-nd/​4.​0/​ ), which permits any noncommercial use, sharing, distribution and reproduction in any medium or format, as long as you give appropriate credit to the original author(s) and the source, provide a link to the Creative Commons licence and indicate if you modified the licensed material. You do not have permission under this licence to share adapted material derived from this chapter or parts of it. The images or other third party material in this chapter are included in the chapter’s Creative Commons licence, unless indicated otherwise in a credit line to the material. If material is not included in the chapter’s Creative Commons licence and your intended use is not permitted by statutory regulation or exceeds the permitted use, you will need to obtain permission directly from the copyright holder.

五、安卓操作系统

Keywords National Electrical Manufacturer Association National Electrical Manufacturer Association Executable File Host Machine Target Machine

所有计算设备的核心是操作系统,即 OS。应用软件的开发和执行是基于操作系统和整个软件平台的。在本章中,您将了解 Android 操作系统——为基于英特尔凌动处理器的机器推荐的软件平台——从而为嵌入式应用的后续开发培养能力。

Android 概述

Android 是基于 Linux 内核的综合操作环境(2014 年,内核版本 3.10.x 已经被各大 OEM 厂商使用)。最初,Android 的部署目标是移动电话类别,包括智能手机和更低成本的翻盖手机设备。然而,Android 的全套计算服务和丰富的功能支持完全有可能扩展到手机市场以外,用于平板电脑等其他平台和应用。

除了内核之外,面向 x86 的 Android 操作系统还需要一些驱动程序和技术,包括移动设备上常见的驱动程序和技术:

  • 主机和客户端的 USB 驱动程序
  • 用于视频编码和解码的视频驱动器
  • 显示和图形:2D 和三维渲染:飞机、管道、港口
  • 闪存驱动器
  • 相机驱动:通常是基于 Linux 的 v41(视频)驱动
  • 音频驱动程序:通常是基于高级 Linux 声音架构(ALSA)的高级 Linux(声音系统)驱动程序
  • 近场通信(NFC)
  • 无线驱动程序:基于 IEEE 802.11 的驱动程序
  • 键盘驱动程序
  • 安全性(DRM、可信引导等)
  • 蓝牙驱动程序
  • binder IPC:Android 的一个特殊驱动程序,具有独立的设备节点来提供进程间通信(IPC)功能
  • 电源管理:三种不同 CPU 待机状态的驱动程序:活动待机(S0i1)、始终在线始终连接(AOAC)待机(S0i2)和深度睡眠待机(S0i3)

鉴于 Android 功能的广泛性,很容易将其与桌面操作系统相混淆;Android 是一个分层的环境,包含了丰富的功能。

Android 应用通常用 Java 编程语言编写,可以包含许多不同种类的资源文件(在res目录中)。编译完 Java 程序和其他相关资源后,会生成一个 APK 包。谷歌还提供了对多个 APK 文件的支持。这是 Google Play 的一个功能,允许开发者为应用发布不同的 apk,每个 apk 针对不同的设备配置。Android 提供了许多核心应用,包括主页、联系人、电话和浏览器。此外,您可以使用应用框架层中的 API 来开发自己的应用。

Android UI 子系统包括

  • Windows 操作系统
  • 视图
  • 用于显示常见元素的小部件,如编辑框、列表和下拉列表

它还包括一个基于 WebKit 的嵌入式浏览器,这是苹果 iPhone 手机 Safari 浏览器的开源浏览器引擎。

Android 拥有一系列健康的连接选项,包括 Wi-Fi、蓝牙和基于蜂窝连接的无线数据(例如,GPRS、EDGE、3G 和 4G/LTE)。Android 应用中一个流行的技术是链接到谷歌地图,直接在应用中显示地址。Android 软件栈中也支持基于位置的服务(如 GPS)和加速度计,尽管并非所有 Android 设备都配备了所需的硬件。还有摄像头支持。从历史上看,移动应用一直难以与桌面应用保持同步的两个领域是图形/媒体和数据存储方法。Android 通过内置对 2D 和 3D 图形的支持,包括 OpenGL ES 库,解决了图形挑战。Android 平台上流行的开源 SQLite 数据库减轻了数据存储负担。

Android 架构

Android 软件架构的简化框图如图 5-1 所示。

A978-1-4842-0100-8_5_Fig1_HTML.jpg

图 5-1。

Android software architecture

如前所述,Android 运行在 Linux 内核之上。基于 Java 的应用在虚拟机(VM)中运行。值得注意的是,VM 不是 JVM,正如您可能期望的那样,而是 Dalvik 虚拟机(DVM),一种开源技术。每个 Android 应用都在 DVM 的一个实例中运行,而 DVM 又驻留在 Linux 内核管理的进程中,如图 5-2 所示。

A978-1-4842-0100-8_5_Fig2_HTML.jpg

图 5-2。

Android application operation layer

从 Android KitKat 4.4 开始,谷歌实现了新的 Android 运行时(ART),也称为 Dalvik 版本 2。Android 开源项目(AOSP) master 正在积极开发 ART,未来的 64 位版本 Android 都将基于 ART,你可以在 https://source.android.com/devices/tech/dalvik/art.html 找到关于 ART 的最新信息。

从编程角度看基本的 Android 功能

Android 是 Linux 的一个版本。本节通过两个场景为 Android 开发人员概括了一些基本的 Linux 命令和常见做法:一个基于 Android SDK 中提供的 Android 仿真器,另一个面向拥有真正的 Android 智能手机或采用 Intel inside 的平板电脑的开发人员(在示例中为联想 K900 智能手机)。如果你是一个有经验的 Android/Linux 开发者,你可以安全地跳过本章的其余部分,进入下一章。

Android 系统界面

Android 移动系统界面是各大厂商产生分歧的一个关键方面。个性化的系统界面不仅是一个卖点,也是手机用户与朋友交流的平台。本节以联想 K900 智能手机为例,讨论 Android 系统 UI 设计。联想 K900 使用的四叶草设计如图 5-3 所示。

A978-1-4842-0100-8_5_Fig3_HTML.jpg

图 5-3。

Lenovo K900 main interface

联想手机采用常规的“上滑解锁”模式,简单方便。长按一个叶子就可以改变主界面中的快捷键,如图 5-4 所示。值得注意的是,在主界面操作过程中,如果你点击菜单键,快捷方式设置选项不会出现,就像在其他一些 Android 手机上一样。此外,您不能添加或删除主屏幕(leaves 中的快捷键可以更改)。

A978-1-4842-0100-8_5_Fig4_HTML.jpg

图 5-4。

All Menu interface

联想手机的下拉菜单是个性化的,包括四个选项:通知、切换、通话和消息。在 Switch 界面中,可以对手机进行快速设置或者长按某个选项进入该选项的详细设置界面。

一些 Android 手机允许你在界面上长按任何空白处时添加小工具和快捷方式,但联想手机没有这项功能。您可以使用菜单键添加工具和快捷键(除了)主屏幕。当你用两个手指按下屏幕时,出现多屏界面,你可以在这里进行快速搜索,如图 5-5 所示。

A978-1-4842-0100-8_5_Fig5_HTML.jpg

图 5-5。

Multiscreen interface of the Lenovo phone

20 矩形网格设计的菜单界面时尚优雅,但有些图标模糊不清。

联想 K900 手机的主要硬件和 OS 配置信息如图 5-6 所示。

A978-1-4842-0100-8_5_Fig6_HTML.jpg

图 5-6。

Parameters of the Lenovo K900 phone

在 Android 中终止应用

Android 提供了三种不同的方法来终止应用,如以下章节所述。

方法 1(针对真实设备)
  1. 在移动设备菜单中选择以下选项:设置➤应用➤运行服务➤运行应用。显示正在执行的应用。该列表在不同的设备上可能会有所不同。例如,在三星 Galaxy Note 上,应用被替换为应用管理器,在戴尔 Venue (Android KitKat 4.4)上,它被替换为应用。
  2. 单击要从列表中删除的应用,屏幕上将弹出强制退出和卸载按钮。单击强制退出以终止该过程。
方法 2

当应用占据当前顶层窗口屏幕(即应用正在运行)时,按下 Return 键。

方法 3

在主机系统上的 Dalvik 调试监控服务器(DDMS)中终止该进程。当 Android 设备连接到一个具有 IDE 的主机系统时,可以实现这一点,该 IDE 带有支持 DDMS 的 Android SDK,如 Eclipse。

在 Android 模拟器中使用 Web 浏览器

如果你有一部智能手机,毫无疑问,你可以很容易地连接到互联网。如果您在模拟器上运行 Android,您可以按照以下步骤使用 web 浏览器:

  1. 点击键盘上的 Home A978-1-4842-0100-8_5_Figa_HTML.jpg按钮,出现图 5-7 所示界面。单击右下角的按钮。

  2. 将弹出网络浏览器窗口。点击地址栏,调出仿真设备的键盘,如图 5-8 所示。在模拟键盘上输入地址,然后单击 Go。

A978-1-4842-0100-8_5_Fig7_HTML.jpg

图 5-7。

Web browser startup interface

A978-1-4842-0100-8_5_Fig8_HTML.jpg

图 5-8。

Web browser initial interface

常见的 Linux 命令和操作

因为 Android 是基于 Linux 的,所以 Android 支持大部分 Linux 命令。以下部分介绍了基于 Ubuntu 系统的主机和基于 Android 系统的目标机支持的一些常见 Linux 命令。如果您使用 Windows 主机系统为 Android 开发应用,您可以使用 Android SDK 提供的 Android Debug Bridge (adb)命令连接到 Android 设备或仿真器来运行这些命令。

检查用户

以下命令检查当前登录的用户名:

$ who am i

或者

$echo $USER

以下命令显示用户名信息:

$ id

uid=1000(cereal) gid=1000(cereal)

groups=4(adm),  20(dialout), 24(cdrom), 46(plugdev), 106(lpadmin), 121(admin), 122(sambashare), 1000(cereal)

输出数据显示当前用户是谷物,用户 ID 是 1000,用户属于谷物组,组 ID 是 1000,依此类推。

更改密码

以下命令更改当前用户的密码:

$ passwd

以下是输出:

Changing password for cereal.

(current) UNIX password:

Enter New UNIX password:

Retype New UNIX password:

Password changed

passwd命令也可用于更改 root 密码。例如,如果您使用用户名“谷物”登录,您可以使用以下命令来设置 root 用户的密码:

$ sudo passwd root

在桌面 Linux 中,系统启动时通常会出现一个登录窗口,要求您输入用户名和密码。但是安卓没有这个过程。Android 自动使用默认用户名和密码登录,用户不可见;系统资源在用户的身份下使用。

如果 Android 安装后 root 用户的密码不确定,可以使用passwd root命令按照前面所示进行设置。s udo表示命令在 root 身份下执行,但命令的执行只需要输入普通用户的密码进行确认。

清除屏幕

您可以使用clear命令来清除屏幕:

$ clear

超级用户根操作

Linux 有一个唯一的超级用户 root(不同于 Windows 的多个超级用户),可以访问所有文件和目录,操作系统中的所有应用。使用su命令输入 root 用户帐户:

$ su

然后,输入 root 用户的密码。输入 root 用户帐户后,命令行提示符从$变为#。如果su命令后面没有任何参数,默认情况下将切换到 root 用户,而不是 root 用户的主目录——也就是说,root 登录环境在切换后不会改变。它仍然是默认的登录环境。当输入一个参数时,根用户以及根用户的环境都会改变:

$ su -

使用exit命令退出 root 用户帐户,返回普通用户身份:

#exit

退出后,命令行提示从#变为$

当您使用su命令输入 root 用户帐户时,后续操作将在 root 身份下进行。要在 root 用户身份下执行当前命令,并在命令执行后返回普通用户身份,请在命令前添加前缀sudo,如前所示。执行sudo时,系统会要求您输入当前用户(非根用户)的密码来确认身份。

理论上,一个普通用户不一定有sudo权限;权限在文档/etc/sudoers中指定。在桌面 Linux 中,这应该总是通过visudo这样的命令来编辑或设置。但在 Android 中,默认是安装程序有sudo权限。

显示文件和目录

使用ls命令显示目录和文件信息。ls的常见格式如下:

ls [-l] [<directory name>]

在这里,ls -l 显示文件或目录,大小,修改日期和时间,文件或文件夹名称和文件的所有者及其权限。默认情况下,显示当前目录的信息。例如(当前目录为/home/cereal):

改变和显示(当前)目录

使用cd命令改变当前目录。例如,要将当前目录更改为\embedded\pkgs,输入以下命令(注意在 Linux 中根目录和目录由/分隔):

$ cd /home/cereal/Document

/开始表示目录的这个路径称为绝对路径。

如果当前目录是/home/cereal,可以使用相对路径:

$ cd Documents

如果没有输入任何参数,cd命令会切换到当前用户的主目录:

$ cd

..表示父目录,通过以下命令访问:

$ cd ..

使用pwd命令显示当前目录的绝对路径名:

$ pwd

/home/cereal

在 Linux 中还有一些符号来表示特殊的目录。例如,表示用户的主目录,因此可以使用以下命令转到主目录:

$ cd ∼

搜索文件

使用find命令搜索文件。该命令的格式是

find directory name -name file name

您可以在指定的目录和所有子目录中搜索指定的文件。例如,输入以下命令在整个文件系统中搜索ifconfig文件:

# find / -name ifconfig

该命令从根目录开始,搜索所有子目录下的ifconfig文件。该命令可以访问所有目录,其中许多目录是普通用户无法访问的,因此建议您在 root 身份下执行该命令。

文件操作

Android 有类似于 Windows 的文件操作命令。这些命令在表 5-1 中列出。

表 5-1。

Android File-Operation Commands

| 命令 | 功能 | 格式 | 格式描述 | | --- | --- | --- | --- | | `cat` | 显示文件内容 | `cat [-n] ` | `n`:显示行号。 | | `ln` | 建立硬/软链接 | `ln [-sf] ` | `s`:软链接;默认为硬链接。`f`:如果目标文件存在,将被替换 | | `mkdir` | 制作目录 | `mkdir ` | 用给定的名称创建目录 | | `rmdir` | 删除空目录 | `rmdir ` | 要删除的目录必须为空。要删除非空目录,请使用以下命令:`rm –rf ` | | `cp` | 复制文件 | `cp [option] [] []` | 将源复制到 DEST,或将多个源复制到目录 | | `mv` | 移动和重命名文件 | `mv ` | 将源重命名为 DEST,或将源移动到目录。 | | `rm` | 删除文件 | `rm [option] ` | `-i`:询问是否删除。`-f`:不要问是否删除。`-r`:递归删除整个目录,像`rmdir`。 | | `cmp` | 比较文件 | `cmp ` | 比较任意类型的两个文件,并将结果写入标准输出。 |

例如,您可以使用以下命令来显示/etc/passwd文件的内容:

$ cat /etc/passwd

使用以下命令将/etc/passwd文件复制到当前目录:

$ cp /etc/passwd .

该命令中的.表示当前目录。

修改文件/目录权限

如上所述,每个文件或目录都有自己的权限。只有具有相应权限的用户才能执行操作。如果不匹配,必须修改权限。表 5-2 列出了修改文件权限的 Linux 命令。

表 5-2。

File/Directory Permission-Modification Commands

| 命令 | 功能 | 格式 | | --- | --- | --- | | `chmod` | 更改文件/目录访问权限(只有文件所有者和根用户可以这样做) | `chmod /<+/- w/r/x> ` | | `chgrp` | 更改文件组 | `chgrp ` | | `chown` | 更改文件所有者(只有根用户可以这样做) | `chown ` |

例如,该命令为其他用户添加对report.doc文件的写权限:

# chmod o+w report.doc

在 Linux 中,所有者、同组用户和其他用户分别用ugo表示。前面的命令为其他用户添加了写权限(o)。

下一个例子设置report.doc tp rw的组权限(删除x权限):

# chmod g=rw report.doc

权限也可以用数字表示:读、写和执行的值分别是 4、2 和 1。分配的权限的数字是三者之和。例如,下面的命令将report.doc的用户权限设置为rw (4 + 2 + 0 = 6),将组权限设置为r (4 + 0 + 0 = 4),不授予其他用户权限(0 + 0 + 0 = 0):

# chmod 640 report.doc

该命令将report.doc分配给经理组:

# chgrp managers report.doc

要将report.doc的所有者设置为“谷物”并将它的组设置为“经理”,可以使用以下命令:

# chown cereal.managers report.doc

该命令将当前目录下所有文件的所有者更改为谷物:

# chown cereal *

一般来说,只有根用户可以使用chown来改变文件或目录的所有者。

在 Windows 中,文件的可执行性是基于它的后缀(比如.exe),而 Linux 对可执行文件的名称没有任何要求,并通过文件属性来决定文件是否可执行。在 Android 中,要使文件可执行或不可执行,通常使用chmod命令来添加或删除执行权限。

许多命令功能,如文件操作和权限操作,通常在文件浏览器、文本编辑器或其他图形用户界面应用中执行。但是通过命令行操作,您有了一种快速执行命令的新方法。Android 不提供 GUI 应用,如文件浏览器,所以在大多数情况下,你必须使用命令行来完成这些功能。

使用可执行文件路径

如果没有为可执行文件指定路径,Linux 可以使用保存在系统变量PATH中的路径找到该文件,也称为默认路径或路径。默认情况下,Windows 在当前目录中查找可执行文件,但是 Linux 只在它们的默认路径中查找可执行文件。因此,您可以使用以下命令来执行当前目录下的可执行文件:

./directory name of executable file

以下命令显示当前路径:

$ echo $PATH

您也可以使用which命令来确定可执行文件的目录是否已经包含在默认路径中。例如,以下命令会查明 gcc 目录是否已包含在默认路径中:

$which gcc

如果它存在于默认路径中,这个命令输出可执行文件的目录:例如,/usr/bin/gcc。否则,该命令不输出任何内容。

要在默认路径中添加目录,可以使用以下命令:

$ PATH=$PATH:/tools/bin

$ export PATH

或者

$ export PATH=$PATH:/tools/bin

PATH下添加一个以:为分隔符的/tools/bin路径。改变PATH值或任何环境变量必须通过export导出;否则,新的PATH值不会生效。这只对当前用户有效。

可以将导出命令添加到 shell 文件中,如.bash_profile.profile.bashrc,这样每次命令行启动前都会执行该命令。

执行可执行文件而不修改默认路径的一个简单方法是在可执行文件名称前指定绝对路径。

管道和屏蔽

Linux 管道操作符号|将一个命令的输出作为第二个命令的输入。其功能与 Windows 中的相同。例如,您可以使用以下命令逐屏显示文件内容:

$ cat /etc/passwd | more

more命令逐屏显示文件,每页输出后暂停执行;你可以按任意键让它继续。在前面的命令中,cat/etc/passwd表示输出passwd文件的内容,输出作为 more 命令的输入(使用管道命令)。

grep命令可以搜索并显示文件中的某些行。例如:

$ grep cereal /etc/passwd

该命令在 passwd 文件中查找并显示包含“谷类”的行。输出是这样的:

cereal:x:1000:1000:cereal,,,:/home/cereal:/bin/bash

要搜索的字符串可以用单引号('')或双引号("")括起来。''中的文字是按字面意思理解的,而""中的一些特殊字符被 shell 赋予了特殊的含义。前面的命令也可以写成以下形式:

$ grep 'cereal' /etc/passwd

$ grep "cereal" /etc/passwd

在大多数情况下,您使用 pipe 和grep命令来显示命令输出的屏幕。例如:

$ ls -l /home/cereal/Document | grep qt

该命令显示包含/home/cereal/Document目录中qt字段的文件/目录的相应行。

您使用的另一个命令是

$ ps -e | grep ssh

其中列出了进程名称中包含文本“ssh”的所有活动进程。

在后台运行命令

命令的执行通常会占用控制台或命令行窗口的输入/输出,这意味着在正在执行的命令结束之前,您无法输入命令。相反,如果应用在后台运行,控制台的输入/输出不会被占用。要在后台运行命令,只需在命令末尾添加&

中断前台命令的执行

按 Ctrl+C 中断在前台执行的命令。例如,Linux ping命令将无休止地 ping 主机,但是您可以按如下方式结束它的执行:

$ ping 127.0.0.1

PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.

64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.027 ms

64 bytes from 127.0.0.1: icmp_seq=2 ttl=64 time=0.028 ms

64 bytes from 127.0.0.1: icmp_seq=3 ttl=64 time=0.029 ms

64 bytes from 127.0.0.1: icmp_seq=4 ttl=64 time=0.030 ms

64 bytes from 127.0.0.1: icmp_seq=5 ttl=64 time=0.031 ms

^C

--- 127.0.0.1 ping statistics ---

5 packets transmitted, 5 received, 0% packet loss, time 4462ms rtt min/avg/max/mdev = 0.027/0.029/0.031/0.001 ms

$

第五个 ping 输出项后,按 Ctrl+C 中断命令,屏幕显示^C

检查硬件信息(如操作系统版本和 CPU)

uname命令显示系统信息,包括与计算机和操作系统相关的信息。该命令的语法是

uname [-amnrsv][--help][--version]

这些参数如下:

  • -a(全部):显示所有信息
  • -m(机器):显示计算机类型
  • -n(节点名):显示网络上的主机名
  • -r (release):显示操作系统的发行号
  • -s (sysname):显示操作系统名称
  • -v(版本):显示操作系统版本
  • --help:显示帮助
  • --version:显示版本信息

例如,以下命令显示机器上的操作系统类型:

$ uname

Linux

这台机器的操作系统是 Linux。

这个命令显示了 Linux 内核的发行号:

$ uname -r

2.6.31-14-generic

发行号是 2.6.31-14。

Linux 将处理器信息放在/proc目录下的cpuinfo文件中,允许您通过该文件进行检查。您可以使用以下命令检查处理器型号:

$ cat /proc/cpuinfo | grep "model name"

华硕 Eee PC 1000HC 上网本的输出为

model name : Intel(R) Atom(TM) CPU N270 @ 1.60GHz

model name : Intel(R) Atom(TM) CPU N270 @ 1.60GHz

该机器采用英特尔凌动处理器 N270。

现在检查逻辑 CPU 的数量:

$ cat /proc/cpuinfo | grep "processor"

华硕 Eee PC 1000HC 上网本的输出为

processor : 0

processor : 1

处理器有两个逻辑 CPU。

以下命令将显示每个逻辑 CPU 的 ID:

$ cat /proc/cpuinfo | grep "core id"

华硕 Eee PC 1000HC 上网本的输出为

core id : 0

core id : 0

两个逻辑 CPU 具有相同的核心 ID,这意味着处理器的超线程是开放的,也就是说,超线程技术模拟两个 CPU。

使用 Android 开发和辅助工具

以下部分描述了如何使用仿真器、帮助文件、DDMS、(adb)以及常见的 Android 和 telnet 命令。

使用模拟器

Android 虚拟设备(AVD),即 Android 模拟器,是调试移动应用的好工具。本书前面简要介绍了 AVD 的使用,本节提供了更深入的讨论。

可以使用三种方法之一启动模拟器。

方法 1

按照以下步骤在 Eclipse 中启动模拟器:

  1. 启动 Eclipse,选择运行➤运行配置,如图 5-9 所示。

  2. 在调试配置框中,选择左栏中的\Android Application\XXX,然后单击目标➤“自动选择兼容设备:始终使用首选 AVD。。. "。检查指定的模拟器后,单击右侧的开始按钮。

  3. 弹出一个启动选项信息框(见图 5-10 )。点击启动,模拟器窗口打开(参见图 5-11 )。

A978-1-4842-0100-8_5_Fig9_HTML.jpg

图 5-9。

Emulator startup menu

A978-1-4842-0100-8_5_Fig11_HTML.jpg

图 5-11。

Initial interface of the emulator when started separately

A978-1-4842-0100-8_5_Fig10_HTML.jpg

图 5-10。

Launch Options information box

方法 2

要在 Eclipse 中运行应用时启动仿真器,请在菜单栏上选择窗口➤ Android 虚拟设备管理器(参见图 5-12 )。

A978-1-4842-0100-8_5_Fig12_HTML.jpg

图 5-12。

The Android Virtual Device Manager

有时,当使用这种方法时,接口会锁定。也就是说,在 Eclipse 中运行项目(应用)后,模拟器有时会显示如图 5-13 所示的界面。应用的界面不可见,表明模拟器被锁定。

A978-1-4842-0100-8_5_Fig13_HTML.jpg

图 5-13。

Interface when the emulator locks up at startup

解决方法是单击菜单按钮。如果弹出一个窗口,点击等待继续,会出现应用的界面,如图 5-14 所示。

A978-1-4842-0100-8_5_Fig14_HTML.jpg

图 5-14。

Interface after unlock

方法 3

AVD 配置也可以使用“android”工具从命令行创建和管理,如下所述: http://developer.android.com/guide/developing/devices/managing-avds-cmdline.html

从英特尔架构命令行创建 AVD 的示例:AVD create AVD–n HC–t Android-13–s WXGA

对于自定义硬件,接受“是”,对于硬件,选择 x86。CPU . arch 属性使用帮助文件

Android 开发帮助文件为应用开发中涉及的类和方法原型提供了描述、解释和使用示例。您可以在线阅读帮助文件,也可以作为本地文件离线阅读。在线阅读功能更强大,因为它支持自动完成和类名搜索。但是,根据网速的不同,在线阅读的响应速度可能不如离线阅读。下面解释这两种方法。

当您使用 Android SDK 管理器(通过从 Eclipse 顶部菜单中选择 Window)下载 Android 包时,您会在每个包中看到 Android SDK 的文档。选择并安装后,所有文档都被复制到docs文件夹中,该文件夹是系统中android-sdk安装目录的子目录。通过 docs 中的index.html文件访问本地帮助文件。要使用本地帮助文件,请按照下列步骤操作:

  1. 打开android-sdk安装目录下docs子目录下的index.html文件(这里是D:\Android\android-sdk\docs\index.html)。在浏览器窗口中,可能会弹出一个询问运行 ActiveX 的信息栏。单击运行控件。
  2. 点击页面顶部下方的参照,如图 5-15 所示。

A978-1-4842-0100-8_5_Fig15_HTML.jpg

图 5-15。

Entry page for class references

出现帮助界面,如图 5-16 所示。

A978-1-4842-0100-8_5_Fig16_HTML.jpg

图 5-16。

Class reference page

脱机阅读不支持自动完成和搜索类名。图 5-17 显示了一个为TextView类搜索帮助的例子。在右上角的搜索栏中输入类名TextView,自动完成列表显示相应的候选名称。

A978-1-4842-0100-8_5_Fig17_HTML.jpg

图 5-17。

Input interface for auto-complete and search of pages in offline reading mode

当选择列表中的候选项时(在本例中,选择了第一个候选项android.widget.TextView,如果没有可用的连接,将出现网络连接错误。帮助文件不会显示相应的信息。

在线阅读功能更强大,步骤如下:

  1. 在网页浏览器的地址栏中输入网址 http://android.com ,出现如图 5-18 所示的画面。

  2. 点击页面底部的获取 SDK,如图 5-19 所示。

A978-1-4842-0100-8_5_Fig18_HTML.jpg

图 5-18。

Initial page of online reading

  1. 点击新建页面上的参照,如图 5-20 所示。

A978-1-4842-0100-8_5_Fig19_HTML.jpg

图 5-19。

SDK entry page

  1. 在右上角的帮助信息搜索框中,输入该项目的搜索字符串(在本例中为“Log”)。包含关键字的候选列表框会下拉。从列表中选择您想要的项目。页面显示相应项目的帮助文件。

A978-1-4842-0100-8_5_Fig20_HTML.jpg

图 5-20。

Entry page for references

使用 DDMS

达尔维克调试监控服务(DDMS)是一个重要的和强大的 Android 开发支持文件。它可以帮助在目标机器上调试软件,在主机和目标机器之间执行所需的交互,以及管理目标机器上的文件系统、进程和其他内容。DDMS 保存在 Android SDK 安装目录的 tools 子目录中。可以集成到 Eclipse 中作为插件使用,也可以在命令行输入其功能。启动 DDMS 也有两种方式:

  • 双击ddms.bat或在命令行窗口输入ddms来运行它。
  • 在 Eclipse 中调试程序时启动 DDMS。

DDMS 可以在模拟器和连接的设备上执行它的功能。如果系统检测到它们都在运行,默认情况下,DDMS 将被定向到模拟器。

DDMS 在 IDE 和目标机器之间建立链接,它们通过各自的端口监听调试信息,而 DDMS 可以实时监控测试终端的连接。当连接新的测试终端时,DDMS 捕获目标机器的 ID 并通过 adb 设置调试器,从而使得指令能够被发送到测试终端。

下面的说明展示了如何使用集成在 Eclipse 中的 DDMS。

显示 DDMS 按钮

在 Eclipse 中,DDMS 接口与开发编辑和调试接口处于同一级别。点击 DDMS 按钮进入 DDMS 界面。默认情况下,Eclipse 不显示按钮,因此您需要遵循以下步骤:

  1. 单击工具栏中的打开透视图。
  2. 在弹出框中选择 DDMS。DDMS 按钮显示在 Eclipse 的左上角,如图 5-21 所示。

A978-1-4842-0100-8_5_Fig21_HTML.jpg

图 5-21。

Displaying the DDMS start button

从 DDMS 开始

DDMS 按钮现在可见,您可以执行以下步骤来使用界面:

  1. 启动 Eclipse,连接手机或启动 AVD 模拟器,点击窗口右上角的 DDMS,如图 5-22 。

  2. 出现 DDMS 界面,如图 5-23 所示。左侧窗格是目标计算机的任务管理器,您可以在其中查看和结束进程。右侧窗格包括文件浏览器(如图 5-23 所示)和网络统计等选项卡。右窗格还显示目标计算机的信息。

A978-1-4842-0100-8_5_Fig22_HTML.jpg

图 5-22。

DDMS entry interface

A978-1-4842-0100-8_5_Fig23_HTML.jpg

图 5-23。

DDMS initial interface

主机和目标机器之间的文件传输,以及文件管理

DDMS 还可以在主机和目标机器(真实设备或仿真器)之间执行文件传输(相互复制),并管理目标机器上的文件,如下所述。

将文件从主机复制到目标机

请遵循以下步骤:

  1. 在 DDMS 界面的右窗格中,单击文件资源管理器。您会看到按钮从设备中拉出文件,将文件推送到设备上,以及删除工具栏中的选择,如图 5-24 所示。

  2. 在右窗格中,您可以看到目标机器的整个文件系统(在本例中模拟器正在运行)。点击选择文件夹(本例中为data文件夹),点击工具栏上的推送文件到设备按钮,在弹出框中选择一个文件(在主机上),如图 5-25 所示。

A978-1-4842-0100-8_5_Fig24_HTML.jpg

图 5-24。

Initial DDMS interface for file transfers

A978-1-4842-0100-8_5_Fig25_HTML.jpg

图 5-25。

Selecting a file to copy from the host machine to the target machine

您可以在目标机器上看到复制的文件(本例中为cats.jpg),如图 5-26 所示。

A978-1-4842-0100-8_5_Fig26_HTML.jpg

图 5-26。

File system of the target machine after copying a file

请注意,由于用户权限的限制,目标计算机上的某些文件夹不允许上传。例如,如果选择根文件夹上传文件,底部消息框会出现错误,如图 5-27 所示。

A978-1-4842-0100-8_5_Fig27_HTML.jpg

图 5-27。

Uploading a file under the root directory on the target machine

将文件从目标机器复制到主机

遵循与上一节相同的步骤,但这次单击按钮“从设备中提取文件”。

删除文件

遵循“将文件从主机复制到目标计算机”一节中的相同步骤,但单击“删除选择”按钮。

目标机器上的进程管理

在 DDMS 中,您可以查看目标计算机上运行的进程,并执行一些管理任务,如停止进程。下面是一个介绍。

启动目标计算机的进程管理

在 Eclipse 中启动一个应用(本例中的测试项目,其源代码文件为MyAppCode.java),如图 5-28 所示。

A978-1-4842-0100-8_5_Fig28_HTML.jpg

图 5-28。

Applications that have started before process view

在仿真器上运行的应用界面如图 5-29 所示。

A978-1-4842-0100-8_5_Fig29_HTML.jpg

图 5-29。

Application’s interface started before process view

通过单击主机 Eclipse 窗口右上角的 DDMS 按钮启动 DDMS 界面。您会在左窗格中看到目标计算机上运行的进程列表。在列表中找到该应用对应的进程(本例中为com.example.test),如图 5-30 所示。

A978-1-4842-0100-8_5_Fig30_HTML.jpg

图 5-30。

List of processes running on target machine

如图所示,DDMS 在端口 8600 上监控第一个进程com.android.phone(进程 ID: 311)。如果有更多的目标机器或更多的应用进程,监控端口将按升序递增:第二个进程监控端口将被分配为 8601,第三个进程监控端口将被分配为 8602,以此类推。DDMS 通过被称为基本端口的端口 8700 接收所有终端命令。

右上角是一排非常重要的按钮:调试选中的进程、更新线程、更新堆、停止进程、截屏,完成相应的操作。以下部分以停止流程为例,说明了这些按钮的用法。

停止指定的进程

要停止应用,请选择应用进程,然后单击上部工具栏中的“停止进程”按钮。可以看到模拟器上的应用已经被终止,如图 5-31;最初的进程(com.example.test)不再显示在 Eclipse 中的 DDMS 进程列表中。

A978-1-4842-0100-8_5_Fig31_HTML.jpg

图 5-31。

DDMS interface on the host machine after process termination

获取目标机器屏幕截图

DDMS 也可以用来捕捉目标机器的屏幕截图。截图可以保存为主机上的文件。步骤如下:

  1. 启动目标机应用,如图 5-32 所示。

  2. 在主机的 Eclipse 环境下,点击左侧窗格上部工具栏的截屏按钮,在弹出的对话框中点击保存按钮,然后点击完成按钮关闭对话框,如图 5-33 所示。

A978-1-4842-0100-8_5_Fig32_HTML.jpg

图 5-32。

Interface of the target machine before screen capture

A978-1-4842-0100-8_5_Fig33_HTML.jpg

图 5-33。

Performing a screen capture on the target machine

你可以在主机上的文件夹里看到截图,如图 5-34 所示。

A978-1-4842-0100-8_5_Fig34_HTML.jpg

图 5-34。

Host machine screen capture result

仿真器操作

如果目标机是仿真器,点击 DDMS 右窗格中的仿真器控制页签,可以看到仿真器操作界面,如图 5-35 所示。

A978-1-4842-0100-8_5_Fig35_HTML.jpg

图 5-35。

Emulator control interface of DDMS

该面板上的功能允许测试设备轻松模拟真实电话的一些交互功能,如接听电话、根据选项模拟各种网络状况、模拟接收短信和发送虚拟地址坐标来测试 GPS 功能。下面是几个函数描述。

  • 电话状态:使用选项模拟语音质量和信号连接模式
  • 电话操作:模拟电话应答和向测试设备发送 SMS
  • 位置控制:模拟地理坐标,或模拟路线坐标的动态变化,显示默认地理标志,有以下三种方式:
  • 手动:手动向测试设备发送二维经纬度坐标
  • GPX:通过 GPX 文件导入一系列动态变化的地理坐标,以模拟移动过程中不断变化的 GPS 值
  • KML:通过 KML 文件导入独特的地理标志,并根据不断变化的地理坐标在测试设备上动态显示

下面以发送短信为例,介绍如何使用这些功能:

  1. 完成仿真器控制\电话操作框。

  2. 点击发送后,在 Android 模拟器中打开消息。你看短信,如图 5-36 所示。

  3. 点击新的短信可以查看其详细信息,如图 5-37 所示。

A978-1-4842-0100-8_5_Fig36_HTML.jpg

图 5-36。

Title of SMS received by the emulator

A978-1-4842-0100-8_5_Fig37_HTML.jpg

图 5-37。

Content of the SMS received by the emulator

在命令提示符下使用 adb

Android Debug Bridge (adb)是 Android 提供的通用调试工具。使用此工具,您可以管理设备或电话模拟器的状态,并执行以下操作:

  • 快速更新设备或手机模拟器上的代码,如应用或 Android 系统
  • 在设备上运行 shell 命令
  • 管理设备或电话仿真器上的保留端口
  • 在设备或手机模拟器上复制和粘贴文件

adb 的功能一般集成到 Eclipse 开发环境中。本节介绍 adb 的其他功能,这些功能通常通过命令行输入。表 5-4 列出了三个常用命令。

表 5-4。

Common Commands for Viewing Information on the Target Machine

| 命令 | 描述 | | --- | --- | | `adb devices` | 查看目标机器的信息 | | `adb get-product` | 查看目标机器的产品型号 | | `adb get-serialno` | 查看目标机器的序列号 |

在命令提示符下使用 adb 非常有用,当模拟器没有启动或电话没有连接时,在主机的 Windows 命令行中键入以下命令后,输出结果为空:

C:\Documents and Settings>adb devices

List of devices attached

如果 adb 服务尚未启动,上述命令将提示启动信息:

C:\Documents and Settings>adb devices

* daemon not running. starting it now on port 5037 *

* daemon started successfully *

List of devices attached

启动模拟器后,运行命令,将显示模拟器设备信息:

C:\Documents and Settings>adb devices

List of devices attached emulator-5554 device

启动模拟器并连接到电话后,该命令会显示以下内容

C:\Documents and Settings>adb devices

List of devices attached emulator-5554 device Medfield04749AFB device

其中emulator-5554指模拟器对应的目标机,Medfield04749AFB指联想手机对应的目标机。

当只连接手机时,adb get-serialno命令只输出有效目标机的序列号:

C:\Documents and Settings>adb get-serialno

Medfield04749AFB

在目标计算机上运行命令

您知道 Android 是基于 Linux 的,并且已经向您介绍了 Android 支持的 Linux 命令。然而,大多数运行 Android 的移动设备没有用于命令输入的物理键盘。即使这样,Linux 命令也是有用的。可以利用一些辅助工具,比如 adb,实现 Android 命令的远程输入。通过使用 adb shell 命令,您可以在主机上输入命令,并让目标机器执行这些命令。换句话说,主机的键盘和屏幕模拟了目标机上的一个终端。这里的目标机器可以是真实的设备,也可以是模拟器。请遵循以下步骤:

  1. 在主机的 Windows 命令行上输入adb shell命令:

C:\Documents and Settings> adb shell

  1. 输入目标机器的 Android 命令。例如:

# pwd

pwd

/

# ls -l

ls -l

drwxr-xr-x root     root     2012-07-09 13:24 acct

drwxrwx--- system   cache    2012-07-09 13:25 cache

dr-x------ root     root     2012-07-09 13:24 config

lrwxrwxrwx root     root     2012-07-09 13:24 d -> /sys/kernel/debug

drwxrwx--x system   system   2012-07-09 19:44 data

-rw-r--r-- root     root     116 1970-01-01 00:00 default.prop

drwxr-xr-x root     root     2012-07-09 13:25 dev

lrwxrwxrwx root     root     2012-07-09 13:24 etc -> /system/etc

-rwxr-x--- root     root     105204 1970-01-01 00:00 init

-rwxr-x--- root     root     2344 1970-01-01 00:00 init.goldfish.rc

-rwxr-x--- root     root     17048 1970-01-01 00:00 init.rc

-rwxr-x--- root     root     1637 1970-01-01 00:00 init.trace.rc

-rwxr-x--- root     root     3915 1970-01-01 00:00 init.usb.rc

drwxrwxr-x root     system   2012-07-09 13:24 mnt

dr-xr-xr-x root     root     1970-01-01 00:00 proc

drwx------ root     root     2011-12-08 23:06 root

drwxr-x--- root     root     1970-01-01 00:00 sbin

lrwxrwxrwx root     root     2012-07-09 13:24 sdcard -> /mnt/sdcard

drwxr-xr-x root     root     1970-01-01 00:00 sys

drwxr-xr-x root     root     2012-06-23 01:56 system

-rw-r--r—root       root     272 1970-01-01 00:00 ueventd.goldfish.rc

-rw-r--r-- root     root     3879 1970-01-01 00:00 ueventd.rc

lrwxrwxrwx root     root     2012-07-09 13:24 vendor -> /system/vendor

# cd

cd

cd: HOME not set

# echo $PATH

echo $PATH

/sbin:/vendor/bin:/system/sbin:/system/bin:/system/xbin

# ifconfig eth0

ifconfig eth0

eth0: ip 10.0.2.15 mask 255.255.255.0 flags [up broadcast running multicast]

  1. 使用exit命令停止在目标机器上的执行,并返回到主机上的命令行界面:

# exit

exit

C:\Documents and Settings>

Note

Android 中的 Linux 外壳被简化了很多。因此,许多常见的 Linux 命令都不被支持。

在目标计算机上安装应用包

您可以使用adb install命令在目标机器上安装或卸载应用包。软件安装命令的格式是

adb install XXX.apk

其中XXX.apk是主机当前目录下的文件。例如,要安装文件浏览器软件,请按照下列步骤操作:

  1. 目标机器(仿真器)上的原始应用如图 5-38 所示。

  2. 在主机上的命令行中运行以下命令:

A978-1-4842-0100-8_5_Fig38_HTML.jpg

图 5-38。

Application executing on the virtual machine before software installation

E:\temp\temp>adb install file_browser.apk

92 KB/s (2617375 bytes in 27.546s)

pkg: /data/local/tmp/file_browser.apk

Success

安装文件file_browser.apk位于主机的当前目录下(本例中为E:\temp\temp)。

你可以看到目标机器已经安装了新的应用 ES File Explorer,如图 5-39 所示。

A978-1-4842-0100-8_5_Fig39_HTML.jpg

图 5-39。

Virtual machine application after software installation

点击图标即可启动新应用,如图 5-40 所示。

A978-1-4842-0100-8_5_Fig40_HTML.jpg

图 5-40。

Operation interface of the newly installed software

该软件可以以同样的方式安装在真正的手机上。

在目标机器上卸载软件

您可以使用adb shell命令rm卸载目标机器上的软件。例如:

E:\temp\temp>adb shell rm /data/app/*.apk

软件(*.apk)通过 adb 安装,位于/data/app/目录下;因此,没有必要在安装过程中指定路径。卸载时只需要执行rm命令。然而,rm命令非常强大,如果使用不当,会导致不可逆转的数据丢失。卸载应用的安全和推荐方法是

adb uninstall packagename

在主机和目标机器之间传输文件

命令adb push可用于将文件从主机复制到目标机,命令adb pull可用于将文件从目标机复制到主机。

启用和禁用 adb 服务

您可以使用adb kill-server命令禁用 adb 服务,并使用adb start-server命令启用它。

其他功能

以下是一些其他有用的 adb 功能列表:

  • 端口转发(将默认端口 TCP5555 转发到端口 1234)

adb forward adb forward tcp:5555 tcp:1234

  • 访问数据库sqlite3

adb shell sqlite3

  • 等待正在运行的设备

adb wait-for-devices

  • 查看错误报告

adb bugreport

  • 记录无线电通信日志

adb shell logcat -b ratio

一般来说,无线电通信日志比较多,操作时不需要获取记录,但是可以通过命令获取记录。

使用 Android 命令

Android 命令由批处理文件android.bat提供,该文件位于android-sdk安装目录下的tools子目录下。这些命令可以管理模拟器和 API,主要是通过 Eclipse。当然,也可以通过在命令行输入 Android 命令来实现管理。以下部分介绍了这些命令。

查看安装的模拟器

您可以通过运行android list avd命令来查看安装的仿真器。例如:

C:\Documents and Settings>android list avd

Available Android Virtual Devices:

Name: AtomAVD

Path: C:\Documents and Settings\hlgu\.android\avd\AtomAVD.avd

Target machine: Android 4.0.3 (API level 15)

ABI: x86

Skin: WVGA800

Sdcard: 1024M

---------

Name: myAndroid

Path: C:\Documents and Settings\hlgu\.android\avd\myAndroid.avd

Target machine: Android 4.1 (API level 16)

ABI: armeabi-v7a

Skin: WVGA800

Sdcard: 1024M

Snapshot: true

输出显示系统安装了两个仿真器:CPU 为 x86 (ABI 线)的 AtomAVD 仿真器(name 线)和 CPU 为 armeabi-v7a 的 myAndroid 仿真器。

查看当前支持的 API 的版本信息

您可以通过运行android list target命令来查看当前支持的 API 的版本信息。例如:

C:\Documents and Settings>android list target

Available Android targets:

----------

id: 1 or "android-15"

Name: Android 4.0.3

Type: Platform

API level: 15

Revision: 3

Skins: HVGA, QVGA, WQVGA400, WQVGA432, WSVGA, WVGA800 (default), WVGA854, WXGA720, WXGA800

ABIs : x86

----------

id: 2 or "Google Inc.:Google APIs:15"

Name: Google APIs

Type: Add-On

Vendor: Google Inc.

Revision: 2

Description: Android + Google APIs

Based on Android 4.0.3 (API level 15)

Libraries:

* com.google.android.media.effects (effects.jar)

Collection of video effects

* com.android.future.usb.accessory (usb.jar)

API for USB Accessories

* com.google.android.maps (maps.jar)

API for Google Maps

Skins: WVGA854, WQVGA400, WSVGA, WXGA720, HVGA, WQVGA432, WVGA800 (default), QVGA, WXGA800

ABIs : armeabi-v7a

----------

id: 3 or "HTC:HTC OpenSense SDK:15"

Name: HTC OpenSense SDK

Type: Add-On

Vendor: HTC

Revision: 2

Based on Android 4.0.3 (API level 15)

Libraries:

* htc-extension (HTCSDK.jar)

HTC generic extension library

Skins: WVGA854, WQVGA400, WSVGA, WXGA720, HVGA, WQVGA432, WVGA800 (default), QVGA, WXGA800

ABIs : no ABIs.

----------

id: 4 or "android-16"

Name: Android 4.1

Type: Platform

API level: 16

Revision: 1

Skins: HVGA, QVGA, WQVGA400, WQVGA432, WSVGA, WVGA800 (default), WVGA854, WXGA720, WXGA800, WXGA800-7in

ABIs : armeabi-v7a

----------

id: 5 or "Google Inc.:Google APIs:16"

Name: Google APIs

Type: Add-On

Vendor: Google Inc.

Revision: 2

Description: Android + Google APIs

Based on Android 4.1 (API level 16)

Libraries:

* com.google.android.media.effects (effects.jar)

Collection of video effects

* com.android.future.usb.accessory (usb.jar)

API for USB Accessories

* com.google.android.maps (maps.jar)

API for Google Maps

Skins: WVGA854, WQVGA400, WSVGA, WXGA800-7in, WXGA720, HVGA, WQVGA432, WVGA 800 (default), QVGA, WXGA800

ABIs : armeabi-v7a

输出显示当前开发环境安装了包括 android-15、Google Inc:Google API:15、HTC:HTC OpenSense SDK:15、android-16、Google Inc:Google API:16 在内的多个 API。

创建模拟器

Android create avd –n命令可以用来创建一个模拟器,但是你通常在 Eclipse 中创建它。

启动模拟器

大多数时候,您使用emulator命令在 Eclipse 中启动模拟器。例如:

C:\Documents and Settings>emulator -avd myAndroid

myAndroid是由android list avd命令列出的仿真器的名称。这个命令将启动myAndroid模拟器。

对模拟器命令使用 Telnet

您可以使用 Telnet 在目标机器(模拟器)上从主机输入命令,并让目标机器(模拟器)执行这些命令。这样,主机(Windows、Linux 和 Mac 系统)就变成了仿真器的控制台终端。Telnet 命令的格式是

telnet localhost <console-port>

例如:

telnet localhost 5554

一般来说,Android 模拟器的序列号是 5554。运行 Telnet 时,您需要将控制台端口更改为您打算连接的仿真器的序列号:例如,5554、5556 或 5558。使用 telnet localhost 5554 连接到模拟器后,输入help命令。

登录 Android 模拟器终端模式后,可用命令有eventgeogsmkillnetworkpowerredirsmsvmwindow;它们用于控制 Android 模拟器。这些命令中的许多可以被前面介绍的 DDMS 仿真器图形操作所取代。以下部分介绍了这些命令。

事件命令

格式是

event text testmessage

该命令可以向仿真器发送四个事件— sendtypescodestext。例如,在event text testmessage向模拟器发送一个文字字符串“测试消息”后,你会立即在 Android 模拟器的屏幕上看到这条消息。

地理命令

格式是

geo <fix|nmea>

例如:

geo fix 121.5 25.4 10

geo nmea $GPRMC,071236,A,3751.65,S,14527.36,E,000.0,073.0,130309,011.3,E*62

geo命令可以将 GPS 位置发送给仿真器。geo fix发送一组用经度、纬度、高度表示的固定 GPS 位置,可以从一些网站的地图上获取,比如经度 121.5,纬度 25.4,高度 10 米。当 Android 设备通过 USB 连接到外部 GPS 设备时,您可以使用geo nmea命令将位置发送到外部 GPS 设备。

美国国家电气制造商协会(NEMA)为 GPS 设备开发了 NEMA 0183 协议。gps nema命令的格式很复杂,由 12 个字段组成,但由于这种复杂性,该命令提供了比geo fix命令更精确的定位。gps nema命令的格式如下:

$GPRMC,<1>,<2>,<3>,<4>,<5>,<6>,<7>,<8>,<9>,<10>,<11>,<12>*hh

$GPRMC,hhmmss.ss,A,IIII.II,a,yyyyy.yy,a,x.x,x.x,ddmmyy,x.x,a*hh

$GPRMC (Recommended minimum specific GPS/Transit data)

这些字段如下所示:

  • UTC,格式为 hhmmss(时、分、秒)。比如:071236
  • 定位状态:A =可用定位,V =无效定位。
  • 纬度,格式为 ddmm.mm (d 指度,m 指分)。例如:3751.65 = 37 度 51.65 分。
  • 纬度半球北(北半球)或南(南半球)。
  • 经度,格式为 dddmm.mm .例如:14527.36 = 145 度 27.36 分。
  • 经度半球 E(东经)或 W(西经)。
  • 地速(000.0 至 999.9 节;0 也将被传输)。比如:静止的 000.0。
  • 地面方向(000.0-359.9 度;0 也将被传输)。比如:073.0。
  • UTC 日期,格式为 ddmmyy(日期、月份和年份)。比如:130309。
  • 磁偏角(000.0-180.0 度;0 也将被传输)。比如:011.3。
  • 磁偏角方向:东或西。
  • 模式指示符(在 NEMA 0183 协议中,A =自主定位,D =差分,E =估计,N =空信息)。hh 是校验和。比如:62 全球通电话 5556688。

gsm 命令

gsm命令可以模拟 GSM 手机的通话状态,参数有callbusyholdcanceldatavoicestatus。只需在参数后面添加任何电话号码,就可以在 Android 模拟器中模拟呼叫 GSM 电话。

kill 命令

kill命令在 Android 模拟器的终端模式下立即关闭模拟器窗口。

网络命令

network命令是网络管理和操作命令。它有各种参数来完成不同的网络功能。例如:

network status

该命令用于查看 Android 模拟器的网络传输状态。

以下是一些额外的例子:

network speed full

network speed umts

网速可以改变手机的网络传输模式,包括gsm(GSM/CSD)gprs(GPRS)edge(EDGE/EGPRS)umts(UMTS/3G)hsdpa(HSDPA/3.5G),全速传输,随机选择。这是安卓系统默认的网络设置。

电源命令

此命令显示电话的当前电源状态是否为交流电源连接,以及电池的剩余电量:

power display

redir 命令

adb forward命令类似,redir命令可以显示和管理仿真器的 TCP 或 UDP 通信端口。例如:

redir add tcp: 5000:6000

您可以使用这个命令将托管系统的 TCP 端口 5000 接收到的消息定向到 Android 模拟器的 TCP 端口 6000。

此命令列出了已定向的 TCP 或 UDP 通信端口:

redir list

redir del命令可以删除已经指向的通信端口;

redir del tcp: 5000

sms 命令

您可以使用sms命令发送短信:

sms send <phone number> <SMS>

例如:

sms send 5556688 this is a test sms

这将发送短信文本“这是一个测试”到电话号码 5556688,Android 模拟器将立即收到它。

窗口比例?命令

window scale命令可以改变模拟器的窗口大小。例如:

window scale factor (factor: 0.1-3.0)

window scale 1.2

第一个命令将屏幕比例因子设置为 0.1 到 3.0 之间。第二个命令将 Android 模拟器的窗口大小缩放 1.2 倍。

摘要

对于应用开发人员来说,您构建的应用能够在不同 OEM 厂商生产的所有设备和不同平台上运行是至关重要的。然而,原始设备制造商一直在裁剪和定制他们设备的 Android 操作系统,以满足他们所使用的软件和硬件的独特需求。因此,了解 Android OS 定制有助于您更好地设计应用。下一章讨论 Android 操作系统的定制,包括 Android 映像的安装和刷新,这是您直接从设备制造商处将测试平台更新到最新 Android 版本的最快方法。

Creative Commons Open Access This chapter is licensed under the terms of the Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License ( http://​creativecommons.​org/​licenses/​by-nc-nd/​4.​0/​ ), which permits any noncommercial use, sharing, distribution and reproduction in any medium or format, as long as you give appropriate credit to the original author(s) and the source, provide a link to the Creative Commons licence and indicate if you modified the licensed material. You do not have permission under this licence to share adapted material derived from this chapter or parts of it. The images or other third party material in this chapter are included in the chapter’s Creative Commons licence, unless indicated otherwise in a credit line to the material. If material is not included in the chapter’s Creative Commons licence and your intended use is not permitted by statutory regulation or exceeds the permitted use, you will need to obtain permission directly from the copyright holder.

六、Android 的定制和安装

Keywords Embed System Image File Target Machine Android Phone Android System

由于任何嵌入式系统的特性,如资源限制,裁剪和定制是嵌入式操作系统的重要特性,Android 也不例外。本章提供了嵌入式操作系统定制的一般讨论,然后具体解释了 Android 的定制。

嵌入式操作系统的裁剪和定制

并非嵌入式操作系统提供的所有功能和服务都包含在特定的嵌入式应用中,原因有二。首先,嵌入式系统总是资源受限的,尤其是在存储空间方面;因此,在发布时不可能在系统中包含所有冗余功能。第二,许多商业嵌入式操作系统根据用户选择的组件收取许可费。因此,用户应该根据自己的需求定制嵌入式操作系统。OS 定制的原理如图 6-1 所示。

A978-1-4842-0100-8_6_Fig1_HTML.jpg

图 6-1。

Principle of OS customization

例如,Windows XP Embedded OS 提供了数以万计的组件,比桌面 Windows XP 的功能还要多。例如,对于基于 Windows XP Embedded 的地铁挡板门系统,不需要 Windows Media Player、Internet Explorer 浏览器、DirectX 设置面板和 Explorer 任务管理器等组件。消除这些组件减少了系统所需的硬件资源,从而降低了成本;这使得系统运行更快,从而提高效率。

大多数嵌入式操作系统都提供了定制和裁剪的手段。但是,有许多不同的裁剪模式:有些从编译源代码开始,需要用户配置条件编译的选项;有些从链接目标文件开始,根据用户的配置链接到不同的库文件;其余模式根据用户的选择从现有的二进制文件库中提取预编译文件。表 6-1 列出了常用嵌入式操作系统提供的定制模式。

表 6-1。

Customization Modes of Different Embedded Operating Systems

| 嵌入式操作系统 | 定制模式 | | --- | --- | | windows ce(windows ce) | 提供平台构建器 ide 和图形组件选项。根据选定的组件链接不同的库文件。 | | 嵌入式 Linux | 对于内核,之前通过`make config`生成配置文件;然后根据配置文件进行编译。 | | 嵌入式 | 提供目标设计器 ide 和图形组件选项。根据选择的组件提取所需的二进制文件;链接过程不需要编译。 | | μ C/OS-II 战斗机 | 根据头文件中 C 语言宏定义的值,有选择、有条件地编译某部分代码。 | | 嵌入式系统 | 选择 Tornado IDE 中需要的模块。 |

在系统定制之后,您将获得一个运行在目标硬件设备上的嵌入式操作系统,并且已经针对特殊应用领域进行了优化。

Android 定制概述

理论上,Android 定制分为两个层次:Linux 内核的定制和整个映像的定制。Linux 内核的定制类似于嵌入式 Linux 的定制:两者都涉及相同的方法和步骤。安卓定制主要以图片定制为主。我们来看看为什么。

ROM 包/映像

安卓镜像俗称只读存储器(ROM)包,是安卓手机的系统包。之所以有这样的命名约定,是因为安卓手机之前的手机,包括智能手机(如诺基亚、WM)和非智能手机(如索尼爱立信、Moto P2K 平台、MTK),都有单独的 ROM 芯片存储系统文件。因此,系统文件被称为 ROM 包或 ROM 映像。

该映像是一个交叉编译的二进制 Linux 文件,可以在某个嵌入式设备上安装和运行,成为该设备的操作系统。为了更好地理解这个概念,让我们回顾一下典型的开发过程,如图 6-2 所示。

A978-1-4842-0100-8_6_Fig2_HTML.jpg

图 6-2。

Development process for Android software

对于嵌入式软件,一般来说,开发 Android 软件需要和通用软件一样的步骤:设计、编码、编译、链接、打包、部署、调试、优化。对于某些 Android 系统,还需要测试和验证步骤。部署在嵌入式逻辑设备上的 OS 也经历这样的阶段。比如,对于 Linux 系统,你获取它的内核源代码,交叉编译,生成可以在嵌入式目标机上执行的代码;然后你压缩打包这段代码,形成镜像文件(见图 6-3 )。最后一步是部署。与应用文件的部署不同,OS 映像文件的部署由于其操作的特殊性而被称为安装。

A978-1-4842-0100-8_6_Fig3_HTML.jpg

图 6-3。

Image use process

嵌入式系统中一个完整的可执行软件系统的镜像文件(包)由 bootloader、OS 内核(简称内核)、文件系统和用户应用组成。实际的映像文件通常采用分区(也称为独立层)结构来存储位于映像不同区域(模块)的所有部分,所有部分都从底层加载到系统中。典型嵌入式系统映像的示例如图 6-4 所示。

A978-1-4842-0100-8_6_Fig4_HTML.jpg

图 6-4。

Example of an embedded system image

Android 映像包括引导程序、核心操作系统、硬件适配模块、文件系统、用户体验和应用。Android 的核心 OS 层包括 Linux 内核和各种中间件模块。核心 OS 层下面是硬件适配层。为了适应不同的硬件,需要为操作系统安装多样化的驱动程序。没有这些驱动程序,操作系统就不能像往常一样使用硬件进行操作。因此,映像由驱动程序和用户开发的任何应用组成。

Android 镜像通常以压缩文件(.ziptar.gz或类似文件格式)的形式存在,通常包含表 6-2 所示的文件和文件夹。压缩文件解压缩后可以看到文件结构。

表 6-2。

File Structure of an Android Image File

| 名字 | 财产 | 评论 | | --- | --- | --- | | `META-IN` | 目录 | 可选;在某些图像中可能不可用 | | `system` | 目录 |   | | `boot.img` | 文件 |   |

文件和文件夹的功能和结构如下:

  • boot.img文件:系统镜像,包括系统启动的 Linux 内核、bootloader、ramdisk。ramdisk 是一个小文件系统,它保存了初始化系统所需的核心文件。使用名为 mkbootimg 的开源工具创建了boot.img文件。
  • META-INF目录:系统更新脚本,路径为META-INF\com\google\android\updater-script
  • system\app目录:所有系统提供的应用,如日历、通讯录、Gmail 等。您可以将您的应用的.apk文件放在这个目录中,这样它就可以在 ROM 刷新时直接安装。
  • system\bin目录:top等系统命令,可以通过 adb shell 登录后执行。
  • system\etc目录:配置文件。
  • system\font目录:各种字体。
  • system\framework目录:Java 核心文件,比如.jar文件。在 Dalvik 虚拟机(DVM)下,支持用户通过 Java 开发的框架。
  • system\lib目录:由.so文件组成的 Android 本地共享库,这些文件是 ELF 二进制形式的共享对象,由汇编、C 或 C++ 编译。
  • system\media目录:媒体文件如bootanimation.zip,由.png图片组成,用于引导动画和改变引导镜像。在audio目录下是一些用作铃声和通知的音频文件。

Android 图像定制概述

Android 镜像定制,俗称创建 Android ROM(简称 creating ROM),是一个学术术语。Android 核心 OS 层有多个组件,应用在不同系统中有所不同;映像定制决定将哪些组件和应用写入目标系统的映像文件。该过程将个人定制的系统文件制作成可闪存的 ROM 映像。这也称为系统固件更新。

现成的 Android 映像可以通过 USB 闪存和 SD 卡安装到基于英特尔凌动处理器的系统(即手机、平板电脑等)上。那么具有 Android 映像的系统将能够在自启动时进入 Android 操作环境。最初被称为 TransFlash 卡的 MicroSD 卡是由 SanDisk 推出的。它是 15 × 11 × 1 毫米,大约指甲盖大小。它可以通过 SD 适配卡在 SD 卡插槽中使用,广泛用于手机。

您可以通过以下方式创建 Android ROM:

  • 编译 Android 源代码,有点复杂。
  • 基于现有的 ROM 创建或定制您自己的 ROM。

Android 图片定制的流程如图 6-5 所示。

A978-1-4842-0100-8_6_Fig5_HTML.jpg

图 6-5。

Process of Android image customization

Android 图像定制示例

下面的例子说明了定制 Android 的第二种方式:使用设备制造商为目标硬件发布的克隆 ROM 映像创建 ROM。这样,Android 定制包括对 Android 系统文件夹的结构解析、应用软件更新和 ROM 签名包的定制。步骤如下:

  1. 从 Android 的官方网站、您的手机制造商的官方网站(例如,联想 K900 手机的网站)或提供 Android 图像的网站下载编译的 ROM 包。例如,联想 K900 手机网站(www . Lenovo care . com . cn/product detail . aspx?id = 719)提供的 ROM 如表 6-3 所示。请注意,由于联想 K800 和 K900 手机在中国市场销售,软件日期仅由联想官方网站以中文提供。

表 6-3。

Information in the ROM Package on the Lenovo K900 Website

| ROM 名称 | 描述 | 安卓版本 | 发布日期 | | --- | --- | --- | --- | | K900_1_S_2_162_0074 | 官方更新 | 4.0.4 | 2012 年 8 月 8 日 | | K900_1_S_2_162_0086 | 官方更新 | 4.0.4 | 2012 年 8 月 15 日 | | K900_1_S_2_019_0113_130903 | 官方更新 | 4.2.0 | 2013 年 9 月 3 日 | | K900 _ 来源 | 官方更新 | Four point four | 2014 年 5 月 23 日 |
  1. 将所有 ROM 文件压缩到一个文件夹中(在本例中命名为“NewsROM”)。
  2. 删除和添加 ROM 文件夹中的文件(本例中为 NewsROM ),以定制和定制 Android。

一些自定义示例如下:

  • 转到data\app目录,检查预装的应用是否是您需要的。此时,您可以删除不必要的应用。您也可以添加您需要的默认安装的应用。
  • 转到system\app目录,为您的设备定制系统应用。您可以删除不需要的系统应用或添加您特别构建或定制的应用(作为定制的.apk文件)。小心:一些系统应用依赖于其他应用,因此最佳实践是在定制之前进行测试,以在实现对 Android 系统映像的更改之前修复依赖性和其他问题。
  • 转到system\media目录进行修改,如更改启动映像或添加自定义铃声。
  • 转到system\bin目录添加命令等等。

如果您担心意外删除一些文件从而导致启动失败,您应该采取保守的方法:仅对data/appsystem/app文件夹中的文件执行删除或添加操作。

  1. 将修改后的 ROM 文件夹压缩为一个.zip文件。双击压缩文件时,确保显示内容,包括META-INFsystemboot.imgdata(可选)。
  2. 安装和配置 Java 环境。以下步骤需要 Java 环境来支持自动签名工具的操作,因此您需要安装和设置 Java 操作。下载最新的 JDK(本例中为 jdk1.7.0),并安装;然后按照以下步骤操作:
    1. 设置 Java 环境变量如下:右键单击我的电脑,在弹出的快捷菜单中选择[属性]➤[高级]➤[环境变量]➤[系统变量]➤[新建]。
    2. 在对话框中,将【变量名】设置为“JAVA_HOME 变量值:JAVA 安装目录”。在同一个地方找到【路径】,双击,添加“C:\ JDK 1 . 7 . 0;。;变量值后的 C:\JDK1.7.0\bin "。
    3. 重启系统。
    4. 测试。在命令行窗口中输入 Java 命令。如果没有出现错误消息,则配置成功。
  3. 使用sign工具对.zip包进行签名。步骤如下:
    1. 下载自动签名工具并将其解压到一个目录下(本例中为myautosign)。该工具可在 http://androidforums.com/developer-101/8665-how-signing-roms.html 下载。
    2. .zip文件包重命名为update.zip,并复制到您解包自动签名的目录下(myautosign目录)。
    3. 运行解压自动签名的目录下的sign.bat文件。
    4. 定制构建完成后,目录中包含一个update_signed.zip文件,这个文件就是你需要的签名 ROM 包和定制 ROM 包。

Android 映像的安装/刷新

需要安装映像才能在目标机器上使用定制的映像。换句话说,镜像定制和使用的过程必须经过镜像生成(生产)和镜像安装两个阶段,如图 6-6 所示。

A978-1-4842-0100-8_6_Fig6_HTML.jpg

图 6-6。

Image generation and installation

映像安装意味着在目标设备或模拟器上安装 Android 映像。这一过程通常被称为重新刷新。重新刷机安卓手机相当于给手机重装系统,类似于电脑系统重装。一般来说,当计算机需要重新安装系统时,您会使用系统盘或映像文件。当 Android 手机需要刷新时,您可以通过工具将官方或第三方 ROM 镜像文件刻录到 ROM 中,并为手机安装新系统。

Android 官方网站经常为用户发布最新的 Android 图像系统,你可以直接下载图像文件,跳过图像生成阶段。对于用户来说,定制和安装过程非常简单:下载映像,然后刷新。

Android 安装还涉及恢复和擦除:

  • 恢复是移动设备的一种模式。通过恢复,用户可以安装系统(即刷新 ROM),清空手机中的各种数据,对存储卡进行分区,备份和恢复数据等等。恢复类似于电脑上的 Ghost 一键恢复功能。
  • 擦拭的意思是擦掉和除去。擦除是恢复模式中的一个选项;它从手机中删除各种数据,类似于恢复出厂默认设置。擦除最常用于重新刷新之前。用户可能会看到擦除提示,提示需要在刷新前清除数据。

如前所述,Android 安装本质上是软件交叉开发过程中的部署问题,但一般你采用离线编程,而不是在线编程。在安装过程中,您使用的介质是 SD 卡和其他便携式外部存储设备。该过程如图 6-7 所示。安装分为两步:第一步,将来自主机的镜像放在便携 SD 卡外存设备上;第二,从便携外接存储设备启动机器,在目标机器上安装 Android。

A978-1-4842-0100-8_6_Fig7_HTML.jpg

图 6-7。

Android image installation

映像安装示例

以下是映像安装的示例。路径/目录可能因不同的 OEM 或不同的 Android 版本而异(此示例基于联想手机):

  1. 清空手机的 SD 卡。此步骤是可选的,可以在主机或电话上完成。在主机上完成步骤非常简单:从手机上拔下 SD 卡,插入主机的 SD 卡读卡器,在主机上(比如在 Windows 中)删除可移动磁盘上的所有文件。

按照以下步骤清空手机上的 SD 卡:

  1. 将手机连接到主机。
  2. 在主机的命令行窗口中连续执行以下命令:

adb devices

adb remount

adb shell

su

rm -r /system/sd/*

  1. 将自定义的 ROM 文件(示例中为update_signed.zip)复制到 SD 卡上,重命名为update.zip
  2. 确保 SD 卡已插入手机。重启手机,进入恢复模式。请遵循以下步骤:
    1. 正常关机。
    2. 同时按下设备的电源键和键:手机震动启动,进入 BKB 预配置操作系统模式。快速双击按钮,系统进入测试模式。
    3. 移动到第六个选项(标清更新),点击左下角的回车。自动刷新开始。
  3. 重启。
  • (注意:sdcard通常安装在/storage/sdcard0/sdcard下,然而,如果你使用的是来自不同 OEM 的设备或另一个 Android 版本,位置可能会有所不同。)

整个刷新过程需要几分钟。手机震动两下自动重启;第一次重启需要更长时间,然后出现熟悉的四叶草界面。

重启后,选择设置➤系统信息检查手机,网络,电池,和版本信息;IMEI 码;和内部版本号,以确认升级是否成功。

使用 flash_device.sh 实现过程自动化

有一个脚本将为您执行前面描述的所有过程。该脚本位于以下位置:

<Path-to-your-project>/vendor/intel/support/flash_device.sh

您可以将该脚本添加到您的bin文件夹中,并从终端窗口运行它。您应该可以在 OEM 的用户手册中找到关于此主题的章节。

英特尔构建工具套件

英特尔开发了一套 Android 构建工具套件(见图 6-8 )来帮助开发人员轻松快速地进行 Android 系统构建和定制。该套件提供以下功能:

A978-1-4842-0100-8_6_Fig8_HTML.jpg

图 6-8。

Intel Build Tools Suite

  • 设备定制
  • 能够生成定制的固件模块和 Android 操作系统映像
  • 最终定制和本地化
  • 能够编译单个图像并将图像加载到支持的设备中
  • 验证配置就绪的能力
  • 故障排除和校准

摘要

本章完成了对 Android 系统级主题的讨论。从下一章开始,您将开始学习在 x86 平台上为 Android 开发应用,并了解如何在 Android 平台上开发适合移动设备 UX 和交互特性的用户界面。你从学习 Android 图形用户界面(GUI)设计开始,因为它是人机交互(HCI)不可或缺的一部分。由于手机或平板电脑的资源有限,Android 系统的 GUI 设计比桌面系统更具挑战性。此外,用户对用户友好的体验有更严格的要求和期望。界面设计已经成为决定市场上 Android 应用成功的重要因素之一。

Creative Commons Open Access This chapter is licensed under the terms of the Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License ( http://​creativecommons.​org/​licenses/​by-nc-nd/​4.​0/​ ), which permits any noncommercial use, sharing, distribution and reproduction in any medium or format, as long as you give appropriate credit to the original author(s) and the source, provide a link to the Creative Commons licence and indicate if you modified the licensed material. You do not have permission under this licence to share adapted material derived from this chapter or parts of it. The images or other third party material in this chapter are included in the chapter’s Creative Commons licence, unless indicated otherwise in a credit line to the material. If material is not included in the chapter’s Creative Commons licence and your intended use is not permitted by statutory regulation or exceeds the permitted use, you will need to obtain permission directly from the copyright holder.

七、Android 应用的 GUI 设计第一部分:概述

Keywords Virtual Machine Desktop Computer Touch Screen Content Provider Java Virtual Machine

自从 20 世纪 80 年代出现以来,图形用户界面(GUI)的概念已经成为人机交互不可或缺的一部分。随着嵌入式系统的发展,它们也逐渐采用了这个概念。运行在英特尔凌动硬件平台上的 Android 嵌入式操作系统处于这一趋势的最前沿。

由于资源有限,Android 系统的 GUI 设计比桌面系统更具挑战性。此外,用户对高质量的用户体验有着更加严格的要求和期望。界面设计已经成为决定市场上系统和应用成功的重要因素之一。本章介绍了如何在 Android 嵌入式系统上开发适合典型用户交互的用户界面。

嵌入式应用的图形用户界面综述

如今,软件的用户界面(UI)和用户体验(UX)是决定软件是否被用户接受并取得市场成功的越来越重要的因素。UX 设计基于输入/输出或交互设备的类型,必须符合它们的特性。与桌面计算机系统相比,Android 系统具有不同的交互设备和模式。如果一个桌面的 UI 设计被不加选择地复制,一个 Android 设备将呈现一个可怕的 UI 和难以忍受的 UX,用户无法接受。此外,随着对引人注目的用户体验有了更高的期望,开发人员在设计系统 ui 和 UXs 时必须更加细致和小心,使它们符合嵌入式应用的特征。

本章首先介绍了桌面系统的通用 GUI 设计方法,然后展示了为嵌入式系统设计 UI 的不同之处。目的是帮助你快速掌握 Android 应用 GUI 设计的一般方法和原则。

Android 设备交互模式的特征

通用台式计算机具有强大的输入/输出(或交互)设备,如大的高分辨率屏幕、全键盘和鼠标以及各种交互方式。典型的台式计算机屏幕至少为 17 英寸,分辨率至少为 1280×960 像素。键盘一般是全键盘或者增强型键盘。在全键盘上,字母、数字和其他字符位于相应的键上,也就是说,全键盘提供与所有字符对应的键。增强型键盘有额外的按键。全键盘上的键间距约为 19 mm,方便用户进行选择。

基于屏幕、键盘和鼠标的桌面计算机的 GUI 交互模式被称为 WIMP(窗口、图标、菜单和指针),这是一种使用这些元素以及包括按钮、工具栏和对话框在内的交互元素的 GUI 风格。WIMP 依靠屏幕、键盘和鼠标设备来完成交互。例如,鼠标(或类似于鼠标的设备,如光笔)用于指向,键盘用于输入字符,屏幕显示输出。

除了屏幕、键盘、鼠标和其他标准交互硬件之外,台式计算机还可以配备操纵杆、头盔、数据手套和其他多媒体交互设备来实现多媒体计算功能。通过安装摄像头、麦克风、扬声器和其他设备,并凭借其强大的计算能力,用户可以以语音、手势、面部表情和其他形式与台式计算机进行交互。

台式电脑一般也配有 CD-r om/DVD 等大容量便携式外部存储设备。有了这些外部存储设备,台式电脑就可以通过 CD/DVD 发布软件,验证所有权和证书。

由于嵌入式系统的可嵌入性和有限的资源,以及用户对便携性和移动性的需求,Android 系统具有不同于桌面系统的交互模态、方法和能力。由于这些特点和条件,Android 系统上的交互比桌面系统上的交互要求更高,也更难实现。

接下来描述 Android 设备和桌面计算机之间的主要区别。

各种尺寸、密度和规格的屏幕

与台式电脑上的大型高分辨率屏幕不同,Android 设备屏幕更小,具有各种尺寸和密度,以每英寸点数(DPI)来衡量。比如 K900 智能手机的屏幕是 5.5 英寸,分辨率为 1920 ×1080 像素,有的智能手机屏幕只有 3.2 英寸。

Android 设备屏幕的长宽比并不是台式电脑使用的 16:9 或 4:3 的常规长宽比。如果 Android 设备采用桌面计算机的交互模式,将会导致许多问题,如显示模糊和选择目标错误。

键盘和特殊按键

台式电脑有全键盘,一个键对应一个字符,键与键之间的距离很大,这使得打字很方便。如果 Android 设备有键盘,通常是小键盘而不是全键盘。小键盘的按键比全键盘少;几个字符通常共享一个键。与全键盘相比,小键盘的按键更小、间隔更紧,这使得选择和键入字符更加困难。因此,小键盘不如全键盘使用方便。此外,一些小键盘提供了标准全键盘上没有的特殊键,因此用户必须在 Android 设备上调整他们的输入。

一般来说,在 Android 设备上,按键和按钮是一个统一的概念。无论您按下按钮还是按键,该操作都被作为具有统一编号方案的键盘事件来处理。Android 中的键盘事件都有对应的android.view.KeyEvent类。图 7-1 的按钮/按键标注对应于表 7-1 中列出的事件信息。

A978-1-4842-0100-8_7_Fig1_HTML.jpg

图 7-1。

Keyboard and buttons of an Android phone

详情参见android.view.KeyEvent的帮助文档。表 7-1 的内容为摘录。

表 7-1。

Android Event Information Corresponding to Key and Button Events

A978-1-4842-0100-8_7_Figa_HTML.jpg A978-1-4842-0100-8_7_Figb_HTML.jpg

触摸屏和触控笔,取代了鼠标

触摸屏是覆盖显示设备以记录触摸位置的输入设备。通过使用触摸屏,用户可以对显示的信息有更直观的反应。触摸屏广泛应用于 Android 设备,并取代鼠标进行用户输入。最常见的触摸屏类型是电阻式触摸屏、电容式触摸屏、表面声波触摸屏和红外触摸屏,其中电阻式和电容式触摸屏最常应用于 Android 设备。用户可以直接点击屏幕上的视频和图像进行观看。

手写笔可以用来执行类似于触摸的功能。有些触控笔是触摸屏的辅助工具,可以代替手指,帮助用户完成精细的指点、选择、画线等操作,尤其是在触摸屏比较小的情况下。其他触控笔与其他系统组件一起实现触摸和输入功能。使用第一种辅助工具触控笔,用户可以用手指触摸和输入字符。但是第二种输入笔是不可或缺的输入工具,用来代替手指。

触摸和触控笔可以执行鼠标通常会做的大部分功能,如点击和拖动,但不能实现鼠标的所有功能,如同时单击右键和左键/右键。在设计嵌入式应用时,应该将交互方式控制在触摸屏或触控笔所能提供的功能范围内,避免不可用的操作。

屏幕键盘

屏幕键盘,也称为虚拟键盘或软键盘,通过软件显示在屏幕上。用户敲击虚拟键就像他们敲击物理键盘上的键一样。

很少多模态交互

多模态交互是指人机交互,其模式涉及人类的五种感官。它允许用户通过语音、手写和手势等输入方式进行交互。因为计算能力有限,安卓设备一般不采用多模态交互。

少数大容量便携式外部存储设备

大多数 Android 设备没有 CD-ROM/DVD 驱动器、硬盘或其他大容量便携式存储外设,如通常在台式计算机上配置的固态驱动器(SSD)。这些设备不能在 Android 设备上安装软件或验证所有权和证书。然而,Android 设备通常支持 microSD 卡,现在的容量高达 128 GB 越来越多的基于云的存储解决方案,如 Dropbox、One Drive 和 Google Drive,正在为 Android 设备开发,可以从谷歌 Play 商店下载 Android 兼容的客户端应用。

嵌入式系统的用户界面设计原则

本节介绍了将传统桌面应用转换为嵌入式应用时的交互设计问题和纠正措施。

屏幕尺寸的考虑因素

与台式电脑系统相比,Android 系统的屏幕更小,显示密度和长宽比也不同。这样的屏幕差异导致应用从桌面系统迁移到 Android 系统时出现很多问题。如果开发人员按比例缩小桌面系统屏幕,图形元素会变得太小而看不清楚。特别是经常很难看到文字和图标,选择并点击一些按钮,并在屏幕上适当放置一些应用图片。如果开发人员将应用图形元素迁移到 Android 系统而不改变它们的大小,屏幕空间是有限的,只能容纳少数图形元素。

文本和图标的大小

另一个问题是文本和图标的大小。当一个应用从典型的 15 英寸桌面屏幕缩小到典型的 5 或 7 英寸手机或平板电脑屏幕时,它的文本太小,看不清楚。除了文本字体的大小之外,文本窗口(如聊天窗口)也会变得太小而无法阅读文本。试图缩小字体以适应较小的窗口会使文本难以识别。

因此,嵌入式系统的设计应尽量少用文本提示信息;例如,用图形或声音信息替换文本。此外,在需要文本的地方,文本大小应该是可调整的。在 Android 上,res目录下有一些预定义的字体和图标,比如drawable-hdpidrawable-mdpidrawable-xhdpi

按钮和其他图形元素的可点击性

与小文本的问题类似,按钮和其他图形元素在迁移应用时也会带来交互问题。在桌面系统上,按钮的大小是为鼠标点击而设计的,而在 Android 系统上,按钮的大小应该适合手指(在触摸屏上)或触控笔。因此,当移植一个基于 Windows 的应用来支持 Android 设备时,需要重新设计应用 UI;并且应该选择由 Android SDK 提供的预定义的可绘制图形,以便适合手指或手写笔。

开发人员应该使用更大、更清晰的按钮或图形元素来避免此类问题,并在图形元素之间留有足够的间隙以避免错误,当使用小触摸屏通过手指或触控笔进行选择时,错误是常见的。此外,如果应用在按钮附近有文本标签,标签应该是与按钮相连的可点击区域的一部分,这样按钮更容易点击。

应用窗口的大小

许多应用(如游戏)使用固定大小的窗口,而不是自动调整以填充任何大小屏幕的窗口。当这些应用迁移到 Android 系统时,由于屏幕的长宽比与其分辨率不匹配,可能会看不到部分画面,或者部分区域无法到达。

这些问题在智能手机和平板电脑上可能更复杂,因为它们的屏幕有各种密度,如小(426 dp × 320 dp)、正常(470 dp × 320 dp)、大(640 dp × 480 dp)和超大(960 dp × 720 dp)。它们的长宽比各不相同,不同于桌面系统通常采用的长宽比。

解决此类问题的一个好方法是将整个应用窗口按比例放置在智能手机或平板电脑屏幕上,比如大屏幕和超大屏幕,通常为 640 × 480 像素和 960 × 720 像素;或者重新排列 UI,充分利用整个宽屏区域;或者使整个应用窗口成为可滚动视图。此外,您可以允许用户使用多个触摸手指触摸来放大、缩小或移动屏幕上的应用窗口。

触摸屏和触控笔带来的考虑

如前所述,许多 Android 系统使用触摸屏和触控笔来执行一些传统的鼠标功能。这种输入设备被称为。然而,仅点击的触摸屏不能提供所有的鼠标功能。没有右键,不触摸屏幕时无法捕捉当前手指/手写笔位置。因此,桌面应用允许在不点击的情况下移动光标、左键和右键的不同操作等功能,无法在使用触摸屏和触控笔的 Android 系统上实现。

下面几节讲的是在使用只需轻触的触摸屏将应用从桌面系统迁移到 Android 系统时经常看到的几个问题。

正确解释仅点击触摸屏上光标(鼠标)的移动和输入

当没有按下鼠标键时,许多应用需要鼠标移动信息。这个操作叫做。例如,很多 PC 射击游戏 1 模拟用户的视野,以至于移动鼠标而不点击被解释为移动游戏玩家的视野;但是光标应该总是停留在新视野的中间。然而,具有仅点击触摸屏的嵌入式设备不支持移动光标而不点击的操作。一旦用户的手指触摸到屏幕,就会触发一个点击事件。当用户在屏幕上移动手指时,触发一系列不同位置的点击事件;这些事件被现有的游戏代码解释为额外的交互事件(即移动游戏玩家枪的瞄准位置)。

将这类应用迁移到 Android 系统时,需要修改原有的交互模式。比如这个问题可以修改为点击操作:一旦用户触摸屏幕,游戏屏幕要立即切换到视野,在视野中光标位于屏幕中心。这样,光标总是显示在屏幕中心,而不是用户实际触摸的位置。您在移动平台上受益的一个优势是,市场上的大多数智能手机和平板电脑都配备了加速度计、陀螺仪、GPS 传感器和指南针等传感器,它们允许应用从传感器中读取数据。因此,开发人员有更多的选择,而不仅仅是触摸输入。

更一般地,如果应用需要跟踪光标从点 A 到点 B 的移动,则仅点击触摸屏可以通过用户首先点击点 A 然后点击点 B 来定义该输入,而不需要跟踪点 A 和点 B 之间的移动

正确设置屏幕映射

许多应用以全屏模式运行。如果这样的应用不能完美地填充整个仅点击触摸屏(也就是说,它们比屏幕小或大),输入映射错误将导致:显示位置和点击位置之间存在偏差。

在将全屏应用迁移到具有低纵横比的仅点击触摸屏时,经常出现的一种情况是应用窗口位于屏幕中央,两边显示空白区域。例如,当分辨率为 640 × 480(或 800 × 600)像素的桌面应用窗口迁移到分辨率为 960 × 720(或 1280 × 800,Dell Venue 8 上的 WXGA)像素的仅点击触摸屏时,它会出现在屏幕上,如图 7-2 所示。由此产生的映射错误会导致应用错误地响应用户交互。当用户点击黄色箭头(目标)的位置时,应用识别的位置就是红色爆炸图标所在的点。当用户点击按钮时,也会出现这种错误。

A978-1-4842-0100-8_7_Fig2_HTML.jpg

图 7-2。

Screen-mapping errors due to a low aspect ratio

您应该考虑位置映射逻辑,并考虑这个空白空间,即使这个空白空间不是迁移应用窗口的一部分。通过进行这些改变,仅点击触摸屏可以正确地映射触摸位置。

另一种情况发生在桌面全屏窗口被迁移到具有更高纵横比的仅点击触摸屏时。原始应用窗口的高度不适合仅点击的触摸屏,并且映射错误发生在垂直方向而不是水平方向。

图 7-3 显示了原始应用窗口水平填充屏幕,但在具有更高纵横比的仅点击触摸屏上不是垂直填充屏幕。这里,当用户点击黄色箭头(目标)的位置时,应用识别的位置就是红色爆炸图标所在的点。这些错误是由物理显示和应用窗口之间的形状差异引起的。

A978-1-4842-0100-8_7_Fig3_HTML.jpg

图 7-3。

Screen-mapping errors due to a high aspect ratio

一种解决方案是确保操作系统准确地将仅点击触摸屏映射到屏幕的整个可视区域。操作系统提供特殊服务来完成屏幕拉伸和鼠标位置映射。另一个解决方案是在应用开发之初,考虑允许配置选项支持 Android SDK 提供的预配置显示密度和纵横比,例如分辨率为 640 × 480、960 × 720 或 1,080 × 800 像素的屏幕。这样,如果最终的尺寸变形是可接受的,应用可以自动拉伸窗口以覆盖整个屏幕。

如何解决悬停问题

许多应用允许悬停操作:也就是说,用户可以将鼠标放在某个对象上,或将鼠标放在应用图标上,以触发动画项目或显示工具提示。该操作常用于为游戏中的新玩家提供指令;但是它与仅点击触摸屏的特征不兼容,因为它们不支持鼠标悬停操作。

您应该考虑选择一个替代事件来触发动画或提示。例如,当用户触摸应用的操作时,自动触发相关的动画主题和提示。另一种方法是设计一种界面交互模式,将点击事件暂时解释为鼠标悬停事件。例如,按下某个按钮并移动光标的动作不会被解释为点击操作。

提供右击功能

如前所述,仅点击触摸屏一般不支持鼠标右键操作。一种常用的替代方法是延迟触摸(比点击时间长得多)来表示右键单击。如果用户意外地过早松开手指,这可能导致错误的操作发生。此外,该方法不能同时执行左键单击和右键单击(也称为双击)。

你应该提供一个用户交互界面来代替右击功能:例如,使用双击或者在屏幕上安装一个可点击的控件来代替右击。

键盘输入问题

如前所述,台式电脑使用全键盘,而 Android 系统通常有更简单的小键盘、按钮面板、用户可编程按钮和有限数量的其他输入设备。这些限制在设计桌面系统中看不到的嵌入式应用时会导致一些问题。

限制各种命令的输入

Android 系统上的键盘限制使得用户很难键入大量字符。因此,需要用户输入许多字符的应用,尤其是那些依赖于命令输入的应用,在迁移到 Android 系统时需要进行适当的调整。

一种解决方案是提供一种输入模式,通过减少命令的数量或选择性地使用方便的工具(如菜单项快捷键)来限制字符的数量。更灵活的解决方案是在屏幕上创建命令按钮,尤其是上下文相关的按钮(即只在需要时才出现的按钮)。

满足键盘需求

应用需要键盘输入,例如命名文件、创建个人数据、保存进度和支持在线聊天。大多数应用倾向于使用屏幕键盘输入字符,但屏幕键盘并不总是运行或显示在应用界面的前端,这使得字符输入问题难以解决。

一种解决方案是为应用设计一种与屏幕键盘应用没有明显冲突的模式(例如,不使用全屏默认操作模式),或者在 UI 中提供一个仅在需要时出现的屏幕键盘。另一种最小化键盘输入的简单方法是提供默认的文本字符串值,如个人数据的默认名称和保存文件的默认名称,并允许用户通过触摸来选择。若要获取文本字符串所需的其他信息(例如,文件名称的前缀和后缀),您可以添加一个选择按钮,该按钮提供您已建立的字符串列表,用户可以从中进行选择。通过组合从屏幕上提取的各种用户信息项或者甚至使用日期-时间标记,也可以唯一地获得保存文件的名称。一些文本输入服务(比如聊天服务)如果不是应用的核心功能,应该被禁用。这不会对用户体验造成任何负面影响。

软件分发和版权保护问题

台式电脑一般配有 CD-ROM/DVD 驱动器,其软件一般通过 CD/DVD 发行。此外,出于反盗版目的,CD/DVD 安装通常要求用户验证磁盘的所有权或从 CD/DVD 动态加载内容,尤其是视频文件。然而,Android 系统(例如智能手机和平板电脑)通常没有 CD-ROM/DVD 驱动器;Android 确实支持外置的 microSD 卡,但是直接从里面安装应用还是不支持的。

一个好的解决方案是允许用户通过互联网下载或安装应用,而不是从 CD/DVD 安装。消费者直接从苹果应用商店、Google Play 和亚马逊应用商店等应用商店购买和安装应用。这种流行的软件发布模式允许移动开发者使用证书、在线账户或其他基于软件的方式来验证所有权,而不是物理 CD/DVD。同样,您应该考虑提供将内容放在在线云服务上的选项,而不是要求用户从 CD/DVD 下载视频和其他内容。

Android 应用概述

以下部分描述了 Android 应用的应用文件框架和组件结构。

应用文件框架

图 7-4 显示了 HelloAndroid app 生成后的文件结构(这是一个 Eclipse 截图)。

A978-1-4842-0100-8_7_Fig4_HTML.jpg

图 7-4。

Example file structure of an Android project

即使您没有使用 Eclipse,您也可以直接访问项目文件夹并看到相同的文件结构,如下所示:

E:\Android Dev\workspace\HelloAndroid>TREE /F

E:.

│  .classpath

│  .project

│  AndroidManifest.xml

│  ic_launcher-web.png

│  proguard-project.txt

│  project.properties

├─.settings

│      org.eclipse.jdt.core.prefs

├─assets

├─bin

│  │  AndroidManifest.xml

│  │  classes.dex

│  │  HelloAndroid.apk

│  │  resources.ap_

│  │

│  ├─classes

│  │  └─com

│  │      └─example

│  │          └─helloandroid

│  │                  BuildConfig.class

│  │                  MainActivity.class

│  │                  R$attr.class

│  │                  R$dimen.class

│  │                  R$drawable.class

│  │                  R$id.class

│  │                  R$layout.class

│  │                  R$menu.class

│  │                  R$string.class

│  │                  R$style.class

│  │                  R.class

│  │

│  └─res

│      ├─drawable-hdpi

│      │      ic_action_search.png

│      │      ic_launcher.png

│      │

│      ├─drawable-ldpi

│      │      ic_launcher.png

│      │

│      ├─drawable-mdpi

│      │      ic_action_search.png

│      │      ic_launcher.png

│      │

│      └─drawable-xhdpi

│              ic_action_search.png

│              ic_launcher.png

├─gen

│  └─com

│      └─example

│          └─helloandroid

│                  BuildConfig.java

│                  R.java

├─libs

│      android-support-v4.jar

├─res

│  ├─drawable-hdpi

│  │      ic_action_search.png

│  │      ic_launcher.png

│  │

│  ├─drawable-ldpi

│  │      ic_launcher.png

│  │

│  ├─drawable-mdpi

│  │      ic_action_search.png

│  │      ic_launcher.png

│  │

│  ├─drawable-xhdpi

│  │      ic_action_search.png

│  │      ic_launcher.png

│  │

│  ├─layout

│  │      activity_main.xml

│  │

│  ├─menu

│  │      activity_main.xml

│  │

│  ├─values

│  │      dimens.xml

│  │      strings.xml

│  │      styles.xml

│  │

│  ├─values-large

│  │      dimens.xml

│  │

│  ├─values-v11

│  │      styles.xml

│  │

│  └─values-v14

│          styles.xml

└─src

└─com

└─example

└─helloandroid

MainActivity.java

我们来解释一下这个 Android 项目文件结构的特点:

  • src目录:包含所有源文件。
  • R.java文件:由 Eclipse 中集成的 Android SDK 自动生成。您不需要修改其内容。
  • Android 库:Android 应用使用的一组 Java 库。
  • 目录:主要存储多媒体文件和其他文件。
  • 目录:存储预配置的资源文件,如应用使用的可绘制布局。
  • values目录:主要存放strings.xmlcolors.xmlarrays.xml
  • AndroidManifest.xml:相当于一个应用配置文件。包含应用的名称、活动、服务、提供者、接收者、权限等等。
  • 目录:主要存储应用使用的图像资源。
  • 目录:主要存储应用使用的布局文件。这些布局文件是 XML 文件。

类似于一般的 Java 项目,src文件夹包含项目的所有.java文件;一个res文件夹包含所有的项目资源,比如应用图标(可绘制)、布局文件和常量值。

接下来的部分将介绍每个 Android 项目的必备文件AndroidManifest.xml和其他 Java 项目中包含的gen文件夹中的R.java文件。

AndroidManifest.xml

该文件包含有关您的应用的信息,这些信息对于 Android 系统至关重要,系统在运行任何应用代码之前必须拥有这些信息。这些信息包括项目中使用的活动、服务、许可、提供者和接收者。示例如图 7-5 所示。

A978-1-4842-0100-8_7_Fig5_HTML.jpg

图 7-5。

The content of AndroidManifest.xml displayed in Eclipse

该文件的代码如下:

<manifest xmlns:android="http://schemas.android.com/apk/res/android

package="com.example.helloandroid"

android:versionCode="1"

android:versionName="1.0" >

<uses-sdk

android:minSdkVersion="8"

android:targetSdkVersion="15" />

<application

android:icon="@drawable/ic_launcher"

android:label="@string/app_name"

android:theme="@style/AppTheme" >

<activity

android:name=".MyMainActivity"

android:label="@string/title_activity_my_main" >

<intent-filter>

<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />

</intent-filter>

</activity>

</application>

</manifest>

AndroidManifest.xml文件是 XML 格式的文本文件,每个属性由一个name = value对定义。例如,在 Android 中,label = "@ string / title_activity_my_main", label表示 Android 应用的名称为activity_my_main

元素由一个或多个属性组成,每个元素由开始(<)和结束(/>)标记包围:

<Type Name [attribute set]> Content </ type name>

<Type Name  Content />

格式[attribute set]可以省略;例如,<intent-filter> ... </ intent-filter>文本段对应元素的活动内容,<action... />对应action元素。

XML 元素嵌套在层中以指示它们的从属关系,如前面的示例所示。action元素嵌套在intent-filter元素中,说明了intent-filter属性或设置的某些方面。关于 XML 的详细信息超出了本书的范围,但是有许多优秀的 XML 书籍可供参考。

在这个例子中,intent-filter描述了一个活动启动的位置和时间,并且每当一个活动(或 OS)要执行一个操作时,创建一个intent对象。intent对象携带的信息可以描述你想做什么,你想处理哪些数据和数据类型,以及其他信息。Android 比较每个应用暴露的intent-filter数据,找到最合适的活动来处理调用者指定的数据和操作。

表 7-2 中列出了AndroidManifest.xml文件中主要属性条目的描述。

表 7-2。

The Main Attribute Entries in the AndroidManifest.xml File

| 参数 | 描述 | | --- | --- | | `Manifest` | 包含包中所有内容的根节点。 | | `xmlns:android` | 包含命名空间的清单。`xmlns:android=` [`http://schemas.android.com/apk/res/android`](http://schemas.android.com/apk/res/android) 。使各种标准属性在文件中可用,并为大多数元素提供数据。 | | `package` | 清单应用包。 | | `Application` | 包含包中应用级组件清单的根节点。该元素还可以包含应用的一些全局和默认属性,如标签、图标、主题和必要的权限。一个清单可以包含零个或一个(不超过一个)元素。 | | `android:icon` | 应用的图标。 | | `android:label` | 应用的名称。 | | `Activity` | 用户启动应用时要加载的初始页面的名称。它是用户交互的重要工具。当执行其他活动或通过其他活动标志显示其他活动时,会显示大多数其他页面。注意:无论是在外部使用还是在自己的包中使用,每个活动都必须有一个对应的``标志。如果活动没有对应的标志,则不能操作。此外,为了支持搜索活动,一个活动可以包含一个或几个``元素来描述它所支持的操作。 | | `android:name` | 应用启动的默认活动。 | | `intent-filter` | 通过显示由指定组件支持的`intent`值来形成。除了指定不同类型的值之外,`intent-filter`还可以指定属性来描述操作所需的唯一标签、图标或其他信息。 | | `Action` | 组件支持的意图动作。 | | `Category` | 组件支持的意图类别。这里指定了应用启动的默认活动。 | | `uses-sdk` | 与应用使用的 SDK 版本相关。 |

R.java

R.java文件是在创建项目时自动生成的。这是一个只读文件,不能修改。R.java 是一个定义项目所有资源的索引文件。例如:

/* AUTO-GENERATED FILE.  DO NOT MODIFY.

... ...

*/

package com.example.helloandroid;

public final class R {

public static final class attr {

}

public static final class dimen {

public static final int padding_large=0x7f040002;

public static final int padding_medium=0x7f040001;

public static final int padding_small=0x7f040000;

}

public static final class drawable {

public static final int ic_action_search=0x7f020000;

public static final int ic_launcher=0x7f020001;

}

public static final class id {

public static final int menu_settings=0x7f080000;

}

public static final class layout {

public static final int activity_my_main=0x7f030000;

}

public static final class menu {

public static final int activity_my_main=0x7f070000;

}

public static final class string {

public static final int app_name=0x7f050000;

public static final int hello_world=0x7f050001;

public static final int menu_settings=0x7f050002;

public static final int title_activity_my_main=0x7f050003;

}

public static final class style {

public static final int AppTheme=0x7f060000;

}

}

可以看到这段代码中定义了很多常量。这些常量的名称与res文件夹中的文件名相同,证明R.java文件存储了项目所有资源的索引。有了这个文件,在应用中使用资源和标识所需资源就更加方便了。因为此文件不允许手动编辑,所以只需在向项目中添加新资源时刷新项目。R.java文件自动生成所有资源的索引。

常数定义文件

项目的values子目录包含字符串、颜色和数组常量的定义文件;字符串常量定义在strings.xml文件中。这些常量被 Android 项目中的其他文件使用。

Eclipse 为strings.xml文件提供了两个图形视图选项卡,Resources 和 strings.xml。Resources 选项卡提供了name-value的结构化视图,strings.xml 选项卡直接显示文本文件格式的内容。HelloAndroid 示例的strings.xml文件如图 7-6 所示。

A978-1-4842-0100-8_7_Fig6_HTML.jpg

图 7-6。

IDE graphic view of the strings.xml file of HelloAndroid

文件内容如下:

<resources>

<string name="app_name">HelloAndroid</string>

<string name="hello_world">Hello world!</string>

<string name="menu_settings">Settings</string>

<string name="title_activity_main">MainActivity</string>

</resources>

代码非常简单;它只定义了四个字符串常量(资源)。

布局文件

布局文件描述了每个屏幕小部件(窗口和小部件的组合)的大小、位置和排列。布局文件是应用的“面孔”。布局文件是 XML 格式的文本文件。

小部件是可视化的 UI 元素,如按钮和文本框。它们相当于 Windows 系统术语中的控件和容器。按钮、文本框、滚动条等等都是小部件。在 Android OS 中,小部件一般属于View类及其子类,存储在android.widget包中。

应用有一个主布局文件,对应于启动时应用的屏幕显示。例如,HelloAndroid 示例的布局文件和主界面如图 7-7 所示。创建应用时,Eclipse 会自动为应用的主屏幕显示生成一个布局文件。该文件位于项目文件夹的res\layout目录中。生成的应用项目中的文件名在下一节中指定:在这种情况下,源代码文件名对应于[Layout Name]键,因此文件被命名为activity_main.xml

A978-1-4842-0100-8_7_Fig7_HTML.jpg

图 7-7。

The main graphic layout and user interface

当你点击设计窗口(本例中为activity_main.xml)时,可以看到 XML 格式的文本文件的相应内容,如图 7-8 所示。

A978-1-4842-0100-8_7_Fig8_HTML.jpg

图 7-8。

The main layout file of the HelloAndroid example

该文件的内容如下:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android

xmlns:tools="http://schemas.android.com/tools

android:layout_width="match_parent"

android:layout_height="match_parent" >

<TextView

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:layout_centerHorizontal="true"

android:layout_centerVertical="true"

android:padding="@dimen/padding_medium"

android:text="@string/hello_world"

tools:context=".MainActivity" />

</RelativeLayout>

在这段代码中,有几个布局参数:

  • <RelativeLayout>:相对位置的布局配置。
  • android:layout_width:定制当前视图的屏幕宽度;match_parent代表父容器(在本例中是活动)匹配;fill_parent填满整个屏幕;wrap_content,表示为文本字段,根据该视图的宽度或高度而变化。
  • android:layout_height:定制当前视图所占的屏幕高度。

此布局文件中未显示的另外两个通用参数如下:

  • android:orientation:此处为水平布局。
  • android:layout_weight:为线性布局的多个视图指定一个重要性值。所有视图都被赋予一个layout_weight值;默认值为零。

尽管布局文件是一个 XML 文件,但您不必理解它的格式或直接编辑它,因为 Android 开发工具和 Eclipse 提供了一个可视化的设计界面。您只需拖放小部件并在 Eclipse 中设置相应的属性,您的操作就会自动记录在布局文件中。当您在下面几节中浏览应用开发示例时,您可以看到这是如何工作的。

源代码文件

构建项目时,Eclipse 会生成一个默认的.java源代码文件,其中包含项目的应用基本运行时代码。它位于src\com\example\XXX目录下的项目文件夹中(其中XXX是项目名称)。本例中生成的应用项目的文件名是与[Activity Name]键相对应的源代码文件名,因此该文件被命名为MainActivity.java

MainActivity.java的内容如下:

package com.example.flashlight;

import android.os.Bundle;

import android.app.Activity;

import android.view.Menu;

import android.view.MenuItem;

import android.support.v4.app.NavUtils;

public class MyMainActivity extends Activity {

@Override

public void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_my_main);

}

@Override

public boolean onCreateOptionsMenu(Menu menu) {

getMenuInflater().inflate(R.menu.activity_my_main, menu);

return true;

}

}

应用的组件结构

Android 应用框架为开发者提供了 API。因为应用是用 Java 构建的,所以程序的第一层包含各种控件的 UI 需求。例如,视图(View组件)包含列表、网格、文本框、按钮,甚至嵌入式 web 浏览器。

一个 Android 应用通常由五个组件组成:

  • 活动
  • 意图接收者
  • 服务
  • 内容供应器
  • 意图和意图过滤器

下面几节将对每个组件进行更多的讨论。

活动

具有可视化用户界面的应用是使用活动实现的。当用户从主屏幕或应用启动器中选择一个应用时,它会启动一个动作或活动。每个活动程序通常采用独立界面(屏幕)的形式。每个活动都是一个独立的类,它扩展并实现了活动的基类。这个类显示为 UI,由响应事件的View组件组成。

大多数程序都有多个活动(换句话说,一个 Android 应用由一个或多个活动组成)。切换到另一个接口会加载新的活动。在某些情况下,先前的活动可能会给出返回值。例如,让用户选择照片的活动会将照片返回给调用者。

当用户打开一个新界面时,旧界面被挂起并放入历史堆栈(界面切换历史堆栈)。用户可以返回到历史堆栈界面中已经打开的活动。没有历史值的堆栈可以从历史堆栈界面中删除。Android 在运行应用的历史堆栈中保留所有生成的界面,从第一个界面到最后一个界面。

活动是一个容器,它本身不显示在 UI 中。您可以大致将一个活动想象成 Windows 操作系统中的一个窗口,但是视图窗口不仅用于显示,还用于完成一项任务。

意图和意图过滤器

Android 通过一个叫做intent的特殊类来实现界面切换。一个intent描述了程序做什么。数据结构的两个最重要的部分是动作和根据已建立的规则处理的数据(数据)。典型的操作有MAIN(活动入口)、VIEWPICKEDIT。操作中使用的数据使用统一资源标识符(URI)表示。例如,要查看一个人的联系信息,您需要使用VIEW操作创建一个intent,该数据是一个指向该人的 URI 的指针。

与一个intent相关联的类被称为一个IntentFilter。一个intent将一个请求封装成一个对象;IntentFilter然后描述一个活动(或者说,一个意图接收者,稍后解释)可以处理什么意图。在前面的例子中,显示一个人的联系信息的活动使用了一个IntentFilter,它知道如何处理应用于这个人的数据VIEW操作。使用IntentFilterAndroidManifest.xml文件中的活动通常通过解析intent活动开关来完成。首先,它使用startActivity (myIntent)函数来启动新的活动,然后系统地检查所有已安装程序的IntentFilter,然后找到与IntentFilter对应的myIntent最匹配的活动。这个新活动接收来自intent的消息,然后开始。intent-解析过程实时发生在被调用的startActivity中。这个过程有两个优点:

  • 活动只发出一个intent请求,可以重用其他组件的功能。
  • 该活动总是可以被IntentFilter的一个等价的新活动替换。

服务

服务是没有用户界面的常驻系统程序。您应该为任何需要连续运行的应用使用服务,例如网络监视器或检查应用更新。

使用服务的两种方式是启动-停止模式和绑定-解除绑定模式。工艺流程图和功能如表 7-3 所示。

表 7-3。

The Usage Model of a Service

| 方式 | 开始 | 目标 | 访问 | 笔记 | | --- | --- | --- | --- | --- | | 开始/停止 | `Context.startService()` | `Context.stopService()` |   | 即使`startService`调用的进程结束,服务仍然存在,直到进程调用`stopService()`或者服务导致自己的终止(`stopSelf()`被调用)。 | | 绑定/解除绑定 | `Context.bindService()` | `Context.unbindService()` | `Context.ServiceConnection()` | 调用`bindService()`时,进程是死的;那么它所绑定的服务必须被终止。 |

当两种模式混合使用时,例如一种模式调用startService()而其他模式调用bindService(),那么只有当stopService调用和unbindService调用都发生时,服务才会被终止。

一个服务进程有自己的生命周期,Android 试图保留一个已经启动或绑定的服务进程。服务流程描述如下:

  • 如果服务是方法onCreate()onStartonDestroy()的实现进程,那么主进程就变成前台进程,以确保这段代码不被停止。
  • 如果服务已经启动,其重要性值低于可见流程,但高于所有不可见流程。因为只有少数进程对用户可见,只要内存不是特别低,服务就不会停止。
  • 如果多个客户端绑定到该服务,只要其中任何一个客户端对用户可见,该服务就是可见的。

广播意图接收器

当你想执行一些与外部事件相关的代码时,比如在半夜执行一个任务或者响应电话铃声,使用IntentReceiver。意向接收者没有 UI,使用NotificationManager通知用户他们的事件已经发生。意向接收方在AndroidManifest.xml文件中声明,但也可以使用Context.registerReceiver()声明。程序不必连续运行来等待IntentReceiver被调用。当意图接收器被触发时,系统启动您的程序。节目还可以使用Context.broadcastIntent()将自己的意图广播发送给其他节目。

Android 应用可用于处理数据元素或响应事件(如接收文本消息)。Android 应用与一个AndroidManifest.xml文件一起被部署到设备上。AndroidManifest.xml包含必要的配置信息,以便应用正确安装在设备上。AndroidManifest.xml还包括应用可以处理的必要类名和事件类型,以及运行应用的必要权限。例如,如果一个应用需要访问网络,比如说,下载一个文件,清单文件必须在许可证中明确列出。许多应用可能会启用这种特定的许可证。这种声明式安全性有助于降低恶意应用损坏设备的可能性。

内容供应器

您可以将内容供应器视为数据库服务器。内容供应器的任务是管理持久的数据访问,比如 SQLite 数据库。如果应用非常简单,您可能不需要创建内容提供者应用。如果您想要构建一个更大的应用,或者需要构建应用来为多个活动或应用提供数据,您可以使用内容提供程序进行数据访问。

如果你想让其他程序使用他们自己程序的数据,内容供应器是非常有用的。content-provider 类实现了一系列标准方法,允许其他程序存储和读取内容提供者可以处理的数据。

安卓模拟器

Android 不使用普通的 Java 虚拟机(JVM);而是使用 Dalvik 虚拟机(DVM)。DVM 和 JVM 是根本不同的。DVM 占用的内存更少,专门针对移动设备进行了优化,更适合在嵌入式环境中使用的移动电话。其他差异如下:

  • 一般的 JVM 是基于栈的虚拟机,但是 DVM 是基于寄存器的虚拟机。后者更好,因为应用可以在硬件的基础上实现最大限度的优化,这更符合移动设备的特点。
  • DVM 可以在有限的内存中同时运行多个虚拟机实例,因此每个 DVM 应用都作为独立的 Linux 进程执行。在一般的 JVM 中,所有的应用都在一个共享的 JVM 中运行,因此各个应用不是作为单独的进程运行的。由于每个应用都作为单独的进程运行,因此可以防止 DVM 在虚拟机崩溃的情况下关闭所有程序。
  • DVM 提供了比一般 JVM 限制更少的许可平台。DVM 和 JVM 支持不同的通用代码。DVM 不运行标准的 Java 字节码,而是运行 Dalvik 可执行格式(.dex)。Android 应用的 Java 代码编译实际上由两个过程组成。第一步是将 Java 源代码编译成普通的 JVM 可执行代码,它使用文件名后缀.class.,第二步是将字节码编译成 Dalvik 执行代码,它使用文件名后缀.dex。第一步将项目目录下src子目录下的源代码文件编译成bin\class目录下的.class文件;第二步将文件从bin\class子目录移动到bin目录中的classes.dex文件。编译过程被集成到 Eclipse 构建过程中;但是,您也可以使用命令行手动编译。

Android 运行时简介(ART)

ART 是一个 Android 运行时,最初作为预览功能出现在 Google Android KitKat (4.4)中。它也被称为 Dalvik 版本 2,正在 Android 开源项目(AOSP)的积极开发中。所有装有 Android KitKat 的智能手机和平板电脑都将 Dalvik 作为默认运行时。这是因为一些 OEM 厂商仍然不支持 Android 实现中的 ART,大多数第三方应用仍然基于 Dalvik 构建,尚未添加对新 ART 的支持。

正如谷歌在 Android 开发者网站上所描述的那样,大多数现有应用在运行 ART 时应该可以工作。然而,一些在 Dalvik 上工作的技术在 ART 上并不工作。Dalvik 和 ART 之间的差异如表 7-4 所示。

表 7-4。

Dalvik vs. ART Summary

|   | 达尔维克 | 艺术 | | --- | --- | --- | | 应用 | 带有 DEX 类文件的 APK 包 | 和达尔维克一样 | | 编译类型 | 动态编译(JIT) | 超前编译(AOT) | | 功能 | 稳定,并通过了广泛的质量保证 | 基本功能和稳定性 | | 安装时间 | 更快的 | 由于编译而变慢 | | 应用启动时间 | 由于 JIT 编译和解释,速度通常较慢 | 由于 AOT 编译,速度更快 | | 存储空间 | 较小的 | 更大,带有预编译二进制文件 | | 内存占用 | 由于 JIT 代码缓存而变大 | 较小的 |

ART 提供了一些新特性来帮助应用开发、性能优化和调试,例如支持采样分析器和调试特性,如监控和垃圾收集。从 Dalvik 过渡到 ART 可能需要一些时间,Dalvik 和 ART 都将在 Android 中提供,以允许智能手机和平板电脑用户进行选择和切换。但是,未来的 64 位 Android 将基于 ART。

摘要

本章介绍了桌面系统的通用 GUI 设计方法,然后展示了为嵌入式系统设计 UI 和 UX 的不同之处。现在,您应该了解了 Android 应用 GUI 设计的一般方法和原则,并准备好学习 Android 特定的 GUI。下一章描述了活动的状态转换、Context类、意图以及应用和活动之间的关系。

Creative Commons Open Access This chapter is licensed under the terms of the Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License ( http://​creativecommons.​org/​licenses/​by-nc-nd/​4.​0/​ ), which permits any noncommercial use, sharing, distribution and reproduction in any medium or format, as long as you give appropriate credit to the original author(s) and the source, provide a link to the Creative Commons licence and indicate if you modified the licensed material. You do not have permission under this licence to share adapted material derived from this chapter or parts of it. The images or other third party material in this chapter are included in the chapter’s Creative Commons licence, unless indicated otherwise in a credit line to the material. If material is not included in the chapter’s Creative Commons licence and your intended use is not permitted by statutory regulation or exceeds the permitted use, you will need to obtain permission directly from the copyright holder. Footnotes 1

一个典型的例子就是游戏《反恐精英》(CS)。

八、Android 应用的 GUI 设计第二部分:特定于 Android 的 GUI

Keywords Finish Function View Class Application Context Android Application Application Interface

本章描述了活动的状态转换,并讨论了Context类、意图以及应用和活动之间的关系。

活动的状态转换

如第七章所述,活动是最重要的组成部分。活动有自己的状态和转换规则,它们是编写 Android 应用所需了解的基础。

活动状态

当创建或销毁活动时,它们会进入或退出活动堆栈。当它们这样做时,它们在四种可能的状态之间转换:

  • 活动:处于活动状态的活动位于堆栈顶部时可见。通常,响应用户输入的是前台活动。Android 会保证不惜一切代价执行。如果需要,Android 将进一步销毁堆栈活动,以确保活动活动所需的资源。当另一个活动变为活动时,该活动暂停。
  • 暂停:在某些情况下,活动是可见的,但没有焦点。此时此刻,它处于暂停状态。当活动活动完全透明或者是非全屏活动时,下面的活动将达到这种状态。暂停的活动被认为是活动的,但不接受用户输入事件。在极端情况下,Android 会终止暂停的活动,以将资源恢复到活动活动。当一项活动完全看不见时,它就停止了。
  • 停止:当活动不可见时,它被停止。该活动保留在内存中,以保存所有状态和成员信息。但是当系统需要内存的时候,这个活动就被“拿出来拍了。”当活动停止时,保存数据和当前 UI 状态非常重要。一旦活动退出或关闭,它将变为非活动状态。
  • 非活动的:当一个活动被终止时,它就变成非活动的。不活动的活动从活动堆栈中删除。当您需要使用或显示该活动时,需要再次启动它。

活动状态转换图如图 8-1 所示。

A978-1-4842-0100-8_8_Fig1_HTML.jpg

图 8-1。

Android activity state transition diagram

状态改变不是人为的,完全由 Android 内存管理器控制。Android 首先关闭包含非活动的应用,然后是那些停止活动的应用。在极端情况下,它会删除暂停的活动。

为了确保完美的用户体验,这些状态的转换对用户来说是不可见的。当活动从暂停、停止或非活动状态返回到活动状态时,UI 必须是非歧视性的。所以,当一个活动停止时,保存 UI 状态和数据是非常重要的。一旦活动被激活,它需要恢复保存的值。

活动的重要功能

活动状态转换触发相应的activity类的函数(即 Java 方法)。Android 调用这些函数;开发人员不必显式调用它们。它们被称为状态转移函数。您可以覆盖状态转换函数,以便它们可以在指定的时间完成工作。还有一些用于控制活动状态的功能。这些功能构成了活动编程的基础。让我们来了解一下这些功能。

onCreate 状态转换函数

onCreate功能原型如下:

void  onCreate(Bundle savedInstanceState);

该函数在首次加载活动时运行。当您启动一个新程序时,它的主活动的onCreate事件被执行。如果活动被销毁(OnDestroy,稍后解释),然后重新加载到任务中,那么它的onCreate事件参与者将被重新执行。

一个活动很可能被强制切换到后台。(一个切换到后台的活动,用户已经看不到了,但它仍然存在于一个任务的中间,比如当一个新的活动开始“覆盖”当前活动的时候;或者用户按下主屏幕按钮返回主屏幕;或者其他事件发生在当前活动之上的新活动中,例如传入的调用者接口。)如果用户在一段时间后没有再次查看该活动,则该活动可能会与任务和流程一起被系统自动销毁。如果您再次检查活动,则必须重新运行onCreate事件初始化活动。

有时您可能希望用户从活动的最后一个打开的操作状态继续,而不是从头开始。例如,当用户在编辑文本消息时接收到突然的来电时,用户可能必须在通话后立即做其他事情,例如将来电号码保存给联系人。如果用户没有立即返回到文本编辑界面,则文本编辑界面被破坏。结果,当用户返回到 SMS 程序时,该用户可能想要从最后的编辑继续。在这种情况下,您可以通过outState在活动状态或信息被破坏之前写入您想要保存的数据来覆盖活动的 void onSaveInstanceState (Bundle outState)事件,这样当活动再次执行onCreate事件时,它会传输之前通过savedInstanceState保存的信息。此时,您可以有选择地使用信息来初始化活动,而不是从头开始。

onStart 状态转换功能

onStart功能原型如下:

void onStart();

onStart功能在onCreate事件之后或当前活动切换到后台时执行。当用户从切换面板选择该活动切换回该活动时,如果该活动没有被销毁,并且只执行了onStop事件,则该活动将跳过onCreate事件活动,直接执行onStart事件。

论状态转移函数

onResume功能原型如下:

void onResume()

onResume功能在OnStart事件后或当前活动切换到后台后执行。当用户再次查看该活动时,如果该活动没有被销毁,并且没有执行onStop事件(活动继续存在于任务中),该活动将跳过onCreateonStart事件活动,直接执行onResume事件。

暂停状态转移函数

onPause功能原型如下:

void onPause()

当当前活动切换到后台时,执行onPause功能。

停止状态转移函数

onStop功能原型如下:

void onStop()

onStop功能在onPause事件之后执行。如果用户一段时间没有再次查看该活动,则执行该活动的onStop事件。如果用户按下 Back 键,也会执行onStop事件,并且该活动会从当前任务列表中删除。

重新启动状态转移函数

onRestart功能原型如下:

void onRestart()

执行onStop事件后,如果活动及其所在的流程没有被系统地破坏,或者如果用户再次查看该活动,则执行该活动的onRestart事件。onRestart事件跳过onCreate事件活动,直接执行onStart事件。

灾难状态转移函数

onDestroy功能原型如下:

void onDestroy()

在活动的一个onStop事件之后,如果用户没有再次查看该活动,则该活动被销毁。

结束功能

finish功能原型如下:

void finish()

finish函数关闭活动并将其从堆栈中移除,这导致对onDestroy()状态转换函数的调用。解决这个问题的一种方法是让用户使用 Back 按钮导航到上一个活动。

除了活动开关之外,finish函数触发活动的状态转换函数,context 类的startActivitystartActivityForResult方法(在接下来的小节中描述)也激活它。像Context.startActivity这样的函数也会导致activity对象的构造(也就是创建新的)。

表 8-1 中列出了触发和相应功能的典型原因。

表 8-1。

Triggers and Their Functions

| 典型触发原因 | 执行活动的相应方法 | 说明 | | --- | --- | --- | | `Context.startActivity[ForResult]()`注意:只要活动在屏幕上显示并可查看,这个方法就会被调用。 | `new Activity()` |   | | `onCreate()` | 完成构造函数,将`activity`对象保存到 application 对象,并初始化各种控件(如`View`)。 | | `onStart()` | 类似于`View.onDraw()`。 | | `Activity.finish()` | `onDestroy()` | 完成构造函数,比如从应用中移除`activity`对象。 |

表 8-1 中的Context.startActivity等函数触发三个动作:构造新的Activity对象、onCreateonStart。当一个活动从屏幕外移到屏幕显示的顶部(即显示在用户面前)时,它通常只包括被onStart调用的功能。

上下文类

Context类是一个需要了解的重要 Android 概念。该类继承自Object函数,其继承如下:

java.lang.Object

√??]

context 的字面意思是相邻区域的文本,位于框架包的android.content.Context中。Context类是一个LONG类型,类似于 Win32 中的Handle处理程序。Context提供关于应用环境的全局信息接口。它是一个抽象类,它的执行由 Android 系统提供。它允许访问资源和应用的特征类型。同时可以启动应用级的操作,比如启动活动和广播接收意向。

许多方法要求通过上下文实例来标识调用方。比如Toast的第一个参数是Context;而且通常你用this来代替 activity,表示调用者的实例是一个 activity。但是其他方法,比如按钮的onClick ( View视图),如果使用this会导致错误。在这种情况下,您可以使用ActivityName.this来解决问题,因为该类实现了几个主要的 Android 特定模型的上下文,如活动、服务和广播接收器。

如果参数——尤其是类的构造函数参数(比如Dialog)——是Context类型,那么实际的参数通常是活动对象,一般是[this]。例如,Dialog构造器原型是

Dialog.Dialog(Context context)

这里有一个例子:

public class MyActivity extends Activity{

Dialog d = new Dialog(this);

Context是 Android 大多数类的祖先,如 broadcasting、intents 等,它提供了全球信息应用环境的接口。表 8-2 列出了Context的重要子类。你可以在 Android Context类的帮助文档中找到详细的描述。

表 8-2。

Important Subclasses of

| 亚纲 | 说明 | | --- | --- | | `Activity` | 用户友好界面类 | | `Application` | 提供全局应用状态维护的基类 | | `IntentService` | 用于处理服务异步请求的基类(以`Intent`的方式表达) | | `Service` | 应用的一个组件,它代表与用户没有交互的耗时操作,或者代表为其他应用任务提供功能的任务 |

类被称为子类,因为它们是Context的直接或间接子类,并且具有类似 activities 的继承关系:

java.lang.Object

↳ android.content.Context

↳ android.content.ContextWrapper

↳ android.view.ContextThemeWrapper

↳ android.app.Activity

Context可以用于 Android 中的很多操作,但它的主要功能是加载和访问资源。有两种常用的上下文:应用上下文和活动上下文。活动上下文通常在各种类和方法之间传递,类似于活动的代码onCreate,如下所示:

protected void onCreate(Bundle state) {

super.onCreate(state);

TextView label = new TextView(this); // Pass context to view control

setContentView(label);

}

当活动上下文被传递给视图时,意味着视图有一个指向活动的引用,并引用活动所占用的资源:视图层次结构、资源等等。

还可以使用应用上下文,它总是伴随着应用的生命,但与活动生命周期无关。应用上下文可以通过Context.getApplicationContextActivity.getApplication方法获取。

Java 通常使用一个静态变量(singleton 等)来同步活动之间(程序内的类之间)的状态。Android 更可靠的方法是使用应用上下文来关联这些状态。

每个活动都有一个上下文,其中包含运行时状态。类似地,应用有一个上下文,Android 使用它来确保它是该上下文的唯一实例。

如果需要定制应用上下文,首先必须定义一个从android.app.Application继承的定制类;然后在应用的AndroidManifest.xml文件中描述这个类。Android 会自动创建这个类的一个实例。通过使用Context.getApplicationContext()方法,您可以获得每个活动内部的应用上下文。下面的示例代码获取活动中的应用上下文:

class MyApp extends Application {

// MyApp is a custom class inherited from android.app.Application

public String aCertainFunc () {

......

}

}

class Blah extends Activity {

public void onCreate(Bundle b){

... ...

MyApp appState = ((MyApp)getApplicationContext());

// Get Application Context

appState.aCertainFunc();

//Use properties and methods of the application

... ...

}

}

您可以使用Contextget函数获得关于应用环境的全局信息。主要函数如表 8-3 所示,可以是ContextWrapper或直接上下文方法。

表 8-3。

Commonly Used Methods for Obtaining Context

| 功能原型 | 功能 | | --- | --- | | `abstract Context ContextWrapper.getApplicationContext ()` | 返回对应于单个应用的全局上下文的当前进程。 | | `abstract ApplicationInfo ContextWrapper.getApplicationInfo ()` | 返回整个应用信息对应的上下文包。 | | `abstract ContentResolver ContextWrapper.getContentResolver ()` | 返回相应应用包的内容解析器实例。 | | `abstract PackageManager ContextWrapper.getPackageManager ()` | 返回用于查找所有包信息的包管理器实例。 | | `abstract String ContextWrapper.getPackageName ()` | 返回当前包名。 | | `abstract Resources ContextWrapper.getResources ()` | 返回(用户)应用包的资源实例。 | | `abstract SharedPreferences ContextWrapper.getSharedPreferences (String name, int mode)` | 查找并保存首选项文件的内容,该文件的名称由参数名称指定。返回您可以查找和修改的共享首选项(`SharedPreferences`)的值。当使用正确的名称时,只有一个`SharedPreferences`实例返回给调用者,这意味着一旦更改完成,结果就可以相互共享。 | | `public final String Context.getString (int resId)` | 从应用包的默认字符串表中返回本地化字符串。 | | `abstract Object ContextWrapper.getSystemService (String name)` | 根据变量名指定的名称返回正在处理的系统级服务。返回的对象类因请求的名称而异。 |

意向介绍

Intent 可以用作一种消息传递机制,允许您声明采取行动的意图,通常带有特定的数据。您可以使用 intent 来实现 Android 设备上任何应用的组件之间的交互。意图将一组独立的组件变成一对一交互的系统。

它也可以用来广播消息。任何应用都可以注册一个广播接收器来侦听和响应这些意向广播。Intent 可用于创建内部、系统或第三方事件驱动的应用。

Intent 负责描述操作和应用的动作数据。Android 负责找到子意图下描述的对应组件,将意图传递给被调用的组件,完成组件调用。意图在调用方和被调用方之间扮演解耦的角色。

意图是运行时绑定的一种机制;它可以在运行程序的过程中连接两个不同的组件。通过意图,程序可以向 Android 请求或表达意愿;Android 根据意图的内容选择适当的组件来处理请求。例如,假设某个活动想要打开 web 浏览器来查看某个页面的内容;这个活动只需要向 Android 发出一个WEB_SEARCH_ACTION请求。基于内容请求,Android 将检查组件注册语句中声明的意图过滤器,并为 web 浏览器找到一个活动。

当发出一个意向时,Android 会找到一个或多个活动、服务或broadcastReceiver的精确匹配作为响应。因此,不同类型的意向消息不会重叠,也不会同时发送到一个活动或服务,因为startActivity()消息只能发送到一个活动,而startService()意向只能发送到一个服务。

意图的主要作用

意向的主要作用如下。

触发新活动或让现有活动实现新操作

在 Android 中,意图直接与活动交互。intent 最常见的用途是绑定应用组件。Intent 用于启动、停止和转移应用活动。换句话说,意图可以激活一个新的活动,或者使一个现有的活动执行一个新的操作。这可以通过调用Context.startActivity()Context.startActivityForResult()方法来完成。

要在应用中打开一个不同的接口(对应于一个活动),您可以调用Context.startActivity()函数来传递一个意图。Intent 既可以明确指定要打开的特定类,也可以包含实现目标所需的操作。在后一种情况下,运行时将使用一个众所周知的意图解析过程来选择打开哪个活动,在这个过程中,Context.startActivity()会找到并启动一个与意图最匹配的活动。

触发新服务或向现有服务发送新请求

打开服务或向现有服务发送请求也是由 intent 类完成的。

触发广播接收器

您可以使用三种不同的方法发送BroadcastIntent:Context.sendBroadcast()Context.sendOrderedBroadcast()Context.sendStickyBroadcast()

意图解析

意向转移流程有两种方式将目标消费者(如另一个活动、IntentReceiver或服务)与意向的回应者匹配起来。

第一种是显式匹配,也称为直接意图。当构造一个intent对象时,您必须将接收者指定为意图的组件属性之一(通过调用setComponent (ComponentName)setClass (Context, Class))。通过指定组件类,应用通知启动相应的组件。这种方法类似于普通的函数调用,但在粒度重用方面有所不同。

第二种是隐性匹配,也叫间接故意。当构造一个intent对象时,意图的发送者不知道或不关心接收者是谁。组件意图中未指定该属性。这个意图需要包含足够的信息,以便系统可以从所有可用的组件中确定使用哪些组件来满足这个意图。这种方法与函数调用明显不同,有助于减少发送方和接收方之间的耦合。隐式匹配解析为单个活动。如果有多个活动可以基于特定数据实现给定的动作,Android 会选择最好的一个开始。

对于直接意图,Android 不需要做解析,因为目标组件非常清楚。但是,Android 需要解决间接意图。通过分析,它将间接意图映射到处理意图的活动、IntentReceiver或服务。

意图解析的机制主要包括以下内容:

  • 寻找所有的<intent-filter>和由这些过滤器定义的意图,这些过滤器注册在AndroidManifest.xml
  • 通过PackageManager找到并处理意图的组件(PackageManager可以获得当前设备上安装的应用包的信息)

意图过滤器非常重要。未声明的<intent-filter>组件只能响应组件名匹配的显式意图请求,但不能响应隐式意图请求。声明的<intent-filter>组件可以响应显式意图或隐式意图请求。当解析隐式意图请求时,Android 使用意图的三个属性——动作、类型和类别——来进行解析。下面介绍具体的解决方法。

动作测试

一个<intent-filter>元素应该包含至少一个<action>,否则没有任何意向请求可以匹配到<intent-filter>。如果一个意向请求的动作在<intent-filter>中至少有一个<action>匹配,那么该意向通过了这个<的动作测试。

如果意向请求或<intent-filter>中没有具体动作类型的描述,则采用以下两种测试之一:

  • 如果<intent-filter>不包含任何动作类型,那么不管意图请求是什么,都不匹配这个<intent-filter>
  • 如果意向请求没有设置动作类型,只要<intent-filter>包含一个动作类型,该意向请求将成功通过<intent-filter>的动作测试。

类别测试

对于通过类别测试的意向,意向中的每个类别都必须与过滤器中的类别相匹配。当每一类意向请求都与其中一个<intent-filter>组件的<category>完全匹配时,意向请求通过测试。<intent-filter><category>声明过多不会导致匹配失败。任何没有指定类别测试的<intent-filter>只匹配没有为其设置配置的意图请求。

dota 测试

<data>元素指定您想要接收的意向请求的数据 URI 和数据类型。一个 URI 被分成三个匹配的部分:方案、权威和路径。由setData()设置的互联网请求的 URI 数据类型和方案必须与在<intent-filter>中指定的相同。如果<intent-filter>也指定了权限或路径,它们必须匹配才能通过测试。

这个决策过程可以表示如下:

  • 如果意图指定了动作,那么目标组件的<intent-filter>的动作列表必须包含该动作。否则,不认为匹配。
  • 如果意向没有提供类型,则系统从数据中获取数据类型。对于一些动作方法,目标组件的数据类型列表必须包含意图的数据类型。否则无法匹配。
  • 如果意图的数据不是内容的 URI,并且类别和意图也没有指定其类型,则匹配基于意图的数据方案(例如,http:mailto:),并且意图的方案必须出现在目标组件的方案列表中。
  • 如果意图指定一个或多个类别,这些类别必须全部出现在组件的类别列表中。例如,如果意图包含两个类别,LAUNCHER_CATEGORYALTERNATIVE_CATEGORY,那么通过解析获得的目标组件必须至少包含这两个类别。

应用和活动之间的关系

初学者容易混淆应用和活动——特别是主要活动(应用启动时发生的活动)。事实上,它们是两个完全不同的对象。行为、属性等等并不相同。以下是应用和活动之间的差异列表:

  • 不管一个应用启动多少次,只要不关闭,它的值(也就是对象)就是不变的。它只有一个实例。
  • 不管一个应用从哪里启动,只要它没有关闭,它的值(也就是对象)就是常量。它只有一个实例。
  • 当一个活动没有完成时,它的值(即对象)是不变的。每次调用onStart()时,活动显示在屏幕前面。
  • startActivity每次启动的对象都不一样。你可以说startActivity其实包含了新的对象。
    • 虽然在startActivity之后无法获得新的 activity 对象,但是当startActivity启动其对应的 activity 对象时,Android 框架可以发送参数值(类似于函数调用的实际参数)。
    • 更令人惊讶的是,Android 可以有一个活动共存于多个对象中。当一个活动关闭时,Android 会将结果返回给通过startActivity启动的主活动。这样一来,它会自动调用启动其活动对象的onActivityResult()方法,可以避免随机分布。
  • 一个应用可以有多个活动对象。

基本的 Android 应用界面

在本节中,您将通过一个示例来了解使用集成在 Eclipse IDE 中的 Android SDK 进行 Android 开发。您使用 Android SDK 创建了一个名为 GuiExam 的应用,并按照流程的步骤学习了 Android 界面设计。

GuiExam 应用代码分析

本节提供了对 GuiExam 示例应用的分析。首先,让我们使用 Eclipse 中的 Android SDK 创建 GuiExam 应用。对于应用名称,键入GuiExam。对于构建 SDK,选择 API 19,它包括 x86 指令。如图 8-2 所示,选择所有其他条目的系统默认配置。

A978-1-4842-0100-8_8_Fig2_HTML.jpg

图 8-2。

Initial setup when generating the GuiExam project

项目的文件结构如图 8-3 所示,用户界面如图 8-4 所示。

A978-1-4842-0100-8_8_Fig4_HTML.jpg

图 8-4。

The application interface of GuiExam

A978-1-4842-0100-8_8_Fig3_HTML.jpg

图 8-3。

File structure of the GuiExam application

应用唯一的 Java 文件(MainActivity.java)的源代码如图 8-5 所示:

A978-1-4842-0100-8_8_Fig5_HTML.jpg

图 8-5。

The typical source codes in Java file MainActivity.java

您知道在创建事件时会调用MainActivity.OnCreate()函数。函数的源代码非常简单。第 12 行调用超类函数,第 13 行调用setContentView函数。此函数设置活动的 UI 显示。在 Android 项目中,大部分 UI 是由 view 和 view 子类实现的。View表示一个可以处理事件的区域,也可以渲染这个区域。

第 13 行的代码表示视图是R.layout.activity_main。项目的gen目录下自动生成的R.Java文件包含如下代码(节选):

Line #    Source Code

......

8 package com.example.guiexam;

9

10 public final class R {

......

26     public static final class layout {

27         public static final int activity_main=0x7f030000;

28     }

29     public static final class id {

30         public static final int menu_settings=0x7f080000;

31     }

32     public static final class string {

33         public static final int app_name=0x7f050000;

34         public static final int hello_world=0x7f050001;

35         public static final int menu_settings=0x7f050002;

36         public static final int title_activity_main=0x7f050003;

37    }

......

41    }

可以看到R.layout.activity_main是主布局文件activity_main.xml的资源 ID。该文件内容如下:

Line#    Source Code

1 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android

2     xmlns:tools="http://schemas.android.com/tools

3     android:layout_width="match_parent"

4     android:layout_height="match_parent" >

5

6     <TextView

7         android:layout_width="wrap_content"

8         android:layout_height="wrap_content"

9         android:layout_centerHorizontal="true"

10         android:layout_centerVertical="true"

11         android:padding="@dimen/padding_medium"

12         android:text="@string/hello_world"

13         tools:context=".MainActivity" />14

15 </RelativeLayout>

这段代码的第一行表明内容是一个RelativeLayout类。通过查看 Android 帮助文档,可以看到RelativeLayout的继承关系是

java.lang.Object

↳ android.view.View

↳ android.view.ViewGroup

↳ android.widget.RelativeLayout

这个类确实被看作是一个视图类。这个布局包含一个TextView类,它也是视图的子类。第 12 行表示其text属性为@string/hello_world,显示文本为strings.xml中变量hello_world的内容:“Hello world!”

作为布局的超类,ViewGroup是一个特殊视图,可以包含其他视图对象,甚至是ViewGroup本身。换句话说,ViewGroup对象将其他视图或ViewGroup的对象视为成员变量(在 Java 中称为 properties)。ViewGroup对象中包含的内部视图对象称为小部件。由于ViewGroup的特殊性,Android 可以自动设置各种复杂的应用界面。

使用布局作为界面

作为应用界面设计的一部分,您可以修改或设计布局。例如,您可以如下修改activity_main.xml文件:

A978-1-4842-0100-8_8_Fig8_HTML.jpg

图 8-8。

The user interface of GuiExam after the layout has been modified

A978-1-4842-0100-8_8_Fig7_HTML.jpg

图 8-7。

Modifying the GuiExam layout to add a text-edit widget

  1. 将一个纯文本小部件从左栏的文本字段部分拖放到activity_main屏幕。将布局参数分支下的Width属性更改为fill_parent,然后拖动纯文本直到其填满整个布局,如图 8-7 所示。

A978-1-4842-0100-8_8_Fig6_HTML.jpg

图 8-6。

Modifying the GuiExam layout to add a button

  1. TextViewText属性改为“在此键入”。
  2. 从表单小部件栏中选择一个按钮小部件,并将其放入activity_main屏幕。将其Text属性设置为“点击我”,如图 8-6 所示。

从这些例子中,你可以看到接口的一般结构。通过setContentView(布局文件资源 ID)设置的活动是:活动包含一个布局,布局包含各种小部件,如图 8-9 所示。

A978-1-4842-0100-8_8_Fig9_HTML.jpg

图 8-9。

Interface structure of the activity

你可能想知道为什么 Android 会引入这种布局概念。事实上,这是 Android 的一个开发者青睐的特性,相比于 Windows 微软基础类(MFC)的编程接口。该布局隔离了设备上的屏幕大小、方向和其他细节的差异,这使得界面屏幕适应各种设备。因此,在不同设备平台上运行的应用可以自动调整小部件的大小和位置,而无需用户干预或修改代码。

例如,您创建的应用可以在不同的 Android 手机、平板电脑和电视设备平台上运行,而无需更改任何代码。小工具的位置和大小会自动调整。即使你将手机旋转 90 度,纵向或横向模式的界面也会自动调整大小,并保持在相对位置。布局也允许根据当地民族习惯排列 widgets(大多数国家从左到右排列,但也有一些国家从右到左排列)。界面设计需要考虑的细节都是由布局来完成的。你可以想象如果没有布局类会发生什么——你必须为每个设备的每个 Android 界面布局编写代码。这一级别的工作的复杂性是不可想象的。

将视图直接用作界面

前面您已经看到了活动的接口结构和代码框架。您还看到了大部分 UI 是由 view 和 view 子类实现的。因此,您可以使用setContentView函数来指定一个视图对象,而不是一个布局。activity 类的setContentView函数的原型包括以下内容。

此函数将布局资源设置为活动的接口:

void setContentView(int layoutResID)

第一种类型的函数将显式视图设置为活动的接口:

void setContentView(View view)

第 2 个和第 1 个类型 f 函数按照指定的格式设置一个显式视图作为活动的接口:

setContentView(View view, ViewGroup.LayoutParams params)

在这里,您将完成一个应用示例,该示例将视图直接用作活动接口,使用第二个函数 setContentView()您可以修改MainActivity.java文件的代码,如下所示:

......

import android.widget.TextView;

public class MainActivity extends Activity {

@Override

public void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

TextView tv = new TextView(this);  // Create a TextView Object that belongs to current Activity

tv.setText("Hello My friends!");   // Set Display text of TextView

setContentView(tv);                // Set View as the main display of the Activity

}

应用界面如图 8-10 所示。

A978-1-4842-0100-8_8_Fig10_HTML.jpg

图 8-10。

GuiExam sets the view directly as the interface

在这种情况下,您有作为应用接口的TextView小部件,它是视图的直接子类;它们直接在setContentView功能中设置。这样,TextView显示的文本就成为了应用界面的输出。要使用TextView类,您可以在文件的开头使用一个import android.widget.TextView语句来导入该类的包。

组件 ID

现在让我们回过头来看看图 8-6 所示的应用布局。布局中添加的文本编辑小工具的 ID 属性为@ + id/editText1,按钮的 ID 属性为@ + id/button1(如图 8-5 )。这是什么意思?

再来看R.java文件(节选):

Line #    Source Code

......

8 package com.example.guiexam;

9

10 public final class R {

......

22     public static final class id {

23 public static final int button1=0x7f080001;

24 public static final int editText1=0x7f080002;

25         public static final int menu_settings=0x7f080003;

26         public static final int textView1=0x7f080000;

27     }

28     public static final class layout {

29         public static final int activity_main=0x7f030000;

30     }

......

43 }

对比“GuiExam 应用”部分的R.java文件,可以看到第 23、24 行是新的;它们是新添加的按钮和文本编辑框的资源 ID 号。类型为int,对应这些小部件的 ID 属性值。从R.java文件中可以找到这些小部件的 ID——静态常量R.id.button1是 ID 属性值为@ + id/button1的小部件(按钮)的资源 ID,静态常量R.id.editText1是 ID 属性值为@ + id/editText1的小部件(文本编辑)的资源 ID。这是什么原因呢?让我想想。

Android 组件(包括小部件和活动)需要使用一个类型为int的值作为标签,这个值就是组件标签的 id 属性值。ID 属性只能接受resources类型的值。即值必须以@开头,;比如@ id/abc@+id/xyz等等。

@符号用于提示 XML 文件的解析器解析@后面的名称。例如,对于@string/button1,解析器从values/string.xml中读取这个变量的button1值。

如果在@后面使用了+符号,则表示当你修改并保存一个布局文件时,系统会自动在R.java中生成相应类型的int变量。变量名是/符号后的值;例如,@+id/xyzR.java中生成int xyz = value,其中值为十六进制数。如果R.java中已经存在相同的变量名xyz,系统不会生成新的变量;相反,组件使用这个现有的变量。

换句话说,如果您使用@+id/name格式,并且在R.java中存在一个名为name的变量,那么组件将使用该变量的值作为标识符。如果变量不存在,系统会添加一个新变量,并为该变量分配相应的值(不重复)。

因为组件的 ID 属性可以是资源 ID,所以可以设置任何现有的资源 ID 值:例如,@drawable/icon@string/ok@+string/。当然也可以设置一个 Android 系统中已经存在的资源 id,比如@id/android:list,其中 ID 中的android:修饰符表示系统的 R 类所在的包(在R.java文件中)。您可以在 Java 代码编辑区输入android.R.id,它会列出相应的资源 ID。例如,可以这样设置 ID 属性值。

出于刚才描述的原因,您通常将 Android 组件(包括小部件、活动等)的 ID 属性设置为@+id/XXX格式。并且您使用R.id.XXX来表示程序中组件的资源 ID 号。

按钮和事件

在“使用布局作为界面”一节中的例子中,您创建了一个包括ButtonEditText和其他小部件的应用,但是当单击按钮时什么也没有发生。这是因为您没有分配对click事件的响应。本节首先介绍 Android 事件和监听器函数的基础知识。在以后涉及 Android 多线程设计的章节中,您将回顾并进一步探索更多关于事件的高级知识。

在 Android 中,每个应用都维护一个事件循环。当应用启动时,它完成适当的初始化,然后进入事件循环状态,在此状态下,它等待用户操作,如单击触摸屏、按键(按钮)或一些其他输入操作。用户动作触发程序生成对事件的响应;系统根据事件位置生成并分发相应的事件类进行处理,比如Activity或者View。回调方法被集成到一个称为事件监听器的接口中。您可以通过覆盖接口的抽象函数来实现指定的事件响应。

不同类接收的事件的范围对于每个类都是不同的。例如,Activity类可以接收keypress事件,但不能接收touch事件,而View类可以接收touchkeypress事件。此外,不同类接收到的事件属性细节也各不相同。例如,View类接收的touch事件由许多触摸点、坐标值和其他信息组成。它被细分为按下、反弹和移动事件。但是Button类是View类的后代,它只检测按压动作,事件不提供触摸点的坐标或其他信息。换句话说,Button处理视图的原始事件,将所有触摸事件整合成一个记录是否被点击的事件。

View类的大多数事件响应接口使用Listener作为后缀,因此很容易记住它们与事件监听器接口的关联。表 8-4 显示了多个类别及其事故响应功能的示例。

表 8-4。

Examples of Classes and Their Incident-Response Functions

| 班级 | 事件 | 监听器接口和功能 | | --- | --- | --- | | `Button` | 点击 | `onClick()``onClickListener`界面的功能 | | `RadioGroup` | 点击 | `onCheckChange()``onCheckChangeListener`界面的功能 | | `View` | 下拉列表 | `onTouch()``TouchListener`界面的功能 | | 输入焦点改变 | `onFocusChange()``onFocusChangeListener`界面的功能 | | 纽扣 | `onKey()``onKeyListener`界面的功能 |

响应事件的过程如下。首先,定义监听器接口的实现类,并覆盖抽象函数。第二,调用set ... Listener()等函数。然后将自定义监视器接口的实现类设置为相应对象的事件侦听器。

例如,您可以修改应用源来执行事件响应。实现 Java 接口有许多编码风格。下一节讨论运行这些样式的代码的结果是相同的几种方式。

内部类监听器

修改MainActivity.java代码如下(增加或修改粗体文本):

Line #    Source Code

1 package com.example.guiexam;

2 import android.os.Bundle;

3 import android.app.Activity;

4 import android.view.Menu;

5 import android.view.MenuItem;

6 import android.support.v4.app.NavUtils;

7 import android.widget.TextView;

8 import android.widget.Button;                // Use Button class

9

10 import android.view.View;                   // Use View class

11 import android.view.View.OnClickListener;   // Use View.OnClickListener class

12 import android.util.Log;

13 // Use Log.d debugging function

public class MainActivity extends Activity {

14 private int iClkTime = 1;

15

16 // Count of Button Click

17

@Override

18     public void onCreate(Bundle savedInstanceState) {

19         super.onCreate(savedInstanceState);

20         setContentView(R.layout.activity_main);

21

22 Button btn = (Button) findViewById(R.id.button1);

23 // Obtain Button object based on the resource ID number

24 final String prefixPrompt ="This is No. ";

25 // Define and set the value of the variable passed

26 final String suffixPrompt ="time(s) that Button is clicked";

27 // Define and set the value of the variable passed

28 btn.setOnClickListener(new /*View.*/OnClickListener(){

29 // Set the event response class of Button's click

30

31 public void onClick(View v) {

32 Log.d("ProgTraceInfo",prefixPrompt + (iClkTime++) + suffixPrompt);

}

});

}

@Override

public boolean onCreateOptionsMenu(Menu menu) {

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

return true;

}

}

在第 1822 行,您分别根据资源 IDEditTextTextView获得相应的对象。要使用OnClickListener作为内部类,需要在变量前面添加final修饰符。在第 23 行和第 24 行,当Button的响应代码点击时,您首先使用EditText.getText()获得EditText的内容。因为该函数返回类型为Editable的值,所以您通过CharSequence.toString()函数将类型Editable转换为类型String(CharSequenceEditable的超类)。然后调用TextView.setText (CharSequence text)函数刷新TextView显示。

在 Android 中,类属性的访问函数通常以set / get开头,比如EditText内容的读/写函数:

Editable  getText()

void  setText(CharSequence text, TextView.BufferType type)

该应用的界面如图 8-11 所示;(a)是开始屏幕,(b)是在编辑文本框中输入文本后的屏幕,以及(c)显示单击按钮后的应用屏幕。

A978-1-4842-0100-8_8_Fig11_HTML.jpg

图 8-11。

The interface of the application with a TextView , a Button , and an EditText

使用 ImageView

前几节讨论了小部件的典型用途,并展示了小部件编程的基本概念。图像是多媒体应用的基础,因此也是 Android 应用的主要部分。本节介绍图像/图片显示小工具ImageView的使用。通过本节中的例子,您将学习如何使用ImageView并将文件添加到项目的资源中。

以下示例最初是在创建 GuiExam 应用时在小节中开发的。按照以下步骤将图片文件添加到项目中:

A978-1-4842-0100-8_8_Fig13_HTML.jpg

图 8-13。

The Package Explorer window after the image is added

  1. 在 Eclipse 中打开项目,并按 F5 键刷新项目。你可以在 Package Explorer 中看到添加到项目中的文件(本例中为morphing.png),如图 8-13 所示。

A978-1-4842-0100-8_8_Fig12_HTML.jpg

图 8-12。

Copy the image file into the project’s res directory

  1. 将图像文件(本例中为morphing.png)复制到对应的/res/drawable-XXX项目目录(存放不同分辨率图像的项目文件的目录),如图 8-12 所示。

要在布局中放置ImageView小部件,请遵循以下步骤:

  1. 保存布局文件。

A978-1-4842-0100-8_8_Fig15_HTML.jpg

图 8-15。

The property settings of the ImageView

  1. 调整ImageView的大小和位置,并设置其属性。这一步可以使用图 8-15 所示的默认值。

A978-1-4842-0100-8_8_Fig14_HTML.jpg

图 8-14。

Place the ImageView widget in the layout

  1. 点击选择“Hello world!”的TextView小工具项目,然后按 Del 键将小部件从布局中移除。
  2. layout.xml的编辑器窗口中,找到图像&媒体分支,将该分支的ImageView拖放到布局文件中。弹出资源选择器对话框,点击选择项目资源,选择项目下刚刚导入的图片文件,点击确定完成操作。该过程如图 8-14 所示。

通常,此时,您必须编译 Java 代码。然而,在这个例子中,编译是不必要的。图 8-16 显示了应用的界面。

A978-1-4842-0100-8_8_Fig16_HTML.jpg

图 8-16。

Application interface of the ImageView

退出活动和应用

在前面的示例中,您可以按下手机的后退按钮来隐藏活动,但这样做不会关闭活动。正如您在“活动的状态转换”一节中看到的,当按下 Back 按钮时,已启动的活动仅从活动状态变为非活动状态,并保留在系统堆栈中。要关闭这些活动并将其从堆栈中移除,您应该使用Activity类的finish函数。

但是,结束活动并不意味着申请流程结束。即使应用的所有组件(活动、服务、广播意图接收器等)都已关闭,应用进程仍会继续存在。退出申请流程主要有两种方式。

一个是 Java 提供的强制结束进程的静态函数System.exit;另一个是 Android 提供的静态函数Process.killProcess (pid)终止指定的进程 ID (PID)。您可以通过Process.myPid()静态函数来获取应用的进程 ID。

您可以将这些方法用于“使用 ImageView”一节中的示例具体步骤如下:

  1. MainActivity.java文件的源代码修改如下(粗体代码表示添加或修改,带删除线的行表示删除的代码):

A978-1-4842-0100-8_8_Fig17_HTML.jpg

图 8-17。

Add Close Activity and Exit Application buttons in the layout

  1. 在布局文件中添加两个按钮,属性分别为Text的“关闭活动”和“退出应用”,ID 属性分别为@+id/closeActivity@+id/exitApplication。调整按钮的大小和位置,如图 8-17 所示。

Line #    Source Code

1 package com.example.guiexam;

2 import android.os.Bundle;

3 import android.app.Activity;

4 import android.view.Menu;

5 //import android.view.MenuItem;

6 //import android.support.v4.app.NavUtils;

7 import android.widget.Button;               // Use Button class

8 import android.view.View;                   // Use View class

9 import android.view.View.OnClickListener;   // Use View.OnClickListenerClass

10 import android.os.Process;                 // Use killProcess method

11 public class MainActivity extends Activity {

12     @Override

13     public void onCreate(Bundle savedInstanceState) {

14         super.onCreate(savedInstanceState);

15         setContentView(R.layout.activity_main);

16 Button btn = (Button) findViewById(R.id.closeActivity);

17 // Get Button object of <Closed activity>

18 btn.setOnClickListener(new /*View.*/OnClickListener(){

19 // Set response code for Clicking

20 public void onClick(View v) {

21 finish();                  // Close main activity

22 }

23 });

24 btn = (Button) findViewById(R.id.exitApplication);

25 // Get Button object of <Exit Application>

26 // Set the response code to Clicking

27 public void onClick(View v) {

28 finish();                  // close main activity

29 Process.killProcess(Process.myPid()); // Exit application process

30 }

31

32

33

34

35         });

}

@Override

public boolean onCreateOptionsMenu(Menu menu) {

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

return true;

}

}

在第 5 行和第 6 行,您删除了未使用的import语句。您在第 1621 行设置了“关闭活动”按钮的响应代码,在第 2228 行设置了“退出应用”按钮的响应代码。唯一的区别是后者增加了应用退出代码Process.killProcess (Process.myPid ())。这两个按钮使用Activity类的同一个finish()函数来关闭活动。第 710 行的代码导入相关的类。

应用界面如图 8-18 所示。

A978-1-4842-0100-8_8_Fig18_HTML.jpg

图 8-18。

The Close Activity and Exit Application interface of the application

当您单击“关闭活动”或“退出应用”按钮时,应用的主界面将关闭。不同的是,应用进程(com.example.guiexam)不会因为关闭活动而退出;但是对于退出应用,该过程关闭。这清楚地显示在 Eclipse 中 DDMS 视图的设备窗格中,您可以在其中看到目标机器上的进程列表,如图 8-19 所示。

A978-1-4842-0100-8_8_Fig19_HTML.jpg

图 8-19。

The process in DDMS when the Close Activity and Exit Application application is running

摘要

本章通过让您创建一个名为 GuiExam 的简单应用来介绍 Android 界面设计。您了解了活动的状态转换、Context类、意图以及应用和活动之间的关系。您还看到了如何通过更改布局文件activity_main.xml将布局用作接口,以及按钮、事件和内部事件监听器是如何工作的。下一章描述了如何使用 activity-intent 机制创建一个包含多个活动的应用,并展示了在AndroidManifest.xml文件中需要做的修改。

Creative Commons Open Access This chapter is licensed under the terms of the Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License ( http://​creativecommons.​org/​licenses/​by-nc-nd/​4.​0/​ ), which permits any noncommercial use, sharing, distribution and reproduction in any medium or format, as long as you give appropriate credit to the original author(s) and the source, provide a link to the Creative Commons licence and indicate if you modified the licensed material. You do not have permission under this licence to share adapted material derived from this chapter or parts of it. The images or other third party material in this chapter are included in the chapter’s Creative Commons licence, unless indicated otherwise in a credit line to the material. If material is not included in the chapter’s Creative Commons licence and your intended use is not permitted by statutory regulation or exceeds the permitted use, you will need to obtain permission directly from the copyright holder.

九、Android 应用的 GUI 设计第三部分:设计复杂的应用

Keywords Intent Object Trigger Activity Source Code File Intent Intent Intent Class

在前一章中,您通过创建一个名为GuiExam的简单应用学习了 Android 界面设计。这一章还涵盖了活动的状态转换、Context类,以及对意图和应用与活动之间关系的介绍。您了解了如何将布局用作接口,以及按钮、事件和内部事件侦听器如何工作。在本章中,您将学习如何创建一个包含多个活动的应用;示例介绍了活动的显性和隐性触发机制。您将看到一个应用示例,其中的参数由另一个应用中的活动触发,这将有助于您理解活动参数的交换机制。

具有多个活动的应用

上例中的应用只有一个活动:主活动,它在应用启动时显示。本章演示了一个具有多个活动的应用,使用活动意图机制,并展示了在AndroidManifest.xml文件中需要的更改。

如前所述,活动由意图触发。有两种意图解析方法:显式匹配(也称为直接意图)和(也称为间接意图)。触发活动也可以有参数和返回值。此外,Android 带有许多内置活动,因此触发的活动可以来自 Android 本身,也可以定制。基于这些情况,本章用四个例子来说明不同的活动。对于显式匹配,您可以看到有或没有参数和返回值的应用。对于隐式匹配,您会看到一个应用,它使用来自 Android 系统或用户定义的活动。

触发不带参数的活动的显式匹配

使用不带参数的显式匹配是活动意图最简单的触发机制。本节首先使用一个示例来介绍这种机制,稍后将介绍更复杂的机制。

显式匹配的活动意图触发机制的代码框架包括两部分:被调用方的活动(被触发)和调用方的活动(触发)。触发器不限于活动;它也可以是一种服务,例如广播意图接收器。但是因为到目前为止您只看到了活动的使用,所以本节中所有示例的触发器都是活动。

  1. 被调用方活动的源代码框架执行以下操作:
    1. 定义从活动继承的类。
    2. 如果有需要传递的参数,那么活动的源代码框架调用onCreate函数中的Activity.getIntent()函数,获取触发该活动的Intent对象,然后通过Intent.getData ()Intent.getXXXExtra ()Intent.getExtras ()等函数获取正在传递的参数。
    3. 为正常活动模式编写代码。
    4. 如果触发器返回值,则在退出活动之前执行以下操作:
      1. 定义一个Intent对象
      2. 使用Intent.putExtras()等功能设置意图的数据值
      3. 通过调用Activity.setResult()函数设置活动的返回代码
    5. AndroidManifest.xml文件中添加被调用者活动的代码。
    6. 被调用方活动的代码框架执行以下操作:
      1. 定义Intent对象,并指定触发器的上下文和被触发活动的class属性。
      2. 如果需要将参数传递给活动,则通过调用类似于setData()putExtras()等意图的函数来设置Intent对象的参数。
      3. 调用Activity.startActivity(Intent intent)函数触发不带参数的活动,或者调用Activity.startActivityForResult(Intent intent, int requestCode)触发带参数的活动。
      4. 如果活动需要由返回值触发,那么代码框架重写Activity类的onActivityResult()函数,该函数根据请求代码(requestCode)、结果代码(resultCode)和意图(Intent)值采取不同的操作。

在步骤 2a 中,使用了触发活动的 class 属性,这涉及到一种称为反射的 Java 机制。这种机制可以根据类名创建并返回该类的对象。被触发活动的对象在触发之前没有被构造;因此,触发活动也意味着创建该类的对象,以便后续操作可以继续。也就是说,触发活动包括新创建的类对象的操作。

下面两个例子详细说明了代码框架。本节描述第一个。在这个示例中,被触发的活动与触发器的活动属于同一个应用,并且被触发的活动不需要任何参数,也不返回任何值。新的活动是通过一个按钮触发的,它的活动界面类似于“退出活动和应用”一节中的示例界面在第八章中,图 8-16。整个应用界面如图 9-1 所示。

A978-1-4842-0100-8_9_Fig1_HTML.jpg

图 9-1。

The application interface with multiple activities in the same application without parameters

应用启动后,显示应用的主活动,如图 9-1(a) 所示。当点击“更改为无参数新界面”按钮时,app 显示新的活动,如图 9-1(b) 所示。点击关闭活动按钮,界面返回到应用的主活动,如图 9-1(c) 所示。

通过修改并重写第八章中部分的示例,创建此示例,如下所示:

  1. 为触发的活动生成相应的布局文件:
    1. 在应用的res\layout子目录中右键单击快捷菜单,选择新建➤其他项目。弹出一个新的对话框。选择\XML\XML File子目录,点击下一步继续。在“新建 XML 文件”对话框中,输入文件名(在本例中为noparam_otheract.xml),然后单击“完成”。整个过程如图 9-2 所示。

Note

文件名是布局文件的名称。为了编译成功,必须只使用小写字母;否则,您将得到错误“无效文件名:必须只包含 a-z0-9_ ..”

A978-1-4842-0100-8_9_Fig2_HTML.jpg

图 9-2。

The layout file for the triggered activity

在项目的包浏览器中可以看到新添加的xxx.xml文件(本例中为noparam_otheract.xml),如图 9-3 所示。

Note

右边的布局编辑器窗口还是空的,至今没有可见的界面。

  1. 在左侧面板中选择Layouts子目录,并将布局控件(在本例中为RelativeLayout)拖动到右侧窗格中的窗口上。你会立即看到一个可视的(手机屏幕形状的)界面,如图 9-4 所示。

A978-1-4842-0100-8_9_Fig3_HTML.jpg

图 9-3。

Initial interface of the application’s newly added layout file

  1. 基于第八章中的“使用 ImageView”一节所述的相同方法,在新布局文件中放置一个ImageView和一个按钮。将ImageView小部件的ID属性设置为@+id/picture,将Button小部件的ID属性设置为@+id/closeActivityText属性为“关闭活动”,如图 9-5 所示。最后,保存布局文件。

A978-1-4842-0100-8_9_Fig4_HTML.jpg

图 9-4。

Drag-and-drop layout for the newly added layout file

  1. 为布局文件(Java 源文件)添加相应的Activity类。为此,右键单击项目目录下的\src\com.example.XXX,并在快捷菜单上选择新建➤类。在“新建 Java 类”对话框中,为“名称”输入对应于新布局文件的Activity类名(在本例中为TheNoParameterOtherActivity)。单击“完成”关闭对话框。整个过程如图 9-6 所示。

A978-1-4842-0100-8_9_Fig5_HTML.jpg

图 9-5。

Final configuration of the newly added layout file

A978-1-4842-0100-8_9_Fig6_HTML.jpg

图 9-6。

Corresponding class for the newly added layout file

可以看到新添加的 Java 文件(这里是TheNoParameterOtherActivity.java)和初始代码,如图 9-7 。

  1. 编辑新添加的.java文件(TheNoParameterOtherActivity.java)。该类执行触发的活动(被调用方)的活动。其源代码如下(加粗文字或修改):

A978-1-4842-0100-8_9_Fig7_HTML.jpg

图 9-7。

Corresponding class and initial source code of the newly added layout

Line #        Source Code

1  package com.example.guiexam;

2 import android.os.Bundle;                   // Use Bundle class

3 import android.app.Activity;                // Use Activity Class

4 import android.widget.Button;               // Use Button class

5 import android.view.View;                   // Use View class

6 import android.view.View.OnClickListener;   // Use OnClickListener Class

7  public class TheNoParameterOtherActivity extends Activity {

8  // Define Activity subclass

9 @Override

10 protected void onCreate(Bundle savedInstanceState) {

11 // Define onCreate method

12 super.onCreate(savedInstanceState);

13 // onCreate method of calling parent class

14 setContentView(R.layout.noparam_otheract);

15 // Set layout file

16 Button btn = (Button) findViewById(R.id.closeActivity);

17 // Set responding code for <Close Activity> Button

18 btn.setOnClickListener(new /*View.*/OnClickListener(){

19 public void onClick(View v) {

finish();

// Close this activity

}

});

}

}

在第 7 行,您为新创建的类添加了超类Activity。第 8 行到第 18 行的代码类似于应用的主活动。注意,在第 14 行,代码调用了setContentView()函数来为Activity设置布局,其中的参数是在第一步中创建的新布局 XML 文件的前缀名。

  1. 编辑触发器(调用方)活动的代码。触发活动是应用的主要活动。源代码为MainActivity.java,布局文件为activity_main.xml。编辑的步骤如下:

    1. 编辑布局文件,删除原来的TextView小部件,添加一个按钮。将其ID属性设置为@+id/goTONoParamNewAct,将其Text属性设置为“无参数切换界面”,如图 9-8 所示。
  2. 编辑触发活动的源代码文件(在本例中为MainActivity.java),如下所示(添加或修改粗体文本):

A978-1-4842-0100-8_9_Fig8_HTML.jpg

图 9-8。

Layout configuration for the trigger activity

Line #        Source Code

1  package com.example.guiexam;

2  import android.os.Bundle;

3  import android.app.Activity;

4  import android.view.Menu;

5 import android.content.Intent;           // Use Intent class

6 import android.widget.Button;            // Use Button class

7 import android.view.View.OnClickListener;

8 import android.view.View;

9  public class MainActivity extends Activity {

10    @Override

11    public void onCreate(Bundle savedInstanceState) {

12      super.onCreate(savedInstanceState);

13      setContentView(R.layout.activity_main);

14 Button btn = (Button) findViewById(R.id.goTONoParamNewAct);

15 btn.setOnClickListener(new /*View.*/OnClickListener(){

16 public void onClick(View v) {

17 Intent intent = new Intent(MainActivity.this, TheNoParameterOtherActivity.class);

18 startActivity(intent);

19 }

20 });

21    }

22     @Override

23     public boolean onCreateOptionsMenu(Menu menu) {

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

25       return true;

26     }

27 }

第 17 行的代码定义了一个意图。这种情况下的构造函数原型是

Intent(Context packageContext, Class<?> cls)

第一个参数是触发活动,在本例中是主活动;this因为用在内部类中,所以前面有类名修饰符。第二个参数是被调用方(被触发)活动的类。它使用.class属性来构造它的对象(所有 Java 类都有.class属性)。

第 18 行调用startActivity,运行 intent。该函数不向触发的活动传递任何参数。函数原型是

void Activity.startActivity(Intent intent)

  1. 编辑AndroidManifest.xml文件。添加被调用方活动的描述性信息(添加了粗体文本)以注册新的Activity类:

Line #        Source Code

1 <manifest xmlns:android="http://schemas.android.com/apk/res/android

2     package="com.example.guiexam"

3     android:versionCode="1"

4     android:versionName="1.0" >

...     ... ...

10     <application

11         android:icon="@drawable/ic_launcher"

12         android:label="@string/app_name"

13         android:theme="@style/AppTheme" >

14         <activity

15             android:name=".MainActivity"

16             android:label="@string/title_activity_main" >

17             <intent-filter>

18                 <action android:name="android.intent.action.MAIN" /> 19

20                 <category android:name="android.intent.category.LAUNCHER" /> 21             </intent-filter>

22         </activity>

23``<activity android:name=".TheNoParameterOtherActivity" android:label="the other Activity"/>

25

26 </manifest>

您也可以用以下方法替换此 XML 代码:

  • 方法 1:

<activity android:name="TheNoParameterOtherActivity" android:label=" the other Activity"> </activity>

  • 方法 2:

<activity android:name=".TheNoParameterOtherActivity " />

  • 方法 3:

<activity android:name=".TheNoParameterOtherActivity"></activity>

android: name文本字段的内容是被调用者活动的类名:TheNoParameterOtherActivity

注意,如果在Activityandroid: name的名称前添加一个句点(.),编译器会在 XML 文件的这一行给出如下警告(只是警告,不是编译错误):

Exported activity does not require permission

触发活动与不同应用参数的显式匹配

前面几节介绍了在同一个应用中触发另一个没有参数的活动。触发器的活动是被调用者允许交换参数:触发器可以为被调用者指定某些参数,被调用者可以在退出时将这些参数值返回给触发器。此外,被调用者和触发器可以在完全不同的应用中。本节显示了一个应用示例,该应用的参数由另一个应用中的活动触发。这个例子将帮助您理解活动参数的交换机制。

使用与第八章中相同的GuiExam应用。界面如图 9-9 所示。

A978-1-4842-0100-8_9_Fig9_HTML.jpg

图 9-9。

The interface of multiple activities in different applications

如图 9-9 所示,触发活动在GuiExam应用中,其中有一个变量接受天气条件条目。图 9-9(a) 中的界面在GuiExam应用打开时显示。点击进入新界面,修改天气盒,触发HelloAndroid中的活动。当该活动开始时,显示在设置新天气文本框中传递的新天气情况,如图 9-9(b) 所示。现在,在 Set New Weather 中输入一个新的天气条件值,然后单击 OK Change 关闭触发器的活动。Set New Weather 返回的新值刷新触发器活动中的Weather变量,如图 9-9(d) 所示。如果你点击取消更改,它做同样的事情并关闭活动,但是值Weather不变,如图 9-9(f) 所示。

正在执行的应用的进程列表如图 9-10 所示(显示在 Eclipse 中主机的 DDMS 窗口中)。

A978-1-4842-0100-8_9_Fig10_HTML.jpg

图 9-10。

Process list in DDMS for the multiple-activity application

图 9-10 显示当应用启动时,只有触发器GuiExam的进程在运行。但是当你点击进入新界面修改天气时,新活动被触发,新活动HelloAndroid的流程运行,如图 9-10(b) 所示。当您点击确认变更或取消变更时,被触发的活动关闭,但是HelloAndroid过程不会退出,如图 9-10(c) 所示。有趣的是,即使GuiExam触发流程存在,被触发的活动所属的HelloAndroid流程仍然处于运行状态。

构建步骤如下:

  1. 修改触发应用的GuiExam代码:

    1. 通过删除原来的TextView小部件来编辑主布局文件(本例中为activity_main.xml);然后添加三个新的TextView部件和一个按钮。如下设置它们的属性:将两个TextViewText属性设置为“这个接口是 GuiExam 应用中调用者的活动”和“今天的天气:”。将第三个TextViewID属性设置为@+id/weatherInfo。按钮的Text属性为“进入新界面改变天气”,其ID属性为@+id/modifyWeather。如图 9-11 所示调整各部件的大小和位置。
  2. 修改MainActivity.java的内容,如下图所示:

A978-1-4842-0100-8_9_Fig11_HTML.jpg

图 9-11。

The main layout design for the GuiExam trigger application

Line #        Source Code

1  package com.example.guiexam;

2  import android.os.Bundle;

3  import android.app.Activity;

4  import android.view.Menu;

5 import android.widget.Button;            // Use Button class

6 import android.view.View;                // Use View class

7 import android.view.View.OnClickListener; // Use View.OnClickListener class

8 import android.widget.TextView;          // Use TextView class

9 import android.content.Intent;           // Use Intentclass

10 public class MainActivity extends Activity {

11 public static final String INITWEATHER = "阳光明媚; // /Initial Weather

12 public static final int MYREQUESTCODE =100;

13 // Request Code of triggered Activity

14 private TextView tv_weather;

15 // The TextView Widget that displays Weather info

16     @Override

17     public void onCreate(Bundle savedInstanceState) {

18         super.onCreate(savedInstanceState);

19         setContentView(R.layout.activity_main);

20 tv_weather = (TextView)findViewById(R.id.weatherInfo);

21 tv_weather.setText(INITWEATHER);

22 Button btn = (Button) findViewById(R.id.modifyWeather);

23 // Get Button object according to resource ID #

24 btn.setOnClickListener(new /*View.*/OnClickListener(){

25 // Set responding code click event

26 public void onClick(View v) {

27 Intent intent = new Intent();

28 intent.setClassName("com.example.helloandroid",

29 // the package ( application) that the triggered Activity is located

30 "com.example.helloandroid.TheWithParameterOtherActivity");

31 // triggered class ( full name)

String wthr = tv_weather.getText().toString();

32 // Acquire the value of weather TextView

33 intent.putExtra("weather",wthr); // Set parameter being passed to Activity

34                 startActivityForResult(intent, MYREQUESTCODE);

35 // Trigger Activity

36 }

37 });

38     }

39

40 @Override

41 protected void onActivityResult(int requestCode, int resultCode, Intent data) {

42 // Triggered Activity finish return

43 super.onActivityResult(requestCode, resultCode, data);

44 if (requestCode == MYREQUESTCODE) {

45 // Determine whether the specified Activity end of the run

if (resultCode == RESULT_CANCELED)

46 {       }

47 // Select "Cancel" to exit the code, this case is empty

48 else if (resultCode == RESULT_OK) {

49 // Select <OK> to exit code

50 String wthr = null;

51 wthr = data.getStringExtra("weather");

// Get return value

if (wthr != null)

tv_weather.setText(wthr);

// Update TextView display of weather content

}

}

}

@Override

public boolean onCreateOptionsMenu(Menu menu) {

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

return true;

}

}

第 23–28 行的代码用其他应用中的参数触发活动。第 23–25 行建立了触发器意图,它使用了Intent.setClassName()函数。原型是

Intent Intent.setClassName(String packageName, String className);

第一个参数是被触发的活动所在的包的名称,第二个参数是被触发的活动的类名(需要使用全名)。通过使用startActivity ...函数触发活动,系统可以准确定位应用和活动类。

第 28 行将参数作为附加数据附加到意向中。Intent有一系列的putExtra函数来附加附加数据,还有一系列的getXXXExtra函数来从意图中提取数据。附加数据也可以由Bundle类组装。Intent提供了添加数据的putExtras函数和获取数据的getExtras函数。putExtra使用属性-值数据对或变量名-值数据对来添加和检索数据。在本例中,Intent.putExtra("weather", "XXX")保存由weather变量名和值“XXX”组成的数据对,作为意图的附加数据。

带有Intent.getStringExtra("weather")的代码行从附加的意图数据中获取weather变量的值,并返回字符串类型。

关于这些函数和Bundle类的更多细节可以在 Android 网站的文档中找到。这里不再进一步讨论它们。

在第 33–46 行,您重写了Activity类的onActivityResult函数。当触发的活动关闭时,调用此函数。在第 36 行,首先根据请求代码确定哪个活动被关闭并返回。然后,根据结果代码和请求代码,判断它是由 OK 还是 Cancel 单击返回。第 40–50 行从返回的意向中获取协商的变量值。第 42 行根据变量的返回值更新接口。在这个函数中,如果用户点击 Cancel 返回,你什么都不做。

  1. 修改被调用程序应用HelloAndroid的代码,如图 9-12 所示:
    1. 使用本章前面的“触发活动与不同应用的参数的显式匹配”一节中描述的方法,添加一个布局文件(在本例中名为param_otheract.xml),并将一个RelativeLayout布局拖放到该文件中。
    2. 通过添加两个TextView部件、一个EditText部件和两个Button部件来编辑这个布局文件。按如下方式设置它们的属性:
      • 两个TextView小部件的Text属性:“该接口是 HelloAndroid 应用中调用者的活动”和“将新天气设置为:”
      • EditTextID属性:@+id/editText_NewWeather
      • 两个ButtonText属性:“确认变更”和“取消变更”
      • 两个ButtonID属性:@+id/button_Modify@+id/button_Cancel

然后调整它们的大小和位置。

  1. 如“触发活动与不同应用参数的显式匹配”一节所述,为新的布局文件添加相应的类(本例中为TheWithParameterOtherActivity)),如图 9-13 所示。

A978-1-4842-0100-8_9_Fig12_HTML.jpg

图 9-12。

New layout design of the triggered (callee) application

  1. 为新添加的布局文件编辑类文件(在本例中为TheWithParameterOtherActivity.java)。内容如下:

A978-1-4842-0100-8_9_Fig13_HTML.jpg

图 9-13。

Add the corresponding class for the newly added layout file in the HelloAndroid project

Line #        Source Code

1  package com.example.helloandroid;

2 import android.os.Bundle;                   // Use Bundle Class

3``import android.app.Activity;

4 import android.content.Intent;              // Use Intent Class

5 import android.widget.Button;               // Use Button Class

6 import android.view.View;                   // Use View Class

7 import android.view.View.OnClickListener;   // Use OnClickListener Class

8 import android.widget.EditText;             // Use  EditText Class

9  public class TheWithParameterOtherActivity extends Activity {

10 private String m_weather;

11 // Save new weather variable

12 @Override

13 protected void onCreate(Bundle savedInstanceState) {

14 // Define onCreate method

15 super.onCreate(savedInstanceState);

16 // method of call onCreate Super Class

17 setContentView(R.layout.withparam_otheract); // Set layout file

18 Intent intent = getIntent();

19 // Get Intent of triggering this Activity

20 m_weather = intent.getStringExtra("weather");

21 // Get extra data from Intent

22 final EditText et_weather = (EditText) findViewById(R.id.editText_NewWeather);

23 et_weather.setText(m_weather,null);

24 // Set initial value of "New Weather" EditText according to extra data of the Intent

25 Button btn_modify = (Button) findViewById(R.id.button_Modify);

26 btn_modify.setOnClickListener(new /*View.*/OnClickListener(){

27 // Set corresponding code of <Confirm Change>

28 public void onClick(View v) {

29 Intent intent = new Intent();

30 // Create and return the Intent of Data storage

31 String wthr = et_weather.getText().toString();

32 // Get new weather value from EditText

33 intent.putExtra("weather",wthr);

34 // Put new weather value to return Intent

35 setResult(RESULT_OK, intent);

36 // Set <Confirm> and return data

37 finish();         // Close Activity

}

});

Button btn_cancel = (Button) findViewById(R.id.button_Cancel);

btn_cancel.setOnClickListener(new /*View.*/OnClickListener(){

//Set corresponding code for <Cancel Change>

public void onClick(View v) {

setResult(RESULT_CANCELED, null);

// Set return value for <Cancel>

finish();     // Close this Activity

}

});

}

}

这段代码遵循活动的框架。它在第 11 行设置了活动布局,使得布局名称与步骤 1 中创建的布局文件名相同(没有扩展名)。在第 1922 行,它首先构造一个返回意图,然后将额外的数据作为返回数据添加到Intent对象中。在第 21 行,它将活动和意图的返回值设置为返回数据载体。setResult函数的原型是

final void Activity.setResult(int resultCode, Intent data);

如果resultCodeRESULT_OK,则用户已经点击确定返回;如果是RESULT_CANCELLED,用户点击了取消返回。在这种情况下,返回数据载体意图可以为空,这在第 27 行中设置。

  1. 用以下代码修改由应用触发的AndroidManifest.xml:

Line #        Source Code

1 <manifest xmlns:android="http://schemas.android.com/apk/res/android

2     package="com.example.helloandroid"

3     android:versionCode="1"

4     android:versionName="1.0" >

5

6     <uses-sdk

7         android:minSdkVersion="8"

8         android:targetSdkVersion="15" /> 9

10     <application

11         android:icon="@drawable/ic_launcher"

12         android:label="@string/app_name"

13         android:theme="@style/AppTheme" >

14         <activity

15             android:name=".MainActivity"

16             android:label="@string/title_activity_main" >

17             <intent-filter>

18                 <action android:name="android.intent.action.MAIN" /> 19

20                 <category android:name="android.intent.category.LAUNCHER" /> 21             </intent-filter>

22         </activity>

23 <activity

24 android:name="TheWithParameterOtherActivity">

25 <intent-filter>

26``<action android:name="android.intent.action.DEFAULT" />``27

28 </activity>

29     </application>

30

31 </manifest>

  1. 第 24–29 行是新的。与前面的小节一样,您添加一个附加的活动描述并指定它的类名,这是在第二步中生成的触发活动的类名。有关修改AndroidManifest.xml文件的信息,请参见“触发不带参数的活动的显式匹配”一节。与该部分不同,您不仅添加了活动及其name属性的文档,还添加了意图过滤指令和状态,以接受在Action元素中描述的默认动作并触发这个Activity类。在缺少活动的意图过滤器描述的情况下,无法激活活动。
  2. 运行被调用方应用来注册活动的组件。在被调用程序应用HelloAndroid执行一次之前,对AndroidManifest.xml文件的修改不会注册到 Android 系统。因此,这是完成其包含活动的注册的重要步骤。

内置活动的隐式匹配

在前两节的例子中,在您通过Activity.startActivity()函数或函数触发同一应用或不同应用的活动之前,Intent对象的构造函数通过.class属性或通过字符串中的类名显式指定了类。这样,系统可以找到要触发的类。这种方法被称为显式意图匹配。下一个示例显示如何触发未指定的类。相反,系统使用一种称为隐式意图匹配的方法来解决这个问题。

此外,Android 还有许多已经实现的活动,如拨号、发送短信等。本节中的示例解释了如何使用 can 隐式匹配来触发这些内置活动。应用界面如图 9-14 所示。

A978-1-4842-0100-8_9_Fig14_HTML.jpg

图 9-14。

The application interface when using implicit intent to trigger a built-in activity

用户启动GuiExam应用并点击屏幕上的输入拨号活动按钮。它触发系统附带的拨号活动。

在这种情况下,您修改了GuiExam项目,并将该应用用作触发器。隐式匹配触发的活动是拨号活动。构建这个示例的步骤如下。

  1. GuiExam应用的布局文件(activity_main.xml)中,删除原来的TextView小部件,添加一个按钮,将其ID属性设置为@+id/goTODialAct,将其Text属性设置为“进入拨号活动”。如图 9-15 所示调整其尺寸和位置。

  2. 修改源代码文件(MainActivity.java),如下所示:

A978-1-4842-0100-8_9_Fig15_HTML.jpg

图 9-15。

Layout file of the application for the implicit match built-in activity

Line #        Source Code

1  package com.example.guiexam;

2  import android.os.Bundle;

3  import android.app.Activity;

4  import android.view.Menu;

5 import android.widget.Button;            // Use Button Class

6 import android.view.View;                // Use View Class

7 import android.view.View.OnClickListener;  // Use View.OnClickListener Class

8 import android.content.Intent;           // Use Intent Class

9 import android.net.Uri;                  // Use URI Class

10 public class MainActivity extends Activity {

11     @Override

12     public void onCreate(Bundle savedInstanceState) {

13       super.onCreate(savedInstanceState);

14       setContentView(R.layout.activity_main);

15 Button btn = (Button) findViewById(R.id.goTODialAct);

16 btn.setOnClickListener(new /*View.*/OnClickListener(){

17 // Set corresponding Code for Click Activity

18 public void onClick(View v) {

19 Intent intent = new Intent(Intent.ACTION_DIAL,Uri.parse("tel:13800138000"));

20 startActivity(intent); // Trigger corresponding Activity

21 }

22 });

}

23

24     @Override

25     public boolean onCreateOptionsMenu(Menu menu) {

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

27       return true;

28     }

}

第 16 行的代码定义了一个间接意图(即隐式匹配的意图。之所以称为间接意图,是因为对象的构造函数中没有指定需要触发的类;构造器只描述了需要触发完成拨号的类的功能。间接意图的构造函数如下:

Intent(String action)

Intent(String action, Uri uri)

这些函数需要在被调用时能够完成指定动作的类(活动)。两者的唯一区别是第二个函数也带有数据。

此示例使用第二个构造函数,它需要可以完成拨号的活动和作为电话号码字符串的额外数据。因为应用没有指定触发类型,所以 Android 从注册的类列表中找到处理这个动作的类(例如,Activity)并触发事件的开始。

如果多个类可以处理指定的动作,Android 会弹出一个选择菜单,用户可以选择运行哪个。

参数action可以使用系统预定义的字符串。在前面的例子中,Intent.ACTION_DIAL是由Intent类定义的ACTION_DIAL的字符串常量。一些系统预定义的ACTION示例如表 9-1 所示。

表 9-1。

Some System-Predefined Constants

| 动作常数名称 | 价值 | 描述 | | --- | --- | --- | | `ACTION_MAIN` | `android.intent.action.MAIN` | 作为任务的初始活动启动,没有数据输入,也没有返回输出。 | | `ACTION_VIEW` | `android.intent.action.VIEW` | 在意向 URI 中显示数据。 | | `ACTION_EDIT` | `android.intent.action.EDIT` | 请求编辑数据的活动。 | | `ACTION_DIAL` | `android.intent.action.DIAL` | 启动电话拨号器,并使用数据中预设的号码进行拨号。 | | `ACTION_CALL` | `android.intent.action.CALL` | 发起电话呼叫,并立即使用数据 URI 中的号码发起呼叫。 | | `ACTION_SEND` | `android.intent.action.SEND` | 开始发送特定数据的活动(接收者由活动解析选择)。 | | `ACTION_SENDTO` | `android.intent.action.SENDTO` | 通常,开始一项活动,向 URI 中指定的联系人发送消息。 | | `ACTION_ANSWER` | `android.intent.action.ANSWER` | 打开一个处理来电的活动。目前它是由本地电话拨号工具处理的。 | | `ACTION_INSERT` | `android.intent.action.INSERT` | 打开一个活动,该活动可以在特定数据字段中的附加光标处插入一个新项目。当它作为子活动被调用时,它必须返回新插入项目的 URI。 | | `ACTION_DELETE` | `android.intent.action.DELETE` | 开始一个活动,删除 URI 位置的数据端口。 | | `ACTION_WEB_SEARCH` | `android.intent.action.WEB_SEARCH` | 打开一个活动,并根据 URI 数据中的文本运行网页搜索。 |

ACTION常量名称是隐式匹配意图的构造函数中使用的第一个参数。在接收该动作的活动的AndroidManifest.xml语句中使用的ACTION常量的值在本节中不使用,但在下一节中使用。您可以在android.content.Intent帮助文档中找到关于预定义的ACTION值的更多信息。

使用自定义活动的隐式匹配

前面的例子使用隐式匹配来触发 Android 系统附带的活动。在本节中,您将看到一个如何使用隐式匹配来触发自定义活动的示例。

这个示例应用的配置类似于“触发活动与不同应用的参数的显式匹配”一节中的配置触发应用托管在GuiExam项目中,隐式匹配触发的自定义活动在HelloAndroid应用中。界面如图 9-16 所示。

A978-1-4842-0100-8_9_Fig16a_HTML.jpg A978-1-4842-0100-8_9_Fig16b_HTML.jpg

图 9-16。

The interface of implicit match that uses a custom activity

图 9-16(a) 显示了GuiExam触发应用启动时的界面。当您单击“显示隐含意图的活动”按钮时,系统会根据ACTION_EDIT动作的要求找到符合条件的候选活动,并显示这些候选活动的事件列表(b)。当用户自定义的HelloAndroid应用被选中时,可以接收HelloAndroid应用中意图过滤器所声明的ACTION_EDIT动作的活动被显示(c)。当您点击关闭活动按钮时,应用返回到原来的GuiExam活动界面(d)。

和前面的例子一样,这个例子是基于修改GuiExam项目。步骤如下:

  1. 编辑主布局文件(activity_main.xml)。删除原来的TextView小部件,然后添加一个TextView和一个按钮。将TextViewText属性设置为“该应用是调用方使用隐式意图触发的活动”。将按钮的Text属性设置为“显示隐含意图触发的活动”,将其ID属性设置为@+id/goToIndirectAct,如图 9-17 所示。

  2. 编辑MainActivity.java如下:

A978-1-4842-0100-8_9_Fig17_HTML.jpg

图 9-17。

The main layout design for the GuiExam trigger application

Line #        Source Code

1  package com.example.guiexam;

2  import android.os.Bundle;

3  import android.app.Activity;

4  import android.view.Menu;

5 import android.widget.Button;             // Use Button Class

6``import android.view.View;

7 import android.view.View.OnClickListener;  // Use View.OnClickListener class

8 import android.content.Intent;            // Use Intent Class

9  public class MainActivity extends Activity {

10    @Override

11    public void onCreate(Bundle savedInstanceState) {

12      super.onCreate(savedInstanceState);

13      setContentView(R.layout.activity_main);

14 Button btn = (Button) findViewById(R.id.goToIndirectAct);

15 btn.setOnClickListener(new /*View.*/OnClickListener(){

16 // Set respond Code for Button Click event

17 public void onClick(View v) {

18 Intent intent = new Intent(Intent.ACTION_EDIT);

19 // Construct implicit Inent

20 startActivity(intent);      // Trigger Activity

21 }

});

22    }

23

24    @Override

25    public boolean onCreateOptionsMenu(Menu menu) {

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

27      return true;

}

}

第 16 行和第 17 行中的代码定义了隐式意图并触发相应的活动,这与前面触发隐式活动的代码基本相同,但这里它使用了没有数据的意图的构造函数。

  1. 修改包含具有相应隐含意图的自定义活动的HelloAndroid应用:
    1. 根据本章前面的“触发不带参数的活动的显式匹配”一节中描述的方法,将一个布局文件(implicit_act.xml)添加到项目中,并将一个RelativeLayout布局拖放到文件中。
    2. 编辑布局文件,添加TextViewImageViewButton小部件。按如下方式设置属性:
      • TextViewText属性:“该接口是 HelloAndroid 的一个活动,负责 ACTION_EDIT 触发的动作”。
      • ImageView:完全按照第八章的中的“使用 ImageView”一节进行设置。
      • ButtonText属性:“关闭活动”。
      • ButtonID属性:@+id/closeActivity

然后调整各自的尺寸和位置,如图 9-18 所示。

  1. 类似于“触发无参数活动的显式匹配”一节中描述的过程,为新的布局文件(TheActivityToImplicitIntent)向项目添加相应的类,如图 9-19 所示。

A978-1-4842-0100-8_9_Fig18_HTML.jpg

图 9-18。

Layout file for the custom activity of the corresponding implicit intent

  1. 为新添加的布局文件(TheActivityToImplicitIntent.java)编辑类文件,如下所示:

A978-1-4842-0100-8_9_Fig19_HTML.jpg

图 9-19。

New class for the custom activity of the corresponding implicit intent

Line #        Source Code

1 package com.example.helloandroid;

2 import android.os.Bundle;

3 import android.app.Activity;

4 import android.widget.Button;             // Use Button Class

5 import android.view.View;                 // Use View class

6 import android.view.View.OnClickListener;  // Use View.OnClickListener class

7 public class TheActivityToImplicitIntent extends Activity {

8 @Override

9 public void onCreate(Bundle savedInstanceState) {

10 super.onCreate(savedInstanceState);

11 setContentView(R.layout.implicit_act);

12 Button btn = (Button) findViewById(R.id.closeActivity);

13 btn.setOnClickListener(new /*View.*/OnClickListener(){

14 // Set response code for <Close Activity> Click

15 public void onClick(View v) {

16 finish();

17 }

18 });

19 }

}

  1. 修改包含相应隐含意图的HelloAndroid自定义应用的AndroidManifest.xml文件,如下所示:

Line #        Source Code

1 <manifest xmlns:android="http://schemas.android.com/apk/res/android

2

3     package="com.example.helloandroid"

4

5     android:versionCode="1"

6

7     android:versionName="1.0" >

8

9     <uses-sdk

10         android:minSdkVersion="8"

11         android:targetSdkVersion="15" /> 12

13     <application

14         android:icon="@drawable/ic_launcher"

15         android:label="@string/app_name"

16         android:theme="@style/AppTheme" >

17         <activity

18             android:name=".MainActivity"

19             android:label="@string/title_activity_main" >

20             <intent-filter>

21                 <action android:name="android.intent.action.MAIN" /> 22

23                 <category android:name="android.intent.category.LAUNCHER" /> 24             </intent-filter>

25         </activity>

26 <activity

27 android:name="TheActivityToImplicitIntent">

28 <intent-filter>

29 <action android:name="android.intent.action.DEFAULT" /> 30 <action android:name="android.intent.action.EDIT" /> 31 <category android:name="android.intent.category.DEFAULT" /> 32 </intent-filter>

33 </activity>

</application>

</manifest>

第 24–32 行的代码(粗体)给出了接收隐含意图的活动信息。第 26 行指定您可以接收一个android.intent.action.EDIT动作。该值对应于触发器意图构造函数的ACTION参数Intent.ACTION_EDIT的常数值(GuiExamMainActivity类)。这是触发器和被呼叫者之间的预定接触信号。第 27 行进一步指定也可以接收默认数据类型。

  1. 运行应用HelloAndroid,它现在包含了对应隐式意图的定制活动,并在系统中注册了它的AndroidManifest.xml文件。

摘要

到目前为止,已经有三章介绍了 Android 界面设计。简单的GuiExam应用演示了活动的状态转换、Context类、意图以及应用和活动之间的关系。您还了解了如何将布局用作接口,以及按钮、事件和内部事件侦听器如何工作。多个活动的例子介绍了活动的显式和隐式触发机制。您看到了一个应用的示例,该应用的参数由另一个应用中的活动触发,现在您已经理解了活动参数的交换机制。

到目前为止讨论的应用界面基本上类似于对话框界面。这种模式的缺点是难以获得准确的触摸屏输入,从而难以基于输入界面显示准确的图像。下一章涵盖了 Android 界面设计的最后一部分,介绍了基于视图的交互风格界面。在这个界面中,您可以通过精确的触摸屏输入来输入信息,并显示详细的图像,这是许多游戏应用所要求的。

Creative Commons Open Access This chapter is licensed under the terms of the Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License ( http://​creativecommons.​org/​licenses/​by-nc-nd/​4.​0/​ ), which permits any noncommercial use, sharing, distribution and reproduction in any medium or format, as long as you give appropriate credit to the original author(s) and the source, provide a link to the Creative Commons licence and indicate if you modified the licensed material. You do not have permission under this licence to share adapted material derived from this chapter or parts of it. The images or other third party material in this chapter are included in the chapter’s Creative Commons licence, unless indicated otherwise in a credit line to the material. If material is not included in the chapter’s Creative Commons licence and your intended use is not permitted by statutory regulation or exceeds the permitted use, you will need to obtain permission directly from the copyright holder.

十、Android 应用的 GUI 设计第四部分:图形界面和触摸屏输入

Keywords Screen Touch View Class Constructor Function Application Interface Touch Point

到目前为止,已经有三章专门讨论了 Android 界面设计。到目前为止讨论的应用界面类似于对话框界面。缺点是难以获得准确的触摸屏输入信息,因此难以基于输入界面显示准确的图像。本章介绍基于视图的交互风格界面。在这种模式下,您可以通过准确的触摸屏输入信息,并显示详细的图像,这恰好是许多游戏应用的要求。

显示输出框架

与由TextViewEditTextButton和其他窗口组件组成的对话框风格的界面不同,交互式 UI 显示直接使用一个View类。本节介绍在视图中绘图的基本框架(即显示图像或图形)。

为了在视图中显示图像和图形,您需要将绘图代码放入它的onDraw函数中。每当需要在视图中重新绘制图像时,调用onDraw函数,例如当应用启动时显示视图时,当图形视图顶部的封面对象(例如视图、事件或对话框)被移走时,当底层的视图随着活动移动到顶层时,或者类似的情况。建议您将绘图代码放在View.onDraw函数中,这样您可以确保视图何时需要显示给用户。视图窗口也可以立即显示在其总输出中;否则,某些图形视图区域可能无法刷新或重新绘制。

画矩形、画椭圆、画直线、显示文本等 Android 绘图功能通常集成在Canvas类中。当View.onDraw回调执行时,它会带来一个用于获取Canvas对象的Canvas参数。

Android 使用Paint类来绘制各种图形。Paint包含多种画笔属性,如颜色、填充样式、字体和字体大小。

如本书前面所述,Eclipse 中生成的应用代码的接口配置样式如下:一个活动包含布局,一个布局包含两层小部件结构。因此,您可以在布局中为活动的onCreate函数中的setContentView函数设置参数,以实现这种效果。要使用基于视图的接口,您需要将setContentView函数的默认参数布局更改为自定义视图类。

这里有一个例子来说明这个过程。使用以下步骤修改GuiExam示例项目:

  1. 使用与第九章中“触发无参数活动的显式匹配”一节相同的步骤,创建一个新类(MyView),如图 10-1 所示。

  2. 编辑新添加的类的源代码(MyView.java)。内容如下所示。

A978-1-4842-0100-8_10_Fig1_HTML.jpg

图 10-1。

Create a new class in the project

Line#    Source Code

1  package com.example.guiexam;

2

3 import android.view.View;

4 import android.graphics.Canvas;

5 import android.graphics.Paint;

6 import android.content.Context;

7 import android.graphics.Color;

8 import android.graphics.Paint.Style;

9 import android.graphics.Rect;

10 import android.graphics.Bitmap;

11 import android.graphics.BitmapFactory;

12 import android.graphics.Typeface;

13 public class MyView extends View {

14 MyView(Context context) {

15 super(context);

16 }

17 @Override

18 public void onDraw(Canvas canvas) {

19 Paint paint = new Paint();

20 paint.setAntiAlias(true);        //  Sett anti-aliasing

21 // paint.setColor(Color.BLACK);          // Set Color Black

22 // paint.setStyle(Style.FILL);           // Set Fill Style

23 canvas.drawCircle(250, 250, 120, paint);   // Draw Circle

24 paint.setColor(Color.RED);       // Set color red

25 paint.setStyle(Style.STROKE);    // Set style-Stroke ( no fill)

26 canvas.drawRect(new Rect(10, 10, 120, 100), paint); // draw rect

27 paint.setColor(0xff0000ff /*Color.BLUE*/ );

28 String str = "Hello!";

29 canvas.drawText(str, 150, 20, paint);  // display text

30 paint.setTextSize(50);                 // Set Text Size

31 paint.setTypeface(Typeface.SERIF);     // Set Typeface: Serif

32 paint.setUnderlineText(true);          // Set Underline Text

33 canvas.drawText(str, 150, 70, paint);  // Display text

Bitmap bitmap = BitmapFactory.decodeResource(getResources(),R.drawable.ic_launcher);

canvas.drawBitmap(bitmap, 0, 250, paint); // Display image

}

}

第 13 行代码添加了extends View,做了一个自定义类;在这种情况下,MyViewView类别继承而来。第 13-16 行创建了一个调用超类的定制类构造函数。此构造函数对于防止以下编译错误至关重要:

Implicit super constructor View() is undefined. Must explicitly invoke another constructor

第 17/34 行覆盖了对各种绘图代码进行编程的功能。您构造了一个画笔——也就是一个Paint对象——用于在第 16 行进行绘制,并设置它来消除第 17 行中的锯齿边缘。第 23 行画圆(x = 250,y = 250);第 24 行将画笔颜色设置为红色,依此类推。

setColor函数的原型是

void Paint.setColor(int color);

在 Android 中,基于α、红色、绿色和蓝色,用一个四字节的整数来表示一种颜色。整数数据格式如下所示:

| 机 管 局 | 阻力比 | gasgenerator 煤气发生器 | bb |

从左到右,前四个字节代表α、红色、绿色和蓝色值。例如,蓝色是 0xff0000ff,这也反映在第 27 行中。此外,Android Color类还为一些颜色定义了一个常量,比如BLACKREDGREENBLUE等等,如第 24 行所示。

该函数设置画笔的填充模式。函数原型是

void  Paint.setStyle(Paint.Style style)

参数style可以取Paint.Style.STROKE(空心填充)、Paint.Style.FILL(实心填充)、或者Paint.Style.FILL_AND_STROKE(实心填充)。这些值是在Paint.Style类中定义的常量;它们对应的显示样式如表 10-1 所示。

表 10-1。

Fill Mode Parameters and Examples

| 显示的图像 | 图形功能参数设置 | | --- | --- | | ![A978-1-4842-0100-8_10_Figa_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-pt2-zh/raw/master/docs/andr-app-dev-intel-plat/img/Image00199.jpg) | `Color=BLACK, Style=FILL` | | ![A978-1-4842-0100-8_10_Figb_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-pt2-zh/raw/master/docs/andr-app-dev-intel-plat/img/Image00200.jpg) | `Color=BLACK, Style=STROKE` | | ![A978-1-4842-0100-8_10_Figc_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-pt2-zh/raw/master/docs/andr-app-dev-intel-plat/img/Image00201.jpg) | `Color=BLACK`,`Style=FILL_AND_STROKE` |
  1. 修改主Activity类(MainActivity.java)如下:

Line#    Source Code

1 package com.example.guiexam;

2 import android.os.Bundle;

3 import android.app.Activity;

4 import android.view.Menu;

5 public class MainActivity extends Activity {

6 @Override

7 public void onCreate(Bundle savedInstanceState) {

8 super.onCreate(savedInstanceState);

9 // setContentView(R.layout.activity_main);

10 setContentView(新 my view(this));

11 }

12 ......

系统自动用第 8 行的代码覆盖第 7 行的代码。这允许一个定制的视图类代替默认的布局作为活动的接口。

应用界面如图 10-2 所示;(a)显示整个界面,而(b)是图形显示的放大部分。

A978-1-4842-0100-8_10_Fig2_HTML.jpg

图 10-2。

The interface of the display output framework of the GuiExam application

响应触摸屏输入的绘图框架

前面的示例应用只显示图像/图形,不能响应触摸屏输入。在本节中,您将看到如何响应触摸屏输入并控制视图显示。

View有一个onTouchEvent函数,其函数原型如下:

boolean View.onTouchEvent(MotionEvent event);

当用户在触摸屏上点击、释放、移动或进行其他交互动作时,会生成触摸输入事件。该触摸输入事件触发对View.onTouchEvent的调用。为了允许用户处理触摸屏输入,您需要重写这个函数。响应代码需要写在函数体中。

View.onTouchEvent有一个类型为MotionEvent的参数,它定义了MotionEvent类的接触点的坐标位置、事件类型等等。事件类型可以是MotionEvent.ACTION_DOWNMotionEvent.ACTION_MOVEMotionEvent.ACTION_UP或等效类型,如MotionEvent类中定义的常量。常量表示交互式操作,如触摸屏按下、触摸屏移动、触摸屏弹出等。

如前所述,每当需要重绘视图时,都会调用该函数,因此需要将绘图代码放入该函数中。大多数时候,系统可以自动触发重绘事件;但是因为用户设计他们自己的重绘,系统不知道他们什么时候需要被触发。例如,也许用户更新了显示内容,但是内容的位置、大小和级别没有改变;因此,系统不会触发 redraw 事件。在这种情况下,用户需要调用View类的类函数postInvalidateinvalidate来主动生成重绘事件。函数原型是

void  View.invalidate(Rect dirty)

void  View.invalidate(int l, int t, int r, int b)

void  View.invalidate()

void  View.postInvalidate(int left, int top, int right, int bottom)

void  View.postInvalidate()

没有参数的postInvalidate和函数重画整个视图;带参数的postInvalidateinvalidate函数重画视图的指定区域(或某个区域)。有常量和没有常量的postInvalidateinvalidate的区别在于,第一种情况需要一个事件循环,直到下一个事件产生 redraw 事件,而第二种情况立即发出 redraw。

下面的示例阐释了绘制响应触摸屏输入的代码的框架。该应用的界面如图 10-3 所示。

A978-1-4842-0100-8_10_Fig3_HTML.jpg

图 10-3。

The interface of a GuiExam input graphics framework that responds to the touchscreen

应用从图 10-3(a) 开始。当用户在一个圆圈内点击(触摸圆圈区域内的屏幕)时,圆圈的颜色发生变化:它在黑色、红色、绿色和蓝色之间循环,如图 10-3(a)–(d)所示。如果您在圆外单击,圆不会改变颜色。

使用与前面部分相同的示例,修改定制视图类MyView.java,如下所示:

Line#    Source Code

1  package com.example.guiexam;

2

3  import android.view.View;

4  import android.graphics.Canvas;

5  import android.graphics.Paint;

6  import android.content.Context;

7

8  import android.graphics.Color;

9 import android.view.MotionEvent;

10       import java.lang.Math;

11 public class MyView extends View {

12 private float cx = 250;     // Default X Coordinate of Circle

13 private float cy = 250;     // Default Y Coordinate of Circle

14 private int radius = 120;   // Radius

15 private int colorArray[] = {Color.BLACK, Color.RED, Color.GREEN, Color.BLUE };

16 // Defines an array of colors

17 private int colorIdx = 0;   // Custom color subscript

private Paint paint;        // Define Paint

18

19     public MyView(Context context) {

20         super(context);

21 paint = new Paint();       // Initialization paintbrush

22 paint.setAntiAlias(true);  // Setting anti-aliasing

23 paint.setColor(colorArray[colorIdx]);    // Set the pen color

}

24

25     @Override

26     protected void onDraw(Canvas canvas) {

27 canvas.drawCircle(cx, cy, radius, paint);

}

28

29 @Override

30 public boolean onTouchEvent(MotionEvent event) {

31 float px = event.getX();

32 // defined the touch point in the X, Y coordinates

33 float py = event.getY();

34 switch (event.getAction()) {

35 case MotionEvent.ACTION_DOWN:

36 // Touch  screen pressed

37``if (Math.abs(px-cx) < radius``&&

38 // Touch location inside the circle

39 colorIdx = (colorIdx + 1) % colorArray.length;

40 paint.setColor(colorArray[colorIdx]);

41 // Set paintbrush color

42 }

43 postInvalidate();          // Repaint

44 break;

45 case MotionEvent.ACTION_MOVE:  // Screen touch and move

46 break;

47 case MotionEvent.ACTION_UP:    // Screen touch unpressed

break;

}

return true;

}

}

第 15 行和第 16 行定义了颜色和颜色索引的数组,第 17 行定义了画笔变量。构造函数的第 20–22 行完成了笔刷属性设置的初始化。你不把画笔属性的代码放在View.Ondraw中的原因是为了避免每次重绘的重复计算。onDraw函数的唯一工作是显示圆。

在第 28–46 行,您创建了新的触摸输入事件响应函数onTouchEvent。在第 30 行和第 32 行,首先使用MotionEvent类的getXgetY函数获得触摸点的 X,Y 坐标。然后您通过第 34 行中的MotionEvent类的getAction函数获得输入动作类型,随后是一个case语句来完成不同的输入动作。第 37–43 行是对按下触摸屏动作的响应。您确定接触点是否在第 37 行的圆内。然后修改设置颜色的代码,并在第 39–40 行更改钢笔颜色。你调用postInvalidate函数通知在第 43 行重画,并为其提供最后的点睛之笔。

多点触摸代码框架

大多数 Android 设备都支持多点触控触摸屏。好消息是 Android 系统软件也提供了多点触控支持。本节介绍多点触控代码框架。

触摸事件类MotionEvent有一个getPointerCount()函数,返回屏幕上触摸点的当前数量。函数原型是

final int MotionEvent.getPointerCount();

还可以使用前面讨论的getXgetY函数来获取触摸点的坐标。原型如下:

final float  MotionEvent.getX(int pointerIndex)

final float  MotionEvent.getX()

final float  MotionEvent.getY(int pointerIndex)

final float  MotionEvent.getY()

在上一节中,您使用不带参数的函数获得了单个触摸点的坐标。带参数的getX / getY函数用于获取多点触摸情况下触摸点的位置,其中参数pointerIndex为触摸点的索引号。这是一个从 0 开始的整数。

这里有一个例子来说明多点触摸编程框架。这个例子是一个两点触摸应用,它放大和缩小一个圆。应用界面如图 10-4 所示。

A978-1-4842-0100-8_10_Fig4_HTML.jpg

图 10-4。

The interface of the two-point touch zoom-in/zoom-out GuiExam graphic application

应用的启动界面如图 10-4(a) 所示。圆总是在视图的中心,但是圆的大小(它的半径)可以通过两点触摸来控制。中心是视图的中心,而不是活动的中心或屏幕的中心。所谓的两点触摸屏是指有两个触摸点,或两个手指同时在屏幕上移动,要么是圆圈变大的扩展手势(b),要么是圆圈变小的挤压手势(c)。代码如下:

Line#    Source Code

1  package com.example.guiexam;

2

3  import android.view.View;

4  import android.graphics.Canvas;

5  import android.graphics.Paint;

6  import android.content.Context;

7

8  import android.view.MotionEvent;

9  import java.lang.Math;

10 public class MyView extends View {

11 private static final int initRadius = 120; // initial value of the radius

12     private float cx;                // X coordinate of the circle

13     private float cy;                // Y coordinate of the circle

14 private int radius = initRadius; // Set initial value of the radius

15 public float graphScale = 1;     // Set Scale factor for one two-point touch move

16 private float preInterPointDistance;    // Pre-distance of two touch points

17 private boolean bScreenPress = false;   // The sign of the screen being pressed down

18     private Paint paint;                    // Define paintbrush

19     public MyView(Context context) {

20         super(context);

21         paint = new Paint();                // Initialize paintbrush

22         paint.setAntiAlias(true);           // Set Anti Alias

23     }

24     @Override

25     protected void onDraw(Canvas canvas) {

26 cx = canvas.getWidth()/2;  // Let circle center positioned at the screen of the screen

27 cy = canvas.getHeight()/2;

28         canvas.drawCircle(cx, cy, radius*graphScale, paint);

29     }

30     @Override

31     public boolean onTouchEvent(MotionEvent event) {

32 float px1, py1;  // Define the X,Y coordinates of 1st touch point

33 float px2, py2;  // Define the X,Y coordinates of 2nd touch point

34 float interPointDistance;  // distance between two  touch points

35 switch (event.getAction()) {

36 case MotionEvent.ACTION_DOWN:   // Screen touch pressed

37 break;

38 case MotionEvent.ACTION_MOVE:   // Screen touch move

39 if (event.getPointerCount() == 2 ) {

40 px1 = event.getX(0);    // Get the X,Y coordinate of the first touch point

41 py1 = event.getY(0);

42 px2 = event.getX(1);    // Get the X,Y coordinate of the second touch point

43 py2 = event.getY(1);

44 interPointDistance = (float) Math.sqrt((px6-px2)*(px6-px2)+(py1 - py2)*(py1 - py2));

45 if (!bScreenPress){

46 bScreenPress = true;

47 preInterPointDistance = interPointDistance;

48 } else {

49 graphScale = interPointDistance / preInterPointDistance;

50 invalidate();       // Redraw graphics

51 }

52 } else {

53 bScreenPress = false;

54 radius = (int)(radius * graphScale);

55 // One downsize/enlarge circle end. Record final scale factor

56 }

57 break;

58 case MotionEvent.ACTION_UP:     // Screen touch lift up

59 bScreenPress = false;

60 radius = (int)(radius * graphScale);

61 // One downsize/enlarge circle end. Record final scale factor

62 break;

63 }

64         return true;

}

}

这段代码在第 15 行定义了两点触摸的比例因子graphScale,在第 16 行定义了变量preInterPointDistance来记录两个触摸点之间的距离。第 17 行定义了屏幕被按下时的标志变量bScreenPress

第 26 行和第 27 行调用onDraw函数中Canvas类的getWidthgetHeight来获取视图的宽度和高度,然后将圆心分配在视图的中心。这一步的好处是,当屏幕旋转 90 度时,圆保持在视图的中心,如图 10-4(d) 所示。这些例子与前一个例子的不同之处在于,这一次所画圆的半径等于圆的半径乘以比例因子graphScale

第 32–61 行包含基于上一节修改示例的onDraw。第 38–56 行是触摸移动活动的响应代码。第 3 行确定是否有两个触摸点;如果有,运行代码行 40–51;否则,运行第 53–54 行。您将标志bScreenPress设置为 false,以指示何时第一次按下两个触摸点,然后记录最终半径,该半径等于半径的当前值乘以比例因子graphScale。您可以在第 40–43 行中获得两个接触点的位置坐标。线 44 计算两个接触点之间的距离。第 45 行确定它是否是第一次按压;如果是,则运行 46 和 47 行,并记录两个触摸点之间的距离;否则,运行第 49/50 行的代码。在这里,您可以根据点之间的当前距离和上一次移动的距离来计算比例因子。在这之后,图形被重新绘制。

为了处理标志bScreenPress的位置,您在第 58–60 行执行屏幕触摸活动的响应代码,它类似于第 53 和 54 行的非两点触摸代码。

响应键盘输入

大多数 Android 设备都有许多硬件按钮,如音量+,音量-,电源,主页,菜单,后退,搜索等等。一些 Android 设备也配备了键盘。键盘,包括设备的硬件按钮,是 Android 应用的重要输入方法。键盘输入对应键盘事件,命名为KeyEvent(也称为按键事件)。在本节中,您将了解响应键盘输入的方法。

在 Android 中,ActivityView类都可以接收按键事件。按键事件触发对ActivityView类的onKeyDown函数的调用。函数原型是

boolean    Activity.onKeyDown(int keyCode, KeyEvent event);

boolean    View.onKeyDown(int keyCode, KeyEvent event);

keyCode参数是被按下的键的索引代码。Android 中的每个键都有一个唯一的数字,就是keyCode。表 7-1 中描述了一些关键代码。按键事件KeyEvent包含与按钮相关的属性,比如按钮被按下的频率。为了处理关键事件,您需要覆盖onKeyDown函数并添加您自己的响应处理代码。

有趣的是,虽然ActivityView类可以接收关键事件,但是视图通常包含在活动中。当按钮被按下时,事件首先发送外部活动;也就是说,活动会更快地收到事件。下面的例子展示了如何通过重写活动的onKeyDown函数来响应按钮的按下。

这个例子显示了如何使用箭头键在应用中移动圆。应用界面如图 10-5 所示。

A978-1-4842-0100-8_10_Fig5_HTML.jpg

图 10-5。

Using keys to control the movement of the circle in the application interface

我们测试的联想手机没有键盘,所以我们选择在虚拟机上运行应用。虚拟机有左、下、右和上键来实现这些圆周运动。应用启动界面如图 10-5(a) 所示。按下左、下、右或上按钮使圆圈向相应的方向移动。界面示例如图 10-5(b)至(e) 所示。

该应用基于本章开头创建的示例(图 10-1 )并按照以下程序进行修改:

  1. 修改MyView.java的源代码如下:

Line#    Source Code

1  package com.example.guiexam;

2

3  import android.view.View;

4  import android.graphics.Canvas;

5  import android.graphics.Paint;

6  import android.content.Context;

7  public class MyView extends View {

8      private float cx = 250;                 // X coordinate of the circle

9      private float cy = 250;                 // Y coordinate of the circle

10     private static final int radius = 120;  // Radius of the circle

11     private Paint paint;                    // define paint brush

12 private static final int MOVESTEP = 10; // the step length for pressing direction key

13     public MyView(Context context) {

14         super(context);

15         paint = new Paint();                // Paint brush initialization

16         paint.setAntiAlias(true);           // Set Anti Alias

17     }

18     @Override

19     protected void onDraw(Canvas canvas) {

20         canvas.drawCircle(cx, cy, radius, paint);

21     }

22     ////// Self-define function:press key to move graphic (circle) //////

23 public void moveCircleByPressKey(int horizon, int vertical){

24 if (horizon < 0)                    // horizontal move

25 cx -= MOVESTEP;

26 else if (horizon > 0)

27 cx += MOVESTEP;

28 if (vertical < 0)

29 cy += MOVESTEP;                 // vertical move

30 else if (vertical > 0)

31 cy -= MOVESTEP;

32 postInvalidate();                   // note to repaint

33 }

34 }

在第 23–33 行,您向 view 类添加了一个函数,通过按 horizon 或 vertical 键来移动图像(圆圈)。这个函数有两个参数:horizonvertical。如果horizon小于 0,则减小圆的 X 坐标值,结果圆向左移动。如果horizon大于 0,则增加圆的 X 坐标值,这将使圆向右移动。对垂直参数执行类似的操作,上下移动圆。第 32 行用新参数更新图形例程,并触发视图重绘。

  1. 修改主活动类MainActivity.java的源代码,如下所示:

Line#    Source Code

1  package com.example.guiexam;

2  import android.os.Bundle;

3  import android.app.Activity;

4  import android.view.Menu;

5 import android.view.KeyEvent;     // Key press event class

6  public class MainActivity extends Activity {

7 private MyView theView =null;  // View object stored inside the variable

8      @Override

9      public void onCreate(Bundle savedInstanceState) {

10         super.onCreate(savedInstanceState);

11 theView = new MyView(this); // record the View class of the Activity

12 setContentView(theView);

13     }

14     @Override

15     public boolean onCreateOptionsMenu(Menu menu) {

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

17         return true;

18     }

19 @Override                     // Key down response function

20 public boolean onKeyDown(int keyCode, KeyEvent event) {

21 int horizon = 0; int vertical = 0;

22 switch (keyCode)

23 {

24 case KeyEvent.KEYCODE_DPAD_LEFT:

25 horizon = -1;

26 break;

27 case KeyEvent.KEYCODE_DPAD_RIGHT:

28 horizon = 1;

29 break;

30 case KeyEvent.KEYCODE_DPAD_UP:

31 vertical = 1;

32 break;

33 case KeyEvent.KEYCODE_DPAD_DOWN:

34 vertical = -1;

35 break;

36 default:

37 super.onKeyDown(keyCode, event);

38 }

39``if (!(horizon == 0``&&

40 theView.moveCircleByPressKey(horizon,vertical);

41 return true;

42 }

43 }

在这段代码中,您希望Activity类接收并响应按键事件,因此您用按钮响应代码覆盖了第 19–42 行中的onKeyDown函数。虽然按键的响应函数位于Activity类中,但是显示更新要在视图MyView类中实现,所以你必须让Activity类知道它对应的视图对象。为此,您在第 7 行添加一个记录视图对象变量theView。在第 11 行和第 12 行,您让theView在构造视图对象时记录这个对象。

在按键响应函数onKeyDown中,您使用一个switchcase语句(第 22-38 行)并根据不同的按键采取不同的动作。该函数的keyCode参数指定了被按下的键的键号。例如,第 24–26 行中的代码是左键的处理代码。它将一个水平标志设置为“left ”,然后调用 view 类的自定义函数moveCircleByPressKey来移动第 39 行和第 40 行中的圆。为了允许处理其他按键事件,您调用系统的默认处理程序来处理第 36 行和第 37 行中的其他按键。

Android 中的对话框

在 Android 中有三种不同的方式来使用对话框,正如本节所讨论的。

使用活动的对话主题

Dialog类实现了一个简单的浮动窗口,可以在活动中创建。通过使用一个基本的Dialog类,您可以创建一个新的实例并设置它的标题和布局。对话框主题可以应用到普通的活动中,使它看起来像一个对话框。

此外,Activity类提供了一个方便的机制来创建、保存和恢复对话框,比如onCreateDialog(int)onPrepareDialog(int, Dialog)showDialog(int)dismissDialog(int)以及其他函数。如果使用这些函数,活动可以通过getOwnerActivity()方法返回管理对话框的Activity对象。

以下是使用这些功能的具体说明。

onCreateDialog(int)函数

当你使用这个回调函数时,Android 会将这个活动设置为每个对话框的所有者,它会自动管理每个对话框的状态,并将其锚定到该活动。这样,每个对话框都继承了该活动的特定属性。例如,当对话框打开时,菜单按钮显示为活动定义的选项菜单。例如,您可以使用音量键来修改活动使用的音频流。

showDialog(int)函数

当您想要显示一个对话框时,您可以调用showDialog(intid)方法并通过这个函数调用传递一个整数来惟一地标识这个对话框。当对话框第一次被请求时,Android 从活动中调用onCreateDialog(intid)。您应该初始化此对话框。这个回调方法被传递给showDialog(intid)拥有的同一个 ID。创建对话框时,对象在活动结束时返回。

onPrepareDialog(int,Dialog)函数

在对话框显示之前,Android 还调用可选的回调函数onPrepareDialog(int id, Dialog)。如果希望每次打开对话框时都更改属性,可以定义此方法。与只能在第一次打开对话框时调用的onCreateDialog(int)函数不同,该方法在每次打开对话框时都会被调用。如果您没有定义onPrepareDialog(),那么对话框将保持上次打开时的状态。对话框的 ID 和在onCreateDialog()中创建的对话框对象也可以通过这个方法传递给函数。

dismissDialog(int)函数

当你准备关闭对话框时,可以通过这个对话框方法调用dismiss()来消除。如果需要,您也可以从活动中调用dismissDialog(int id)方法。如果你想用onCreateDialog(int id)方法来保留你的对话框的状态,那么每次对话框被消除时,这个对话框对象的对象状态都被保留在活动中。如果你决定不再需要这个对象或者清除状态,那么你应该调用removeDialog(intid)。这将移除任何内部对象引用,即使对话框正在显示,它也会被消除。

使用特定的对话框类

Android 提供了多个作为Dialog类扩展的类,比如AlertDialogProgressDialog等等。每个类都被设计为提供特定的对话框功能。基于Dialog类的屏幕接口在所有调用特定类的活动中创建。因此它不需要在清单文件中注册,它的生命周期由调用该类的活动控制。

使用 Toast 提醒

Toasts 是特殊的、非模块化的、瞬时的消息对话框,通常用于广播接收器和 backgroundservices,并用于提示用户事件。

对话框示例

在讨论的对话框方法中,如果用函数的实现方式来衡量,第一个函数是最强大的,其次是第二个和第三个。就实现代码的复杂程度而言,第三种方法最简单,第一种和第二种更复杂。

下面的示例演示了第二种方法。请参阅 Android 的帮助文档和示例(在 Android SDK 安装目录下的samples目录中),了解其他实现方法的更多信息。

这个示例应用使用的特定对话框类是AlertDialogBuilder内部类。当你按下返回按钮时,会弹出一个对话框,让你决定是否退出应用。应用界面如图 10-6 所示。在这个例子中使用 Android 对话框会帮助你理解它的用法。

A978-1-4842-0100-8_10_Fig6_HTML.jpg

图 10-6。

The application interface with an Exit dialog box

应用启动并显示主活动界面,如图 10-6(a) 所示。按下设备的后退按钮,弹出退出对话框,如图 10-6(b) 所示。当您单击 Exit 按钮时,应用退出,界面也关闭。当你点击取消按钮时,应用返回到上一个屏幕,类似于图 10-6(a) 。

将活动类MainActivity.java的源代码修改如下:

Line#    Source Code

1  package com.example.guiexam;

2  import android.os.Bundle;

3  import android.app.Activity;

4  import android.view.Menu;

5  import android.view.KeyEvent;            // Key event class

6 import android.app.Dialog;               // Use Dialog class

7 import android.app.AlertDialog;          // Use AlertDialog class

8 import android.content.DialogInterface;  // Use DialogInterface interface

9  public class MainActivity extends Activity {

10     private MyView theView =null;        // View objects stored inside the variable

11 private AlertDialog.Builder exitAppChooseDlg = null; // Exit App dialog box

12 private Dialog dlgExitApp = null;

13     @Override

14     public void onCreate(Bundle savedInstanceState) {

15         super.onCreate(savedInstanceState);

16         theView = new MyView(this);  // View class of Record My Activity

17         setContentView(theView);

18 exitAppChooseDlg = new AlertDialog.Builder(this);

19 // Define  AlertDialog.Builder object

20 exitAppChooseDlg.setTitle("Exit Selection");

21 // Define the title of the dialog box

exitAppChooseDlg.setMessage("Confirm to exit application");

22 // Define the display text of the dialog box

23 exitAppChooseDlg.setIcon(android.R.drawable.ic_dialog_info);

24 // Define the icon of the dialog box

25

26 // Set the leftmost button and click response class

27 exitAppChooseDlg.setPositiveButton("Exit", new DialogInterface.OnClickListener() {

28 public void onClick(DialogInterface dialog, int which) {

29 dialog.dismiss();        // Close Dialog Box

/*MainActivity.*/finish(); // Exit (main) Activity

30 System.exit(0);          // Exit Application

31 }

32 });

33

34 // Set the rightmost button and click response class

35 exitAppChooseDlg.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {

36 public void onClick(DialogInterface dialog, int which) {

37 dialog.cancel();          // Close dialog box

}

38 });

39 dlgExitApp = exitAppChooseDlg.create();

40 // Create dialog box exit object

41     }

42

@Override

43     public boolean onCreateOptionsMenu(Menu menu) {

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

45         return true;

46     }

47

48     @Override        // Key down response function

49     public boolean onKeyDown(int keyCode, KeyEvent event) {

50         int horizon = 0; int vertical = 0;

51         switch (keyCode)

52         {

53             case KeyEvent.KEYCODE_DPAD_LEFT:

54                 horizon = -1;

55                 break;

56             case KeyEvent.KEYCODE_DPAD_RIGHT:

57                 horizon = 1;

58                 break;

59             case KeyEvent.KEYCODE_DPAD_UP:

60                 vertical = 1;

61                 break;

62             case KeyEvent.KEYCODE_DPAD_DOWN:

63                 vertical = -1;

64                 break;

65 case KeyEvent.KEYCODE_BACK:

66 if (event.getRepeatCount() == 0) {

67 dlgExitApp.show();

68 // Display AlertDialog.Builder dialog box

69 }

70 break;

71             default:

72                 super.onKeyDown(keyCode, event);

}

if (!(horizon == 0 && vertical == 0))

theView.moveCircleByPressKey(horizon,vertical);

return true;

}

}

第 11 行和第 12 行定义了AlertDialog.Builder类及其在Activity类中与Dialog类相关的变量。您修改第 18–36 行的onCreate功能代码,并定义代码以准备对话框。在第 18 行,您构造了AlertDialog.Builder类对象;这个构造函数的原型是

AlertDialog.Builder(Context context)

AlertDialog.Builder(Context context, int theme)

在这个例子中,您使用第一个原型来传递Activity对象,该对象将对话框构造为构造函数的上下文。接下来在第 19 行和第 21 行设置对话框的标题显示文本、图标和其他属性。

AlertDialog.Builder对话框最多可以有三个按钮:左、中和右。它们分别由setPositiveButtonsetNeutralButtonsetNegativeButton功能设置。您可以指定需要多少个对话框按钮。这个例子使用了两个按钮:左和右。

第 23–29 行设置对话框的左键并单击-响应代码。AlertDialog.Builder类的函数原型是

AlertDialog.Builder  setPositiveButton(int textId, DialogInterface.OnClickListener listener)

AlertDialog.Builder  setPositiveButton(CharSequence text, DialogInterface.OnClickListener listener)

在示例中使用了第二个原型,其中第一个参数是按钮显示的文本,第二个参数是单击响应的接口对象。

在第 25 行,首先调用该类的解散或取消函数来关闭对话框。DialogInterface是对话框类的操作界面(AlertDialogDialog等)。在第 25 行使用dismiss函数关闭对话框,在第 33 行使用cancel函数关闭对话框。

第 26–27 行关闭活动和应用,如“退出活动和应用”一节所述在第八章,图 8-16。有趣的是,内部类DialogInterface.OnClickListener使用了非点外部类MainActivity的成员函数,不需要在“类名”前面加上前缀。

您在第 36–35 行设置了右键单击的对话框-响应代码。点击响应代码相对简单,在第 33 行使用DialogInterface类的cancel函数关闭对话框。

最后,第 36 行调用AlertDialog.Builder类的create函数来创建退出对话框对象dlgExitApp。该函数返回一个AlertDialog对象,其原型是

AlertDialog  create()

因为AlertDialog是从Dialog类派生的,所以返回值可以赋给Dialog变量。

在第 60/64 行添加响应函数的返回键响应代码。代码相对简单:您确定在第 61 行是否按下了重复的键,然后您调用Dialog类的show函数来显示一个对话框。

应用属性设置

在 Android 设备中,有两个不同的地方可以找到关于安装的应用的信息。一个是菜单列表(按下设置按钮后的界面),另一个是通过进入设置➤应用➤管理应用➤下载的菜单项。见图 10-7 :

A978-1-4842-0100-8_10_Fig7_HTML.jpg

图 10-7。

The difference of Menulist and Application Setting display on target device

到目前为止,几乎所有的例子都是基于两个应用的代码框架:GuiExamHelloAndroid。但是很难在目标设备的菜单中区分它们。这些应用在菜单列表中无法区分,因为您使用了默认设置,而没有应用它们自己的属性设置。本节说明如何应用属性设置。

图 10-8 显示了应用属性设置前后的应用设置界面。

A978-1-4842-0100-8_10_Fig8_HTML.jpg

图 10-8。

The application on the target device before and after applying property setting

该示例使用GuiExam应用来展示更改应用设置的步骤:

  1. 在目标机器上修改菜单中应用的图标。根据应用res\drawable-XXX目录下的ic_launcher.png文件大小(其中XXX代表不同的分辨率,例如drawable-hdpi代表高分辨率图像的目录),编辑您的图像文件,并将其命名为ic_launcher.png

Android 设备的常见屏幕分辨率和应用图标文件存储的目录如表 10-2 所示。

  1. 将自定义图片文件放入相应的目录res\drawable-XXX,替换原文件。例如,对于高分辨率屏幕应用,将res\drawable-xhdpi中的文件ic_launcher.png替换为自己的文件,如图 10-9 所示。

表 10-2。

Common Android Device Screen Resolutions and the Directories Containing Application Icon Sizes

| 目录名 | 大小 | 描述 | | --- | --- | --- | | `drawable-ldpi` | 36 × 36 dpi | 低分辨率屏幕 | | `drawable-mdpi` | 48 × 48 dpi | 中等分辨率屏幕 | | `drawable-hdpi` | 72 × 72 dpi | 高分辨率屏幕 | | `drawable-xhdpi` | 96 × 96 dpi | 超高分辨率屏幕 | | `drawable-xxhdpi` | 144 × 144 dpi | 超高分辨率屏幕 |
  1. 在目标机器上修改应用的菜单文本注释。

A978-1-4842-0100-8_10_Fig9_HTML.jpg

图 10-9。

Replacing the application icon

打开\res\values\strings.xml文件的包浏览器窗格。将title_activity_my_main字符串值设置为自定义字符串(本例中为“GUI 示例”),如图 10-10 所示。

A978-1-4842-0100-8_10_Fig10_HTML.jpg

图 10-10。

Modifying the icon text of the application

完成这些修改后,您可以看到目标应用的菜单项的图标和文本标签已经改变。

注步骤 1 也可以通过另一种方法来实现,这种方法可以在创建应用时生成自己的图标集。该过程如下:

  1. 在“配置启动器图标”对话框中,单击图像按钮,然后单击图像文件右侧的浏览按钮。
  2. 在打开的对话框中选择图片文件作为应用图标(本例中为graywolf.png),如图 10-11 所示。

A978-1-4842-0100-8_10_Fig11_HTML.jpg

图 10-11。

Selecting the icon file when generating the application

配置启动器图标对话框如图 10-12 所示。

A978-1-4842-0100-8_10_Fig12_HTML.jpg

图 10-12。

Configuring the launcher icon when generating the application

换句话说,Eclipse 可以基于用户指定的图像文件,在res\drawable-XXX目录中自动生成具有适当维度的各种ic_launcher.png文件。这消除了手动编辑图像的需要。

摘要

在介绍 Android GUI 设计的最后一章中,您将了解视图中绘图的基本框架,绘图框架如何响应触摸屏输入的概念,以及如何控制视图的显示和多点触摸代码框架。您将使用一个示例来说明多点触摸编程框架和键盘输入响应。您将学习如何响应 Android 设备上可用的键盘输入和硬件按钮,如音量+、音量-、电源、主页、菜单、后退、搜索等。向您介绍了 Android 的三个不同的对话框,包括活动对话框主题、特定类对话框和 Toast 提醒。在本章的最后,您将学习如何更改应用属性设置。在下一章中,您将介绍 x86 平台上 android 应用的性能优化。Android 是一个资源有限的系统,因此它在空间和时间上需要非常严格的资源利用。与桌面系统相比,Android 应用的性能优化更为关键和紧迫。您将首先介绍 SOC 性能优化的基本原则,然后介绍基于英特尔架构的 Android 开发的性能优化原则和方法。

Creative Commons Open Access This chapter is licensed under the terms of the Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License ( http://​creativecommons.​org/​licenses/​by-nc-nd/​4.​0/​ ), which permits any noncommercial use, sharing, distribution and reproduction in any medium or format, as long as you give appropriate credit to the original author(s) and the source, provide a link to the Creative Commons licence and indicate if you modified the licensed material. You do not have permission under this licence to share adapted material derived from this chapter or parts of it. The images or other third party material in this chapter are included in the chapter’s Creative Commons licence, unless indicated otherwise in a credit line to the material. If material is not included in the chapter’s Creative Commons licence and your intended use is not permitted by statutory regulation or exceeds the permitted use, you will need to obtain permission directly from the copyright holder.

十一、x86 平台上 Android 应用的性能优化

性能优化是每个应用开发人员想要追求的最重要的目标之一,无论该应用是用于一般的桌面 Windows 计算机还是 Android 设备。Android 是一个资源有限的系统,因此它需要非常严格的资源利用。与桌面系统相比,Android 应用的性能优化更为关键。

不同的应用在优化方面有不同的关注点。Android 系统的性能优化通常分为三类:应用运行速度、代码大小和功耗。一般来说,英特尔凌动处理器上 Android 的存储空间和成本并不是瓶颈,因此本章重点关注使应用运行更快的性能优化。第十三章介绍了功耗优化。

本章首先介绍了片上系统(SoC)性能优化的基本原理,然后介绍了基于英特尔架构的 Android 开发的性能优化原理和方法。第十二章讨论使用原生开发套件(NDK)在英特尔架构上为 Android 开发应用。

性能优化的原则

优化应用的性能实际上意味着优化应用的执行速度。优化旨在减少完成特定任务所需的时间。这是通过基于硬件或软件对应用进行结构调整来实现的。

当您优化应用的性能时,您需要遵循几个基本原则:

  • 等值原则:性能优化后,应用执行的结果没有变化。
  • :性能优化后,目标代码运行速度更快。
  • :有时性能优化在某些方面实现了性能提升,但在其他方面却降低了性能。在确定是否需要性能优化时,您必须考虑综合的整体性能。

最重要的一个考虑因素涉及到时间和空间的交易。例如,为了执行函数计算,可以预先计算函数值,并将其作为表格放入程序存储区(内存)。程序在运行时,不用花时间反复计算函数,程序可以直接从表中取值,减少执行时间。类似地,可以使用散列方法在大空间上进行搜索,从而消除对比较操作的需要。

性能优化基于各种技术。下面几节描述几个主要的。

减少指令和执行频率

优化性能最常用的技术包括减少指令和执行频率。比如从数据结构和算法的角度来看,冒泡排序中比较和交换的指令需要执行 O (n 2 次。但是,通过使用快速排序,可以将指令的执行次数减少到 O (n log n)。

在循环优化中,代码运动可以从循环中提取无关的公共代码,将公共代码的执行时间从 N 减少到 1,从而大幅降低执行频率。另外,可以使用 C 和 C++ 支持的内联函数,避免嵌入函数调用代码;您可以省略函数调用指令和返回指令的实现。

选择更快的指令

您可以使用不同的指令执行相同的功能。不同的指令占用不同的机器时钟周期,因此执行时间也不同。这为您提供了选择更快指令的机会。

降低计算强度是通过选择更快的指令集来实现性能优化的一个典型例子。例如,通过将运算符左移两位,可以将一个整数乘以 4。移位指令占用的时钟周期比乘法或除法指令少得多,运行速度也快得多。

另一个例子是使用硬件提供的特殊指令来代替通用指令。例如,英特尔凌动处理器支持流式 SIMD 扩展(SSE)指令集。对于向量运算,您应该总是使用 SSE 指令:由于指令级并行处理,它们运行得更快。英特尔凌动处理器的普通加法指令宽度为 32 位,而 SSE 指令能够处理四倍于 32 位的数据。因此,使用 SSE 指令优化的代码大大缩短了消耗的时间。

提高并行度

您可以在多个级别提高并行度,包括指令、表达式、函数和线程。许多现代嵌入式处理器,包括英特尔凌动处理器,都支持指令流水线执行。这使您可以使用一种称为指令级并行的优化方法。代码链可以分解成几个不依赖于链的代码单元,可以在流水线中并行执行。

此外,许多嵌入式系统处理器,如英特尔凌动处理器,在物理上支持线程的并发执行。使用适当数量的并发线程而不是单线程可以提高运行速度。为了利用线程并发优化,您需要有意识地采用多线程技术;有时优化必须在编译器的支持下完成。

有效使用寄存器缓存

读写缓存寄存器比读写内存要快得多。缓存优化的目标是将正在使用和将要使用的数据和指令放在缓存中,以降低缓存命中率和减少缓存冲突。缓存优化经常出现在嵌套循环的优化过程中。寄存器优化包括有效使用寄存器,并尽可能多地将常用数据保存在寄存器中。

缓存基于局部性。也就是说,高速缓存假定要使用的数据位于已经在使用的最新数据中,或者在它自己的寄存器附近。这被称为局部性原理或局部性原理,它深深地影响着硬件、软件和系统的设计和性能。处理器所需的指令和数据总是首先被高速缓存访问读取。如果高速缓存有需要的数据,处理器总是直接访问高速缓存。在这种情况下,这样的访问称为;如果高速缓存不包含所需的数据,这被称为失败的命中或缓存未命中。

如果发生这种情况,处理器需要将数据从内存复制到高速缓存中。如果高速缓存的相应位置被其他数据占用,缓存中不再需要的数据将被驱逐并写回内存。失败的命中导致访问时间急剧增加;因此,提高缓存效率的目标是提高命中率和降低故障率。缓存和内存之间的数据交换是通过块单元完成的,块单元用于块复制或写回包含所需数据的块,以及将块写回内存。

局部性有两种含义:

  • :由于时间局部性,同一数据对象可能会被多次重用。一旦一个数据对象在一次失败的命中后被复制到缓存中,这个对象就会有许多后续的命中。后续点击比最初失败的点击运行得更快。
  • :一个块通常包含多个数据对象。由于空间局部性,失败命中后的块拷贝的成本由对其他对象的后续引用来分担。

性能优化方法

许多方法和技术可用于性能优化。您可以同时使用一种方法或多种全面的优化原则,例如修改源代码以提高运行速度。根据标准的类型,优化方法可以分为不同的类别。

根据优化是否与硬件相关,它可以是机器相关的优化,也可以是机器无关的优化。在机器相关的优化中,应用和代码执行与机器的特性无关。这些技术适用于所有的机器。例如,将代码移出循环、消除归纳变量和使用强度降低技术可以应用于任何机器或架构(x86 或 ARM)以获得相同的最佳结果。

依赖于机器的优化只能在特定的硬件或架构上进行。例如,将普通向量指令计算切换到使用 SSE 指令取决于英特尔凌动处理器的许多底层细节,并且只能在支持 SSE 指令的英特尔处理器上使用。一般来说,与机器无关的优化比与机器相关的优化更复杂、更难实现。

性能优化方法

在理想的情况下,编译器应该能够编译您编写的任何代码,并将其优化为最高效的机器代码。但事实是,编译器只能自动化所有可能的优化中的一部分,并且优化可能会被优化拦截器拦截。根据人工或自动化工具的作用大小,性能优化可以由编译器自动执行,由程序员手动执行,或者在开发工具的帮助下执行。以下部分介绍了几种可以用来实现性能优化的方法。

编译器的自动优化

现代编译器可以自动完成最常见的代码优化,这是优化的首选方式。这也称为编译器优化或编译优化。它必须由适当的扩展或开关变量触发。

使用位于 NDK(Android 本地开发工具包)或英特尔编译器(ICC)的 GCC 编译器(GNU 工具链中的工具之一)可以实现 Android 应用的 C/C++ 代码优化。下一章将详细讨论这个话题。

开发工具辅助的性能优化

很难对一个大型程序进行整体的、全面的优化。幸运的是,对于基于英特尔架构的应用,有许多有用的工具可以帮助您完成优化。例如,英特尔 VTune 放大器、图形性能分析器(GPA)、功耗监控工具等等可以帮助您分析程序并完成优化。

GPA 是一种英特尔产品开发工具,可用于英特尔处理器,如英特尔凌动处理器以及 ARM 设备。英特尔档案器是一个 GNU 工具链工具,可用于所有类型的处理器。您可以使用 Profiler 来创建一个分析过程,显示程序中哪些区域经常执行并使用更多的计算资源,哪些区域不经常实现。分析数据提供了有价值的信息,您可以使用这些信息来完成优化。

profile-guided optimization (PGO)的一个典型例子就是对switch语句的优化(比如 C#中的switch - case语句)。基于收集的样本的概要,在获得每个case语句出现的频率后,您将按照频率对switch语句中的case语句进行排序:最频繁的语句被移到前面(执行该语句需要最少的比较),以通过最少的比较获得最佳结果。

英特尔 GPA 最初是一种用于图形处理单元(GPU)分析的工具。它现在已经发展成为一个分析 CPU 速度、内存分析、帧速率和设备功耗的综合工具。您可以使用 GPA 来获取有关 CPU 负载、工作频率和功耗的信息。它可以指导您优化应用,对于多线程优化尤其有用。英特尔 GPA 不仅是一个速度优化工具,也是一个非常方便的功耗优化工具。更详细的讨论和使用案例将在本章后面的第十三章中介绍。

有了优化工具,当你试图寻找一个优化大型程序的起点时,你将不再迷失方向或困惑。您可以很容易地找到最需要优化的地方:可能最有问题的代码段。快速找到热点可以让您用更少的时间和精力实现优化。当然,性能优化是复杂的。该工具只起到指导和支持的作用——真正的优化还是要由编译器或者你手动完成。

使用高性能库

是一组软件库,通常由硬件 OEM 或特殊 OEM 开发,提供常用的操作和服务。该代码基于处理器特性的组合进行了精心优化,具有比普通代码更高的计算速度。这种高性能数据库充分发挥了处理器的潜力。例如,英特尔集成性能基元(IPP)库已经基于面向处理器的 SSE 指令、超线程/多线程并行流水线执行和瀑布流程进行了优化。

对于一些计算密集型的代码和算法,使用高性能库是一种简单、实用的优化方法,就像站在巨人的肩膀上一样。IPP 可用于数学计算、信号处理、多媒体、图像和图形处理、矢量计算以及其他领域。它使用 C/C++ 编程接口。

手动优化

在优化过程中,你不应该忽略人的因素。一些高级的全局优化,比如优化算法和数据结构,是编译器无法自动完成的。您必须手动完成优化。作为程序员,为了写出高效的代码,你应该学习算法和优化技术,帮助你养成良好的编程习惯和风格。即使编译器可以自动完成优化,程序员仍然需要编写高效的代码来辅助编译器在以下级别进行优化:

  • 源代码(高级语言)级别:您修改源代码以实现更好的算法或数据结构,从而手动完成优化。
  • :有时高级语言不足以达到最佳效果,您需要在汇编语言级别修改代码。在一些关键的计算部分,虽然汇编级优化的过程很繁琐,但性能上的好处是完全值得的。
  • :这种优化通常是通过添加和修改编译器指令来实现的,例如修改典型的编译器指令pragma和增加 OpenMP 中的并行度。

程序交互优化是编程艺术的体现,其成就水平进入了人机合一的境界。这是本章的重点。相对而言,在汇编语言级或指令级编译阶段执行的优化需要您拥有关于处理器架构、硬件、系统等方面的全面专业知识。因此,对于基于英特尔架构的 Android 系统,我们建议在源代码级别优化性能。以下示例介绍了 Android 多线程设计的性能优化。

优化可以通过几种相关的、结构上不可分割的方式来实现,尽管每种方式都有独特的功能。整个过程如图 11-1 所示。

A978-1-4842-0100-8_11_Fig1_HTML.jpg

图 11-1。

Recommended user optimization

如图 11-1 所示,手动优化、编译器优化、高性能库函数捆绑在一起,是优化的最后步骤;您可以选择其中之一来实现优化。手动优化和使用高性能库都涉及修改源代码。在开始这些优化之前,使用优化工具分析程序是至关重要且有益的一步。

英特尔图形性能分析器(英特尔 GPA)

英特尔 GPA 是英特尔几年前推出的一套用于分析和优化的图形工具。它已经发展成为分析处理器运行状态、系统功率和其他功能的综合工具。

英特尔 GPA 简介

英特尔 GPA 仅适用于支持基于英特尔酷睿和英特尔凌动处理器的硬件平台的英特尔处理器。它为 CPU/GPU 速度分析和定制功能提供了一个 GUI。它使您能够在基于英特尔芯片组平台的设备上找到性能瓶颈并优化应用。英特尔 GPA 由系统分析器、帧分析器和软件开发套件(SDK)组成。

英特尔 GPA 系统分析器 2014 R2 版支持基于英特尔凌动处理器的 Android 平台。它提供的功能包括:

  • 实时显示几十个关键指标,包括 CPU、GPU 和 OpenGL ES API
  • 许多图形管线测试,以隔离图形瓶颈
  • 可以使用微软视窗、苹果 OS X 或 Ubuntu 操作系统的主机开发系统

Intel GPA 目前只支持真实的 Android 设备,不支持仿真器的分析。它使用典型的硬件部署模型,也称为,其中主机系统(Windows 和 Ubuntu)和目标设备(基于 Android Intel 的设备)通过 USB 连接,以监控 Android 应用。英特尔 GPA 使用 Android 调试桥(adb)来监控目标设备上的应用:adb 服务器运行在 Android 设备上,英特尔 GPA 作为 adb 客户端应用运行在主机系统上。该结构如图 11-2 所示。

A978-1-4842-0100-8_11_Fig2_HTML.jpg

图 11-2。

Intel GPA configuration for monitoring applications on an Android device

鉴于英特尔 GPA 要求 adb 工作,您应该谨慎。Eclipse 和 Dalvik 调试监控服务器(DDMS)也使用 adb,因此如果 GPA、DDMS 和 Eclipse 同时运行,由于 adb 冲突,英特尔 GPA 可能无法正常工作。使用英特尔 GPA 时,最好关闭其他 Android 软件开发工具,如 Eclipse 和 DDMS。

图 11-3 显示了英特尔 GPA 图形界面监控 Android 设备上运行的应用。

A978-1-4842-0100-8_11_Fig3_HTML.jpg

图 11-3。

The Intel GPA graphic interface monitoring an app running on an Android device

如您所见,英特尔 GPA 有两个主窗口和一个工具栏窗格。左窗格中的树状结构显示了被监控的指示器:

  • 在 CPU 下是聚合 CPU 负载、CPU XX 负载、CPU XX 频率和目标应用 CPU 负载。CPU XX 数量由英特尔 GPA 监控的 CPU 数量决定。要获得 CPU 信息,如内核数量、型号和频率,可以在终端窗口中使用cat /proc/cpuinfo命令。图 11-3 是一款联想 K800 智能手机的截图,该手机采用了单核英特尔凌动 Z2460 处理器;它显示了两个逻辑处理器,因为处理器支持英特尔超线程技术(HTT)。因此,CPU 负载和 CPU 频率中显示了两个项目,索引为 00 和 01。在 CPU XX Load 中,XX 是 CPU 编号:它显示 CPU XX 的负载状态,而 CPU XX Frequency 显示 CPU XX 的频率状态。聚合 CPU 负载是 CPU 的总负载。目标应用 CPU 负载是目标设备上应用的 CPU 负载。
  • 设备 IO 下面是磁盘读取、磁盘写入、网络接收和网络发送。这些指标分别列出了磁盘读取、磁盘写入、发送的网络数据包和通过网络接收的网络数据包的状态和信息。
  • 内存下面是应用驻留内存和可用内存。
  • 下电是电流充放电,提供充放电状态。

默认情况下,右窗格中有两个实时状态显示窗口。这些实时窗口显示指定指示器的类似示波器的状态。横轴是经过的时间,纵轴是相应指示器的值。您可以将索引条目从左窗格拖放到两个窗口之一,以显示该条目的实时指示器。在图 11-3 中,CPU 00 负载已经被拖拽到顶部显示窗口,CPU 01 负载显示在底部显示窗口;纵轴显示 CPU 利用率。最大值是 100%。实时状态显示窗口上方是截屏、暂停显示等工具。您可以使用这些工具调试应用。

安装英特尔 GPA

GPA for Windows 在 Beacon Mountain 安装(Mac OS X 和 Ubuntu OS 主机系统)或英特尔 INDE 安装(Windows 主机系统)期间安装。对于 Ubuntu 主机,到 Intel 网站( http://intel.com/software/gpahttp://software.intel.com/en-us/vcsource/tools/intel-gpa )下载 Intel GPA(本书使用gpa_12.5_release_187105_windows.exe版本进行测试),如图 11-4 。

A978-1-4842-0100-8_11_Fig4_HTML.jpg

图 11-4。

Intel GPA software download site

在 Android 上使用英特尔 GPA

以下示例展示了如何使用英特尔 GPA 监控 Android 设备上的应用。在这种情况下,目标机器是运行在英特尔凌动处理器上的联想 K800 智能手机。

必须满足特殊要求,英特尔 GPA 才能监控和控制 Android 设备上的应用。只有满足这些设定的条件,英特尔 GPA 才能监控应用。您必须遵循这两个步骤:设置 Eclipse 应用参数并生成和部署应用,然后使用 Intel GPA 来监控应用。

这里用作示例的应用的名称是。操作界面如图 11-5(a) 所示。

该应用是一个简单的游戏。用户界面非常基本:只是一个圆形。当用户触摸圆圈内的任何一点并四处拖动时,黑色圆圈会跟随触摸点移动。当用户停止触摸圆圈中的点时,圆圈是静止的。当用户拖动到圆圈外(即圆圈内的初始触摸点)时,圆圈不会移动。如果用户按下手机的后退键,就会弹出退出对话框。点击退出,退出应用,如图 11-5(b) 所示。

A978-1-4842-0100-8_11_Fig5_HTML.jpg

图 11-5。

The MoveCircle application

从应用界面描述来看,应用的主要计算任务集中在拖动圆,不断计算圆的新位置,刷新(重绘)显示。该应用的代码框架应用与第十章第 33 页的“对话框示例”部分相似,因此在此省略源代码。

按照以下步骤使用英特尔 GPA 监控示例应用:

  1. 在 Eclipse 中构建和部署应用。

  2. 使用常规过程创建应用项目。命名应用MoveCircle:

    1. 为项目编写相关代码。文档框架如图 11-6 所示。
  3. 编辑AndroidManifest.xml文件,并添加以下代码:

A978-1-4842-0100-8_11_Fig6_HTML.jpg

图 11-6。

Document framework for the MoveCircle application

1\. <manifest xmlns:android="http://schemas.android.com/apk/res/android

2.     package="com.example.movecircle"

3.     android:versionCode="1"

4.     android:versionName="1.0" >

5.

6.     <uses-sdk

7.         android:minSdkVersion="8"

8.         android:targetSdkVersion="15" />``9.     <uses-permission android:name="android.permission.INTERNET"/>

11.     <application

12.         android:icon="@drawable/ic_launcher"

13.         android:debuggable="true"

14.         android:label="@string/app_name"

15.         android:theme="@style/AppTheme" >

16.         <activity

17.             android:name=".MainActivity"

18.             android:label="@string/title_activity_main" >

19.             <intent-filter>

20.                 <action

21.                 android:name="android.intent.action.MAIN" />

22.                 <category android:name="android.intent.category.LAUNCHER" /> 23.             </intent-filter>

24.         </activity>

25.     </application>

26.

27\. </manifest>

在第 9 行,您添加了一个uses-permission元素,并授予应用 Internet 写/读权限。第 13 行指定应用是可调试的。

  1. 生成应用包,并将应用部署到真正的目标设备上。
  2. 在主机上启动英特尔 GPA 来监控应用。
  3. 将 Android 手机连接到 PC。确保屏幕未被锁定,否则您可能会收到错误消息“电话连接不成功”:
  4. 确保关闭所有使用 adb 的工具,比如 Eclipse 和 DDMS。否则,您可能会得到错误“电话连接不成功。”
  5. (这一步是可选的。)确保 adb 已启动并正在运行:

C:\Documents and Settings>adb devices

List of devices attached

Medfield04749AFB        device

  1. 在 Windows 中,选择开始➤计划➤英特尔图形性能分析器 2012 RS ➤英特尔 GPA 系统分析器以启动英特尔 GPA。

  2. 弹出 Intel GPA 初始窗口,提示要监控的机器,如图 11-7 所示。因为在这种情况下调优目标是一部电话,所以您可以通过单击 Connect 按钮来选择该电话(在这种情况下是 Medfield04749AFB)。

  3. 连接后,英特尔 GPA 会对安装在受监控智能手机上的应用进行初步分析,将应用分为两组:可分析应用和不可分析应用,如图 11-8 所示。

A978-1-4842-0100-8_11_Fig7_HTML.jpg

图 11-7。

Intel GPA interface for connecting to a monitored device

A978-1-4842-0100-8_11_Fig8_HTML.jpg

图 11-8。

Initial interface (apps list) after Intel GPA is connected to the monitored phone

在可分析应用列表中是示例应用。如果英特尔 GPA 无法分析应用,通常是因为应用的参数没有设置,如本节前面所述,或者是因为设备不是根设备。作为一个很好的练习,你可以跳过修改AndroidManifest.xml的步骤 2b;这将导致应用从可分析应用列表中消失,并出现在不可分析应用列表中。

  1. 在可分析应用列表中,单击您希望英特尔 GPA 监控的应用的名称(在本例中为MoveCircle)。应用旁边会出现一个滚动圆圈,显示正在进行的进度。见图 11-9 。

A978-1-4842-0100-8_11_Fig9_HTML.jpg

图 11-9。

App initialization interface in Intel GPA

同时,应用启动屏幕显示在手机上。屏幕提示等待调试器的信息,如图 11-10 所示。注意不要点击强制关闭按钮:等到消息框在界面中自动关闭。

A978-1-4842-0100-8_11_Fig10_HTML.jpg

图 11-10。

Initial screen on the target phone when Intel GPA starts the application to be monitored

  1. 出现英特尔 GPA 监控界面,如图 11-11 所示。

A978-1-4842-0100-8_11_Fig11_HTML.jpg

图 11-11。

Initial Intel GPA Monitoring interface when the application is started

同时,MoveCircle app 开始在手机上运行,如图 11-12 所示。

  1. 将 CPU 00 负载拖放到显示窗口顶部的实时状态显示面板,将 CPU 01 负载拖放到底部的实时状态显示面板。开始与MoveCircle交互:用手指点击拖动圆圈几秒钟,然后停止交互几秒钟。相应的英特尔 GPA 监视器屏幕如图 11-13 所示。

A978-1-4842-0100-8_11_Fig12_HTML.jpg

图 11-12。

The MoveCircle app running on the target phone

A978-1-4842-0100-8_11_Fig13_HTML.jpg

图 11-13。

Intel GPA monitoring the MoveCircle app and displaying CPU loads in real time

在图 11-13 中可以看到一个规律:当你拖动圆圈时,两个 CPU 负载都上升到一定高度;当您不与应用交互时,两个 CPU 的负载会立即下降到接近 0%。应用的主要计算任务集中在圆圈拖动和移动中,当圆圈不移动时,没有或很少计算(低或没有 CPU 负载)。

  1. 要结束英特尔 GPA 分析,请退出应用,如图 11-5(b) 所示。英特尔 GPA 返回到如图 11-9 所示的启动界面。

这个示例只演示了如何监控 CPU 上的负载。当你分析一个正在做 OpenGL 渲染的应用时,英特尔 GPA 更有用;这里的截图没有显示所有的 GPU 和 OpenGL 指标。如果您感兴趣,可以尝试其他示例并监控其他指标。例如,对于MoveCircle,我们为顶部显示窗口选择了磁盘读取指标,为底部选择了磁盘写入指标。在切换应用以查看一些照片文件并返回到MoveCircle后,该动作立即变得明显(见图 11-14 )。

A978-1-4842-0100-8_11_Fig14_HTML.jpg

图 11-14。

Intel GPA monitoring Disk Read and Disk Write for MoveCircle and other apps

Android 多线程设计

英特尔凌动处理器支持超线程和多核配置。多线程设计是增加并行度和提高性能的好方法。英特尔凌动 N 系列处理器支持多线程的并行执行。大多数英特尔凌动处理器都是带超线程技术的双核处理器,最新的 Bay Trail 处理器具有双核或四核处理器,并在物理上支持一定程度的并行执行。

请注意,这里使用的词是平行的,而不是并发的。对于某些任务,您可以遵循经典的分而治之方法,将它们分成两个或更多的基本单元。您将这些单元分配给不同的线程同时执行。通过这种方式,处理器的性能潜力得到了充分利用,您加快了软件的执行速度。因此,软件运行速度更快,效率更高。

基于 Java 多线程编程接口,Android 提供了更强大的多线程编程接口。借助这个编程接口,您可以轻松地在 Java 语言级别实现多线程开发和设计,而无需使用繁琐的底层 OS 调用接口。

一个线程的 Android 框架

Android 线程编程框架是基于 Java 的。Java 中多线程编程有两种方法:从Thread类继承和覆盖run方法;并使用Runnable接口和run方法。

Java 线程编程接口

第一个方法的通用代码框架继承自Thread类,如下所示:

  1. 定义Thread类(在本例中为MyThread)及其代码:

class MyThread extends Thread  // Thread inheritance, custom thread

{

public MyThread()          // Define a constructor

{

super();               // Call the parent class builder to create objects

}

@Override

public void run()          // To write run code in the run method of the thread body

{

......                 // The real Run Code of the thread.

}

}

  1. 开始线程代码:

MyThread myThread = new MyThread();    // create a new thread

myThread.start();                      // start a thread

  1. 等待正在运行的线程结束:

try {

myThread.join();                  // Wait for thread process                                          to end

} catch (InterruptedException e) {

}

第二种方法使用Runnable接口实现。下面是一般的代码框架:

  1. 编写一个自定义的Runnable接口实现类:

class MyRunnableThread implements Runnable  // implement runnable interface

{

public void run()

{

......        // actual implementation codes of the thread

}

}

  1. 开始一个线程:

MyRunnableThread target = new MyRunnableThread();     // create custom runnable interface implementation object//

Thread myThread = new Thread(target); // create a Thread class object

myThread.start();                    // Start Thread

这两种方法效果相同,只是用在不同的场合。如果你熟悉 Java,就知道 Java 在 C++ 中没有多重继承;而是实现接口。要单独实现一个线程,可以使用第一种方法,线程继承。

但是有些类是从另一个类继承的。在这种情况下,如果你想让线程运行,你必须使用第二种方法(Runnable接口)。在这种情况下,您可以声明该类实现了Runnable接口,然后将作为线程运行的代码放到run函数中。这样,它不会影响其先前的继承层次,并且还可以作为线程运行。

关于 Java 的线程框架,请注意以下几点:

  • 在 Java 运行时中,系统实现了一个线程调度器,它决定线程在 CPU 上运行的时间。
  • 在 Java 技术中,线程通常是抢占式的,不需要时间片分配过程(给每个线程分配相等的 CPU 时间)。在抢占式调度模型中,所有线程都处于准备运行状态(等待状态),但只有一个线程在运行。线程继续运行,直到它终止或返回到可运行(等待)状态,或者另一个更高优先级的线程变为可运行。在后一种情况下,低优先级线程终止,将运行权交给高优先级线程。
  • Java 线程调度器支持优先级不同的线程的抢占式方法,但不支持优先级相同的线程的时间片轮换。
  • 如果运行 Java 运行时的操作系统支持时间片的轮换,那么 Java 线程调度器支持具有相同优先级的线程的时间片轮换。
  • 不要过度依赖系统的线程调度程序。例如,低优先级线程也必须获得运行的机会。

关于 Java 多线程编程方法的更多详细信息,可以参考相关的 Java 编程书籍,包括 Learn Java for Android(www.apress.com/9781430264545)、Pro Android Apps 性能优化( www.apress.com/9781430239994 )、Android Recipes ( www.apress.com/9781430246145 )。

Android 线程编程扩展和支持

Android 运行时,系统(DVM)支持并发多 CPU。也就是说,如果机器有不止一个逻辑处理器,DVM 会遵循一定的策略来自动分配不同的线程在不同的 CPU 上运行。这样,Android 可以在物理上并行运行不同的线程。除了 Java 提供的线程编程接口,Android 还提供了重要的扩展和支持。首先是。

Android 的界面,包括各种各样的活动,运行在应用的主线程中(也称为 UI 线程、接口线程或默认线程)。默认情况下,应用只有一个线程,即主线程。因此,应用被认为是单线程的。一些耗时的任务(计算),如果默认在主线程上运行,会导致主界面长时间没有响应。为了防止这种情况,应该将那些耗时的任务分配给独立的线程来执行。

后台运行的独立线程(也称为辅助线程或后台线程)经常需要与主线程的接口进行通信,比如更新显示。如果后台线程调用一个接口对象的函数来更新接口,Android 给出执行错误消息CalledFromWrongThreadException

例如,在一个应用中(这里是GuiExam),如果一个工作线程直接调用界面中TextView对象的setText函数来更新显示,系统会立即遇到错误并终止正在运行的应用,如图 11-15 所示。

A978-1-4842-0100-8_11_Fig15_HTML.jpg

图 11-15。

Running error when a worker thread directly calls a function of the UI object

为了让工作线程和主线程接口进行通信,您需要了解 looper-message 机制。Android 有一个消息队列,可以结合线程、处理处理器和 looper 组件来交换信息。

消息

是线程之间交换的信息。当幕后的线程需要更新接口时,它会向 UI 线程(主线程)发送包含数据的消息。

处理者

是消息的主处理器,负责发送消息以及执行和处理消息内容。后台线程使用传入的处理对象,调用sendMessage(Message)函数来发送消息。要使用 handler,需要一个方法实现类handleMessage(Message),负责处理消息操作内容(比如更新接口)。handleMessage方法通常需要子类化。

该处理程序不用于打开新线程。它更像是主线程的秘书,负责管理来自子线程的更新数据,然后更新主线程中的接口。幕后线程处理sendMessage()方法发送消息,处理程序回调(自动调用)在HandlerMessage方法中的处理来处理消息。

消息队列

用于根据先入先出的执行规则存储处理程序发送的消息。对于每个消息队列,都有相应的处理程序。处理程序使用两种方法向消息队列发送消息:SendMessagepost。这两种方法发送的消息执行方式略有不同 a:SendMessage发送的消息是一个消息队列对象,由处理程序的HandlerMessage函数处理;通过post方法发送的消息是一个可运行的对象,并且是自动实现的。

Android 没有全局消息队列。它自动为主线程(UI 线程之一)建立消息队列,但在子线程中不建立消息队列;所以必须调用Looper.getMainLooper()来获取主线程的 looper。主线程循环不到NULL;而是调用Looper.myLooper()来获取当前线程循环的活套

尺蠖

循环是每个线程的消息队列的管家。它是处理程序和消息队列之间的桥梁。程序组件首先通过处理程序将消息传递给循环程序,然后循环程序将消息放入队列。

对于应用默认 UI 的主线程,系统建立消息队列和 looper:不需要在源代码中写消息队列和 looper 操作代码,两者对默认主线程都是透明的。然而,该处理程序对于默认的主线程是不透明的。为了向主线程发送消息并处理该消息,您必须建立自己的处理程序对象。

除了使用 looper-message 机制来实现工作线程和主 GUI 线程之间的通信之外,还可以使用一种称为异步任务(AsyncTask)机制的技术来实现这些线程之间的通信。AsyncTask 框架的一般用法如下:

  1. 异步任务。
  2. 实现由以下一种或几种方法定义的 AsyncTask:
    • onPreExecute():开始执行任务前的准备工作doInBackground(Params...):开始后台执行。您可以调用publishProgress函数来更新实时任务进度。
    • onProgressUpdate(Progress...):调用publishProgress函数后,UI 线程调用该函数显示任务界面的进度——比如显示进度条。
    • onPostExecute(Result):操作完成后,将结果发送给 UI 线程。

这些函数都不能手动调用。除了doInBackground(Params...)函数,其余三个都是 UI 线程调用的,所以要求是:

  1. 必须在 UI 线程中创建AsyncTask实例;
  2. 必须在 UI 线程中调用AsyncTask.execute函数。

请记住,该任务只能执行一次。多次通话不正常。您可以在 Android 帮助文档中找到详细的 AsyncTask 示例。

线程示例

本节使用一个例子来说明 Android 线程编程。运行中的GuiExam应用如图 11-16 所示。

A978-1-4842-0100-8_11_Fig16_HTML.jpg

图 11-16。

Demo UI of a multithreaded code framework

如图 11-16 所示,演示 app 有三个主要的活动按钮:开始线程运行、停止线程运行、退出 App。前两个控制辅助线程的操作。点击开始线程运行按钮,线程开始运行,如图 11-16(b) 所示。点击停止线程运行结束线程运行,如图 11-16(c) 所示。工作线程每半段刷新一次TextView中的文本显示,显示完整的步骤。x 从 0 到 x 递增。单击退出应用关闭活动并退出应用。

演示应用和忍耐力的结构如下:

  1. activity_main类编辑MainActivity.java,如下所示:

A978-1-4842-0100-8_11_Fig17_HTML.jpg

图 11-17。

Multithreaded code framework in activity_main.xml

  1. 编辑主活动文件(activity_main.xml),删除originalTextView窗口组件,然后添加三个按钮和两个TextView窗口组件。按钮的ID属性分别为@+id/startTaskThread@+id/stopTaskThread@+id/exitAppText属性分别是开始线程运行、停止线程运行、退出 App。TextView的 ID 属性是@+id/taskThreadOuputInfo,用于显示工作线程的文本输出。整个过程如图 11-17 所示。

1.  package com.example.guiexam;

2.  import android.os.Bundle;

3.  import android.app.Activity;

4.  import android.view.Menu;

5.  import android.widget.Button;

6.  import android.view.View;

7.  import android.view.View.OnClickListener;

8.  import android.os.Process;

9.  import android.widget.TextView;

10\. import android.os.Handler;

11\. import android.os.Message;

12\. public class MainActivity extends Activity {

13.     private Button btn_StartTaskThread;

14.     private Button btn_StopTaskThread;

15.     private Button btn_ExitApp;

16.     private TextView threadOutputInfo;

17.     private MyTaskThread myThread = null;

18.     private Handler mHandler;;

19.     @Override

20.     public void onCreate(Bundle savedInstanceState) {

21.         super.onCreate(savedInstanceState);

22.         setContentView(R.layout.activity_main);

23.         threadOutputInfo = (TextView)findViewById(R.id.taskThreadOuputInfo);

24.         threadOutputInfo.setText("Thread Not Run");

25.         mHandler = new Handler() {

26.             public void handleMessage(Message msg) {

27.                switch (msg.what)

28.                 {

29.                 case MyTaskThread.MSG_REFRESHINFO:

30.                     threadOutputInfo.setText((String)(msg.obj));

31.                     break;

32.                 default:

33.                     break;

34.                 }

35.             }

36.         };

37.         btn_ExitApp = (Button) findViewById(R.id.exitApp); // Code for <Exit App>Button

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

39.             public void onClick(View v) {

40.                 finish();

41.                 Process.killProcess(Process.myPid());

42.             }

43.         });

44.         btn_StartTaskThread = (Button) findViewById(R.id.startTaskThread);

45.         // Code for<Start Thread Run>

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

47.             public void onClick(View v) {

48.                 myThread = new MyTaskThread(mHandler);  // Create a thread

49.                 myThread.start();    // Start Thread

50.                 setButtonAvailable();

51.              }

52.         });

53.         btn_StopTaskThread = (Button) findViewById(R.id.stopTaskThread);

54.         //code for <Stop Thread Run>

55.         btn_StopTaskThread.setOnClickListener(new /*View.*/OnClickListener(){

56.   public void onClick(View v) {

57.      if (myThread!=null``&&

58.                     myThread.stopRun();

59.                 try {

60.                     if (myThread!=null){

61.                         myThread.join();

62.                         // Wait for Thread Run to end

63.                         myThread =null;

64.                     }

65.                 } catch (InterruptedException e) {

66.                     // Empty statement block, ignored forcibly abort exception

67.                 }

68.                 setButtonAvailable();

69.              }

70.         });

71.         setButtonAvailable();

72.     }

73.     @Override

74.     public boolean onCreateOptionsMenu(Menu menu) {

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

76.         return true;

77.     }

78.     private void setButtonAvailable()    // New function is used to set the button optional

79.     {

80.         btn_StartTaskThread.setEnabled(myThread==null);

81.         btn_ExitApp.setEnabled(myThread==null);

82.         btn_StopTaskThread.setEnabled(myThread!=null);

83.     }

84\. }

第 17 行和第 18 行分别定义了已定义线程类的变量MyTaskThread,以及默认的主线程处理程序对象mHandler。第 25–36 行定义了该类。消息类的What属性字段表示消息的类型。自定义处理程序类根据消息类型对不同的处理程序使用switch - case语句;MSG_REFRESHINFO是自定义线程类MyTaskThread的消息类型,表示工作线程需要更新界面显示消息。第 29–31 行处理消息。代码非常简单;它根据参数对象中的消息更新TextView小部件显示。

第 47–49 行是单击 Start Thread Run 按钮时的响应代码。它首先创建自定义的线程对象,然后调用Thread.start函数使自定义的线程类MyTaskThread运行,该类将run函数中的执行代码作为单线程运行。最后,第 49 行调用自定义函数来设置每个按钮的选项(灰色不可选,或者白色可选)。

第 55–65 行是“停止线程运行”按钮的响应代码。第 55 行首先确定线程是否已经存在或者正在运行。然后,它通过调用自定义线程类MyTaskThread中定义的原型函数,然后调用Thread.join()来停止第 56 行中的线程运行;然后,它等待线程运行结束。最后,它设置界面按钮的可选状态。

第 75–80 行是一个自定义函数,用于确定每个按钮的可选状态:白色可选或灰色可选。

  1. 在应用中创建一个新的类MyTaskThread。这个类继承自Thread,用于实现工作线程。这个类的源代码文件MyTaskThread.java如下:

1\. package com.example.guiexam;

2\. import android.os.Handler;

3\. import android.os.Message;

4.

5\. public class MyTaskThread extends Thread {

6.     private static final int stepTime = 500;

7\. // Execution timeof each step(unite:ms)

8.     private volatile boolean isEnded;

9\. // mark if the thread is running. Used to stop thread run

10.      private Handler mainHandler;

11\. // Handler used to send message

12.     public static final int MSG_REFRESHINFO = 1;  // Update message on interface

13.

14.     public MyTaskThread(Handler mh)   // Define a constructor

15.     {

16.         super();   // Call the parent class builder to create objects

17.         isEnded = false;

18.         mainHandler = mh;

19.     }

20.

21.     @Override

22.     public void run()  // Write run code in thread body run method

23.     {

24.         Message msg ;

25.         for (int i = 0; !isEnded; i++)

26.         {

27.             try {

28.                 Thread.sleep(stepTime);  // designate time for every  step of the thread to sleep

29.                 String s = "Complete" + i +"step";

30.                 msg = new Message();

31.                 msg.what = MSG_REFRESHINFO;  // Define message type

32.                 msg.obj = s;   // attach data to message

33.                 mainHandler.sendMessage(msg);  // send message

34.             } catch (InterruptedException e) {

35.                 e.printStackTrace();

36.             }

37.         }

38.     }

39.

40.     public void stopRun()  // Stop control function for stop thread run

41.     {

42.         isEnded = true;

43.     }

42\. }

这个文档是自定义线程类的实现代码,它是这个应用的关键。应用使用第一种方法,线程继承,来实现线程化。第 5 行,自定义类继承自Thread;然后,从第 14–39 行开始,线程在重写的run函数上运行代码。为了处理线程的工作,第 6–9 行定义了相关的变量。常量stepTime代表线程延迟时间的每一步的长度,以毫秒为单位。isEnded控制是否继续run功能中循环体的每一步。注意,变量前面有一个volatile修饰符:每次线程访问变量时,都会在变量被修改后读取内存中的最终值。写请求也必须写入内存。这避免了缓存或寄存器中的副本与内存变量中的值不匹配,否则会导致错误。mainHandler变量保存主线程处理程序。MSG_REFRESHINFO是处理自定义消息的常量。

第 10–15 行是一个构造函数。在这个函数体中,初始化线程运行控制变量isEnded的值,然后将mainHandler保存为作为参数传递的主线程处理程序对象。

第 16–33 行是重写run函数的核心线程代码。代码由一个循环组成,决定是否继续使用控制变量isEnded。这里一个循环是一个步骤。每一步也很简单:在指定的时间后,当第 28 行调用Thread类静态函数sleep时,会生成一条消息,并在第 24–27 行组装。最后,在第 28 行,消息被发送到指定的(消息循环)处理程序。

第 34–37 行是一个定制的控制函数,用来停止线程的运行。代码的目的非常简单:改变运行循环控制变量的值。

线程同步

多线程进程不可避免地涉及一个问题:如何处理线程对共享数据的访问,这涉及到线程同步。线程数据共享也称为临界区。对共享数据的访问也被称为对资源访问的竞争。在一般的操作系统教科书中,线程同步不仅包括这种被动选择访问共享数据的同步,还包括线程间主动选择协作完成任务的同步。在 Java 中,线程同步关注的是对共享数据的访问。本节讨论与共享数据访问相关的同步问题。

在多线程编程中,如果对共享数据的访问不使用某些同步机制,就无法保证数据的一致性和完整性。有两种方法可以执行 Java 线程同步:内部锁数据对象和同步。这两种方法都是用关键字synchronized实现的。由synchronized块修改的语句可以保证线程间操作的排他性:它们是唯一的,或者说是原子的。在 Java 中,这个过程简称为同步。同步块也称为。

在锁定数据对象的第一种方法中,在任何时候,只有一个线程可以访问被锁定的对象。代码框架如下:

Object var;    // Object variable

synchronized (var) {

... ...    // Operation of the shared variable

}

在这段代码中,var必须是每个线程都可以访问的变量,所以就成了同步变量。实际上,同步变量和共享变量可以相同,也可以不同。前面代码中的Object类可以替换为Object的子类,因为除了 Java 中的简单类,任何类都可以是Object的后代类。

注意,同步变量不能是简单类型(如intfloat,但不能是String类):

int var;

synchronized(var) {``// compiler error:??

... ...

}

当您使用第二种方法(同步方法)时,任何时候都只有一个线程访问一个代码段:

class MyClass {

public``synchronized

{ ... }

}

前面的代码是通用类(函数)的同步。此外,该类的静态函数也有同步:

class MyClass {

public``synchronized static

{ ... }

}

使用同步方法,调用同步方法的对象被锁定。当MyClass: obj1的一个对象在不同的线程中实现同步方法时,互斥实现同步结果。但是由类MyClass生成的另一个对象obj2,可以用关键字synchronized调用这个方法。因此,前面的代码可以等价地写成如下所示:

  • 同步(常规)方法:

class MyClass {

public void method1()

{

synchronized (this)

{ .../* function body */ }

}

}

  • 静态同步方法:

class MyClass {

public static void method2()

{

synchronized (MyClass.class)

{ .../* function body */ }

}

}

在静态方法中,类文字被视为锁。它生成与synchronized静态函数相同的结果。获取锁的时机也很特殊:锁是在调用该对象所属的类时获取的,而不再是该类生成的特定对象。

以下是 Java 通过synchronized函数实现锁的通用规则:

  • 规则 1:当两个并行线程访问同一个对象的synchronized(this)同步代码段时,任何时候只能运行一个线程。其他线程必须等到当前线程运行完该代码段后才能运行同一代码段。
  • 规则二:当一个线程访问一个对象的synchronized(this)同步代码段时,另一个线程仍然可以访问一个对象的非synchronized(this)同步代码段。
  • 规则 3:当一个线程访问一个对象的synchronized(this)同步代码段时,所有其他线程对该对象的所有其他synchronized(this)同步代码段的访问都被阻塞。
  • 规则 4:当一个线程访问一个对象的synchronized(this)同步代码段时,它获取该对象的对象锁。因此,其他线程对一个对象的所有synchronized(this)同步代码段的访问都被暂时锁定。
  • 规则 5:这些规则适用于所有其他对象锁。

虽然synchronized可以保证对象或执行的语句块的粒度,但是这种粒度的互斥性降低了线程并发性;因此,原本可以并行运行的代码必须串行执行。因此,您在使用synchronized功能时需要小心,并将其限制在需要synchronized锁的情况下。另一方面,您应该使锁粒度尽可能小,以便既确保程序的正确性,又通过使并发程度尽可能大来提高操作效率。

线程通信

在多线程设计中,随着线程之间的数据交换,设置信号协作来完成任务是一个常见的问题。最重要的是一般性的线程问题,比如生产者-消费者问题的典型例子。这些是必须合作完成任务的线程。

在操作系统的经典书籍中,通常建议您使用信号量来实现线程同步原语。Java 不直接提供信号量原语或编程接口,而是用waitnotifynotifyAll等类函数来实现信号量的功能。

waitnotifynotifyAll属于Object类的功能,不属于Thread类。Java 中每个对象都有一个等待队列(Wait Set)。当一个对象刚刚被创建时,它的等待队列是空的。

该函数可以使当前线程中的对象等待,直到另一个线程调用该对象的notifynotifyAll方法。换句话说,当调用在对象队列中等待时,线程进入等待状态。只有当notify方法被调用时,你才能从队列中移除线程,使其成为可运行线程。notifyAll方法等待对象内队列中的所有线程成为可运行线程。NotifynotifyAll功能相似。

waitnotifynotifyAll函数需要和synchronized一起使用来建立同步模型,这样可以保证前面函数的粒度。例如,在调用wait之前,你需要获得对象的同步锁,这样这个函数才能被调用。否则,编译器可以调用wait函数,但会收到一个IllegalMonitorStateException运行时异常。

下面是几个关于waitnotifynotifyAll的代码框架的例子:

  • 等待资源代码:

synchronized(obj) {

while(!condition)

try {

obj.wait();

} catch (InterruptedException e) {

}

......Use code of obj

}

  • 提供资源(例如:完全使用资源并返回系统):

synchronized(obj) {

condition = true;

obj.notify();

}

前面的代码是同步对象obj的独立用例。您也可以在类中编写同步代码。这段代码的框架可以写成如下:

class MyClass{

public synchronized void func1 {

while (!condition)

try {

wait();

} catch (InterruptedException e) {

}

...... codes for using MyClass resource

}

public synchronized void func2 {

condition = true;

notifyAll();

}

}

等待资源的线程可以调用myclass.func1函数,提供资源的线程调用myclass.func2函数。

英特尔凌动处理器的多线程优化原则

多线程软件设计允许不同线程中的程序代码同时运行。然而,盲目使用多线程或过度使用多线程编程可能不会带来性能的提升,甚至可能会降低软件性能。所以你需要了解 Android x86 上多线程优化的原理。

首先,线程的启动或调度需要一定量的开销,并占用一定量的处理器时间。不支持超线程和多核处理的处理器无法在物理上让这些线程同时运行。为了支持多线程程序,如果您使用虚拟化技术将一个物理处理器分成多个逻辑处理器,以便每个线程都可以在一个逻辑内核上运行,则会产生巨大的开销。这种多线程策略不仅难以提高性能,甚至可能导致多线程的执行速度比单线程程序慢。因此,要使用多线程设计实现多线程性能加速(比单线程执行速度更快的先决条件),处理器必须支持超线程或多核。

其次,对于支持超线程或多核的处理器来说,线程越多软件运行越快并不总是正确的。你必须考虑性价比。性能调优的多线程设计的物理基础是允许多个线程在物理层上同时并行运行。因此,处理器支持的并发运行线程的最大数量是多线程优化的最佳线程数量。

根据英特尔官方的说法,英特尔超线程技术可以支持两个线程并行运行,多核支持多个线程并行运行。例如,对于支持英特尔超线程技术的双核英特尔处理器,支持并行运行的最大线程数为

A978-1-4842-0100-8_11_Figa_HTML.jpg

因此,这台机器支持多线程优化,线程(并发运行的线程)的最大数量等于四。

对于使用支持超线程技术的单核英特尔凌动 Z2480 处理器的摩托罗拉 MT788 目标机器,最佳线程数量为两个。如果目标机器是配备英特尔超线程技术的双核英特尔凌动 Z2580 处理器的联想 K900,则最佳线程数为 4。

一般来说,当你在 Android 平台上考虑多线程优化时,有必要仔细查看处理器信息,看看它是否支持超线程或多核技术。

案例研究:英特尔 GPA 辅助的 Android 应用多线程优化

上一节解释了几种优化技术和原则。本节使用一个全面的示例来解释优化。在这种情况下,多线程优化与英特尔 GPA 辅助优化相结合,使应用运行更快。

示例应用计算 pi (π)。让我们来看看这个应用的一些背景。数学公式如下:

A978-1-4842-0100-8_11_Figb_HTML.jpg

积分公式可以用不定式来表示:

A978-1-4842-0100-8_11_Figc_HTML.jpg

x 不可能无限小——你只能让 x 尽可能小。所以,公式的结果更接近π。用步长表示 x,

A978-1-4842-0100-8_11_Figd_HTML.jpg

步长的值必须是最大值,以获得 pi 的精确值。考虑一下

A978-1-4842-0100-8_11_Fige_HTML.jpg

而 f(x)是一个凸函数。这里你取一个中间值来计算总和。也就是说,您使用

A978-1-4842-0100-8_11_Figf_HTML.jpg

替换

A978-1-4842-0100-8_11_Figg_HTML.jpg

计算总数。该公式计算的结果并不总是小于π的实际值。所以,最终,你得到了这个应用所基于的最终公式:

A978-1-4842-0100-8_11_Figh_HTML.jpg

根据这个公式写源代码并不难。

原始应用和英特尔 GPA 分析

首先,从上一节中的公式推导出应用的非优化计算源代码。这个应用被命名为SerialPi

这个 app 的设计和前面“线程示例”一节的设计是一样的。计算π的任务放在一个工作线程(这里称为任务线程)中运行。在 main activity 上设置一个按钮来控制线程的运行,用一个TextView来显示任务线程的结果。显示 app 单次运行的界面如图 11-18 所示。

A978-1-4842-0100-8_11_Fig18_HTML.jpg

图 11-18。

SerialPi app interface

应用启动后的界面如图 11-18(a) 所示。当您单击开始计算按钮时,界面上的所有按钮都会变灰,直到计算完成。然后,界面显示计算结果以及线程的总运行时间。点击退出应用,如图 11-18(b) 所示,退出应用。从界面画面可以看到,这个 app 计算π大概需要 22 秒。重复运行应用,计算时间保持不变(22 秒)。

构建应用和编写关键代码的步骤如下:

  1. 创建一个名为。建议的项目属性应使用默认值。设置[Build SDK]支持 x86 API。

  2. 编辑activity_main.xml。在布局中放置两个Button组件和两个TextView组件。将一个TextViewID属性设置为@+id/taskOuputInfo:显示任务线程的结果,如图 11-19 所示。

  3. 在项目中创建新的线程类MyTaskThread,并编辑源代码文件MyTaskThread.java,如下所示:

A978-1-4842-0100-8_11_Fig19_HTML.jpg

图 11-19。

Layout for the SerialPi App

1\. package com.example.serialpi;

2\. import android.os.Handler;

3\. import android.os.Message;

4\. public class MyTaskThread extends Thread {

5.     private Handler mainHandler;

6.     public static final int MSG_FINISHED = 1;

7\. // Defined the message type  for the end of the calculation

8.     private static final long num_steps = 200000000;

9\. // num_steps variables in Formula, the total number of steps

private static final double step = 1.0 / num_steps;

10\. // Step variable  in formula, step length

11.     public static double pi = 0.0;

12.//π的计算结果

13.

14.     static String msTimeToDatetime(long msnum){

15\. // The function converts the number of milliseconds into hours: minutes: seconds. Milliseconds "format

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

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

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

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

20.         hh = tt % 60;

21.         String s = "" + hh +"hour "+mm+"minute "+ss + "第二次" + ms +"Milliseconds";

22.         return s;

23.     }

24.

25.     @Override

26.     public void run()

27.     {

28.         double x, sum = 0.0;

long i;

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

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

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

31.         }

32.         pi = step * sum;

33.         Message msg = new Message();

34.         msg.what = MSG_FINISHED;       // Define message Type

35.         mainHandler.sendMessage(msg);  // Send Message

36.     }

37.

38.     public MyTaskThread(Handler mh)    // Constructor

39.     {

40.         super();

41.         mainHandler = mh;

42.     }

43\. }

与前面 thread 示例中列出的框架和示例代码类似,线程继承法则用于初始化线程。请密切注意粗体显示的代码段,它们与π的计算最直接相关。第 7 行和第 8 行定义了一个静态变量,其名称与计算π的公式中使用的名称相同。第 9 行定义了保存π计算结果的变量。注意,这个变量是公共的,所以主线程可以访问它。

A978-1-4842-0100-8_11_Figi_HTML.jpg

第 22–28 行根据公式计算π。x变量是函数的自变量,sum是σ的累积变量。第 28 行计算最终结果。请参考第 32 页本章前面部分中提到的代码框架。”线程示例”;应该不难理解。

注意,在线程的run函数中,一旦计算完成,消息将在第 29 行被发送到主线程(接口)。

  1. 编辑主活动类文件MainActivity.java中的源代码。这段代码控制线程的运行并显示计算结果:

1\. package com.example.serialpi;

2\. import android.os.Bundle;

3\. import android.app.Activity;

4\. import android.view.Menu;

5\. import android.widget.Button;

6\. import android.view.View;

7\. import android.view.View.OnClickListener;

8\. import android.os.Process;

9\. import android.widget.TextView;

10\. import android.os.Handler;

11\. import android.os.Message;

12\. public class MainActivity extends Activity {

13.     private MyTaskThread myThread = null;

14.     private TextView tv_TaskOutputInfo;  // Display (Calculated) Task thread output

15.     private Handler mHandler;;

16.     private long end_time;

17.     private long time;

18.     private long start_time;

19.     @Override

20.     public void onCreate(Bundle savedInstanceState) {

21.         super.onCreate(savedInstanceState);

22.         setContentView(R.layout.activity_main);

23.         tv_TaskOutputInfo = (TextView)findViewById(R.id.taskOuputInfo);

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

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

26.             public void onClick(View v) {

27.                 exitApp();

28.             }

29.         });

30.         final Button btn_StartTaskThread = (Button) findViewById(R.id.startTaskThread);

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

32.             public void onClick(View v) {

33.                 btn_StartTaskThread.setEnabled(false);

34.                 btn_ExitApp.setEnabled(false);

35.                  startTask();

36.               }

37.         });

38.         mHandler = new Handler() {

39.             public void handleMessage(Message msg) {

40.                switch (msg.what)

41.                 {

42.                 case MyTaskThread.MSG_FINISHED:

43.                     end_time = System.currentTimeMillis();

44.                     time = end_time - start_time;

45.                     String s = " The end of the runPi="+ MyTaskThread.pi+ "  Time consumed:"

46.                             +

47\. MyTaskThread.msTimeToDatetime(time);

48.                     tv_TaskOutputInfo.setText(s);

49.                     btn_ExitApp.setEnabled(true);

50.                     break;

51.                 default:

52.                     break;

53.                 }

54.             }

55.         };

}

56.

57.     @Override

58.     public boolean onCreateOptionsMenu(Menu menu) {

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

60.         return true;

}

61.

62.     private void startTask() {

63.         myThread = new MyTaskThread(mHandler);    // Create a thread

64.         if (! myThread.isAlive())

65.         {

66.                start_time = System.currentTimeMillis();

67.             myThread.start();  // Start thread

68.         }

}

69.

70.     private void exitApp() {

71.         try {

72.             if (myThread!=null)

73.             {

74.                 myThread.join();

75.                 myThread = null;

76.             }

77.         } catch (InterruptedException e) {

78.         }

79.         finish();   // Exit the activity

80.         Process.killProcess(Process.myPid());  // Exit the application process

81.     }

}

这段代码类似于“线程示例”一节中示例MainActivity类的代码框架。添加以灰色背景显示的代码行是为了估计任务的运行时间。第 16–18 行定义了三个变量:start_time是任务的开始时间,end_time是任务的结束时间,time是任务的运行时间。这三个变量是以下公式的一部分:

time = end_time - start_time

在第 65 行,当您启动任务线程时,机器的当前时间同时被记录在start_time变量中。在第 43–44 行,当收到任务线程已经完成运行的消息时,机器的时间被记录在end_time中。currentTimeMillis函数是java.lang包中 Java System类提供的静态函数;它以毫秒为单位返回当前时间。

  1. 参考“线程通信”部分的示例,修改项目的AndroidManifest.xml文件,使其符合英特尔 GPA 监控的要求。

编码完成后,您已经编译并生成了应用,将其部署到目标设备。

现在,您可以使用英特尔 GPA 来分析该应用。请参见“线程通信”一节中的步骤。首先,您监视和分析两个 CPU 负载(CPU XX 负载指示器)。在监控期间,单击开始按钮开始运行并监控英特尔 GPA 下记录的信息。分析结果如图 11-20 所示。

A978-1-4842-0100-8_11_Fig20a_HTML.jpgA978-1-4842-0100-8_11_Fig20b_HTML.jpg

图 11-20。

Intel GPA analysis screen for SerialPi

图 11-20(a) 显示点击开始按钮时的分析,图 11-20(b) 显示任务线程运行,图 11-20(c) 显示运行结束时的任务线程。从三个屏幕可以看到,在应用开始运行之前和运行结束之后,CPU 上的负载都保持在较低的水平。一旦计算任务线程开始运行,CPU 上的负载急剧上升到 100%的负载。您还可以看到,当任务线程正在运行时,两个 CPU 中只有一个处于满负荷状态;另一个是在低负载水平。通过分析图表,您可以看到 100%的负载并不总是出现在特定的 CPU 上。相反,100%的负载在两个 CPU 之间交替,这反映了 Java 运行时对任务调度的支持:处理器系统对应用是透明的。尽管两个 CPU 的负载率会发生变化,但负载率是一种互补状态:一个 CPU 上的负载增加意味着另一个 CPU 上的负载减少。因此,总负载(任何时候两个 CPU 的负载之和)不会超过单个 CPU 的 100%负载。

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

前面的示例使用直接从计算π的公式中导出的代码。有优化的空间吗?答案肯定是肯定的。这样做需要您检查应用的算法并应用您所学的优化原则,充分利用英特尔凌动处理器的硬件特性。

您如何挖掘英特尔凌动处理器的全部性能潜力?如前所述,采用英特尔超线程技术的多核英特尔凌动处理器支持在多个物理内核上并行运行多线程。例如,联想 K900 手机使用英特尔凌动 Z2580 处理器,并支持并行运行的两个线程。这是你算法优化的切入点:可以分而治之。通过仔细分析上一节示例MyTaskThread类中的run函数,可以使分配给多个(在本例中是两个)线程的计算任务运行;而并行运行的线程可以让 app 运行得更快。

为了计算π的积分面积的累积值,在第 24 行中,您一次一步地计算积分面积,并添加累积和。在本节中,您将采用不同的方法:将积分区域划分为许多块,并让每个线程负责计算一个块。通过将线程计算的块的累积面积相加,可以得到π值。这样你就用分而治之的策略来完成任务分配,得到最终的结果。优化后的应用称为。当ThreadPi在计算积分面积的累积值(也就是π值)时,每个线程的计算步长会累积步长,以增加线程总数,这样每个线程负责自己的块面积总和。

正在运行的ThreadPi app 的界面如图 11-21 所示。

A978-1-4842-0100-8_11_Fig21_HTML.jpg

图 11-21。

User interface of ThreadPi

这个优化后的应用(ThreadPi)的界面和原来的应用(SerialPi)是一样的。在图 11-21(b) 中,你可以看到这个应用用了 13 秒来计算π的值。时间减少到原来申请时间的一半(22 秒)。唯一的区别是应用使用两个线程来计算π。

这个应用是基于修改原来的应用代码。主要变化如下。

  1. 修改计算任务的MyTaskThread源代码文件MyTaskThread.java的线程类如下:

1\. package com.example.threadpi;

2\. import android.os.Handler;

3\. import android.os.Message;

4\. public class MyTaskThread extends Thread {

5.     private Handler mainHandler;

6.     public static final int MSG_FINISHED = 1;

7.     private static final long num_steps = 200000000;

8\. // num_steps variable in formula, total steps

9.     private static final double step = 1.0 / num_steps;

10\. // step variable in formula, step length

11.     public static double pi = 0.0;   //  Calculated result of π

12.     public static final int num_threads = 2;   // Thread count

13.     private int myNum;                         // Thread #

private static Object sharedVariable = new Object();

14\. // synchronization lock variable for Pi variable

15.     private static int finishedThreadNum = 0;

16\. // count of threads finishing calculation

17.

18.     static String msTimeToDatetime(long msnum){

19\. // The function to convert the number of milliseconds into hours: minutes: seconds. Millis

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

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

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

mm = tt % 60; tt = tt / 60;

23.         hh = tt % 60;

24.         String s = "" + hh +"hour "+mm+"minute "+ss + " second " + ms +"milliseconds";

25.         return s;

26.     }

27.     public void setStepStartNum(int n)

28\. // set thread # for thread, in response to starting position of i

29.     {

30.         myNum = n;

31.     }

32.

33.     @Override

34.     public void run()

35.     {

36.         double x, partialSum = 0.0;

37.         long i;

38.         for (i = myNum; i < num_steps; i += num_threads) {

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

40.             partialSum += 4.0 / (1.0 + x * x);

41.         }

42.         synchronized (sharedVariable) {

43.             pi += partialSum * step;

44.             finishedThreadNum++;

45.             if (finishedThreadNum >=num_threads) {

// waiting all threads finishing run and send message

46.             Message msg = new Message();

47.             msg.what = MSG_FINISHED; //Define message type

48.             mainHandler.sendMessage(msg);   //Send message

49.             }

50.         }

51.     }

public MyTaskThread(Handler mh)  // constructor

{

super();

mainHandler = mh;

}

}

粗体显示的代码段是ThreadPiSerialPi的主要区别。第 10–13 行定义了多线程计算任务所需的变量。变量num_threads在计算任务开始时计算线程的数量。在这种情况下,联想 K900 的英特尔凌动处理器有两个逻辑 CPU,因此该值设置为 2。myNum变量计算线程数,范围从 0 到num_threads–1。变量sharedVariable由应用于变量pi的同步锁引入。因为pi是简单变量,所以不能直接锁定。finishedThreadNum变量是用于完成计算的线程数。当finishedThreadNum的值等于num_threads的值时,所有的计算线程都已经运行完毕。

第 23–26 行是专门为计算线程MyTaskThread添加的函数。它标记了线程的索引号。

第 30–44 行是计算线程的原型代码。第 30–35 行是计算π的直接代码。对比原应用中的对应代码,可以看到原 app 中的sum变量已经被替换为partialSum,这反映了这个线程的面积只是总面积的一部分。最重要的区别在第 32 行:步长变量i不是 1 而是num_threads,这意味着线程每次向前移动几步。变量I的初始位置不是 0,而是从线程号中导出的。这有点像田径比赛,每个运动员(线程)从他们的泳道的起点出发,而不是从同一个起点出发。线程计算就像运动员在自己的跑道上跑自己的道。

每个线程计算其总和,并需要将该数据添加到总累积总和中(变量pi)。这个变量由多个线程共享,所以需要添加一个同步锁。此步骤对应于第 36–44 行。第 36 行添加同步锁,第 37 行将线程的计算结果添加到pi的公共结果中。第 38 行在计算结束时将线程数加 1。在第 39 行,通过比较已经完成计算的线程数量与线程总数,您可以确定是否所有线程都已经完成运行。只有在所有线程完成后,消息才会发送到主线程。

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

1\. package com.example.threadpi;

2\. import android.os.Bundle;

3\. import android.app.Activity;

4\. import android.view.Menu;

5\. import android.widget.Button;

6\. import android.view.View;

7\. import android.view.View.OnClickListener;

8\. import android.os.Process;

9\. import android.widget.TextView;

10\. import android.os.Handler;

11\. import android.os.Message;

12\. public class MainActivity extends Activity {

13.     private MyTaskThread thrd[] = null;

14.     private TextView tv_TaskOutputInfo;

15.     private Handler mHandler;;

16.     private long end_time;

17.     private long time;

18.     private long start_time;

19.     @Override

20.     public void onCreate(Bundle savedInstanceState) {

21.         super.onCreate(savedInstanceState);

22.         setContentView(R.layout.activity_main);

23.         tv_TaskOutputInfo = (TextView)findViewById(R.id.taskOuputInfo);

24.         TextView tv_Info = (TextView)findViewById(R.id.textView1);

25.         String ts = "This example currently has"+ MyTaskThread.num_threads + "threads in this run on calculating the value of Pi";

26.         tv_Info.setText(ts);

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

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

30.             public void onClick(View v) {

31.                 exitApp();

32.             }

33.         });

34.         final Button btn_StartTaskThread = (Button) findViewById(R.id.startTaskThread);

35.         btn_StartTaskThread.setOnClickListener(new /*View.*/OnClickListener(){

36.             public void onClick(View v) {

37.               btn_StartTaskThread.setEnabled(false);

38.               btn_ExitApp.setEnabled(false);

39.                  startTask();

40.              }

41.         });

42.         mHandler = new Handler() {

43.             public void handleMessage(Message msg) {

44.                switch (msg.what)

45.                 {

46.                 case MyTaskThread.MSG_FINISHED:

47.                     end_time = System.currentTimeMillis();

48.                     time = end_time - start_time;

49.                     String s = "Run EndPi="+ MyTaskThread.pi+ "  Time spent:"

50.                             + MyTaskThread.msTimeToDatetime(time);

51.                     tv_TaskOutputInfo.setText(s);

52.                     btn_ExitApp.setEnabled(true);

53.                     break;

54.                 default:

55.                     break;

56.                 }

57.             }

58.         };

59.     }

60.

61.     @Override

62.     public boolean onCreateOptionsMenu(Menu menu) {

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

return true;

64.     }

65.

66.     private void startTask() {

67.         thrd = new MyTaskThread[MyTaskThread.num_threads];

68.         start_time = System.currentTimeMillis();

69.         for( int i=0; i < MyTaskThread.num_threads; i++){

70.             thrd[i] = new MyTaskThread(mHandler);  // Create a thread

71.             thrd[i].setStepStartNum(i);

72.             thrd[i].start();

}

73.     }

74.

75.     private void exitApp() {

76.         for (int i = 0; i < MyTaskThread.num_threads``&&

77.             try {

78.                 thrd[i].join();   // Wait for thread running to end

79.             } catch (InterruptedException e) {

80.             }

81.         }

82.         finish();

83.         Process.killProcess(Process.myPid());

}

}

粗体显示的代码段是这个应用和原始应用的主要区别。在第 13 行,原始应用的单线程对象变量变成了一个线程数组。在第 67–71 行,在原始应用中启动单个线程被更改为启动数组中的所有线程,并在应用启动时设置线程的索引号。线程号的含义在MyTaskThread代码描述中介绍。您不是等待单个线程的结束,而是等待线程数组的结束(第 74–79 行)。

完成这些优化后,您需要在目标设备上编译、生成和部署应用,就像您处理原始应用一样。您可以独立运行应用并测量其运行时间。计算时间减少到原来的一半。

接下来,您可以使用英特尔 GPA 来分析优化的应用(ThreadPi)。分析过程与用于 SerialPi 的过程相同。结果如图 11-22 所示。

A978-1-4842-0100-8_11_Fig22a_HTML.jpgA978-1-4842-0100-8_11_Fig22b_HTML.jpg

图 11-22。

Intel GPA analysis of ThreadPi

如您所见,当您单击 Start 按钮时,计算(任务)线程开始运行。两个 CPU 负载都从低负载上升到 100%容量。当计算完成时,CPU 负载回落到低负载状态。与原始应用不同,当计算任务运行时,两个 CPU 都是 100%负载的。不再有任何负载旋转。这表明优化的应用有两个并行的 CPU 在计算任务上满负荷工作,这使得应用运行得更快。

摘要

本章介绍了性能优化的基本原理、优化方法以及 Android 应用开发的相关工具。因为 Java 是 Android 开发者选择的应用开发语言,所以上一章介绍的优化工具主要是针对 Java 的。Java 应用运行在虚拟机中,天生比 C/C++ 应用慢,后者直接编译并运行在硬件指令上。此外,由于 C/C++ 的基本特性,许多开发人员对 C/C++ 应用有更多的经验,并创建了更多的优化工具。因此,C/C++ 开发不应该被排除在 Android 应用开发之外。下一章将介绍用于 C/C++ 应用开发的 Android NDK,以及相关的优化方法和优化工具。

Creative Commons Open Access This chapter is licensed under the terms of the Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License ( http://​creativecommons.​org/​licenses/​by-nc-nd/​4.​0/​ ), which permits any noncommercial use, sharing, distribution and reproduction in any medium or format, as long as you give appropriate credit to the original author(s) and the source, provide a link to the Creative Commons licence and indicate if you modified the licensed material. You do not have permission under this licence to share adapted material derived from this chapter or parts of it. The images or other third party material in this chapter are included in the chapter’s Creative Commons licence, unless indicated otherwise in a credit line to the material. If material is not included in the chapter’s Creative Commons licence and your intended use is not permitted by statutory regulation or exceeds the permitted use, you will need to obtain permission directly from the copyright holder.

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

Keywords Java Code Android Application Compiler Optimization Head File Executable File

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

JNI 简介

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

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

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

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

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

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

A978-1-4842-0100-8_12_Fig1_HTML.jpg

图 12-1。

JNI general workflow

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

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

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

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

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

  • c 代码:

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

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

  • C++ 代码:

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

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

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

方法和 C 函数原型

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

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

public class HelloJni extends Activity

1.  {

2.      public void onCreate(Bundle savedInstanceState)

3.      {

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

5.      }

6.      public native String  stringFromJNI();

7.  }

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

1.  JNIEXPORT jstring JNICALL Java_com_example_hellojni_HelloJni_stringFromJNI

2.    (JNIEnv *, jobject);

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

1.  /*

2.  ......

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

4.  */

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

6.    {

7.      ......

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

9.  }

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

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

Java 和 C 数据类型映射

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

表 12-1。

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

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

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

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

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

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

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

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

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

jobject myNewObjet = env->AllocObject(objectClass);

Note

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

Java 数组处理

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

资源释放

资源释放的原理如下:

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

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

NDK 简介

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

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

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

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

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

NDK 包括以下主要部分:

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

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

A978-1-4842-0100-8_12_Fig2_HTML.jpg

图 12-2。

Flowchart of Android NDK application development

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

A978-1-4842-0100-8_12_Fig3_HTML.jpg

图 12-3。

Application components for an Android NDK application

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

表 12-2。

Relationship between the Main Android API and NDK Versions

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

TIP: THE MEANING OF APPLICATION BINARY INTERFACE (ABI)

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

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

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

Android 目前支持以下 ABI 类型:

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

安装 NDK 和设置环境

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

安装 CDT

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

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

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

A978-1-4842-0100-8_12_Fig4_HTML.jpg

图 12-4。

Detailed information for the CDT component installation

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

NDK 的例子

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

使用命令行生成库文件

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

A978-1-4842-0100-8_12_Fig5_HTML.jpg

图 12-5。

Setting up the jnitest project parameters

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

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

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

A978-1-4842-0100-8_12_Fig6_HTML.jpg

图 12-6。

File structure of the jnitest project

1.  package com.example.jnitest;

2.  import android.app.Activity;

3.  import android.widget.TextView;

4.  import android.os.Bundle;

5.  public class MainActivity extends Activity

6.  {

7.      @Override

8.      public void onCreate(Bundle savedInstanceState)

9.      {

10.         super.onCreate(savedInstanceState);

11.         TextView tv = new TextView(this);

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

13.         setContentView(tv);

14.     }

15.     public native String stringFromJNI();

16.

17.     static {

18.         System.loadLibrary("jnitestmysharelib");

19.     }

20.  }

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

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

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

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

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

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

测试目录是否已经建立:

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

......

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

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

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

命令参数如下:

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

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

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

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

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

package com.example.jnitest;

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

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

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

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

......

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

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

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

2.  #include <jni.h>

3.  /* Header for class com_example_jnitest_MainActivity */

4.

5.  #ifndef _Included_com_example_jnitest_MainActivity

6.  #define _Included_com_example_jnitest_MainActivity

7.  #ifdef __cplusplus

8.  extern "C" {

9.  #endif

10\. #undef com_example_jnitest_MainActivity_MODE_PRIVATE

11\. #define com_example_jnitest_MainActivity_MODE_PRIVATE 0L

12\. #undef com_example_jnitest_MainActivity_MODE_WORLD_READABLE

13\. #define com_example_jnitest_MainActivity_MODE_WORLD_READABLE 1L

14\. #undef com_example_jnitest_MainActivity_MODE_WORLD_WRITEABLE

15\. #define com_example_jnitest_MainActivity_MODE_WORLD_WRITEABLE 2L

16\. #undef com_example_jnitest_MainActivity_MODE_APPEND

17\. #define com_example_jnitest_MainActivity_MODE_APPEND 32768L

18\. #undef com_example_jnitest_MainActivity_MODE_MULTI_PROCESS

19\. #define com_example_jnitest_MainActivity_MODE_MULTI_PROCESS 4L

20\. #undef com_example_jnitest_MainActivity_BIND_AUTO_CREATE

21\. #define com_example_jnitest_MainActivity_BIND_AUTO_CREATE 1L

22\. #undef com_example_jnitest_MainActivity_BIND_DEBUG_UNBIND

23\. #define com_example_jnitest_MainActivity_BIND_DEBUG_UNBIND 2L

24\. #undef com_example_jnitest_MainActivity_BIND_NOT_FOREGROUND

25\. #define com_example_jnitest_MainActivity_BIND_NOT_FOREGROUND 4L

26\. #undef com_example_jnitest_MainActivity_BIND_ABOVE_CLIENT

27\. #define com_example_jnitest_MainActivity_BIND_ABOVE_CLIENT 8L

28\. #undef com_example_jnitest_MainActivity_BIND_ALLOW_OOM_MANAGEMENT

29\. #define com_example_jnitest_MainActivity_BIND_ALLOW_OOM_MANAGEMENT 16L

30\. #undef com_example_jnitest_MainActivity_BIND_WAIVE_PRIORITY

31\. #define com_example_jnitest_MainActivity_BIND_WAIVE_PRIORITY 32L

32\. #undef com_example_jnitest_MainActivity_BIND_IMPORTANT

33\. #define com_example_jnitest_MainActivity_BIND_IMPORTANT 64L

34\. #undef com_example_jnitest_MainActivity_BIND_ADJUST_WITH_ACTIVITY

35\. #define com_example_jnitest_MainActivity_BIND_ADJUST_WITH_ACTIVITY 128L

36\. #undef com_example_jnitest_MainActivity_CONTEXT_INCLUDE_CODE

37\. #define com_example_jnitest_MainActivity_CONTEXT_INCLUDE_CODE 1L

38\. #undef com_example_jnitest_MainActivity_CONTEXT_IGNORE_SECURITY

39\. #define com_example_jnitest_MainActivity_CONTEXT_IGNORE_SECURITY 2L

40\. #undef com_example_jnitest_MainActivity_CONTEXT_RESTRICTED

41\. #define com_example_jnitest_MainActivity_CONTEXT_RESTRICTED 4L

42\. #undef com_example_jnitest_MainActivity_RESULT_CANCELED

43\. #define com_example_jnitest_MainActivity_RESULT_CANCELED 0L

44\. #undef com_example_jnitest_MainActivity_RESULT_OK

45\. #define com_example_jnitest_MainActivity_RESULT_OK -1L

46\. #undef com_example_jnitest_MainActivity_RESULT_FIRST_USER

47\. #define com_example_jnitest_MainActivity_RESULT_FIRST_USER 1L

48\. #undef com_example_jnitest_MainActivity_DEFAULT_KEYS_DISABLE

49\. #define com_example_jnitest_MainActivity_DEFAULT_KEYS_DISABLE 0L

50\. #undef com_example_jnitest_MainActivity_DEFAULT_KEYS_DIALER

51\. #define com_example_jnitest_MainActivity_DEFAULT_KEYS_DIALER 1L

52\. #undef com_example_jnitest_MainActivity_DEFAULT_KEYS_SHORTCUT

53\. #define com_example_jnitest_MainActivity_DEFAULT_KEYS_SHORTCUT 2L

54\. #undef com_example_jnitest_MainActivity_DEFAULT_KEYS_SEARCH_LOCAL

55\. #define com_example_jnitest_MainActivity_DEFAULT_KEYS_SEARCH_LOCAL 3L

56\. #undef com_example_jnitest_MainActivity_DEFAULT_KEYS_SEARCH_GLOBAL

57\. #define com_example_jnitest_MainActivity_DEFAULT_KEYS_SEARCH_GLOBAL 4L

58\. /*

59.  * Class:     com_example_jnitest_MainActivity

60.  * Method:    stringFromJNI

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

62\. */

63\. JNIEXPORT jstring JNICALL Java_com_example_jnitest_MainActivity_stringFromJNI

64\. (JNIEnv *, jobject);

65.

66\. #ifdef __cplusplus

67\. }

68\. #endif

69\. #endif

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

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

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

1\. #include <string.h>

2\. #include <jni.h>

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

4\. {

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

6\. }

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

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

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

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

2\. include $(CLEAR_VARS)

3\. LOCAL_MODULE    := jnitestmysharelib

4\. LOCAL_SRC_FILES := jnitestccode.c

5\. include $(BUILD_SHARED_LIBRARY)

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

Note

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

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

Note

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

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

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

APP_ABI := x86

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

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

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

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

anifest.xml

"Compile x86  : jnitestmysharelib <= jnitestccode.c

SharedLibrary : libjnitestmysharelib.so

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

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

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

A978-1-4842-0100-8_12_Fig7_HTML.jpg

图 12-7。

jnitest application interface

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

在 IDE 中生成库文件

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

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

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

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

A978-1-4842-0100-8_12_Fig8_HTML.jpg

图 12-8。

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

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

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

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

A978-1-4842-0100-8_12_Fig12_HTML.jpg

图 12-12。

Select source code and directories where related files are located

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

A978-1-4842-0100-8_12_Fig11_HTML.jpg

图 12-11。

Edit Configuration dialog box Build Options tab settings

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

A978-1-4842-0100-8_12_Fig10_HTML.jpg

图 12-10。

Edit Configuration dialog box Refresh tab settings

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

A978-1-4842-0100-8_12_Fig9_HTML.jpg

图 12-9。

Main tab setting in the Edit Configuration dialog box

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

Cygwin        : Generating dependency file converter script

Compile x86   : jnitestmysharelib <= jnitestccode.c

SharedLibrary : libjnitestmysharelib.so

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

NDK 应用开发的工作流分析

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

A978-1-4842-0100-8_12_Fig13_HTML.jpg

图 12-13。

The jnitest project structure after NDK library files are generated

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

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

A978-1-4842-0100-8_12_Fig14_HTML.jpg

图 12-14。

jnitest application deployment target file structure

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

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

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

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

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

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

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

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

NDK 编译器优化

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

Note

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

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

A978-1-4842-0100-8_12_Fig15_HTML.jpg

图 12-15。

The working mechanism of NDK tools

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

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

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

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

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

-O 或-O1

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

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

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

-氧气

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

-臭氧

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

-O0

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

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

inline long factorial (int i)

{

return factorial_table[i];

}

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

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

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

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

{

do_stuff(i);

}

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

for (i = 0; i < 100; )

{

do_stuff(i); i++;

do_stuff(i); i++;

do_stuff(i); i++;

do_stuff(i); i++;

do_stuff(i); i++;

do_stuff(i); i++;

do_stuff(i); i++;

do_stuff(i); i++;

do_stuff(i); i++;

do_stuff(i); i++;

}

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

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

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

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

表 12-3。

Intel Processor-Related gcc Switch Options

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

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

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

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

-march=cpu-type

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

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

表 12-4。

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

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

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

$ gcc int_sin.c

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

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

$ gcc int_sin.c -o sinnorm

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

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

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

$ gcc -O0 int_sin.c

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

$ gcc -O3 int_sin.c

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

$ gcc -msse int_sin.c

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

$ gcc -mno-sse int_sin.c

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

$ gcc -mtune=atom int_sin.c

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

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

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

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

2\. include $(CLEAR_VARS)

3\. LOCAL_MODULE    := jnitestmysharelib

4\. LOCAL_SRC_FILES := jnitestccode.c

5\. LOCAL_CFLAGS    := -O3

6\. include $(BUILD_SHARED_LIBRARY)

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

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

LOCAL_CFLAGS      := -msse3

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

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

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

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

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

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

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

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

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

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

NDK 集成优化示例

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

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

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

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

A978-1-4842-0100-8_12_Fig16_HTML.jpg

图 12-16。

Original version of NdkExp

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

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

该示例实现如下:

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

A978-1-4842-0100-8_12_Fig17_HTML.jpg

图 12-17。

Layout of the original NdkExp

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

1.  package com.example.ndkexp;

2.  import android.os.Bundle;

3.  import android.app.Activity;

4.  import android.view.Menu;

5.  import android.widget.Button;

6.  import android.view.View;

7.  import android.view.View.OnClickListener;

8.  import android.os.Process;

9.  import android.widget.TextView;

10\. import android.os.Handler;

11\. import android.os.Message;

12.

13\. public class MainActivity extends Activity {

14.     private JavaTaskThread javaTaskThread = null;

15.     private CCodeTaskThread cCodeTaskThread = null;

16.     private TextView tv_JavaTaskOuputInfo;

17.     private TextView tv_CCodeTaskOuputInfo;

18.     private Handler mHandler;;

19.     private long end_time;

20.     private long time;

21.     private long start_time;

22.     @Override

23.     public void onCreate(Bundle savedInstanceState) {

24.         super.onCreate(savedInstanceState);

25.         setContentView(R.layout.activity_main);

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

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

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

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

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

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

32.             public void onClick(View v) {

33.                 exitApp();

34.             }

35.         });

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

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

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

39.             public void onClick(View v) {

40.                 btn_StartJavaTask.setEnabled(false);

41.                 btn_StartCCodeTask.setEnabled(false);

42.                 btn_ExitApp.setEnabled(false);

43.                 startJavaTask();

44.             }

45.         });

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

47.             public void onClick(View v) {

48.                 btn_StartJavaTask.setEnabled(false);

49.                 btn_StartCCodeTask.setEnabled(false);

50.                 btn_ExitApp.setEnabled(false);

51.                 startCCodeTask();

52.             }

53.         });

54.         mHandler = new Handler() {

55.             public void handleMessage(Message msg) {

56.             String s;

57.                switch (msg.what)

58.                {

59.                case JavaTaskThread.MSG_FINISHED:

60.                     end_time = System.currentTimeMillis();

61.                     time = end_time - start_time;

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

63.                          + JavaTaskThread.msTimeToDatetime(time);

64.                     tv_JavaTaskOuputInfo.setText(s);

65.                     btn_StartCCodeTask.setEnabled(true);

66.                     btn_ExitApp.setEnabled(true);

67.                   break;

68.                 case CCodeTaskThread.MSG_FINISHED:

69.                     end_time = System.currentTimeMillis();

70.                     time = end_time - start_time;

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

72.                          + JavaTaskThread.msTimeToDatetime(time);

73.                     tv_CCodeTaskOuputInfo.setText(s);

74.                     btn_StartJavaTask.setEnabled(true);

75.                     btn_ExitApp.setEnabled(true);

76.                   break;

77.                 default:

78.                   break;

79.                 }

80.             }

81.         };

82.     }

83.

84.     @Override

85.     public boolean onCreateOptionsMenu(Menu menu) {

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

87.        return true;

88.     }

89.

90.     private void startJavaTask() {

91.         if (javaTaskThread == null)

92.             javaTaskThread = new JavaTaskThread(mHandler);

93.         if (! javaTaskThread.isAlive())

94.         {

95.                start_time = System.currentTimeMillis();

96.                javaTaskThread.start();

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

98.         }

99.     }

100.

101.     private void startCCodeTask() {

102.         if (cCodeTaskThread == null)

103.             cCodeTaskThread = new CCodeTaskThread(mHandler);

104.         if (! cCodeTaskThread.isAlive())

105.         {

106.                start_time = System.currentTimeMillis();

107.                cCodeTaskThread.start();

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

109.         }

110.     }

111.     private void exitApp() {

112.         try {

113.             if (javaTaskThread !=null)

114.             {

115.                 javaTaskThread.join();

116.                 javaTaskThread = null;

117.             }

118.         } catch (InterruptedException e) {

119.         }

120.         try {

121.             if (cCodeTaskThread  !=null)

122.             {

123.                 cCodeTaskThread.join();

124.                 cCodeTaskThread = null;

125.             }

126.         } catch (InterruptedException e) {

127.         }

128.         finish();

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

130.     }

131.

132.     static {

133.         System.loadLibrary("ndkexp_extern_lib");

134.     }

135\. }

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

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

1.  package com.example.ndkexp;

2.  import android.os.Handler;

3.  import android.os.Message;

4.  public class CCodeTaskThread extends Thread {

5.     private Handler mainHandler;

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

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

8.     static String msTimeToDatetime(long msnum){

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

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

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

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

13.        hh = tt % 60;

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

15.        return s;

16.    }

17.    @Override

18.    public void run()

19.    {

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

21.        Message msg = new Message();

22.        msg.what = MSG_FINISHED;

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

24.        msg.obj = dPi;

25.        mainHandler.sendMessage(msg);

26.    }

27.    public CCodeTaskThread(Handler mh)

28.    {

29.        super();

30.        mainHandler = mh;

31.    }

32\. }

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

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

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

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

......

23\. JNIEXPORT jdouble JNICALL Java_com_example_ndkexp_CCodeTaskThread_cCodeTask

24\. (JNIEnv *, jobject);

......

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

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

1.  #include <jni.h>

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

3.  {

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

5.      const double step = 1.0 / num_steps;

6.      double x, sum = 0.0;

7.      long i;

8.      double pi = 0;

9.

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

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

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

12.     }

13.     pi = step * sum;

14.

15.     return (pi);

16\. }

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

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

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

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

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

2.  include $(CLEAR_VARS)

3.  LOCAL_MODULE        := ndkexp_extern_lib

4.  LOCAL_SRC_FILES     := mycomputetask.c

5.  include $(BUILD_SHARED_LIBRARY)

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

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

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

扩展编译器优化

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

A978-1-4842-0100-8_12_Fig18_HTML.jpg

图 12-18。

Extended version of NdkExp

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

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

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

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

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

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

A978-1-4842-0100-8_12_Fig19_HTML.jpg

图 12-19。

Extended NdkExp layout

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

...

13.  public class MainActivity extends Activity {

14.      private JavaTaskThread javaTaskThread = null;

15.      private CCodeTaskThread cCodeTaskThread = null;

16.      private AnotherCCodeTaskThread anotherCCodeTaskThread = null;

17.      private TextView tv_JavaTaskOuputInfo;

18.      private TextView tv_CCodeTaskOuputInfo;

19.      private TextView tv_AnotherCCodeTaskOuputInfo;

......

182.     static {

183.         System.loadLibrary("ndkexp_extern_lib");

184.         System.loadLibrary("ndkexp_another_lib");

185.     }

186\. }

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

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

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

1.  package com.example.ndkexp;

2.  import android.os.Handler;

3.  import android.os.Message;

4.  public class AnotherCCodeTaskThread extends Thread {

5.      private Handler mainHandler;

6.      public static final int MSG_FINISHED = 3;

// The message after the end of the task

7.      private native double anotherCCodeTask();

// Calling external C functions to complete computing tasks

8.      static String msTimeToDatetime(long msnum){

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

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

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

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

13.         hh = tt % 60;

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

15.         return s;

16.     }

17.     @Override

18.     public void run()

19.     {

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

21.         Message msg = new Message();

22.         msg.what = MSG_FINISHED;

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

24.         msg.obj = dPi;

25.         mainHandler.sendMessage(msg);

26.     }

27.     public CCodeTaskThread(Handler mh)

28.     {

29.         super();

30.         mainHandler = mh;

31.     }

32\. }

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

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

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

2.  include $(CLEAR_VARS)

3.  LOCAL_MODULE    := ndkexp_extern_lib

4.  LOCAL_SRC_FILES := mycomputetask.c

5.  LOCAL_CFLAGS    := -O0

6.  include $(BUILD_SHARED_LIBRARY)

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

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

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

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

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

  1. 运行项目。

比较编译器优化

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

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

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

LOCAL_CFLAGS := -mno-sse

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

LOCAL_CFLAGS := -msse3

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

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

A978-1-4842-0100-8_12_Fig20_HTML.jpg

图 12-20。

Optimization comparison of compiler SSE instructions for NdkExp

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

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

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

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

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

摘要

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

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

Creative Commons Open Access This chapter is licensed under the terms of the Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License ( http://​creativecommons.​org/​licenses/​by-nc-nd/​4.​0/​ ), which permits any noncommercial use, sharing, distribution and reproduction in any medium or format, as long as you give appropriate credit to the original author(s) and the source, provide a link to the Creative Commons licence and indicate if you modified the licensed material. You do not have permission under this licence to share adapted material derived from this chapter or parts of it. The images or other third party material in this chapter are included in the chapter’s Creative Commons licence, unless indicated otherwise in a credit line to the material. If material is not included in the chapter’s Creative Commons licence and your intended use is not permitted by statutory regulation or exceeds the permitted use, you will need to obtain permission directly from the copyright holder.

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

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

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

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

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

低功耗设计概述

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

消费的基础

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

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

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

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

Tip

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

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

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

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

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

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

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

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

功耗控制技术

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

动态电压/频率调节技术

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

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

Tip

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

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

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

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

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

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

时钟门控

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

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

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

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

节能电路设计和制造流程

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

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

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

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

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

第一代英特尔 SpeedStep 技术

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

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

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

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

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

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

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

APM 和 ACPI 标准

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

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

A978-1-4842-0100-8_13_Fig1_HTML.jpg

图 13-1。

ACPI structure

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

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

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

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

低功耗操作系统状态

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

备用的

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

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

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

冬眠

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

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

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

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

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

睡眠

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

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

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

Linux 电源控制机制

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

轻松空闲

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

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

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

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

PowerTOP(超级用户)

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

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

图 13-2 为 PowerTOP 运行截图。

A978-1-4842-0100-8_13_Fig2_HTML.jpg

图 13-2。

PowerTOP interface example

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

英特尔电源优化辅助工具

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

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

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

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

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

A978-1-4842-0100-8_13_Fig3_HTML.jpg

图 13-3。

Intel GPA CPU frequency analysis

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

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

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

A978-1-4842-0100-8_13_Fig4a_HTML.jpg A978-1-4842-0100-8_13_Fig4b_HTML.jpg

图 13-4。

Intel GPA machine overall power analysis

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

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

应用设计中的低功耗考虑

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

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

低功耗优化的最基本原则

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

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

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

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

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

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

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

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

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

轮询是低功耗优化的敌人

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

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

volatile boolean canGo = false;            // Shared variables

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

void run()                                 void run()

{                                          {

......

canGo = true;                               while (!canGo);

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

}                                           }

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

volatile boolean canGo = false;

Object sema;                      // The synchronization lock canGo variable

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

void run()                         void run()

{                                  {

synchronized(sema){                synchronized(sema){

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

sema.notifyAll()                    sema.wait();

}                                  }

...........// Access to the resource code

}                                  }

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

事件驱动编程

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

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

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

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

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

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

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

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

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

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

建立节能计划

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

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

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

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

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

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

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

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

A978-1-4842-0100-8_13_Fig5_HTML.jpg

图 13-5。

Separate PollLast applications running in the interface

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

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

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

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

A978-1-4842-0100-8_13_Fig6_HTML.jpg

图 13-6。

The main layout of the  application

1\. package com.example.polllast;

2\. import android.os.Handler;

3\. import android.os.Message;

4\. public class MyTaskThread extends Thread {

5.  private Handler mainHandler;

6.  public static final int MSG_FINISHED = 1;

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

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

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

10\. @Override

11\. public void run()

12\. {

13.     long start_time = System.currentTimeMillis();

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

15.         long endtime = start_time + millisecduration;

16.         long now;

17.         do {

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

19.         }    while (now < endtime);

20.         Message msg = new Message();

21.         msg.what = MSG_FINISHED;

22.         mainHandler.sendMessage(msg);

23.     }

24.     public MyTaskThread(Handler mh)

25.     {

26.         super();

27.         mainHandler = mh;

28.     }

29\. }

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

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

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

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

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

A978-1-4842-0100-8_13_Fig8a_HTML.jpgA978-1-4842-0100-8_13_Fig8b_HTML.jpg

图 13-8。

PollLast Intel GPA charge/discharge analysis

A978-1-4842-0100-8_13_Fig7a_HTML.jpgA978-1-4842-0100-8_13_Fig7b_HTML.jpg

图 13-7。

PollLast Intel GPA CPU frequency analysis

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

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

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

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

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

1\. package com.example.sleeplast;

2\. import android.os.Handler;

3\. import android.os.Message;

4\. public class MyTaskThread extends Thread {

5.     private Handler mainHandler;

6.     public static final int MSG_FINISHED = 1;

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

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

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

10.     @Override

11.     public void run()

12.     {

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

14.         try {

15.             Thread.sleep(millisecduration);

16.         } catch (InterruptedException e) {

17.             e.printStackTrace();

18.         }

19.         Message msg = new Message();

20.         msg.what = MSG_FINISHED;

21.         mainHandler.sendMessage(msg);

22.     }

23.     public MyTaskThread(Handler mh)

24.     {

25.         super();

26.         mainHandler = mh;

27.     }

28\. }

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

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

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

package com.example.sleeplast;

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

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

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

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

A978-1-4842-0100-8_13_Fig10a_HTML.jpgA978-1-4842-0100-8_13_Fig10b_HTML.jpg

图 13-10。

Intel GPA charge/discharge analysis of

A978-1-4842-0100-8_13_Fig9a_HTML.jpgA978-1-4842-0100-8_13_Fig9b_HTML.jpg

图 13-9。

Intel GPA CPU frequency analysis of the  application

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

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

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

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

按照以下步骤创建应用:

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

1.  package com.example.timerlast;

2.  import android.os.Bundle;

3.  import android.app.Activity;

4.  import android.view.Menu;

5.  import android.widget.Button;

6.  import android.view.View;

7.  import android.view.View.OnClickListener;

8.  import android.os.Process;

9.  import android.widget.TextView;

10\. import android.os.Handler;

11\. import android.os.Message;

12\. import java.util.Timer;

13.

14\. public class MainActivity extends Activity {

15.     private TextView tv_TaskStatus;

16.     private Button btn_ExitApp;

17.     private Handler mHandler;

18.     private Timer timer =null;            // Timer

19.

20.     @Override

21.     public void onCreate(Bundle savedInstanceState) {

......

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

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

37.             public void onClick(View v) {

38.                 btn_StartTask.setEnabled(false);

39.                 btn_ExitApp.setEnabled(false);

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

41.                  startTask();

42.              }

43.         });

......

58.     }

......

66.     private void startTask() {

67.            long millisecduration =

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

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

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

71.     }

......

79\. }

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

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

1\. package com.example.timerlast;

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

3\. import android.os.Handler;

4\. import android.os.Message;

5.

6\. public class MyTaskTimer extends TimerTask {

7.     private Handler mainHandler;

8.     public static final int MSG_FINISHED = 1;

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

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

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

12.

13.     public MyTaskTimer(Handler mh)

14.     {

15.         super();

16.         mainHandler = mh;

17.     }

18.

19.     @Override

20.     public void run(){

21.         Message msg = new Message();

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

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

24.     }

25\. }

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

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

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

A978-1-4842-0100-8_13_Fig12a_HTML.jpgA978-1-4842-0100-8_13_Fig12b_HTML.jpg

图 13-12。

Intel GPA charge/discharge analysis of

A978-1-4842-0100-8_13_Fig11a_HTML.jpgA978-1-4842-0100-8_13_Fig11b_HTML.jpg

图 13-11。

Intel GPA CPU frequency analysis of

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

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

书籍摘要

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

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

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

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

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

Creative Commons Open Access This chapter is licensed under the terms of the Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License ( http://​creativecommons.​org/​licenses/​by-nc-nd/​4.​0/​ ), which permits any noncommercial use, sharing, distribution and reproduction in any medium or format, as long as you give appropriate credit to the original author(s) and the source, provide a link to the Creative Commons licence and indicate if you modified the licensed material. You do not have permission under this licence to share adapted material derived from this chapter or parts of it. The images or other third party material in this chapter are included in the chapter’s Creative Commons licence, unless indicated otherwise in a credit line to the material. If material is not included in the chapter’s Creative Commons licence and your intended use is not permitted by statutory regulation or exceeds the permitted use, you will need to obtain permission directly from the copyright holder.

posted @   绝不原创的飞龙  阅读(19)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek “源神”启动!「GitHub 热点速览」
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 我与微信审核的“相爱相杀”看个人小程序副业
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· spring官宣接入deepseek,真的太香了~
点击右上角即可分享
微信分享提示