http://blog.csdn.net/21cnbao/article/details/7835255

本文内容,主题是透过应用程序来分析Android系统的设计原理与构架。我们先会简单介绍一下Android里的应用程序编程,然后以这些应用程序在运行环境上的需求来分析出,为什么我们的Android系统需要今天这样的设计方案,这样的设计会有怎样的意义, Android究竟是基于怎样的考虑才变成今天的这个样子,所以本文更多的分析Android应用程序设计背后的思想,品味良好架构设计的魅力。分五次连载完成,第一部分是最简单的部分,解析Android应用程序的开发流程。

特别声明:本系列文章LiAnLab.org著作权所有,转载请注明出处。作者系LiAnLab.org资深Android技术顾问吴赫老师。本系列文章交流与讨论:@宋宝华Barry

1.  Android应用程序

在目前Android大红大紫的情况下,很多人对编写Android应用程序已经有了足够深入的了解。即便是没有充分的认识,在现在Android手机已经相当普及的情况下,大家至少也会知道Android的应用程序会是一个以.apk为后缀名的文件(在Windows系统里,还会是一个带可爱机器人图标的文件)。那这个apk包又有什么样的含义呢?

如果您正在使用Linux操作系统,可以使用命令file命令来查看这一文件的类型。比如我们下载了一个Sample.apk的文件,则使用下面的命令:

[plain] view plain copy print ?
  1. $file Sample.apk  
  2. Sample.apk: Zip archive data, at least v1.0 to extract  
$file Sample.apkSample.apk: Zip archive data, at least v1.0 to extract

对,没有看错,只一个简单的zip文件。要是做过Java开发的人,可以对这种格式很亲切,因为传说中的.jar、.war格式,都是Zip压缩格式的文件。我们可继续使用unzip命令将这一文件解压(或是任何的解压工具,zip是人类历史是最会古老最为普及的压缩格式之一,几乎所有压缩工具都支持)。通过解压,我们就得到了下面的文件内容:

[cpp] view plain copy print ?
  1. AndroidManifest.xml,  
  2. classes.dex,  
  3. resources.arsc,  
  4. META-INF,  
  5. res,  
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版本来进行开发。而下载完之后的版本信息,我们既可以在这一图形界面里看到,也可以通过命令行来查看。

[plain] view plain copy print ?
  1. $ANDROID_SDK_PATH/tools/android list targets  
  2. id: 1 or "android-16"  
  3.      Name: Android 4.1  
  4.      Type: Platform  
  5.      API level: 16  
  6.      Revision: 1  
  7.      Skins: HVGA, QVGA, WQVGA400, WQVGA432, WSVGA, WVGA800 (default), WVGA854, WXGA720, WXGA800, WXGA800-7in  
  8.      ABIs : armeabi-v7a  
  9. ----------  
  10. id: 2 or "Google Inc.:Google APIs:16"  
  11.      Name: Google APIs  
  12.      Type: Add-On  
  13.      Vendor: Google Inc.  
  14.      Revision: 1  
  15.      Description: Android + Google APIs  
  16.      Based on Android 4.1 (API level 16)  
  17.      Libraries:  
  18.       * com.google.android.media.effects (effects.jar)  
  19.           Collection of video effects  
  20.       * com.android.future.usb.accessory (usb.jar)  
  21.           API for USB Accessories  
  22.       * com.google.android.maps (maps.jar)  
  23.           API for Google Maps  
  24.      Skins: WVGA854, WQVGA400, WSVGA, WXGA800-7in, WXGA720, HVGA, WQVGA432, WVGA800 (default), QVGA, WXGA800  
  25.      ABIs : armeabi-v7a  
$ANDROID_SDK_PATH/tools/android list targetsid: 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应用程序工程的能力。我们输入:

[plain] view plain copy print ?
  1. $ANDROID_SDK_PATH/tools/android create project -n Hello -t 1 -k org.lianlab.hello -a Helloworld -p hello  
$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编译工程,

[plain] view plain copy print ?
  1. $ ant -projecthelp  
  2. Buildfile: /Users/wuhe/android/workspace/NotePad/bin/tmp/hello/build.xml  
  3.   
  4. Main targets:  
  5.   
  6.  clean       Removes output files created by other targets.  
  7.  debug       Builds the application and signs it with a debug key.  
  8.  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.  
  9.  installd    Installs (only) the debug package.  
  10.  installi    Installs (only) the instrumented package.  
  11.  installr    Installs (only) the release package.  
  12.  installt    Installs (only) the test and tested packages.  
  13.  instrument  Builds an instrumented packaged.  
  14.  release     Builds the application in release mode.  
  15.  test        Runs tests from the package defined in test.package property  
  16.  uninstall   Uninstalls the application from a running emulator or device.  
  17. Default target: help  
