了解了构建Android应用的一些基础架构后,现在我们一起来探索其物理组成部分。
第1章介绍了Android SDK的基础知识。第5章将详细探讨如何使用最常见的Android开发工具Eclipse IDE来管理项目。现在,我们先继续来了解项目内的代码是如何组织的。
重述一下,项目(project)是用于生成一个可部署的产出(artifact)的工作区。在Java领域中,该“产出”可能只是一个库文件(.jar),库文件自己无法运行,但是实现了某些特定的功能。另外,它可能是可部署的Web应用,或者是可双击运行的桌面应用。在Android领域中,该“产出”很可能是一个可单独运行的服务:一个内容提供者、服务或活动。只被一个活动所使用的内容提供者很可能作为活动项目的一部分而存在。但是,当有第二个活动使用它时,就需要考虑对它进行重构,创建其自己的项目。
Android应用的项目目录看起来如下:
AndroidManifest.xmlbin/ ... compiled classes ...gen/ ... code automatically generated by the android build system ...res/ layout/ ... contains application layout files ... drawable/ ...contains images, patches, drawable xml ... raw/ ... contains data files that can be loaded as streams ... values/ ... contains xml files that contain strings, number values used in code ...src/ java/package/directories/
传统上,Java编译器期望项目的目录树包含其要解析的源文件(.java)和输出生成的二进制文件(.class)。虽然没有规定,但是如果这些文件树有不同的根文件夹,通常是命名为src和bin,以便于项目管理。
在Android项目中,还有其他两个重要的目录树,即res和gen。res目录包含静态资源的定义:颜色、常量字符串、布局等。Android工具会对这些定义进行预处理,通过相关的应用代码,把它们变成高度优化的表现方式和Java源代码。自动生成的代码和为对象AIDL(参见P122“AIDL和远程过程调用”的内容)所创建的代码,都放在gen目录中。编译器对这两个目录下的代码进行编译,生成的内容文件放到bin目录中。下面将会揭晓为何res目录对于使用Context对象访问应用数据尤为重要。
注意:当把项目添加到版本控制系统中时,如Git、Subversion或Perforce,一定要排除bin目录和gen目录!
组织Java源代码
把应用程序源代码放在src目录中。正如第2章所指出的,应该把所有的代码放到一个包(package)中,该包的名称是根据代码所有者的域名生成的。举个例子,假设你是一家大型网站awesome-android.net的开发人员,要为网站voracious-carrier.com开发天气预测应用。你很可能会选择把所有的代码放在包com.voraciouscarrier.weatherprediction或com.voracious_carrier.weather_prediction中。虽然符号“-”是完全合法的DNS域名,但是它不是合法的Java包名。该应用的UI包名可能是com.voraciouscarrier.weatherprediction.ui,其模型所在的包名可能是com.voraciouscarrier.weatherprediction.futureweather。
如果查看项目的src目录,会发现它只包含com目录,com又包含voraciouscarrier目录,依此类推。源目录树与package的树结构一一对应。Java编译器期望以这种组织方式来安排代码结构,如果违反这种方式则可能会导致无法编译代码。
最后,当FutureWeather内容提供者使用较多时,需要创建自己的项目,要把它分解出来生成一个新的项目,其包的名称不受最初创建它的应用的名称所限制。手动完成这一分解将是一场噩梦。需要创建一个新的目录结构,把文件正确地放到该目录结构中,修改每个源文件第一行的包名,最后修改任何相关的引用变化。
Eclipse重构工具是执行这一过程的最好帮手。只需要单击几下,就可以创建包含独立子树结构的新项目,将内容提供者代码剪切和粘贴到该子树中,然后相应地对包进行重命名。Eclipse会修复大多数问题,包括引用变化。
值得提醒的是,过于简化包名,如使用weatherprediction作为包名,将会是个坏习惯。即使你非常确定现在编写的代码使用范围永远不会超出当前上下文,但你也可能需要使用外部生成的代码。不要使自己陷入命名冲突的困境中!
Resources
除了代码以外,应用可能还需要存储大量的数据来控制其运行时的行为。这些数据可能是要显示的图片或简单的文本字符串,以指出要使用什么样的背景颜色或字体。这些数据称为resources,这种方式和软件最佳实践一致,把数据和代码分离开。所有这些信息一起构成了应用的context(上下文),在Android中对这些信息进行访问需要借助于Context类。Activity类和Service类都是对Context类的扩展,这意味着所有的活动和服务都可以通过该指针访问Context类型的数据。在后续的章节中,还将描述如何使用Context对象在运行时访问应用的资源。
Android应用把图像、图标和用户界面布局文件都放在名为res的目录中。res目录通常至少包括4个子目录,如下所示:
layout
包含Android用户界面XML文件,这个文件将在第6章详细介绍。
drawable
包含绘制相关的工具,如之前所描述的应用图标。
raw
保存了应用执行时可能以stream(流)方式读取的文件。原始文件是为运行的应用提供调试信息的一种很好的方式,不需要访问网络来检索数据。
values
包含应用在执行中要读取的值,或应用在某些使用中的静态数据,如对UI字符串的国际化。
应用通过方法Context.getResour ces和类R来访问这些目录下的资源。
要访问res目录下的数据,传统的Java开发人员可能会考虑编写代码构建相关的资源文件路径,然后使用文件API打开这些资源。加载完资源字节后,应用开发人员可能会期望解析特定应用格式,从而能够访问每个应用都需要的各种项:图像、字符串和数据文件。预计到每个应用都会加载类似的信息,Android提供了一个工具,它集成在Eclipse中,通过它应用可以很容易地访问这些资源,并且也对项目资源进行了标准化。
Eclipse和Android SDK一起创建了一个名为gen的目录,该目录中包含了名字都是R的类,这个类保存在Java应用包中,而包的名字由Android Manifest指定。类R文件包含了多个字段,这些字段能够唯一标识应用包中的所有资源。开发人员调用方法Context.getResources可以获取包含有应用资源的android.content.res.Resources实例(可以直接调用类Context的方法,因为类Activity和类Service都是对类Context的扩展)。如下所示为开发人员调用Resource对象来获取期望类型的资源的一种方式:
// code inside an Activity methodString helloWorld = this.getResources.getString(R.string.hello_world);int anInt = this.getResources.getInteger(R.integer.an_int);
你会发现在Android中,类R几乎无处不在,它使得对资源的访问变得非常容易,如访问UI布局文件的组件。
警告:Java作用域规则指出同一个package中的类彼此可见,即使这些类不是显式导入(通过import语句)。即使这些类不在文件系统的同一个目录下,这个规则也适用。因为Android工具会自动把R类放到以清单文件的package属性命名的Java包中,如果该包刚好包含了应用的大部分代码,使用就会很方便。清单文件中的package属性是应用的唯一命名空间,系统并不要求该命名空间名称和根所在的package名称相同,但是通常建议保持一致。
应用的清单文件
Android要求应用在XML文件AndroidManifest.xml中显式地描述其内容。在AndroidManifest.xml文件中,应用对内容提供者、服务、需要的权限及其他元素进行声明。应用上下文会准备好这些数据,使之在Android运行时中可用。清单文件把Android应用组织成了定义良好的结构,这个结构所有应用都必须遵守,Android操作系统由此可以在可管理的环境中加载并执行这些应用。这个结构由一个通用的目录布局和目录中一组通用的文件类型组成。
可以看到,Android应用的四个组件(Activity、Service、ContentProvider和BroadcastReceiver)奠定了Android应用开发的基础(如图3-6所示)。要使用其中任何一个组件,Android应用都必须在其AndroidManifest.xml文件中包含相应的声明。
图3-6:四类Android组件
Application类
Android的第5个组件是Application类。但是很多Android应用没有继承类Application。因为在大多数情况下,继承类Application是不必要的,Android项目向导(project wizard)不会自动创建类Application。
初始化AndroidManifest.xml文件中的参数
下面这段代码就是在第1章中介绍的test应用的Android清单文件。test应用的功能只是显示Android应用的基础布局。它的清单文件中包含了之前讨论过的一些基础元素:
如同所有结构良好的XML文件一样,该文件的第1行是标准的XML版本声明和使用的字符编码,然后定义了一些参数并声明了整个应用所需要的权限。下面这个列表总结并介绍了用到的标签:
manifest
package=/"com.oreilly.demo.pa.ch01.testapp/"
应用组件所在的默认包。
android:versionCode
这是一个整数,指应用的版本号。每个应用都应该包含一个版本号,而且发布给用户的版本号应该总是递增的。通过这种方式,其他程序(如Android Market、安装包和启动包)能够很容易地根据版本号判断出哪个版本更新一些。.apk文件的文件名中应该包含这个版本号,这样可以很清楚地知道这个文件中包含的是哪个版本。
android:versionName
它是一个字符串,跟我们通常看到的应用的版本号更像一些,如1.0.3。这个字符串就是展示给用户的版本号(根据你的应用,或者根据其他应用)。命名规则可以自己定,但是一般来说,采用的是类似m.n.o这样的机制(可以使用任意个数字表示)来识别应用的连续变化级别。
uses-permission android:name=...
在TestApp清单文件中,有4个声明描述了应用希望使用的Android特征,需要得到运行该应用的设备的用户的显式许可。当安装应用时,会要求输入许可。然后,Android会记住用户认为可以(或不可以)运行该应用,允许访问安全特征。在Android中定义了很多许可,所有许可都在Android文档中有描述(搜索android.Manifest.permission)。此外,还可以定义自己的许可,并使用它们来限制其他应用对你的应用中的功能的访问,除非用户对其他应用赋予该权限。例如,通常需要开通如下权限:
ACCESS_FINE_LOCATION
需要从GPS传感器获取地理位置信息。
CALL_PHONE
允许来自用户的电话呼叫。
ACCESS_MOCK_LOCATION
当在模拟器中运行时,允许获取伪位置信息。
INTERNET
允许建立因特网连接来检索数据。
application
label
应用的可读标签。
icon=/"@drawable/icon/"
PNG文件的文件名包含你想要用于应用的图标(icon)。在这个例子中,告诉Android SDK查找TestApp应用中res(resource)目录的子目录下的图标文件。在Android Desktop(桌面)应用中,Android将为应用使用该图标。
activity
现在,我们一起来看一下TestActivity的定义,首先一起来定义一些属性,其中最重要的属性如下:
android:name
Activity类名称,activity的全名包括包名(该应用的包名是com.oreilly.demo.pa.ch01.testapp),但是由于该文件总是在包的名字空间下使用,因此不需要包括包名。可以把名字简化成.TestActivity。实际上,即使是最开头的点(.)也是可有可无的。
android:label
当activity显示在屏幕上时,我们希望在Android屏幕上方显示的内容。在strings.xml文件中定义了和应用匹配的标签。
intent-filter
intent-filter告诉Android什么时候可以运行该Activity。当应用要求Android实现某个Intent时,runtime(运行时)会查看可用的活动和服务,找到某项可以提供服务的。有两个属性可以设置:
action
一旦运行时确定要运行哪个应用,action会告诉Android如何启动该应用。Android会查找可以解析MAIN action的活动。Launcher启动的任意一个应用都需要有且仅有一个活动或服务完成MAIN action解析。
category
Android中的Intent resolver使用该属性进一步限定查找其需要的Intent。因此,前提是我们希望Activity在User菜单中显示,从而用户可以通过选中该Activity来启动应用。指定LAUNCHER可以完成这一点。如果没有category属性,应用也完全可以正常工作——只是你无法从Android桌面启动应用。再次说明,通常情况下,每个应用应该是有且只有一个LAUNCHER,而且它应该和应用的启动Activity在同一个intent filter中。
provider(提供者)
对内容提供者的声明,name是provider类的名字,authorities是内容提供者应该处理的URI权限(URI authority)。URI权限提供了内容提供者URI的域字段,使得Android内容分辨率系统能够定位要处理的某种类型的URI的提供者。在本章后面,将详细说明客户端是如何使用内容提供者的。并且声明了一个名为TestProvider的提供者。
service(服务)
对应用支持的服务的声明,其中name指定服务对应的类,label为服务提供可读的标签。我们定义了一个名为.TestService的service。
receiver(接收器)
对应用支持的广播接收器的声明,name还是表示接收类,label为接收器提供可读的标签。我们声明了一个名为TestBroadcastReceiver的接收器。
Android应用打包:.apk文件
显然,应用的最终静态组成部分是对应用本身进行打包。Android提供了一个名为apkbuilder的应用,它能够生成可安装的Android应用文件,这些应用文件的扩展名是.apk。.apk是一种ZIP文件格式,它和很多其他面向Java的应用类似,包含应用说明、已经编译的应用类和应用资源。Android还提供了aapt工具对文件进行打包,这些文件也是生成.apk文件,但是开发人员通常倾向于在其开发环境中使用该工具构建他们的应用。绝大多数用户只是简单地依赖其IDE来创建其.apk文件。
一旦开发人员创建了.apk文件,他可以选择下面任意一种方式在设备上进行安装:
·使用adb接口路径,或者更常见的是使用IDE。
·使用SD卡。
·把文件存储在Web服务器上。
·把文件上传到Android Market,然后选择Install。