本文内容,主题是透过应用程序来分析Android系统的设计原理与构架。我们先会简单介绍一下Android里的应用程序编程,然后以这些应用程 序在运行环境上的需求来分析出,为什么我们的Android系统需要今天这样的设计方案,这样的设计会有怎样的意义, Android究竟是基于怎样的考虑才变成今天的这个样子,所以本文更多的分析Android应用程序设计背后的思想,品味良好架构设计的魅力。分五次连 载完成,第一部分是最简单的部分,解析Android应用程序的开发流程。
特别声明:本系列文章LiAnLab.org著作权所有,转载请注明出处。作者系LiAnLab.org资深Android技术顾问吴赫老师。
1. Android应用程序
在 目前Android大红大紫的情况下,很多人对编写Android应用程序已经有了足够深入的了解。即便是没有充分的认识,在现在Android手机已经 相当普及的情况下,大家至少也会知道Android的应用程序会是一个以.apk为后缀名的文件(在Windows系统里,还会是一个带可爱机器人图标的 文件)。那这个apk包又有什么样的含义呢?
如果您正在使用Linux操作系统,可以使用命令file命令来查看这一文件的类型。比如我们下载了一个Sample.apk的文件,则使用下面的命令:
$file Sample.apk
Sample.apk: Zip archive data, at least v1.0 to extract
对,没有看错,只一个简单的zip文件。要是做过Java开发的人,可以对这种格式很亲切,因为传说中的.jar、.war格式,都是 Zip压缩格式的文件。我们可继续使用unzip命令将这一文件解压(或是任何的解压工具,zip是人类历史是最会古老最为普及的压缩格式之一,几乎所有 压缩工具都支持)。通过解压,我们就得到了下面的文件内容:
AndroidManifest.xml,
classes.dex,
resources.arsc,
META-INF,
res,
到这里,我们就可以看到一个Android应用程序结构其实是异常简单的。这五部分内容(其中META-INF和res是目录,其他是 文件)除了META-INF是这一.apk文件的校验信息,resources.arsc是资源的索引文件,其他三部分则构成了Android应用程序的 全部。
Þ AndroidManifest.xml,这是每个Android应用程序包的配置文件,这里会保存应用程序名字、作者、所实现的功能、以及一些权限验证信息。但很可惜,在编译完成的.apk文件里,这些文件都被编译成了二进制版本,我们暂时没有办法看到内容,后面我们可以再看看具体的内容。
Þ classes.dex,这则是Android应用程序实现的逻辑部分,也就是通过Java编程写出来而被编译过的代码。这种特殊的格式,是Android里特定可执行格式,是可由Dalvik虚拟机所执行的代码,这部分内容我们也会在后续的介绍Dalvik虚拟机的章节里介绍。
Þ res,这一目录里则保存了Android所有图形界面设计相关的内容,比如界面应该长成什么样子、支持哪些语言显示等等。
从一个android应用程序的包文件内容,我们可以看到android应用程序的特点,这也是Android编程上的一些特征:
1 简单: 最终生成的结果是如些简单的三种组成,则他们的编程上也不会有太大的困难性。这并不是说Android系统里无法实现很复杂的应用程序,事实上 Android系统拥有世界上仅次于iOS的应用程序生态环境,也拥有复杂的办公软件、大型3D游戏。而只是说,如果要实现和构成同样的逻辑,它必然会拥 有其他格式混杂的系统更简化的编程模式。
2 Java操作系统:既然我们编译得到的结果,classes.dex 文件,是用于Java虚拟机(虽然是Dalvik虚拟机,但实际上这一虚拟机只是一种特定的Java解析器和虚拟机执行环境 )解析执行的,于是我们也可以猜想到,我们的Android系统,必然是一个Java操作系统。我们在后面会解释,如果把Android系统直接看成 Linux内核和Java语言组合到一起的操作系统很不准确,但事实上Android,也还是Java操作系统,Java是唯一的系统入口。
使用MVC设计模式
: 所谓的MVC,就是Model,View,Controller的首字母组合起来的一种设计模式,主要思想就是把显示与逻辑实现分离。Model用于保存 上下文状态、View用于显示、而Controller则是用于处理用户交互。三者之间有着如下图所示的交互模型,交互只到Controller,而显示 更新只通过View进行,这两者再与Model交换界面状态信息:
在 现代的图形交互相关的设计里,MVC几乎是在图形交互处理上的不二选择,这样系统设计包括一些J2EE的应用服务器框架,最受欢迎的Firefox浏览 器,iOS,MacOSX等等。这些使用MVC模式的最显著特点就是显示与逻辑分离,在Android应用程序里我们看到了用于逻辑实现的 classes.dex,也看到用于显示的res,于是我们也可以猜想到在UI上便肯定会使用MVC设计模式。
当然,所谓的Android应用程序编程,不会只有这些内容。到目前为止,我们也只是分析.apk文件,于是我们可以回过头来看看Android应用被编译出来的过程。
2. Android编程
从 编程角度来说,Android应用程序编程几乎只与Java相关,而Java平台本身是出了名跨平台利器,理论上来说,所有Java环境里使用的编程工 具、IDE工具,皆可用于Android的编程。Android SDK环境里提供的编程工具,是基于标准的Java编译工具ant的,但事实上,一些大型的Android软件工程,更倾向于使用Maven这样的并行化 编译工具(maven.apache.org)。如果以前有过Java编程经验,会知道Java环境里的图形化IDE(Integrated Development Environment)工具,并非只有Eclipse一种,实际上Java的官方IDE是NetBeans,而商用化的Java大型项目开发者,也可能 会比较钟意于使用IntelliJ,而从底层开发角度来说,可能使用vim是更合适的选择,可以灵活地在C/C++与Java代码之间进行切换。总而言 之,几乎所有的Java环境的编程工具都可以用于Android编程。
对于这些工具呢,熟悉工具的使用是件好事,所谓“磨刀不误砍柴工”,为将来提升效率,这是件好事。但是要磨刀过多,柴没砍着,转型成“磨刀工”了。如果过多地在这些编程工具上纠结尝试,反而忽视了所编代码的本身,这倒会舍本逐末。
我们既然是研究Android编程,这时仅说明两种Android官方提供的编程方法:使用Android SDK工具包编程,或是使用Eclipse + ADT插件编程。
2.1 使用Android SDK工具包
在Android开发过程中,如果Eclipse环境不可得的情况下,可以直接使用SDK来创建应用程序工程。首先需要安装某一个版本的Android SDK开发包,这个工具包可以到http://developer.android.com/sdk/index.html这 个网址去下载,根据开发所用的主机是Windows、Linux还是MacOS X(MacOS仅支持Intel芯片,不支持之前的PowerPC芯片),下载对应的.zip文件,比如android-sdk_r19- linux.zip。下载完成后,解压到一个固定的目录,我们这里假定是通过环境变量$ANDROID_SDK_PATH指定的目录。
下载的SDK包,默认是没有Android开发环境支持的,需要通过tools目录里的一个android工具来下载相应的SDK版本以用于开发。我们通过运行$ANDROID_SDK_PATH/tools/android会得到如下的界面:
在 上面的安装界面里选择不同的开发工具包,其中Tools里包含一些开发用的工具,如我们的SDK包,实际上也会在这一界面里进行更新。而对于不同的 Android版本,1.5到4.1,我们必须选择下载某个SDK版本来进行开发。而下载完之后的版本信息,我们既可以在这一图形界面里看到,也可以通过 命令行来查看。
$ANDROID_SDK_PATH/tools/android list targets
id: 1 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: 2 or "Google Inc.:Google APIs:16"
Name: Google APIs
Type: Add-On
Vendor: Google Inc.
Revision: 1
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, WVGA800 (default), QVGA, WXGA800
ABIs : armeabi-v7a
通过android list targets列出来的信息,可以用于后续的开发之用,比如对于不同的target,最后得到了id:1、id:2这样的信息,则可以被用于应用程序工程 的创建。而细心一点的读者会看到同一个4.1版本的SDK,实际可分为”android-16”和"Google Inc.:Google APIs:16",这样的分界也还有有意义的,”android-16”用于“纯”的android 4.1版的应用程序开发,而“Google Inc.:Google APIs:16”则加入了Google的开发包。
配置好环境之后,如果我们需要创建Android应用程序。tools/android这个工具,同时也具备可以创建Android应用程序工程的能力。我们输入:
$ANDROID_SDK_PATH/tools/android create project -n Hello -t 1 -k org.lianlab.hello -a Helloworld -p hello
这 样我们就在hello目录里创建了一个Android的应用程序,名字是Hello,使用API16(Android 4.1的API版本),包名是org.lianlab.hello,而默认会被执行到的Activity,会是叫Helloworld的Activity 类。
掌握Android工具的一些使用方法也是有意义的,比如当我们的Eclipse工程被破坏的情况下,我们依然可以手工修复这一Android应用程序工程。或是需要修改该工程的API版本的话,可以使用下面的命令:
$ANDROID_SDK_PATH/tools/android updateproject -t 2 -p .
在这个工程里,如果我们不加任何修改,会生成一个应用程序,这个应用程序运行的效果是生成一个黑色的图形界面,打印出一行"Hello World, Helloworld"。如果我们需要对这一工程进行编译等操作的话,剩下的事情就属于标准的Java编译了,标准的Java编译,使用的是 ant(ant.apache.org)编译工具。我们先改变当前目录到hello,然后就可以通过” ant –projecthelp”来查看可以被执行的Android编译工程,
$ ant -projecthelp
Buildfile: /Users/wuhe/android/workspace/NotePad/bin/tmp/hello/build.xml
Main targets:
clean Removes output files created by other targets.
debug Builds the application and signs it with a debug key.
install Installs the newly build package. Must be used in conjunction with a build target (debug/release/instrument). If the application was previously installed, the application is reinstalled if the signature matches.
installd Installs (only) the debug package.
installi Installs (only) the instrumented package.
installr Installs (only) the release package.
installt Installs (only) the test and tested packages.
instrument Builds an instrumented packaged.
release Builds the application in release mode.
test Runs tests from the package defined in test.package property
uninstall Uninstalls the application from a running emulator or device.
Default target: help
但如果只是编译,我们可以使用antdebug生成Debug的.apk文件,这时生成的文件,会被放到 bin/Hello-debug.apk。此时生成的Hello-debug.apk,已经直接可以安装到Android设备上进行测试运行。我们也可以 使用ant release来生成一个bin/Hello-release-unsigned.apk,而这时的.apk文件,则需要通过jarsigner对文件进 行验证才能进行安装。
通过antdebug这一编译脚本,我们可以看到详细的编译过程。我们可以看到,一个Android的工程,最后会是通过如图所示的方式生成最后的.apk文件。
把一个Android的源代码工程编译成.apk的Android应用程序,其过程如下:
1) 所有的资源文件,都会被aapt进行处理。所有的XML文件,都会被aapt解析成二进制格式,准确地说,这样的二进制格式,是可以被直接映射到内存里的 二进制树。做过XML相关开发的工程师,都会知道,XML的验证与解析是非常消耗时间与内存的,而通过编译时进行XML解析,则节省了运行时的开销。当然 解析的结果最后会被aapt通过一个R.java保存一个二进制树的索引,编程时可通过这个R.java文件进行XML的访问。aapt会处理所有的资源 文件,也就是Java代码之外的任何静态性文件,这样处理既保证了资源文件间的互相索引得到了验证,也确保了R.java可以索引到这个应用程序里所有的 资源。
2) 所有的Java文件,都会被JDK里的javac工具编译成bin目录下按源代码包结构组织的.class文件(.class是标准的Java可解析执行 的格式),比如我们这个例子里生成的bin/classes/org/lianlab/hello/*.class文件。然后这些文件,会通过SDK里提 供的一个dx工具转换成classes.dex文件。这一文件,就是会被Dalvik虚拟机所解析执行的
3) 最后我们得到的编译过的二进制资源文件和classes.dex可执行文件,会通过一个apkbuilder工具,通过zip压缩算法打包到一个文件里,生成了我们所常见的.apk文件。
4) 最后,.apk文件,会通过jarsigner工具进行校验,这一校验值会需要一个数字签名。如果我们申请了Android开发者帐号,这一数字签名就是 Android所分发的那个数字证书;如果没有,我们则使用debug模式,使用本地生成的一个随机的数字证书,这一文件位于~/.android /debug.keystore。
虽然我们只是下载了SDK,通过一行脚本创建了Android应用程序工程,通过另一行完成了编译。但也许还是会被认为过于麻烦,因为需要进行字符界面的 操作,而且这种开发方式也不是常用的方式,在Java环境下,我们有Eclipse可用。我们可以使用Eclipse的图形化开发工具,配合ADT插件使 用。
2.2 使用Eclipse+ADT插件。
在Android环境里可以使用Java世界里几乎一切的开发 工具,比如NetBeans等,但Eclipse是Android官方标准的开发方式。使用Eclipse开发,前面提到的开发所需SDK版本下载,也是 必须的,然后还需要在Eclipse环境里加装ADT插件,Android Development Toolkit。
我们在Eclipse的菜单里,选择”Help” à “Install New Software…”,然后在弹出的对话框里的Workwith:输入ADT的发布地址:https://dl-ssl.google.com/android.eclipse,回车,则会得到下面的软件列表。选择Select All,将这些插件全都装上,则得到了可用于Android应用程序开发的环境。
这 里还需要指定SDK的地址,Windows或是Linux里,会是在菜单“Window” à “Preferences”,在MacOS里,则会是”Eclipse” à“Preferences” 。在弹出的对话框里,选择Android,然后填入Android SDK所保存的位置。
点 击OK之后,则可以进行Android开发了。选择”File” à “New”à “Project” à “Android”,在Eclipse 3.x版本里,会是“Android Project”,在Eclipse 4.x版本里,会是“Android Application Project”。如果我们需要创建跟前面字符界面下一模一样的应用程序工程,则在弹出的创建应用程序对话框里填入如下的内容:
然 后我们选择Next,一直到弹出最后界面提示,让我们选择默认Activity的名字,最后点击”Finish”,我们就得到一个Android应用程序 工程,同时在Eclipse环境里,我们既可以通过图形化界面编辑Java代码,也可以通过图形化的界面编辑工具来绘制图形界面。
(注意: 如果Android工程本身比较庞大,则最好将Eclipse里的内存相关的配置改大。在Windows和Linux里,是修改eclipse里的 eclipse.ini文件,而在MacOS里,则是修改Eclipse.app/Contents/MacOS/eclipse.ini。一般会将如下 的值加大成两倍:
--launcher.XXMaxPermSize
512m
-vmargs
-Xms80m
-Xmx1024m
)
我们得到工程目录,在Eclipse环境里会是如下图所示的组织方式。代码虽然是使用一模一样的编译方式,唯一的改变 是,我们不再需要使用脚本来完成编译,我们可以直接使用Eclipse的”Project”à“Build project”来完成编译过程。如果我们使用默认设置,则代码是使用自动编译的,我们的每次修改都会触发增量式的编译。
我 们从这些android编程的过程,看不出来android跟别的Java编程模式有什么区别,倒至少验证了我们前面对android编程特点的猜想,就 是很简单。如果同样我们使用Eclipse开发Java的图形界面程序,需要大量地时间去熟悉API,而在Android这里学习的曲线被大大降低,如果 我们只是要画几个界面,建立起简单的交互,我们几乎无须学习编程。
而从上面的步骤,我们大概也可以得到Android开发的另一个好处,就 是极大的跨平台性,它的开发流程里除了JDK没有提及任何的第三方环境需求,于是这样的开发环境,肯定可以在各种不同的平台执行。这也是Android上 进行开发的好处之一,跨平台,支持Windows,Linux与MacOS三种。
我们再来看一个,我们刚才创建的这个工程里,我们怎么样进行下一步的开发。在Android开发里,决定我们应用程序表现的,也就是我们从一个.apk文件里看到的,我们实际上只需要:
Þ 修改AndroidManifest.xml文件。AndroidManifest.xml是Android应用程序的主控文件,类型于Windows里的注册表,我们通过它来配置我们的应用程序与系统相关的一些属性。
Þ 修改UI显示。在 Android世界里,我们可以把UI编程与Java编程分开对待,处理UI控件的语言,我们可以叫它UI语言,或是layout语言,因为它们总是以 layout类型的资源文件作为主入口的。Android编程里严格地贯彻MVC的设计思路,使我们得到了一个好处,就是我们的UI跟要实现的逻辑没有任 何必然联系,我们可先去调整好UI显示,而UI显示后台的实现逻辑,则可以在后续的步骤里完成。
Þ 改写处理逻辑的Java代码。也 就是我们MVC里的Controller与Model部分,这些部分的内容,如果与UI没有直接交互,则我们可以放心大胆的改写,存在交互的部分,比如处 理按钮的点击,取回输入框里的文字等。作为一个定位于拓展能力要求最高的智能手机操作系统,android肯定不会只实现画画界面而已,会有强大的可开发 能力,在android系统里,我们可以开发企业级应用,大型游戏,以及完整的Office应用。
无论是通过tools/android工 具生成的Android源代码目录,还是通过Eclipse来生成的Android源代码工程,都需要进一步去自定义这个步骤来完成一个Android应 用程序。当然,还有一种特殊的情况就是,这个源代码工程并非直接是一个Android应用程序,只是Unit Test工程或是库文件工作,并不直接使用.apk文件,这里则可能后续的编程工作会变得不同。我们这里是分析Android应用程序,于是后面分别来看 应用程序编程里的这三部分的工作如何进行。
2.3 AndroidManifest.xml
先来看看AndroidManifest.xml文件,一般出于方便,这一文件有可能也被称为manifest文件。像我们前面的例子里的创建的Android工程,得到的AndroidManifest.xml文件就很简单:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.lianlab.hello"
android:versionCode="1"
android:versionName="1.0">
<application android:label="@string/app_name">
<activity android:name=".Helloworld"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
作为xml文件,所有的<>都会是成对的,比如我们看到 的<manifest></manifest>,这被称为标签(Tag)。标签可以包含子标签,从而可以形成树型的结点关系。如 果没有子标签,则我们也可以使用</>来进行标识,比如我们上面看到 的<actionandroid:name=”android.intent.action.MAIN” />。
<manifest>,是主标签,每个文件只会有一个,这是定义该应用程序属性的主入口,包含应用程序的一切信息,比如我们的例子里定义了xml的命名空间,这个应用程序的包名,以及版本信息。
<application>,则是用于定义应用程序属性的标签,理论上可以有多个,但多个不具有意义,一般我们一个应用程序只会有一个,在这个标签里我们可以定义图标,应用程序显示出来的名字等。在这一标签里定义的属性一般也只是辅助性的。
<activity>,这是用来定义界面交互的信息。我们在稍后一点的内容介绍Android编程细节时会描述到这些信息,这一标签里的属性定义会决定应用程序可显示效果。比如在启动界面里的显示出来的名字,使用什么样的图标等。
<intent-filter>,这一标签则用来控制应用程序的能力的,比如该图形界面可以完成什么样的功能。我们这里的处理比较简单,我们只是能够让这个应用程序的HelloWorld可以被支持点击到执行。
从 这个最简单的AndroidManifest.xml文件里,我们可以看到Android执行的另一个特点,就是可配置性强。它跟别的编程模型很不一样的 地方是,它没有编程式规定的main()函数或是方法,而应用程序的表现出来的形态,完全取决于<activity>字段是如何定义它的。
2.4 图形界面(res/layout/main.xml)
我 们可以再来看android的UI构成。UI也是基于XML的,是通过一种layout的资源引入到系统里的。在我们前面看到的最简单的例子里,我们会得 到的图形界面是res/layout/main.xml,在这一文件里,我们会看到打开显示,并在显示区域里打印出Hello World, Helloworld。
<?xml version="1.0" encoding="utf-8"?>
<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:id="@+id/textView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:padding="@dimen/padding_medium"
android:text="@string/hello_world"
tools:context=".HelloWorld" />
</RelativeLayout>
在这个图形界面的示例里,我们可以看到,这样的图形编程方式,比如传统的方式里要学习大量的API要方便得多。
<LinearLayout>,这一标签会决定应用程序如何在界面里摆放相应的控件
<TextView>,则是用于显示字符串的图形控件
使 用这种XML构成的UI界面,是MVC设计的附属产品,但更大的好处是,有了标准化的XML结构,就可以创建可以用来画界面的IDE工具。一流的系统提供 工具,让设计师来设计界面、工程师来逻辑,这样生产出来的软件产品显示效果与用户体验会更佳,比如iOS;二流的系统,界面与逻辑都由工程师来完成,在这 种系统上开发出来的软件,不光界面不好看,用户体验也会不好。我们比如在Eclipse里的工程里查看,我们会发现,我们打开res/layout /main.xml,会自动弹出来下面的窗口,让我们有机会使图形工具来操作界面。
在 上面IDE工具里,左边是控件列表,中间是进行绘制的工作区,右边会是控件一些微调窗口。一般我们可以从左边控制列表里选择合适的控件,拖到中间的工作区 来组织界面,原则上的顺序是layout à 复合控件 à 简单控件。中间区域可以上面还有选择项用于控制显示属性,在工作区域里我们可以进一步对界面进行微调,也可以选择控件点击左键,于是会出来上下文菜单来操 作控件的属性。到于右边的操作界面,上部分则是整个界面构成的树形结构,而下部分则是当我们选择了某个界面元素时,会显示上下文的属性。最后,我们还可以 在底部的Graphic Layout与main.xml进行图形操作界面与源代码编辑两种操作方式的切换。
有了这种工具,就有可能实现设 计师与工程师合作来构建出美观与交互性更好的Android应用程序。但可惜的是,Android的这套UI设计工具,太过于编程化,而且由于版本变动频 繁的原因,非常复杂化,一般设计师可能也不太容易学好。更重要的一点,Android存在碎片化,屏幕尺寸与显示精度差异性非常大,使实现像素级精度的界 面有技术上的困难。也这是Android上应用程序不如iOS上漂亮的原因之一。但这种设计至少也增强了界面上的可设计性,使Android应用程序在观 感上也有不俗表现。
我们可以再回过头来看看应用程序工程里的res目录,res目录里包含了Android应用程序里的可使用的资源,而资 源文件本身是可以索引的,比如layout会引用drawable与values里的资源。对于我们例子里使用的<TextView … android:text="Hello World,Helloworld" …>,我们可以使用资源来进行引用,<TextView …android:text=” @string/hello_string” …>,然后在res/values/strings.xml里加入hello_string的定义。
<string name="hello_world">Hello world!</string>
从通过这种方式,我们可以看另外一些特点,就是Android应用程序在多界面、多环境下的自适应性。对于上面的字符串修改的例子,我们如果像下面的示例环境那样定义了res/layout-zh/strings.xml,并提供hello_string的定义:
<string name="hello_world">欢迎使用!</string>
最后,得到的应用程序,在英文环境里会显示‘Hello world!’,而如果系统当前的语言环境是中文的话,就会显示成‘欢迎使用!’。这种自适应方式,则是不需要我们进行编程的,系统会自动完成对这些显示属性的适配。
当 然,这时可能会有人提疑问,如果这时是韩文或是日文环境会出现什么情况呢?在Android里,如果不能完成相应的适配,就会使用默认值,比如即使是我们 创建了res/values-zh/strings.xml资源,在资源没有定义我们需要使用的字符串,这时会使用英文显示。不管如何,Android提 供自适应显示效果,但也保证总是不是会出错。
这些也体现出,一旦Android应用程序写出来,如果对多语言环境不满意,这时,我们完全可以把.apk按zip格式解开,然后加入新的资源文件定义,再把文件重新打包,也就达到了我们可能会需要的汉化的效果。
在 res目录里,我们看到,对于同一种类型的资源,比如drawable、values,都可以在后面加一个后缀,像mdpi,hdpi, ldpi是用于适配分辨率的,zh是用来适配语言环境的,large则是用来适配屏幕大小的。对于这种显示上的自适应需求,我们可以直接在Eclipse 里通过创建Android XML文件里得到相应的提示,也可以参考http://developer.android.com/training/basics/supporting-devices/查看具体的使用方式。
于 是,透过资源文件,我们进一步验证了我们对于Android MVC的猜想,在Android应用程序设计里,也跟iOS类似,可以实现界面与逻辑完全分离。而另一点,就是Android应用程序天然具备屏幕自适应 的能力,这一方面带来的影响是Android应用程序天生具备很强的适应性,另一方面的影响是Android里实现像素精度显示的应用程序是比较困难的, 维护的代价很高。
我们可以再通过应用程序的代码部分来看看应用程序是如何将显示与逻辑进行绑定的。
2.5 Java编程(src/org/lianlab/hello/HelloWorld.java)
在Android编程里,实现应用程序的执行逻辑,几乎就是纯粹的Java编程。但在编程上,由于Android的特殊性,这种Java编程也还是被定制过的。
我们看到我们例子里的源代码,如果写过Java代码,看到这样的源代码存放方式,就可以了解到Android为什么被称为Java操作系统的原因了,像这 种方式,就是标准的Java编程了。事实上,在Android的代码被转义成Dalvik代码之前,Android编程都可被看成标准的Java编程。我 们来看这个HelloWorld.java的源代码。
package org.lianlab.hello;
import android.os.Bundle;
import android.app.Activity;
public class HelloWorld extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
}
}
代码结构很简单,我们所谓的HelloWorld,就是继承了Activity的基类,然后再覆盖了Acitivity基于的onCreate()方法。
Activity类,是Android系 统设计思路里的很重要的一部分,所有与界面交互相关的操作类都是Activity,是MVC框架里的Controller部分。那Model部分由谁来提 供呢?这是由Android系统层,也就是Framework来提供的功能。当界面失去焦点时,当界面完全变得不可见时,这些都属于Framework层 才会知道的状态,Framework会记录下这些状态变更的信息,然后再回调到Activity类提供的相应状态的回调方法。关于Activity我们后 面来详细说明,而见到Activity类的最简单构成,我们大体上就可以形成Android世界里的完整MVC框架构成完整印象了。
我们继承了Activity类 之后,就会覆盖其onCreate()回调方法。这里我们使用了”@Override”标识,这是一种Java语言里的Annotation(代码注释) 技术,相当于C语言里的pragma,用于告诉编译器一些相应参数。我们的Override则告诉javac编译器,下面的方法在构建对象时会覆盖掉父类 方法,从而提高构建效率。Activity类里提供的onXXX()系列的都可以使用这种方法进行覆盖,从而来实现自定义的方法,切入到Android应 用程序不同状态下的自定义实现。
我们覆盖掉的onCreate()
方 法,使用了一个参数,savedInstanceState,这个参数的类型是Bundle。Bundle是构建在Android的Binder IPC之上的一种特殊数据结构,用于实现普通Java代码里的Serialization/Deserializaiton功能,序列化与反序列化功能。 在Java代码里,我们如果需要保存一些应用程序的上下文,如果是字符串或是数据值等原始类型,则可以直接写到文件里,下次执行时再把它读出来就可以了。 但假设我们需要保存的是一个对象,比如是界面的某个状态点,像下面的这样的数据结构:
class ViewState {
public int focusViewID;
public Long layoutParams ;
public String textEdited;
…
}
这时,我们就无法存取这样的结构了,因为这样的对象只是内存里的一些标识,存进时是一个进程上下文环境,取回来时会是另一种,就会出错。为了 实现这样的功能,就需要序列化与反序列化,我们读写时都不再是以对象为单位,而是以类似于如下结构的一种字典类型的结构,最后进行操作的是一个个的键值 对, ViewState[‘focusViewID’]的值会是valueOfViewID,一个整形值。
‘ViewState’ {
‘focusViewID’: valueOfViewID,
‘LayoutParams’:valueOfLayoutParams,
‘textEdited’: ‘User input’,
}
我们按这种类似的格式写到文件里,当再读取出来时,我们就可以新建一个ViewState对象,再使用这些保存过的值 对这一对象进行初始化。这样就可以实现对象的保存与恢复,这是我们onCreate()方法里使用Bundle做序列化操作的主要目的,我们的 Activity会有不同生存周期,当我们有可能需要在进程退出后再次恢复现象时,我们就会在退出前将上下文环境保存到一个 onSavedInstance的Bundle对象里,而在onCreate()将显示的上下文恢复成退出时的状态。
而另一个必须要使用Bundle的理由是,我们的Activity与实现Activity管理的Framework功能部件 ActivityManager,是构建在不同进程空间上的,Activity将运行在自己独立的进程空间里,而Framework则是运行在另一个系统 级进程SystemServer之上。我们的Bundle是一种进行过序列化操作的对象,于是相应的操作是系统进程会触发Activity的进行 onCreate()回调操作,而同时会转回一个上下文环境的Bundle,可将Activity恢复到系统指定的某种图形界面状态。Bundle也可能 为空,比如Activity是第一个被启动的情况下,这个空的onSavedInstance则会被忽略掉。
我们进入到onCreate()方法之后,第一行便是
super.onCreate(savedInstanceState);
从 字面上看,这种方式相当于我们继承了父类方法,然后又回调到父类的onCreate()来进行处理。这种方式貌似很怪,但这是设计模式(Design Pattern)里鼎鼎大名的一种,叫IoC ( Inversion of Control)。通过这样的设计模式,我们可以同时提供可维护性与可调试性,我们可以在通过覆盖的方法提供功能更丰富的子类,实际上每次调用子类的 onCreate()方法,都将调用到各个Activity拓展类的onCreate()方法。而这个方法一旦进入,又会回调到父类的 onCreate()方法,在父类的onCreate()方法里,我们可以提供更多针对诸多子类的通用功能(比如启动时显示的上下文状态的恢复,关闭时一 些清理性工作),以及在这里面插入调试代码。
然后,我们可以加载显示部分的代码的UI,
setContentView(R.layout.main);
这 一行,就会使我们想要显示的图形界面被输出到屏幕上。我们可以随意地修改我们的main.xml文件,从而使setContentView()之后显示出 来的内容随之发生变化。当然,作为XML的UI,最终是在内存里构成的树形结构,我们也可以在调用setContentView()之前通过编程来修改这 个树形结构,于是也可以改变显示效果。
到目前为止,我们也只是实现了将内容显示到屏幕上,而没有实现交互的功能。如果要实现交互的功能,我们也只需要很简单的代码就可以做到,我们可以将 HelloWorld.java改成如下的内容,从而使用我们的”Hello world”字符串可以响应点击事件:
package org.lianlab.hello;
import android.os.Bundle;
import android.app.Activity;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.TextView;
public class HelloWorld extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
((TextView)findViewById(R.id.textView1)).setOnClickListener(
new OnClickListener() {
@Override
public void onClick(View v) {
finish();
}
});
}
}
我们使用Activity类的findViewById()方法,则可以找到任何被R.java所索引起来的资源定义。我们在这里使用了R.id.textView1作为参数,是因为我们在main.xml就是这么定义TextView标签的:android:id="@+id/textView1"。
而 我们找到字段之后,会调用TextView对象的setOnClickListener()方法,给TextView注册一个 onClickListener对象。这样的对象,是我们在Android世界里遇到的第二次设计模式的使用(事实上Android的实现几乎使用到所有 的Java世界里的通用设计模式),Listener本身也会被作为Observer设计模式的一种别称,主要是用于实现被动调用逻辑,比如事件回馈。
Observer(Listener) 设计模式的思路,跟我们数据库里使用到的Trigger功能类似,我们可对需要跟踪的数据操作设置一个Trigger,当这类数据操作进行时,就会触发数 据库自动地执行某些操作代码。而Observer(Listener)模式也是类似的,监听端通过注册Observer来处理事件的回调,而真正的事件触 发者则是Observer,它的工作就是循环监听事件,然后再调用相应监听端的回调。
这样的设计,跟效率没有必然联系,太可以更大程度地降低设计 上的复杂度,同时提高设计的灵活性。一般Observer作为接口类,被监听则会定位成具体的Subject,真正的事件处理,则是通过实现某个 Observer接口来实现的。对于固定的事件,Subject对象与Observer接口是无须变动的,而Observer的具体实现则可以很灵活地被 改变与扩充。如下图所示:
如果我们对于监听事件部分的处理,也希望能加入这样的灵活性,于是我们可以继续抽象,将Subject泛化成一个Observable接口,然后可以再提供不同的Observable接口的实现来设计相应的事件触发端。
针 对于我们的Android里的OnClickListener对象,则是什么情况呢?其实不光是OnClickListener,在Android里所有 的事件回调,都有类似于Observer的设计技巧,这样的回调有 OnLongClickListener,OnTouchListener,OnKeyListener,OnContextMenuListener, 以及OnSetOnFocusChangeListener等。但Android在使用设计模式时很简洁,并不过大地提供灵活性,这样可以保证性能,也可 以减小出错的概率(基本上所有的设计复杂到难以理解的系统,可维护性远比简单易懂但设计粗糙的系统更差,因为大部分情况下人的智商也是有限的资源)。于 是,从OnClickLister的角度,我们可以得到下图所示的对象结构。
Click 事件的触发源是Touch事件,而当前View的Touch事件在属于点击事件的情况下,会生成一个performClick的Runnable对象(可 交由Thread对象来运行其run()回调方法)。在这个Runnable对象的run()方法里会调用注册过的OnClickListener对象的 OnClick()方法,也就是图示中的mOnClickListener::onClick()。当这个对象被post()操作发送到主线程时(作为 Message发送给UI线程的Hander进行处理),我们覆盖过的OnClick()回调方法就由主线程执行到了。
我们注册的 Click处理,只有简单的一行,finish(),也就是通过点击事件,我们会将当前的Activity关闭掉。如果我们觉得这样不过瘾,我们也可通过 这次点击触发另一个界面的执行,比如直接搜索这个字符串。这样的改动代码量很小,首先,我们需要在HelloWorld.java的头部引入所需要的 Java包,
import android.app.SearchManager;
import android.content.Intent;
然后可以将我们的OnClick()方法改写成启动一个搜索的网页界面,查找这个字符串,而当前界面的Activity则退出。这时,我们新的OnClick()方法则会变成这个样子:
public void onClick(View v) {
Intent query = new Intent(Intent.ACTION_WEB_SEARCH);
query.putExtra(SearchManager.QUERY,
((TextView)v).getText());
startActivity(query);
finish();
}
但是可能还是无法解决我们对于Android应用程序与Java环境的区别的疑问:
Þ Android有所谓的MVC,将代码与显示处理分享,但这并非是标准Java虚拟机环境做不到。一些J2EE的软件框架也有类似的特征。
Þ AndroidManifest.xml与On*系列回调,这样的机制在JAVA ME也有,JAVA ME也是使用类似的机制来运行的,难道Android是JAVA ME的加强版?
Þ 至 于Listener模式的使用,众所周知,Java是几乎所有高级设计模式的实验田,早就在使用Listener这样模式在处理输入处理。唯一不同的是 ClickListener,难道Android也像是可爱的触摸版Ubuntu手机一样,只在是桌面Java界面的基础加入了触摸支持?
Þ Activity从目前的使用上看,不就是窗口(Window)吗?Android开发者本就有喜欢取些古怪名字的嗜好,是不是他们只是标新立异地取了个Activity的名字?
对于类似这样的疑问,则是从代码层面看不清楚了,我们得回归到Android的设计思想这一层面来分析,Android应用程序的执行环境是如何与众不同的。
不 过,我们可以从最后的那行Activity调用另一个Activity的例子里看出一些端倪,在这次调用里,我们并没有显式地创建新的Activity, 如果从代码直接去猜含义的话,我们只是发出了个执行某种操作的请求,而这个请求并没有指定有谁来完成。这就是Android编程思想的基础,一种全开放的 “无界化”编程模型。