$ ant -projecthelpBuildfile: /Users/wuhe/android/workspace/NotePad/bin/tmp/hello/build.xmlMain 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。一般会将如下的值加大成两倍:

[plain] view plain copy print ?
  1. --launcher.XXMaxPermSize  
  2. 512m  
  3. -vmargs  
  4. -Xms80m  
  5. -Xmx1024m  
  6. )  
--launcher.XXMaxPermSize512m-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文件就很简单:

[plain] view plain copy print ?
  1. <?xml version="1.0" encoding="utf-8"?>  
  2.       package="org.lianlab.hello"  
  3.       android:versionCode="1"  
  4.       android:versionName="1.0">  
  5.       
  6.         
  7.                   android:label="@string/app_name">  
  8.               
  9.                   
  10.                   
  11.               
  12.           
  13.       
  14.   
<?xml version="1.0" encoding="utf-8"?>                                                                                

作为xml文件,所有的<>都会是成对的,比如我们看到的,这被称为标签(Tag)。标签可以包含子标签,从而可以形成树型的结点关系。如果没有子标签,则我们也可以使用来进行标识,比如我们上面看到的。

,是主标签,每个文件只会有一个,这是定义该应用程序属性的主入口,包含应用程序的一切信息,比如我们的例子里定义了xml的命名空间,这个应用程序的包名,以及版本信息。

,则是用于定义应用程序属性的标签,理论上可以有多个,但多个不具有意义,一般我们一个应用程序只会有一个,在这个标签里我们可以定义图标,应用程序显示出来的名字等。在这一标签里定义的属性一般也只是辅助性的。

,这是用来定义界面交互的信息。我们在稍后一点的内容介绍Android编程细节时会描述到这些信息,这一标签里的属性定义会决定应用程序可显示效果。比如在启动界面里的显示出来的名字,使用什么样的图标等。

,这一标签则用来控制应用程序的能力的,比如该图形界面可以完成什么样的功能。我们这里的处理比较简单,我们只是能够让这个应用程序的HelloWorld可以被支持点击到执行。

从这个最简单的AndroidManifest.xml文件里,我们可以看到Android执行的另一个特点,就是可配置性强。它跟别的编程模型很不一样的地方是,它没有编程式规定的main()函数或是方法,而应用程序的表现出来的形态,完全取决于字段是如何定义它的。

2.4 图形界面(res/layout/main.xml)

我们可以再来看android的UI构成。UI也是基于XML的,是通过一种layout的资源引入到系统里的。在我们前面看到的最简单的例子里,我们会得到的图形界面是res/layout/main.xml,在这一文件里,我们会看到打开显示,并在显示区域里打印出Hello World, Helloworld。

[html] view plain copy print ?
  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  3.     xmlns:tools="http://schemas.android.com/tools"  
  4.     android:layout_width="match_parent"  
  5.     android:layout_height="match_parent" >  
  6.     <TextView  
  7.         android:id="@+id/textView1"  
  8.         android:layout_width="wrap_content"  
  9.         android:layout_height="wrap_content"  
  10.         android:layout_centerInParent="true"  
  11.         android:padding="@dimen/padding_medium"  
  12.         android:text="@string/hello_world"  
  13.         tools:context=".HelloWorld" />  
  14. RelativeLayout>  
<?xml version="1.0" encoding="utf-8"?>    

在这个图形界面的示例里,我们可以看到,这样的图形编程方式,比如传统的方式里要学习大量的API要方便得多。

,这一标签会决定应用程序如何在界面里摆放相应的控件

,则是用于显示字符串的图形控件

使用这种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里的资源。对于我们例子里使用的,我们可以使用资源来进行引用,,然后在res/values/strings.xml里加入hello_string的定义。

[html] view plain copy print ?
  1. <string name="hello_world">Hello world!string>  
Hello world!

从通过这种方式,我们可以看另外一些特点,就是Android应用程序在多界面、多环境下的自适应性。对于上面的字符串修改的例子,我们如果像下面的示例环境那样定义了res/layout-zh/strings.xml,并提供hello_string的定义:

[html] view plain copy print ?
  1. <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的源代码。

[java] view plain copy print ?
  1. package org.lianlab.hello;  
  2. import android.os.Bundle;  
  3. import android.app.Activity;  
  4.   
  5. public class HelloWorld extends Activity {  
  6.     @Override  
  7.     public void onCreate(Bundle savedInstanceState) {  
  8.         super.onCreate(savedInstanceState);  
  9.         setContentView(R.layout.main);  
  10.     }  
  11. }  
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代码里,我们如果需要保存一些应用程序的上下文,如果是字符串或是数据值等原始类型,则可以直接写到文件里,下次执行时再把它读出来就可以了。但假设我们需要保存的是一个对象,比如是界面的某个状态点,像下面的这样的数据结构:

