2.3 Android系统架构剖析
“剖析”二字有“解剖分析”之意,是指对一个人或一件事做深入的分析,让别人了解这个人或明白事情的来龙去脉。学习Android之路是一条不平坦的路,需要付出一定的时间和精力。为了更加深入理解Android的精髓,很有必要了解Android的系统架构,了解它的组成。这样才能知道Android究竟能干什么、开发人员所要学的是什么。
2.3.1 Android体系结构介绍
Android是一个移动设备的开发平台,其软件层次结构包括操作系统(OS)、中间件(MiddleWare)和应用程序(Application)。根据Android的软件框图,其软件层次结构自下而上可以分为以下4层。
(1)操作系统层(OS)
(2)各种库(Libraries)和Android运行环境(RunTime)
(3)应用程序框架(Application Framework)
(4)应用程序(Application)
上述各层的具体结构如图2-19所示。
图2-19 Android操作系统的组件结构图
1. 操作系统层(OS)——底层
因为Android源于Linux,使用了Linux内核,所以Android使用Linux 2.6作为操作系统。Linux 2.6是一种标准的技术,Linux也是一个开放的操作系统。Android对操作系统的使用包括核心和驱动程序两部分,Android的Linux核心为标准的Linux 2.6内核,Android更多的是需要一些与移动设备相关的驱动程序。主要的驱动如下。
· 显示驱动(Display Driver):基于Linux的帧缓冲(Frame Buffer)驱动;
· Flash内存驱动(Flash Memory Driver):基于MTD的Flash驱动程序;
· 相机驱动(Camera Driver):基于Linux的v4l(Video for Linux)驱动;
· 音频驱动(Audio Driver):基于ALSA(Advanced Linux Sound Architecture,高级Linux声音体系)驱动;
· Wi-Fi驱动(Wi-Fi Driver):基于IEEE 802.11标准的驱动程序;
· 键盘驱动(KeyBoard Driver):作为输入设备的键盘驱动;
· 蓝牙驱动(Bluetooth Driver):基于IEEE 802.15.1标准的无线传输技术;
· Binder IPC驱动:Android中一个特殊的驱动程序,具有单独的设备节点,提供进程间通信的功能;
· Power Management(能源管理):管理电池电量等信息。
2. 各种库(Libraries)和Android运行环境(RunTime)——中间层
本层次对应一般嵌入式系统,相当于中间件层次。本层次分成两个部分,一个是各种库,另一个是Android运行环境。本层的内容大多是使用C和C++实现的。其中包含的各种库如下所示。
· C库:C语言的标准库,也是系统中的底层库,C库是通过Linux的系统调用来实现的;
·多媒体框架(MediaFramework):这部分内容是Android多媒体的核心部分,是基于PacketVideo(即PV)的OpenCORE,从功能上本库一共分为两大部分,一部分是音频、视频的回放(PlayBack),另一部分则是音、视频的记录(Recorder);
· SGL:2D图像引擎;
· SSL:即Secure Socket Layer,位于TCP/IP协议与各种应用层协议之间,为数据通信提供安全支持;
· OpenGL ES 1.0:提供了对3D的支持;
· 界面管理工具(Surface Management):提供了管理显示子系统等功能;
· SQLite:一个通用的嵌入式数据库;
· WebKit:网络浏览器的核心;
· FreeType:位图和矢量字体的功能。
Android的各种库一般是以系统中间件的形式提供的,它们均有的一个显著特点,就是与移动设备平台的应用密切相关。
Android运行环境主要是指虚拟机技术——Dalvik。Dalvik虚拟机和一般Java虚拟机(Java VM)不同,它执行的不是Java标准的字节码(Bytecode),而是在Dalvik可以执行的格式文件(.dex)。在执行的过程中,每一个应用程序即一个进程(Linux的一个Process)。二者最大的区别在于Java VM是基于栈的虚拟机(Stack-based),而Dalvik是基于寄存器的虚拟机(Register-based)。显然,后者最大的好处在于可以根据硬件实现更大的优化,这更符合移动设备的特点。
3. 应用程序(Application)
Android的应用程序主要是用户界面(User Interface)方面的,通常用Java语言编写,其中还可以包含各种资源文件(放置在res目录中)。Java程序及相关资源经过编译后,将生成一个APK包。Android本身提供了主屏幕(Home)、联系人(Contact)、电话(Phone)、浏览器(Browser)等众多的核心应用。同时,应用程序的开发者还可以使用应用程序框架层的API实现自己的程序。这也是Android开源巨大潜力的体现。
4. 应用程序框架(Application Framework)
Android的应用程序框架为应用程序层的开发者提供API,它实际上是一个应用程序的框架。由于上层的应用程序是以Java构建的,因此本层次提供的首先包含了UI程序中所需要的各种控件。例如,Views(视图组件),其中又包括了List(列表)、Grid(栅格)、Text Box(文本框)、Button(按钮)等,甚至一个嵌入式的Web浏览器。
一个基本的Android应用程序可以利用应用程序框架中的以下五个部分。
· Activity(活动)
· Broadcast Intent Receiver(广播意图接收者)
· Service(服务)
· Content Provider(内容提供者)
· Intent and Intent Filter(意图和意图过滤器)
本书的主要目的是讲解Android游戏开发知识,介绍的是应用程序(Application)方面的知识。这些知识都是与Java开发相关的,当然也还需要掌握一些其他层的相关知识,如底层的内核和驱动等知识。
2.3.2 Android应用工程文件组成
介绍完Android的整体结构之后,接下来就介绍Android工程文件的组成。因为Android游戏项目是用Eclipse创建的工程,所以很有必要了解一个Android工程文件的结构。
在Eclipse中,一个基本的Android项目的目录结构如图2-20所示。
图2-20 Android应用工程文件组成
1. src目录——程序文件
在这里保存了程序员直接编写的程序文件。和一般的Java项目一样,“src”目录下保存的是项目的所有包及源文件(.java),“res”目录下包含了项目中的所有资源,例如,程序图标(drawable)、布局文件(layout)和常量(values)等。不同的是,在Java项目中没有“gen”目录,也没有每个Android项目都必须有的AndroidManfest.xml文件。
“.java”格式文件是在建立项目时自动生成的,这个文件是只读模式,不能更改。R.java文件是定义该项目所有资源的索引文件。先来看看HelloAndroid项目的R.java文件,如下面的代码:
package com.yarin.Android.HelloAndroid; public final class R { public static final class attr { } public static final class drawable { public static final int icon=0x7f020000; } public static final class layout { public static final int main=0x7f030000; } public static final class string { public static final int app_name=0x7f040001; public static final int hello=0x7f040000; } }
从上述代码中,可以看到定义了很多常量,并且会发现这些常量的名字都与res文件夹中的文件名相同,这再次证明.java文件中存储的是该项目所有资源的索引。有了这个文件,在程序中使用资源将变得更加方便,可以很快地找到要使用的资源,由于这个文件不能被手动编辑,所以当在项目中加入了新的资源时,只需要刷新一下该项目,.java文件便自动生成了所有资源的索引。
2. AndroidManfest.xml文件——设置文件
文件AndroidManfest.xml是一个控制文件,在里面包含了该项目中使用的Activity、Service、Receiver,看下面“HelloAndroid”项目中的AndroidManfest.xml文件。
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.yarin.Android.HelloAndroid" android:versionCode="1" android:versionName="1.0"> <application android:icon="@drawable/icon" android:label="@string/app_name"> <activity android:name=".HelloAndroid" 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> <uses-sdk android:minSdkVersion="5" /> </manifest>
在上述代码中,intent-filters描述了Activity启动的位置和时间。每当一个Activity(或者操作系统)要执行一个操作时,它将创建出一个Intent的对象,这个Intent对象能承载的信息可描述你想做什么、你想处理什么数据与数据的类型,以及一些其他信息。而Android能够将每个intent-filter数据进行比较,找到最合适Activity来处理调用者所指定的数据和操作。下面来仔细分析AndroidManfest.xml文件,如表2-1所示。
表2-1 AndroidManfest.xml文件分析
3. 常量的定义文件——露脸的文件
下面看看资源文件中一些常量的定义,如String.xml,如下面的代码:
<?xml version="1.0" encoding="utf-8"?> <resources> <string name="hello">Hello World, HelloAndroid!</string> <string name="app_name">HelloAndroid</string> </resources>
上述代码很简单,就定义了两个字符串资源,请不要小看上面的几行代码。它们的内容很“露脸”,里面的字符直接显示在手机屏幕中,就像动态网站中的HTML一样。
在Android中还有一个露脸的文件,那就是布局(layout)文件,这个文件一般位于“res\layout\main.xml”。虽然它里面的字符没有“露脸”,但是里面的代码能够生成一个显示界面,这个可以显示人名和图片的界面就“露脸”了。例如:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" > <TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="@string/hello" /> </LinearLayout>
在上述代码中,有以下几个布局和参数。
· < LinearLayout></LinearLayout>:线性版面配置,在这个标签中,所有元件都是按自上而下的顺序排列而成的。
· android:orientation:表示这个介质的版面配置方式是“从上到下垂直地排列其内部的视图”。
· android:layout_width:定义当前视图在屏幕上所占的宽度,fill_parent即填充整个屏幕。
· android:layout_height:定义当前视图在屏幕上所占的高度,fill_parent即填充整个屏幕。
· wrap_content:随着文字数量的不同而改变这个视图的宽度或高度。
在上述布局代码中,使用了一个TextView来配置文本标签Widget(构件),其中设置的属性android:layout_width为整个屏幕的宽度,android:layout_height可以根据文字来改变高度,而android:text则设置了这个TextView要显示的文字内容,这里引用了@string中的“hello”字符串,即String.xml文件中的“hello”所代表的字符串资源。“hello”字符串的内容“Hello World, HelloAndroid!”就是在HelloAndroid项目运行时看到的字符串。
注意:上面介绍的只是主要文件,在项目中需要自行编写。在项目中还有很多其他的文件,这些文件很少需要编写,所以在此就不进行讲解了。
2.3.3 应用程序的生命周期
作为一个程序,也如同自然界的生物一样,有自己的生命周期。开发一个程序的目的是为了完成一项功能,如银行中计算加息的软件。每当一个用户去柜台办理取款业务时,银行工作人员便启动了这个软件,当用这个软件完成利息计算时,这个软件当前的任务就完成了,此时就需要结束。肯定有读者会提出疑问:“生生死死”多麻烦,就让这个程序一直是“活着”的状态,一个用户办理完取款业务后,继续等待下一个用户办理取款业务,这样岂不是更方便?但是很不幸,不能这样做。原因是计算机的处理性能是一定的,计算机能够很轻松地完成与一个人、两个人或三个人相关的存取款业务,但是我们开发的程序软件需要每天处理成千上万个存取款业务,如果每个任务都一直活着,一台有限配置的计算机能承受得了吗?
由此可见,应用程序的生命周期就是一个程序的存活时间,即在什么时间内有效。Android是一构建在Linux之上的开源移动开发平台,在Android中,大多数情况下每个程序都是在各自独立的Linux进程中运行的。当一个程序或其某些部分被请求时,它的进程就“出生”了;当这个程序没有必要再运行下去且系统需要回收这个进程的内存用于其他程序时,这个进程就“死亡”了。可以看出,Android程序的生命周期是由系统控制而非程序自身控制的。这和编写桌面应用程序时的思维有一些不同,一个桌面应用程序的进程也是在其他进程或用户请求时被创建,但是往往是在程序自身收到关闭请求后执行一个特定的动作而结束进程。要想做好某种类型的程序或者某种平台下的程序的开发,最关键的就是要弄清楚这种类型的程序或整个平台下的程序的一般工作模式,并且要做到熟记在心。在Android系统中,程序的生命周期控制就属于这个范畴,开发者必须理解不同的应用程序组件,尤其是Activity、Service和Intent Receiver,了解这些组件是如何影响应用程序的生命周期的。如果不正确地使用这些组件,可能会导致系统终止正在执行重要任务的应用程序进程。
一个常见的进程生命周期漏洞的例子是Intent Receiver(意图接收器),当Intent Receiver在onReceive方法中接收到一个Intent(意图)时,它会启动一个线程,然后返回。一旦返回,系统将认为Intent Receiver不再处于活动状态,因而Intent Receiver所在的进程也就不再有用了(除非该进程中还有其他的组件处于活动状态)。因此,系统可能会在任意时刻终止该进程以回收占用的内存。这样进程中创建出的那个线程也将被终止。解决这个问题的方法是从Intent Receiver中启动一个服务,让系统知道进程中还有处于活动状态的工作。为了使系统能够正确决定在内存不足时应该终止哪个进程,Android应根据每个进程中运行的组件及组件的状态把进程放入一个“Importance Hierarchy(重要性分级)”中。
进程的类型多种多样,按照重要程度主要有如下几类进程。
1. 前台进程(Foreground)
前台进程是看得见的,与用户当前正在做的事情密切相关的。不同的应用程序组件能够通过不同的方法将它的宿主进程移到前台。在如下的任何一个条件下“①进程正在屏幕的最前端运行一个与用户交互的活动(Activity),它的onResume方法被调用;②进程有一正在运行的Intent Receiver(它的IntentReceiver.onReceive方法正在执行);③进程有一个服务(Service),并且在服务的某个回调函数(Service.onCreate、Service.onStart或Service.onDestroy)内有正在执行的代码”,系统将把进程移动到前台。
2. 可见进程(Visible)
可见进程也是可见的,它有一个可以被用户从屏幕上看到的活动,但不在前台(它的onPause方法被调用)。例如,前台的活动是一个对话框,以前的活动就隐藏在对话框之后,就会出现这种进程。可见进程非常重要,一般不允许被终止,除非是为了保证前台进程的运行而不得不终止它。
3. 服务进程(Service)
服务进程是无法看见的,拥有一个已经用startService方法启动的服务。虽然用户无法直接看到这些进程,但它们做的事情却是用户所关心的(如后台MP3回放或后台网络数据的上传与下载)。因此,系统将一直运行这些进程,除非内存不足以维持所有的前台进程和可见进程。
4. 后台进程(Background)
后台进程也是看不见的,只有打开之后才能看见。例如,迅雷下载,可以将其最小化,虽然桌面上看不见了,但是它一直在进行下载的工作。拥有一个当前用户看不到的活动(它的onStop方法被调用)。这些进程对用户体验没有直接的影响。如果它们正确执行了活动生命周期,系统可以在任意时刻终止该进程以回收内存,并提供给前面三种类型的进程使用。系统中通常有很多这样的进程在运行,因此要将这些进程保存在LRU列表中,以确保当内存不足时用户最近看到的进程最后一个被终止。
5. 空进程(Empty)
不拥有任何活动的应用程序组件的进程。保留这种进程的唯一原因是在下次应用程序的某个组件需要运行时,不需要重新创建进程,这样可以提高启动速度。
系统将以进程中当前处于活动状态组件的重要程度为基础对进程进行分类。进程的优先级可能也会根据该进程与其他进程的依赖关系而增长。例如,进程A通过在进程B中设置Context.BIND_AUTO_CREATE标记或使用ContentProvider被绑定到一个服务(Service),那么进程B在分类时至少要被看成与进程A同等重要。
例如,Activity的状态转换图如图2-21所示。
图2-21 Activity状态转换图
图2-21所示的状态的变化是由Android内存管理器决定的,Android会首先关闭那些包含Inactive Activity的应用程序,再关闭Stopped状态的程序。在极端情况下,会移除Paused状态的程序。