[java] view plain copy print ?
  1. class ViewState {  
  2.     public int focusViewID;  
  3.     public Long layoutParams ;  
  4.     public String textEdited;  
  5. …  
  6. }  
class ViewState {    public int focusViewID;    public Long layoutParams ;    public String textEdited;…}
这时,我们就无法存取这样的结构了,因为这样的对象只是内存里的一些标识,存进时是一个进程上下文环境,取回来时会是另一种,就会出错。为了实现这样的功能,就需要序列化与反序列化,我们读写时都不再是以对象为单位,而是以类似于如下结构的一种字典类型的结构,最后进行操作的是一个个的键值对, ViewState[‘focusViewID’]的值会是valueOfViewID,一个整形值。

[plain] view plain copy print ?
  1. ‘ViewState’ {  
  2.   
  3.             ‘focusViewID’: valueOfViewID,  
  4.   
  5.             ‘LayoutParams’:valueOfLayoutParams,  
  6.   
  7.             ‘textEdited’:  ‘User input’,  
  8.   
  9. }  
‘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()方法之后,第一行便是

[java] view plain copy print ?
  1. super.onCreate(savedInstanceState);  
super.onCreate(savedInstanceState);

从字面上看,这种方式相当于我们继承了父类方法,然后又回调到父类的onCreate()来进行处理。这种方式貌似很怪,但这是设计模式(Design Pattern)里鼎鼎大名的一种,叫IoC ( Inversion of Control)。通过这样的设计模式,我们可以同时提供可维护性与可调试性,我们可以在通过覆盖的方法提供功能更丰富的子类,实际上每次调用子类的onCreate()方法,都将调用到各个Activity拓展类的onCreate()方法。而这个方法一旦进入,又会回调到父类的onCreate()方法,在父类的onCreate()方法里,我们可以提供更多针对诸多子类的通用功能(比如启动时显示的上下文状态的恢复,关闭时一些清理性工作),以及在这里面插入调试代码。

            然后,我们可以加载显示部分的代码的UI,

[java] view plain copy print ?
  1. setContentView(R.layout.main);  
setContentView(R.layout.main);

这一行,就会使我们想要显示的图形界面被输出到屏幕上。我们可以随意地修改我们的main.xml文件,从而使setContentView()之后显示出来的内容随之发生变化。当然,作为XML的UI,最终是在内存里构成的树形结构,我们也可以在调用setContentView()之前通过编程来修改这个树形结构,于是也可以改变显示效果。

            到目前为止,我们也只是实现了将内容显示到屏幕上,而没有实现交互的功能。如果要实现交互的功能,我们也只需要很简单的代码就可以做到,我们可以将HelloWorld.java改成如下的内容,从而使用我们的”Hello world”字符串可以响应点击事件:
[java] view plain copy print ?
  1. package org.lianlab.hello;  
  2.   
  3. import android.os.Bundle;  
  4. import android.app.Activity;  
  5. import android.view.View;  
  6. import android.view.View.OnClickListener;  
  7. import android.widget.TextView;  
  8.   
  9. public class HelloWorld extends Activity {  
  10.     @Override  
  11.     public void onCreate(Bundle savedInstanceState) {  
  12.         super.onCreate(savedInstanceState);  
  13.         setContentView(R.layout.main);  
  14.       ((TextView)findViewById(R.id.textView1)).setOnClickListener(  
  15.                 new OnClickListener() {  
  16.                    @Override  
  17.                    public void onClick(View v) {  
  18.                            finish();  
  19.                    }  
  20.            });  
  21.     }  
  22. }  
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包,

[java] view plain copy print ?
  1. import android.app.SearchManager;  
  2. import android.content.Intent;  
import android.app.SearchManager;import android.content.Intent;

然后可以将我们的OnClick()方法改写成启动一个搜索的网页界面,查找这个字符串,而当前界面的Activity则退出。这时,我们新的OnClick()方法则会变成这个样子:

[java] view plain copy print ?
  1. public void onClick(View v) {  
  2.        Intent query = new Intent(Intent.ACTION_WEB_SEARCH);  
  3.        query.putExtra(SearchManager.QUERY,  
  4. ((TextView)v).getText());  
  5.        startActivity(query);  
  6.        finish();  
  7.   }             
 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编程思想的基础,一种全开放的“无界化”编程模型。

特别声明:本系列文章LiAnLab.org著作权所有,转载请注明出处。作者系LiAnLab.org资深Android技术顾问吴赫老师。本系列文章交流与讨论:@宋宝华Barry

Android的系统设计,与别的智能手机操作系统有很大区别,甚至在以往的任何操作系统里,很难找到像Android这样进行全面地系统级创新的操作系统。从创新层面上来说,Android编程上的思想和支持这种应用程序运行环境的系统,这种理念本身就是一种大胆的创新。

整个Android系统,实际主要目的,就是打造一个功能共享的世界。

功能共享最重要的交互,于是Android创造出一种Intent和IntentFilter配合的低耦合的交互模型,Intent只是一种描述要完成什么工作跨进程的结构体,而最终如何解析这些Intent并完成其响应,是由IntentFilter来进行换算,最终是由用户来决定如何完成。

而在Intent这种超级交互消息之上,Android进一步把应用程序的实现逻辑拆分成多种特殊的实现:

Þ     Activity:带显示与交互能力的部分

Þ     Service:不带显示与交互能力的部分

Þ     Content Provider:在功能交互之外,提供数据交互能力的部分

Þ     Broadcast Receiver:用来处理广播交互的部分

这四种功能上的拆分,也体现了Android设计者在设计上抽象思绪能力,即便是随着Android迅猛发展,目前已经到了4.1这么功能丰富、用户体验良好的状态,我们编程也还是与这四种功能组件打交道,可以满足我们任何的编程时所需要的任何行为。

而这四种基本组件组成部分,使Android应用程序反倒成了一个“空壳子”。静态上看,应用程序只是一种包装这些功能的容器;从运行态来看,所谓的应用程序,也只是承载某些功能的进程。

1.1   所谓的Android应用程序

 我们从前面的例子中看到,无论是编写的代码,还是最后生成的.apk文件,都是没有所谓的应用程序的。应用程序本身是一种虚无的概念,只是一种以zip格式进行压缩的一个文件,一种容器而已。


如我们前面的Helloworld的例子里所看到的那样,其实一个应用程序里最重要的一个配置文件就是AndroidManifest.xml文件。一个最简单的项目,除了基本的代码与UI资源,也会需要有个AndroidManifest.xml文件。甚至一些极端一点的例子,我们去市场上下载一些什么主题包、插件包、权限包之类的.apk文件,解压开,这时可以发现这样的.apk文件里,连代码都没有,只有一些图片之类的文件。

于是,我们可以得到Android里关于应用程序的第一个印象,作为Android应用程序的载体,.apk文件只是一种进行包装与传输的格式,而每个.apk文件必然包含一个AndroidManifest.xml文件,由这一文件来描述该.apk文件提供的内容。当然,我们在稍后会看到,这一文件里,还会包含一些权限控制的信息。

我们可以给我们的应用程序创建两个一模一样的图形界面,直接从我们的前面的HelloWorld开始下手,比如将HelloWorld.java在Eclipse里拷贝到HelloAgain.java(这样可以减少改代码的麻烦)。这时可以得到两个界面的应用程序,然后我们再把我们的AndoridManifest.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">

    <applicationandroid: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>

        <activity android:name=".HelloAgain"

                  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>

这时我们编译、安装到Android设备(或者是虚拟机)里,这时再打开主界面查看安装过的应用程序,这时是不是发生了什么很奇怪的现象?这时界面上会出现两个叫Helloworld的应用程序。我们这时如果在设备里去“设置”à “应用程序”,我们仍只看到一个应用程序。

通过对AndroidManifest.xml的小恶作剧,我们可以看到Android应用程序的第二个特点,就是没有所谓的主入口(即我们点击的图标时触发的执行效果)。应用程序在安装完成后,只是通过AndroidManifest.xml来决定在系统上应该表现成什么样子。

如果希望应用程序可以表现不如此变态,这时,我们可以回到AndroidManifest.xml里,把标签删掉,这时应用程序的表现就正常了。

到目前为止,我们就已经接触了android编程里的两个概念,一个是Activity,另一个是Intent(而我们AndroidManifest.xml文件里的Intent Filter实际是辅助Intent的)。Android毕竟是种图形界面的编程环境,我们常见的应用程序里,可能绝大部分只会与这两种概念打交道。而两者的概念组合,就很容易体现出Android应用程序在编程上的“无界化”思想。

1.2   Android世界里的共享

 作为一个智能手机操作系统,其用户可能在功能上有各种各样的功能组合。比如最简单的打电话,则后续动作会有保存联系人,同时需要给联系人拍照做来电大头贴。又比如需要来了个短信通信用户到某个地方干什么事情,这时,用户需要打开地图,搜索一下地址,然后还有可能需要定位到那个位置。


用户在主界面里点击相应功能的应用程序之后,就可能有非常多的功能性的组合,因为用户的想法是不可预估的。我们当然也可以限制用户当前菜单下可以干什么事情,但这样就失去了智能系统的意义。

我们也可以假设用户都会按一个“Home”键回到主界面,这时原来的执行的程序就会被锁定当前状态,用户重新打开另外一个应用程序,操作完再按“Home”键可以退回到原来的应用程序。通过这种“应用程序”到“Home”到“应用程序”的循环,我们也可以达到我们想要达到的目的。但这时,出于交互性的考虑,我们也还是需要有限地提供一些交互手段,比如“短信”应用程序里包含地址信息,一点击可以直接打开“地图”进行后续操作,但这些有限交互是可以在系统设计阶段被固化。这时,我们是不是就得到了我们想要的能够应付用户任何操作组合的系统?是的,恭喜您,您得到了iPhone的设计思路。但此时的用户交互流程则被改变成这个样子:


这种解决问题的办法也不是不可以,但需要很固化的设计,应用程序的行为比较受限。虽然通过横扫全世界的iPhone证明了这样的设计可能是比较合乎用户体验之道的(不容易出错),但这样的解决思路从系统设计角度来看,并不是很灵活。另外一个麻烦是必须要有苹果级设计功底的“Home”键,山寨货则用不了多久就会因为键盘失灵而失效。当然即使苹果级设计,iPhone里的“Home”键还是会失效,于是又不得不在屏幕上加上触摸的Home手势。

作为开源系统的Android,当然不可能基于iOS的交互思路来解决问题,何这种交互时多了一步不停要回到主界面这一步。在Android的设计里,最重要的是能够解决一个应用程序之间进行交互的问题,然后可以实现我们想要在Android系统里完成某种操作时,可以享受从一路顺畅完成的快感。

Android的解决之道,则是将传统意义上的应用程序,细化成一个个完成某项功能的部分,这种功能部分,在Android世界里被称为Activity。Activity都应该被设计成可以独立地被执行以解决某个问题,当它完成或是用户选择退出执行时,又会自动跳回到调用这一Activity的界面,当然这时跳回的位置肯定是另外一个Activity。当然,在一个Android系统里有可能存在无限多的Activity,在他们进行跳转切换时,我们就需要一种很灵活的消息传输机制(因为我们必须兼容系统里所有可能的互相调用的情况)。而且这种传输机制还必须能够跨进程,不然,我们所有的涉及Activity互相调用部分都必须在同一进程里完成。于是,Android系统里又有了Intent,用于解决交互通信。


这样的编程模型也需要有一定前提,那就是我们Application概念必须被弱化,我们不能有main函数入口(如果系统执行依赖main作入口,则不能实现Activity之间互相调用了,所有的Activity执行之前,必须先通过main入口来初始化环境)。出于这样的设计,所以Application必须只是一个容器,将各种不同的Activity实现包装起来加载到系统里。

当然,将功能拆分成一个个的单一功能界面之后,我们需要有种机制可以将用户一路点击过去历史记录下来,当用户处理完时,可以退回到他们之前操作过的界面,这次就可以由多个应用程序组合出像是在用同一个应用程序的效果。有了Activity,有了Activity之间起到调用作用的Intent,这时所有界面间操作变得有点像是函数调用一样,于是我们可以找函数调用时的基本数据结构—栈来帮忙,发生调用时,需要退出的Activity及其状态压栈,当从调用退出时则进行栈的弹出操作,这时我们的Activity管理就演变成如下图所示的简单栈管理。


有了这样的概念,于是我们响应用户点击操作的问题便迎刃而解,我们在设计应用程序时,不再是设计一个复杂的功能实现,而是实现一组完成单项功能的实现,也就是Activity。然后这些Activity,只会通过用户点击来驱动它们之间是如何进行交互的。比如,我们前面看到的地图、搜索、定位三个功能,虽然它都会被包装到同一个地图的应用程序里,但在实现上会是地图、搜索、定位三个不同的Activity。

因为现在我们的界面上的互相调用,已经变成了一种函数式的调用,这样,整个手机上的功能都被切分成各个单一的小功能,而真正要在Android系统上完整地实现某复杂个操作,则会提交由用户的点击来组合生成。这样的复杂功能,则已经不是一个编程上的概念了,在Android系统里,这种需要完成什么事情的操作被抽象成一个虚拟的概念Task。比如我们前面提到的打电话加拍大头贴的操作组合,就构成一个Task,这一Task需要由Launcher.apk,Contacts.apk,Gallery.apk来协同完成。


如果我们有两个能够提供同样功能的Activity,这种执行模式的灵活性表现得会更加明显。比如中间打电话的功能,我们系统里有三个Activit(CallScreen, SipPhone, Dialer)都可以完成电话呼叫的功能,这时执行上的路径则会有三种可能性,会在进行跳转时弹出圣对话框由用户来选择:


通过Activity的这种可以动态被用户选择的特点,当用户对某一功能不满意时,完全就有可能通过下载另一个能实现这种功能的应用程序进行替换,甚至可以自己写一个。事实上,Android系统里除了系统状态条与锁屏界面之后,没有任何的不可被替换的功能,这也是Android设备总是会长得千奇百怪的原因之一。

到这时,我们就可以看到Activity之所以会不被称为Window的原因,它也是单个界面或是MVC里的Controller实现部分那么简单,Activity这个名字代表的是某种单一交互功能上的实现。这种功能的实现将在系统里通过Intent串接起来,构成了一个在功能上具备极大可拓展性的系统。基于这样的特点,Android也就被称作是“无边界”系统,因为它在功能上延展不再受限于系统的能力,而只受限于智商与创意。

这就是Android世界里的功能共享。

在这种功能共享模型之下,可能还是会有一些微调的需求:

1.             我们有一些情况下不宜使用这种栈式Activity管理,比如我们写一个需要注册的应用程序,注册完开始使用,然后再按退出,我们又会一步步退回到注册填个人信息的界面,而不合理地完全退出。这样可能不合适。这时,我们可以使用Intent的Flag参数, 加上Activity的Affinity属性进行组合控制。

2.             如果不停地跳出对话框让用户选,用户会崩溃掉。当然,用户可以在选择时点选一个“始终”的默认选择,这时下次就会使用默认的Activity处理某种操作。但还是有可能会不合理地使用跨.apk文件里使用Activity,造成性能上的开销,这时,我们也可以在执行下次Activity执行操作时进行强制性地指定。

当然,我们通过Activity这种概念还需要另外一个前提,这就是Android会有别于传统操作系统的前提,那就是单窗口。想像一下,在多窗口环境下,我们的栈式管理Activity在进行跳转和返回时将会构成多大的灾难啊。好在使用电容屏的设备,单窗口是天生的需求。由于手指触摸的精度非常低,无法点准过小的按钮,比如窗口上的关闭按钮,如果将这些按钮放大,又造成了屏幕显示空间上的浪费。iPhone带来的“后PC时代”革命,最重要的一点就是使用“返祖”式的单窗口显示。

这种怪异的操作方式,实际上在我们生活中也有类似的例子,就比如说我们的动态网页。动态网页,特别是HTML5构建的网络应用程序,其操作模式,就是可以在不同的链接里不断地点击下去,如果不是弹出新窗口,我们始终还可以退回到发起这一连串点击的起始页面。Android应用程序,XML构成的UI语言的作用跟Html页面类似,而Java构建的Activity就相当与网页交互中使用的JavaScript,有了这样的相似性,Android编程环境可以说是最接近HTML5的一种编程环境了,但可惜不能像HTML5那样可以跨平台。

我们解析了能完成单一功能的Activty,这时还需要了解Intent,就像是我们了解过了函数实现原理,我们还需要掌握函数之间的参数传递。当然,一般在介绍编程的思路里,会结合起来说明,或是先说明参数传递。但Android环境里有点特殊性,一是Intent是一种能够实现跨进程调用的信息传递机制,二是Intent在消息传递上又很灵活,有一定的动态性。Intent不光服务于Activity之间的调用,还会用于一些不直接与界面打交道的逻辑实现部分,比如我们后面将提到的Service,Broadcast Receiver,以及 Notification。

1.3   Intent与Intent Filter

 Intent,英文原意就是要“干什么”的意思,之所以取这个名字,也是因为在Android系统里,Intent所起到的作用就是用来指明下一步具体是做什么,具体是不是执行,由谁来执行,则会由根据当前的系统状态(能不能解析这个Intent请求)来决定。这不只是简简单单地发个消息而已,而是一种更安全的、更加松散的消息机制。

在一个Intent消息对象里,共有六个成员(并不都是必须赋值的,只要一个Intent对象能够被解析,就会得以执行,否则就会会被舍弃):

 

成员

类型

说明

示例

ComponentName

String

用于定义谁将处理这一Intent。它由一个Activity的具体实现的全名(加上包名)来指定

org.lianlab.hello.HelloActivity

Action

String

用于定义这一动作是做什么,可以被拓展自定义类型

ACTION_CALL  

   开始通话.         

ACTION_EDIT                     进行编辑.

Data

String

用一个URI来指定Intent的操作对象,因为URI一般会包含种类信息,于是这个值也可能被用作MIME设别。

“content://contacts/people/1”

指定联系列表中的第一个

Category

String

用来进一步明确什么样的可执行实体将处理这一Intent。是可选项,也可多选。

CATEGORY_HOME

   主界面应用程序

CATEGORY_LAUNCHER

  可在主界面里被点击

Type

String

用来指定特定的MIME类型

"video/*"

   视频

Extra

Bundle

用来传递额外的数据传递,前面我们也介绍了Bundle是一种key:value配对的字典类型,于是Extra里可以转递复杂的数据

putExtra("sms_body", "some text");

发短信时指定内容

Flags

int

预定义一系列用来控制Intent行为的属性值

 

 

在这个成员变量里,最能体现灵活性的就是Component,如果指定了这个值,则我们在通过startActivity()方法来发送Intent时,就会自动启动Component指定的Activity。如果没有指定,则会由系统来选择一个能够处理这一Intent的Activity来执行,这时就引入了Intent Filter的概念。

Intent Filter在Android里是一种类似于Windows里的注册表一样的东西,虽然我们也可以通过编程来进行Intent Filter的控制,但一般情况下,我们只在AndroidManifest.xml文件里进行定义,对它一个的标签进行指定。应用程序在安装过程中,它的AndroidManifest.xml会被系统扫描并汇总到系统环境里,这时也会被导入。当Activity发送出来的Intent,没有指定Component时,系统就会通过找到合适的处理对象,如果只有一个或是用户设置了默认项,则启动这个功能部件来完成任务;如果有多个匹配同时用户又没有指定默认项,则会弹出对话框让用户选择。当然,默认项也会随着系统里新增了同一Intent匹配项而失效,用户也可以通过“设置”à “应用程序”来取消默认值。

在AndroidManifest.xml里面定义很简单,就是通过指定Intent对象的Action,Data,Type,和Category这四个成员变量来指定。比如:

        android:name=".PlayerActivity"android:label="@string/app_name"

                       android:configChanges="orientation" >

           

                android:name="android.intent.action.MAIN" />

                android:name="android.intent.category.LAUNCHER" />

           

                                   

                                      android:name="android.intent.action.VIEW" />

                                      android:name="android.intent.category.DEFAULT" />

                                      android:scheme="file" />

                                   

...

当我们的某个Activity,发送了一个Intent,其Action是”android.intent.action.VIEW”,data又是以file:///开始的URI指定的内容(也就是文件类型),这时上面例子里的PlayerActivity就会成为播放时的候选项。

我们可以继续修改我们前面的HelloWorld的例子,我们新建一个Intent,将Action设成”android.intent.action.VIEW”(可以通过Intent.ACTION_VIEW这个预变量来转义),data使用某个文件“file:///sd-ext/Movies/test.mp4”,这时就会匹配到我们上面的定义:

    public void onClick(Viewv) {

       Intent request = new Intent(Intent.ACTION_VIEW);

       request.putData(“file:///sd-ext/Movies/test.mp4”);

       startActivity(request);

       finish();

   }    

当然,我们并不一定需要代码来进行这样的测试,我们也可以使用设备上的am命令来完成。要完成与上面的点击操作一样的功能,也可以通过adb来执行这条命令:

$adb shell am start –aandroid.intent.action.VIEW –d file:///sd-ext/Movies/test.mp4

在我们具体写代码过程中,我们可以根据需求来定义我们所需要的,可以将过滤规则写得很细,也可以写得很粗,让我们的Activity有更多地被执行到的机会。在这些规则里,可能最重要的规则,就是我们前面也示范过的:

android:name="android.intent.action.MAIN" />

android:name="android.intent.category.LAUNCHER" />

这两行规则,将使用我们的应用程序可以被主界面所收集,使用户可以在主界面里点击运行这个Activity。

有了Activity,我们就可以构建基于功能共享而实现所谓应用程序,而有了Intent,使用我们在共享时所受的限制可以变得更小。而且,由于是简单化的单窗口模式,再加上一些在性能设计上的精细设计,于是我们的Android系统便有了良好的人机交互体验。

1.4   编程角度的应用程序

 光有Activity与Intent,并不是Android应用程序编程时的全部。应用程序除了有人机交互界面之外,有可能还需要使用到一些不直接与人交互而在后台长期运行操作;我们还需要有某种机制,能够提供数据共享,并且在数据共享时能使用统一的访问机制;最后,我们可能还需要处理以广播方式发送的消息,广播与Intent不同之处在于一到多的方式传播,同时消息只在某个时间段内有效。

事实上,我们的Android编程,是被包装成四个不同的类型,同时通过Intent将这些类包装起来,以解决我们上面提到的,在编写图形应用程序里可能遇到的问题的:

l   Intent: 全局性的、松散的消息传递机制

l   Activity:带图形界面的,可以与用户进行交互的逻辑实现。

l   Service:  不带图形界面的,不直接与用户交互的代码。一般会被用于在后台做些什么事情,比如监听网络、下载、拷贝文件等。(这可能是一般的Android工程师觉得没有必要实现的部分,笔者在讲解Android应用程序相关的课程时,就常有问及,Actiivity会进入到后台,然后有可能被杀死掉,这样的问题如何解决?实际上Activity只解决交互,需要在后台时还需要继续执行的代码,需要用Service来实现。只自己可访问的Service,可以使用简单的本地Service,而需要提供给别的进程来访问的情况下,我们需要通过AIDL编写Remote Service。Service的实现,我们在后台再详细说明,因为Android系统的核心Framework,本身就是由大量这样的Remove Service来组成的。)

l   Content Provider:  提供数据层共享,以CRUD(Create Read Update Delete)方式进行数据访问来统一化数据读写指口一种模型。如果使用了Sqlite做后台的数据支持(实际上相当于应用程序MVC模型里的Model部分被Sqlite延展开来),我们可以通过ContentProvider来各系统内的其他部分提供数据源,当然系统本身也给我们提供了大量这样的ContentProvider,像Setting里的设置的值、联系列表、多媒体文件扫描结果等。(这种数据层上的共享机制,也是应用程序编程上需要加强的技巧之一,因为有了Content Provider,我们则有可能使用Cursor式进行访问,这时我们就可以使用CursorAdapter来自动化地处理数据源。)

l   Broadcast Receiver:处理广播类消息的监听器,从而可以给应用程序提供广播式的信息处理,同时也提供系统消息的广播式分发。比如,Android会将一些系统事件广播出来,像电话振铃、电量状态变化、网络状态变化等,我们需要能够处理这样的事件,电话振铃时我们写的多媒体播放器就应该静音、电量过低时需要保存状态等。对我们应用程序而言,广播方式也是一种很好的通信机制,我们不需要写一个循环通知所有的Activity、Service我们状态发生了改变,而只需要发一个广播,则所有关心这一事件的部分都可以收到。

这些功能实体,都是我们通过Java代码根据不同的基类(Service、 ContentProvider、BroadcastReceiver)派生出相应的子类,再加以具体实现。这样的功能实现不需要自己去创建这个对象,会通过AndroidManifest.xml里的定义,由系统按执行的需要自动创建。有了这些不同的功能实体,我们最后的应用程序,实际上就成了这个样子:


而我们的所有代码,从运行态行为来看,都不再是直接的互相调用关系,而是全部都通过Intent来进行彼此之间的交互。而这样的交互,也不再是传统式地自己陪自己玩,而是会进入到一个大的功能集合体时,提供功能给系统内其他应用程序所使用,而自己也会调用其他部分的代码。


当然,随着Android版本变更,Android系统又新增了一些新的概念,比如针对多窗口功能的Fragment、针对于使用异步机制操作Cursor的Loader等。但万变不离其宗,这些Android的核心原理则一直如此。

总结一下Android编程思想,我们就会知道其实在Android整个生态环境最重要的元素,应用程序,反倒是Android编程上最不重要的。所谓的Android编程,就是要通过编写一个个的Activity、Service、Content Provider、Broadcast Receiver实现,通过功能上的共享与数据上的共享进一步丰富用户可用的功能。当用户可以通过Market或自己下载取得我们封装到.apk文件里的实现之后,这些功能就会无缝地被Intent整合到了一起。

从Android这个编程原则来看,我们可以看到,如果我们使用某种Java执行环境,将Android应用程序的这些组成部分的支持都加入进来,我们也可以得到一个Android兼容的环境。的确如此,已经有人在打这方面的主意,有将Android环境移植到Windows环境里的BlueStack商业解决方案,也有号称兼容Android应用程序,通过一个类似于JAVA ME的虚拟机环境来支持Android应用程序的BlackBerryOS。但我们可以再来看看Android的真正的支持环境,我们可以看到,Android有其独特的特性,也不是那么容易被取代。

 

更多相关文章

  1. Android(安卓)初学者入门(一个最简单的应用程序)
  2. Android开发之旅:应用程序基础及组件
  3. 宋立波:教你如何在Android(安卓)market上注册并发布应用程序
  4. Android(安卓)Notification保留导航功能
  5. [置顶] ArcGIS Runtime SDKs 10.2 for iOS & Android& OS X发布
  6. 第二部分 MediaPlayer的概述
  7. 【生命周期】Android中Activity的生命周期
  8. react-native的兼容性(Android、Ios)
  9. Android(安卓)Activity中启动另一应用程序的方法,无需得到类名

随机推荐

  1. prototype.Function没有在node.js中导出
  2. JavaScript:使用函数参数检索javascript对
  3. 入职必备技能(三)HTML、CSS、JAVASCRIPT
  4. Javascript 排序算法(转)
  5. AngularJS我在哪里可以访问加载的控制器
  6. 动画在画布中移动图像
  7. 掌握JavaScript中的事件监听
  8. JavaScript中的对象描述符
  9. 在JavaScript中访问PHP变量[重复]
  10. JQuery Image滑块从json加载图片