Android应用程序以Java编程语言写成。Android SDK工具将代码连同数据和资源文件一起编译进一个以.apk为后缀的档案文件——Android包(Android package)中。一个.apk文件中的所有代码被认为是一个程序,它也是使用Android的设备的程序安装文件。
一旦被安装,每一个Android程序就活在其自己的安全沙盒中:
- 每一个程序默认都运行于其独立的Linux进程中。当任何一段代码需要被执行时Android会开始一个进程,而当它不再被需要或当其它程序请求系统资源时进程将被关闭。
- 每一个进程有其自己的虚拟机,因此程序代码的运行和所有其他程序是孤立开的。
- 每一个程序默认被分配一个专属的Linux用户ID。由于设立的权限,因此程序的文件仅对该用户和程序可见。同时也有很多方法将程序文件输出给外部程序。
两个不同的程序可以共享同一个用户ID,这样它们的文件将会相互可见。为了节省系统资源,两个程序还可以运行于同一个进程中,并使用同一个虚拟机。
应用程序组件
Android的一大核心特性是一个程序可以使用其他程序的部件(如果那些程序允许的话)。例如,当你的程序需要显示一个滚动式列表时,如果有另外的某个程序拥有一个合适的滚动式列表程序并且向其他程序开放使用权限,那你就可以让这个已经存在的滚动列表来完成你的工作,而不必自己写一个新的滚动列表。并且,它只会在你需要的时候启动你所需要的那一部分。 为了实现这个工作,这个系统必须在当任何一个部分被需要时都能创建一个新的程序进程,并为这个部分实例化一个Java对象。因此,和其他系统上的程序不同,Android程序没有一个单独的入口指针以进入整个程序(例如,没有main()函数),并且这些程序有一些必需的组件使得系统可以在需要时初始化并运行它们。组件有以下四种:
活动(Activity)
为了让用户可以集中处理一个操作,一个活动呈现为一个视觉用户界面。例如,一个活动可以呈现为一个项目列表以供用户进行选择,或者显示照片及其注释。一个短消息程序可能会有一个活动来列出可用的收件人,一个活动可以给选定的收件人撰写消息,以及另外一个活动用来查看旧消息或改变设定。尽管它们一起工作以形成一个完整的用户界面,但每一个活动都是独立于其他活动的。每一个活动都是Activity类的一个子类。
一个程序可以仅由一个活动组成,或者像之前提到的短信息程序那样由多个活动组成。用怎样的活动和多少个活动取决于程序和它的设计。通常其中的一个活动会被标识为当程序启动时第一个被呈现在用户面前的那个。让当前活动启动另一个活动就可以从一个活动切换到下一个。每一个活动被分配了一个默认的窗口来绘图。一般来说这个窗口将填满屏幕,但它也可以小于屏幕而浮在其他窗口的上方。一个活动还可以使用更多的窗口——比如,一个在活动中出现的弹出式的对话框,提示用户进行操作,或者当用户选择了屏幕上一个特定项目后出现一个窗口向用户显示重要的信息。
窗口的视觉内容由视图(View)的一个继承提供,这些视图是从基本的View类衍生出的对象。每一个视图控制窗口中的一块特定的矩形区域。父视图包含并组织它们的子视图的布局(Layout)。叶视图(那些位于继承底部的视图)在它们控制的矩形区中绘图并对指向该区域的用户操作进行回应。因此,视图是一个活动与用户的交互之处。比如,一个视图可以显示一个小的图片并在用户轻触这张图片时启动一个动作。Android中有很多你能马上使用的视图——包括按钮、输入框、滚动条、菜单项目、可选框等等。
视图的继承由Activity.setContentView()方法来设置到一个活动的窗口中。一个内容视图是这个继承的根视图。(参阅另外的用户界面文档来获得关于视图和继承的更多信息)
服务(Service)
服务没有视觉用户界面,它在后台运行不确定的一段时间。比如,一个服务可以在用户处理别的事的同时在后台播放音乐,或从网络接收数据,或计算并给活动提供所需的结果。每一个服务都继承于Service类。
一个基本的例子就是音乐播放器播放一个播放列表中的音乐。这个播放程序可能有一个或多个活动来允许用户选择歌曲并开始播放。但是,播放本身不应该由一个活动来操作,因为用户希望他们离开播放器进行别的工作时音乐仍然继续播放。为了让音乐继续,播放器活动应该在后台开启一个服务。系统会保持音乐播放服务一直运行即使启动它的活动已经不在屏幕上。
关联一个正在运行的服务(并且要是它没在运行的话启动这个服务)是可能的。当关联后,你可以通过这个服务的一个接口和它通信。对于音乐服务来说,这个接口将允许用户暂停,停止和重新播放。
如同活动和其他组件一样,服务运行于程序进程的主线程中。因此它们不会阻碍其他组件或是用户界面。它们常产生一个另外的线程来执行消耗时间的任务(比如音乐播放)。之后请看进程和线程。
如同活动和其他组件一样,服务运行于程序进程的主线程中。因此它们不会阻碍其他组件或是用户界面。它们常产生一个另外的线程来执行消耗时间的任务(比如音乐播放)。之后请看进程和线程。
广播接收者(Broadcast Receivers)
作为一种组建,广播接收者仅仅接收并回应广播通知。很多广播是在系统代码中创建的——比如,通知时区已被改变,通知电量低下,通知拍摄了一张照片,或通知语言偏好被用户改变。程序也可以初始化各种广播——比如,让别的程序知道设备完成了某些数据的下载并已经能够被使用。
一个程序能够有任意多的广播接收者来对任何它认为重要的通知进行回应。所有的广播接收者都继承与BroadcastReceiver基类。
广播接收者不会显示出一个用户界面。然而,它们可能会启动一个活动来响应接收到的信息,或者是使用通知管理器(NotificationManager)来提示用户。通知可以通过各种途径来引起用户的注意——背光的闪烁,设备的震动,声音的播放等。它们通常会在状态栏显示一个图标,用户可以打开它来获得更多的信息。
内容提供者(Content Providers)
内容提供者使程序中特定的一组数据可被其他程序使用。这些数据能够被储存在文件系统中,在SQLite数据库中,或其他任何适合的地方。内容提供者继承于ContentProvider基类,它运用一系列标准的方法来使得其他程序能够接受并储存它所控制的这部分数据。然而,那些程序不会直接使用这些方法。通常他们会使用一个ContentResolver对象并使用它的方法。ContentResolver能够和任何内容提供者交流信息,它与它们合作管理任何类型的进程间交流。
请参阅另外的内容提供者文档来获取更多关于如何使用内容提供者的信息。
当存在一个请求需要被某一特定的组件处理时,Android会确保这个组件的程序进程处于运行状态,并在必要时启动它。如果该组件的一个正确的实例是可用的,系统会在必要时创建这个实例。
激活组件:意图(Intent)
内容提供者会在有一个来自ContentResolver的请求时被激活。其他三种组件——活动,服务和广播接收者——是由被称为意图的异步信息来激活的。一个意图是一个Intent对象,这个对象包含了激活信息的内容。对于活动和服务来说,它命名了被请求的活动并分配了要执行操作的数据的URI等。比如,一个意图可以传达一个请求,让一个活动向用户显示一张图像或是允许用户编辑文字内容。对于广播接收者来说,Intent对象命名了被通知的操作内容。比如,它能通知相关联的部分照相机的拍摄键被按下的这一消息。 有几种不同的方法来激活每一种组件:
- 可以通过向Context.startActivity()或是Activity.startActivitiForResult()传递一个Intent对象来启动一个活动(或让这个活动做一些新的工作)。接受响应的活动会调用它的getIntent()方法来查找最初启动它的那个意图。Android调用该活动的onNewIntent()方法来传递之后其他的意图。
一个活动常常会启动另一个活动。如果希望从启动的活动中获得一个返回的结果,就要改用startAcitivityForResult()方法而不是startAcitity()。比如说,如果一个活动启动了另一个活动来让用户选择一张照片,那么应该要能收到返回的那张被选择的照片。其结果会被返回到一个Intent对象,然后被传递到前一个活动的onActivityResult()方法。
- 通过向Context.startService()传递一个Intent对象可以启动一个服务(或给正在运行的服务一个新的指令)。Andoird会调用这个服务的onStart()方法然后将它传递给Intent对象。
同样的,可以传递一个意图给Context.bindService()来给正在调用的组件和目标服务间建立联系。该目标服务会在onBind()中收到一个Intent对象(如果这个服务没有在运行,bindService()还可以选择启动它)。比如说。一个活动可以和前面提到过的音乐播放服务相连接,这样它能给用户提供一种途径(或者说一个用户界面)来控制播放。这个活动将调用bindService()来建立这个连接,然后调用在服务内定义的一些方法来控制播放。 在之后会更详细说明远程步骤调用(Remote procedure call)如何与服务建立连接。
- 通过传递一个Intent对象给比如Context.sendBroadcast()、Context.sendOrderedBroadcast()、Context.sendStickyBroadcast()或是其他任何它们的变体,一个程序能够初始化一个广播。Android会调用任何可能接收的广播接收者的onReceive()方法来把这个广播递送给它们。
关于意图的更多信息,请参阅另外的“意图与意图过滤器”条目。
关闭组件
内容提供者只有在它对来自ContentResolver的请求进行响应的时候才处于活动状态。类似地,广播接收者只有在响应一个广播通知的时候才是活动的。因此,没有必要显式地关闭这两种组件。 另一方面来说,活动(Activity)负责提供用户界面。它们要与用户进行长期的交互,因此只要这个交互仍在继续,一个活动即使处于空闲时也要保持活动(Active)状态。同样地,服务也可能要保持长时间的运行状态。因此Android中提供了关闭活动和服务的方法:
- 可以通过调用finish()方法关闭一个活动。而通过调用finishActivity()则可以关闭另一个(由startActivityForResult()方法启动的)活动。
- 调用一个服务自身的stopSelf()方法可以关闭这个服务,或者也可以调用Context.stopService()来实现这个操作。
当组件不再被使用时或剩余内存不足以启动其他更多的组件时,系统可能会自动关闭这些组件。在稍后的组件生命周期板块中会更加具体地讨论这一问题及其后果。
manifest文件
Android在开始一个应用程序组件之前必须先确认其存在。因此,程序必须在manifest文件中声明这些组件,并把这个文件与Android包相绑定。此外,.apk文件中则是包含了程序的代码、文件和资源。 manifest文件是一种结构化的XML文件,并且对于任何程序来说都只能被命名为AndroidManifest.xml。除了声明应用程序组件,它还有一系列的功能,比如对程序要关联的(Android默认库以外的)库进行命名,或者对程序希望取得的操作权限进行认证。 不过manifest的主要任务还是告诉Android一个程序有哪些组件。比如,一个活动将会被这样声明:
<?xml version="1.0" encoding="utf-8"?> <manifest . . . > <application . . . > <activity android:name="com.example.project.FreneticActivity" android:icon="@drawable/small_pic.png" android:label="@string/freneticLabel" . . . > </activity> . . . </application> </manifest>
<activity>元素的name属性命名了这个活动(它是一个Activity类的子类)。而icon和label属性则指向了包含了图标和标签的资源文件,它们会被显示以表示这个活动。 其他的组件也被以相似的方式来声明——<service>元素声明服务,<receiver>元素声明广播接收者,<provider>元素声明内容提供者。没有在manifest中被声明的活动、服务和内容提供者对于系统来说是不可见的,所以也就无法被运行。然而,广播接收者既可以在manifest中声明,也能够在代码中作为一个BroadcastReceiver对象声明后通过调用Context.registerReceiver()来向系统注册这个组件。 关于如何构建一个manifest文件的更多信息,请参见”AndroidManifest.xml文件“。
意图过滤器(Intent filters)
一个Intent对象可以命名一个目标组件。当这么做时,Android会通过manifest文件中的声明来找到这个组件并激活它。但如果这个目标组件没有被显式地命名,Android会比较可能的目标组件的意图过滤器和这个Intent对象,之后选择一个最合适的组件来响应这个意图。一个组件的意图过滤器告诉Android这个组件可以处理哪些类型的意图。和别的一个组件所必需的信息一样,意图过滤器需要在manifest文件中被声明。下面是前面的例子的一个扩充,它向这个活动添加了两个意图过滤器:
<?xml version="1.0" encoding="utf-8"?> <manifest . . . > <application . . . > <activity android:name="com.example.project.FreneticActivity" android:icon="@drawable/small_pic.png" android:label="@string/freneticLabel" . . . > <intent-filter . . . > <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> <intent-filter . . . > <action android:name="com.example.project.BOUNCE" /> <data android:mimeType="image/jpeg" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> </activity> . . . </application> </manifest>
例子中的第一个过滤器——行动(action)“android.intent.action.MAIN”和类别(category)“android.intent.category.LAUNCHER”的组合——一种常见的情况。它标明了这个活动会被显示在应用程序启动器中(也就是设备中那个列出了所用用户可以启动的程序列表的画面)。换句话说,这个活动将作为这个程序的入口,当用户从启动器中选择了这个程序后,该活动会被第一个启动。 第二个过滤器声明了这个活动可以执行某种数据类型的操作。 一个组件可以有任何数量的意图过滤器,每一个声明一种不同的能力。如果一个组件不含有任何过滤器,它将只能被那些明确以其为目标的意图启动。 对于在代码中创建并注册的广播接收者来说,意图过滤器会被直接实例化为一个IntentFilter对象。其他所有的过滤器则是在manifest中被建立的。 关于意图过滤器的更多信息,参见另外的“意图和意图过滤器”篇目。
活动和任务(Task)
正如前面提到过的,一个活动可以启动另一个活动,甚至是在其他的应用程序中的活动。想象一下,比如,你希望向用户显示某地的一张街道地图,而现在已经有了一个活动可以做到这个功能,那么你的活动要做的就只是把需要的信息放入一个Intent对象然后通过startActivity()传递这个意图。那个活动将会显示地图,当用户按下返回键时,你的活动将会重现屏幕。 对于用户来说,地图显示似乎是你的程序的一部分,尽管它其实是在另外一个程序中定义并在另一个程序的进程中运行的。Android通过在同一个任务(task)中保持这两个活动来呈现这样的用户体验。简单说,一个任务是用户所感受到的“一个程序”。它是一组相关的活动,被分配在一个栈中。栈底部活动是开始整个任务的那个活动。通常它也是用户从应用程序启动器中选择的那个活动。栈顶部的活动是正在运行的活动,它是用户操作的接收者。当一个活动启动了另一个活动,新的活动被推入栈中,成为正在运行的那个活动。前一个活动则保持在栈中。当用户按下返回键,当前活动会从栈中弹出,而前一个活动成为当前正在运行的活动。 栈中包含有对象,因此如果一个任务中有同一个活动子类的多个实例处于开启状态,比如说多个地图显示程序,栈对于每个实例会有单独的入口。栈中的活动不会被重新排序,仅仅是被推入和弹出而已。 一个任务是一个包含多个活动的栈,不是manifest文件中的一个类或是元素。因此无法单独为一个任务中的活动设定值。在栈底的活动中,整个任务的值会被设定。比如,下一节中将讨论“任务的关系(affinity)”,通过读取一个任务的底部活动的“关系”可以获得这个任务的值。 一个任务中所有的活动作为一个整体一起移动。整个任务(即整个活动栈)可以被带至前台或是送往后台。设想一下,比如说当前任务有四个活动在栈中,其中三个活动在当前活动之下。用户按下了HOME键,前往了程序启动器并选择了一个新的程序(事实上是一个新的任务)。当前的任务被送往了后台,同时新的任务的底部活动被显示。一段时间以后,用户回到主界面并再次选择了之前的那个程序(或者说任务)。那个有四个活动在栈的任务将返回前台。当用户按下返回键,用户刚刚离开的那个活动不会被显示(即前一个任务的底部活动),而是显示同一个栈中的上一个活动(同时该栈的顶部活动被移除)。 上一段所描述的是活动和任务的默认行为。但是可以通过一些方法来完全地改变它们的行为。活动和任务间的关联,以及一个任务中某一个活动的行为,是由交互旗标(interaction flag)和属性(attribute)所控制的,前者存在于启动了活动的Intent对象中,后者存在于manifest中活动的<activity>元素中。两者都能改变活动和任务间的行为。
主要的意图旗标有:
FLAG_ACTIVITY_NEW_TASK
FLAG_ACTIVITY_CLEAR_TOP
FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
FLAG_ACTIVITY_SINGLE_TOP
主要的<activity>属性有:
taskAffinity
launchMode
allowTaskReparenting
clearTaskOnLaunch
alwaysRetainTaskState
finishOnTaskLaunch
下面将描述其中一些旗标和属性的作用,以及它们如何进行交互工作和使用上的注意事项。
关系(affinity)和新任务
一个程序中的所有活动默认和其他活动有一种“关系”——即,它们默认属于同一个任务。然而,可以用<activity>元素中的taskAffinity属性来为每一个活动设置单独的关系。不同程序中的活动可以被定义为共享同一个关系,同一个程序中的活动也可以被分配不同的关系。在两种情况下关系将发挥作用:当Intent对象启动了一个包含有FLAG_ACTIVITY_NEW_TASK旗标的活动时,或者这个活动的allowTaskReparenting属性值为true。
FLAG_ACTIVITY_NEW_TASK旗标
正如前面所说的,一个新的活动默认会加入调用了startActivity()的那个活动的任务中。它会被推入相同的一个栈中。然而,如果被传递给startActivity()的Intent对象里包含有FLAG_ACTIVITY_NEW_TASK旗标的话,系统会给新的活动分配一个不同的任务。如旗标的名称,通常那会是一个新的任务。不过也可以不是新任务。如果存在一个任务和这个新的活动有同样的关系,这个活动就会加入这个活动。不然则是创建一个新的任务。
allowTaskReparenting属性
如果一个活动的allowTaskReparenting属性值为“true”,这个活动能在另一个和它有关系的任务来到前台时从启动它的任务转移到入这个任务中。例如,在一个旅行用程序中有一个活动能报告所选地点的天气状况。这个活动和该程序中的其他活动一样有着相同的关系(也就是默认关系),并且允许重新指定父级。当你自己的一个活动启动了天气报告活动,这两个活动是属于同一个任务的。然而,当那个旅行用程序再次来到前台,天气报告活动将被重新分配给该任务并被显示。
如果,从用户的角度来看,一个.apk文件包含了不止一个的程序,将有必要对与这些程序相关的活动分配不同的关系。
启动模式
有四种不同启动模式可供指定给一个<activity>元素的launchMode属性:
“standard” (默认模式)
“singleTop”
“singleTask”
“singleInstance“
这些模式在以下四个方面有所不同:
- 对意图进行响应的活动的所属任务的不同。对于“standard”和“singleTop”模式,活动属于创建了意图(并调用了startActivity())的任务——除非Intent对象包含了FLAG_ACTIVITY_NEW_TASK旗标。因为这种情况下会像之前在“关系和新任务”一节中说的那样选择一个不同的任务。 与此相对应的,“singleTask”和“singleInstance”模式将活动标记为始终处于任务底部。它们这些活动定义了一个任务并将不会在其他任务中运行。
- 活动是否存在多个不同的实例。一个“standard”或是“singleTop”的活动可以被多次实例化。它们可以属于多个任务,并且一个任务里可以有该活动的多个实例。 相对应的,“singleTask”和“singleInstance”的活动被限定于只能有一个实例。这些活动处于任务的底部,因此这个限制意味着在设备上该任务不可能同时存在一个以上的实例。
- 活动的实例是否在它所属的任务中有其他活动。一个“singleInstance”活动作为它所属的任务中唯一的一个活动而存在。如果它启动了另一个活动,无论那个活动的启动模式是什么都必定会在另一个不同的任务中被创建,就好象意图中含有FLAG_ACTIVITY_NEW_TASK。在其他的方面,“singleInstance”模式等同于”singleTask“。另外的三种模式允许多个活动属于一个任务。“singleTask”活动将始终是任务的根活动,这个活动可以启动其他将被分配到该任务的活动。“standard”和“singleTop”活动的实例可以存在于栈的任何位置。
- 一个类的新的实例是否会被启动以处理一个新意图。对于默认的“standard”模式,在响应每一个新意图时都会创建一个新的实例。每一个实例只能处理一个意图。对“singleTop”模式来说,一个已经存在的类实例在当它处于目标任务的活动栈的顶部时,它可以被重新使用以处理一个新的意图。如果这个活动不在顶部,则无法被重用。这时,会有一个新的实例被创建并被推入栈中来处理这个新意图。
例如,假设一个任务的活动有一个根活动A,在其上按顺序有活动B,C,D。所以这个栈就是A-B-C-D。这时传来了一个意图启动类型D的活动,如果D是“standard”的启动模式,这个类就启动一个新的实例,同时栈变为A-B-C-D-D。然而要是D的启动模式是“singleTop”,那么现有的这个实例会去处理那个新的意图(因为它已经在栈顶了),这个栈会保持为A-B-C-D。
另一方面来说,如果这个意图是启动B类型的,无论B的模式是“standard”还是“singleTop”,都会启动一个新的实例(因为B不再栈顶),使这个栈变为A-B-C-D-B。
如前面所说到的,“singleTask”或“singleInstance”类型的活动只能有一个实例,所以该实例会处理所有的新意图。“singleInstance”活动永远保持在栈的顶部(因为它是这个栈里唯一的活动),所以它会始终处理新的意图。然而,“singleTask”活动可能会存在一些在栈中位于其上方的活动,那样它就不处于处理意图的位置了,这个意图回被舍弃。(即使意图被舍弃了,它也会使任务按照本应该的那样来到前台。)
当一个存在的活动被要求处理一个新的意图时,Intent对象通过onNewIntent()被传递至这个活动。(最初启动这个活动的意图对象可以通过调用getIntent()来检索)
需要注意,当一个活动的新实例被创建以处理新的意图时,用户可以按下返回键来回到前一个状态(或者说前一个活动)。但是当一个已经存在的活动处理一个新的意图时,用户无法通过返回键来回到这个实例在接受意图之前的状态。
关于启动模式的更多内容,参见<activity>元素的描述。
栈的清除
如果用户长时间不运行一个任务,系统会清除除了根活动外其他所有的活动。当用户再次返回到这个任务时,这个任务会和离开之前一样,不过只有最初的活动会被呈现。这一思想的实质是认为用户在一段时间后返回之前的任务时会希望放弃之前行进的工作而开始新的。
这是默认的情况。可以通过一些活动属性来控制并修改这一行为:
alwaysRetainTaskState属性
如果这个属性在一个任务的根活动里被设置为“true”,那么之前描述的默认行为就不会发生。任务会长时间保持所有的活动于栈中。
clearTaskOnLaunch属性
如果这个属性在一个任务的根活动里被设置为“true”,那无论用户是离开任务还是回到任务,该栈都会被清除到只剩根活动。换言之,这是alwaysRetainTaskState的相反极。即使只经过了短暂的离开,用户也会回到任务的最初状态。
finishOnTaskLaunch属性
这个属性类似于clearTaskOnLaunch属性,不过它只对单个活动有效而不是整个任务。而且它能清除包括根活动在内的任何活动。当它的值为“true”时,该活动仅仅在当前状态下才是任务的一部分。一旦用户离开后,即使再次返回,这个活动也不会被呈现。
还有一种方法可以强制活动从栈中清除。如果一个Intent对象含有FLAG_ACTIVITY_CLEAR_TOP旗标,同时目标任务已经存在有一个应当处理此意图的活动类型的实例,那么这个实例之上的所有活动将被清除从而该实例能处于栈顶来响应这个意图。如果这个活动的启动模式是“standard”,它同样也会被移除出栈,会有一个新的实例被启动来处理那个意图。之所以会这样的原因是“standard”启动模式下会为每一个新意图创建一个新的实例。
FLAG_ACTIVITY_CLEAR_TOP通常都会和FLAG_ACTIVITY_NEW_TASK一起使用。此时,这两个旗标的作用是将一个已存在的活动置于另一个任务中的某个合适的位置使其可以响应某个意图。
任务的开始
通过给一个活动设置“android.intent.action.MAIN”和“android.intent.category.LAUNCHER”这两个意图过滤器来设定其动作和目录,可以将这个活动设为一个任务的入口。(在之前的意图过滤器部分有这样的一个例子。)这种类型的过滤器会在应用启动器中显示该活动的图标和标签,使用户可以启动这个任务,或在启动后的任何时刻返回到该任务。
这里的第二个能力很重要:用户必须可以在离开一个任务之后再回到这个任务。为此,“singleTask”和“singleInstance”这两个会使活动始终初始化任务的启动模式应当仅被用于有MAIN或LAUNCHER过滤器的活动。想象一下,假如没有过滤器会发生什么:一个意图启动了“singleTask”的活动,初始化了一个新任务,然后用户在这个任务中进行了操作。之后用户按下HOME键。任务被置于后台,被主界面遮挡住。但是,由于它没有被显示于应用启动器中,因此用户将无法返回到这个任务。
对于FLAG_ACTIVITY_NEW_TASK旗标来说也有类似的问题。在这个旗标使一个活动开始一个新的任务后,如果用户按了HOME键离开的话,必须提供某种方式使用户可以重返这个任务。有些实体(比如通知管理器)始终会在一个外部而非其自身的任务中启动活动,所以它们始终会在传递至startActivity()的意图中包含FLAG_ACTIVITY_NEW_TASK。如果你的活动涉及某个会使用该旗标的外部实体时,注意要提供用户一个返回到任务的途径。
如果你不希望用户可以返回到原来的活动的话,把<activity>元素的finishOnTaskLaunch属性设置为“true”即可。具体的可以参见之前的栈的清除。
进程和线程
当一个应用程序组件的第一个部分要运行时,Android会启动为它启动一个Linux进程及一个单独的执行线程。默认来说,所有的组件会在这个线程和进程里运行。
不过,你也可以让组件运行于其他进程,或者是在任意其他进程中生成新的线程。
进程
一个组件将运行于哪一个进程是由manifest文件控制的。组件元素——<activity>,<service>,<receiver>和<provider>——每一个都含有一个process属性,指定了该组件应当运行于哪一个进程。可以设置这些属性使每一个组件运行于其自身的进程,或者可以让一部分的组件共用同一个进程。甚至可以设定让不同程序中的组件运行于同一个进程——假设这些程序共享同样的Linux用户ID并被相同的权限标记。<application>元素也有<process>属性,用来给所有的组件设定默认值。
所有组件都是在制定进程的主线程中被实例化的,系统对组件的调用也是从这个线程中分发的。系统不会对每一个实例创建单独的线程。因此,那些响应调用的方法——比如View.onKeyDown()这类的报告用户操作的方法,或者是在之后的组件生命周期一节中会讨论的生命周期通知(lifecycle notification)——总是会运行于进程的主线程中。这意味着组件在被系统调用时不应该长时间或阻断性地运行(比如网络工作或是循环计算),因为这样会阻断在进程中的其他组件。你可以为长期操作创建单独的线程,如同接下来会讨论的那样。
Android在某些时候(比如内存不足而用户又马上需要更多的内存来运行其他的进程)会做出关闭某个进程的决定。运行于该进程的应用程序组件也就相应地会被销毁。当再次需要它们时将会重启一个进程。
在决定终止哪一个进程时,Android会权衡它们对于用户的重要性。比如,系统更有可能关闭一个活动在屏幕上不可见的进程而不是一个活动仍然可见的进程。因此,是否终止一个进程取决于进程中组件的运行状态。而这则是之后的章节,组件生命周期,的内容了。
线程
即使你可能会限制你的程序运行于单独的进程,有时你也可能需要创建一个线程用于后台工作。由于用户界面必须快速响应用户操作,含有活动的线程不应当也含有消耗时间的操作(比如网络下载)。任何可能无法迅速完成的工作都应该被分配给不同的线程。
线程在代码中通过Java的Thread对象创建。Android提供了一系列类来便捷地管理线程——在线程中进行消息循环的Looper,执行消息的Handler,以及设立含有消息循环的线程的HandlerThread。
远程过程调用
Android有一种轻量级的远程过程调用机制(remote procedure call — RPC)——一种方法会被本地调用,之后(在另一个进程中)远程执行,最后结果会被传回调用者。这样势必会在一定程度上分解方法调用以及所有相关数据使系统得以能够理解它们,将其从本地进程传送出去,在远程进程中指定空间,重组和重新调用。返回值必须被反向传输。Android提供了完成这些工作的所有代码,因此你只需要集中注意力于定义和实现RPC接口自身就好。
RPC接口只包含方法。默认地,即使没有返回值,所有方法也都是同步执行的(本地方法在远程方法完成前将被阻塞)。
简单来说,这个机制将这样运作:由通过一个简单的IDL(接口定义语言)声明你想要执行的RPC接口开始。在这个声明中,
aidl工具生成一个必须在本地和远程进程中都可用的Java接口定义。它如下图所示含有两个类:
内部类含有所有需要的用于管理通过IDL声明的接口远程过程调用的代码。两个内部类都有IBinder接口。其中一个在系统内部被本地使用,你写的代码可以忽略它。另一个,Stub,继承于Binder类。另外,为使内部代码有效化IPC调用,它包含了你所声明的RPC接口的方法的声明。如图所示,你需要Stub的子类来使用这些方法。
一般来说,远程进程由一个服务来管理(因为服务可以告知系统这个进程与其他进程的联系)。该进程含有由aidl工具生成的接口文件和执行RPC方法的Stub子类。服务的客户端则只含有aidl工具生成的接口文件。
这里将说明服务和它的客户端之间的连接是如何被建立的:
- 服务的(本地端的)客户端会执行onServiceConnected()和onServiceDisconnected()方法以获知何时与远程服务的连接被成功建立记忆何时中断。它们之后将调用bindService()来建立连接。
- 服务的onBind()方法可以被用来接收或是拒绝连接,这取决于它所收到的意图(即传递到bindService()的意图)。如果一个连接被接受,它将返回一个Stub子类的实例。
- 如果服务接受了连接,Android会调用客户端的onServiceConnected()方法并传给它一个IBinder对象。这是一个由服务管理的Stub子类的代理。通过这个代理,客户端能够在远程服务中进行调用。
以上的描述中省略了RPC机制的细节。更多信息请参见“使用AIDL设计远程接口”和IBinder类的描述。
线程安全(thread-safe)方法
在一些context中,你使用的方法可能会被不止一个线程调用,因此必须使这段代码线程安全。
对于会被远程调用的方法来说,这是非常正确的——就像在前一节的RPC机制中所提到的那样。当IBinder对象内的一个方法的调用在与IBinder相同的进程中生成时,这个方法会在调用者的线程中被执行。然而,当这个调用在另外的进程中生成时,Android会在IBinder所在进程中的线程里选择一个来执行这个方法;它不在进程的主线程中被执行。比如说,一个服务的onBind()方法会被服务所在进程的主线程调用,而onBind()所返回的对象(比如使用RPC方法的一个Stub子类)中使用的方法会被池线程调用。由于服务可以有不止一个客户端,多个池线程可以同时对同一个IBinder方法操作。因此IBinder方法必须线程安全。
同样地,内容提供者可以接收能创建其他进程的数据请求。尽管ContentResolver和ContentProvider类隐藏了多进程通信管理的细节,响应了query(),insert(),update()和getType()方法的ContentProvider方法是被内容提供者所在进程的池线程而非主线程调用的。由于这些方法可能会同时被多个线程调用,因此它们也必须确保线程安全。
组件生命周期
应用程序组件具有生命周期——从Android响应意图而实例化组件开始到该实例被销毁为止。在这之间,它们有时处于活动状态,有时处于非活动状态(对于活动来说,则是可见与不可见)。这一节会讨论活动、服务、广播接收者的生命周期——包括在它们的存在期间内的各种状态,提示状态转换的方法,以及这些状态对组件所属进程是否被终止及实例是否被销毁等的可能性的影响。
活动的生命周期
一个活动有三种必要的状态:
- 当它在屏幕前台(处于当前任务的活动栈的顶部)时,它是活动的(active)或者说是正在运行的。该活动处于用户操作的焦点之下。
- 当它失去焦点而仍然可见时,它是暂停的(paused)。也就是说,有另一个活动在其上方,但这个活动是透明的或没有覆盖整个屏幕,因此一些暂停的活动依旧是可见的。一个暂停的活动仍然是存在的(alive),它保持着所有的状态和成员信息并和窗口管理器(window manager)相关联,不过在内存过低的情况下系统会杀死这个活动。
- 如果它完全被另一个活动遮盖,它是停止的(stoped)。它仍保持着所有的状态和成员信息。不过,它已经不再对用户可见了,因此它的窗口会被隐藏,通常会被系统在需要内存时关闭。
当一个活动是暂停或者停止的时候,系统可以通过要求其终止(调用其finish()方法)或者直接杀死它的进程来将它从内存中清除。当它再次呈现给用户时,它将是完全重启启动的,并被还原为它先前的状态。
随着一个活动在不同状态之间的切换,以下protected方法的调用能指示出这些变化:
void onCreate(Bundle savedInstanceState)
void onStart()
void onRestart()
void onResume()
void onPause()
void onStop()
void onDestroy()
所有这些方法都能被override以使在状态转换时正常工作。所有的活动都必须在对象被初次初始化时使用onCreate()来初始,不少活动还会使用onPause()来确认数据变化或准备停止用户交互操作。
调用父类的方法
使用任何活动生命周期的方法前都应先调用其父类的版本,比如
protected void onPause() {
super.onPause();
. . .
}
总的来说,这七种方法定义了一个活动的整个生命周期。通过使用这些方法,可以监控三种嵌套循环:
- 一个活动的完整的生命周期处于第一次对onCreate()的调用与对onDestory()的调用之间。一个活动在onCreate()中完成左右的“全局(global)”状态设置,并在onDestroy()中将所有的资源释放。比如,对于一个从网络上下载数据的背景线程,活动将在onCreate()中创建该线程,之后在onDerstroy()中停止它。
- 一个活动的可见的生命周期始于对onStart()的调用而止于对onStop()的调用。在此期间,用户可以在屏幕上看到活动,尽管它可能没有处于最前端和用户发生交互操作。在这两种方法之间,可以保留需要的资源以向用户显示该活动。比如,可以在onStart()中注册一个广播接收者来监视对UI会有影响的变化,之后当用户不会再看到屏幕显示内容时在onStop()中注销该接收者。onStart()和onStop()方法可以多次被调用,活动也会在可见和不可见之间转换。
- 一个活动的前台生命周期始于对onResume()的调用而止于对onPause()的调用。在此期间,该活动处于所有其他活动的上方并与用户发生交互。一个活动可能会在resumed和paused状态间频繁切换——比如,当设备进入睡眠状态或一个新的活动启动时,onPause()会被调用,而当一个活动得出结果或是有新的意图被传递时,onResume()会被调用。因此,在这两种方法间的代码应当是非常轻量级的。
下面的图描绘出了以上这些循环以及一个活动在状态间可能的转换路径。彩色的椭圆是一个活动的主要状态。线框中代表当活动转换状态时你可以用来执行操作的反馈方法。
下面的表格更加详细地介绍了这些方法,并说明了它们在整个生命周期中的位置:
方法 | 描述 | 可杀? | 后继 | ||
---|---|---|---|---|---|
| 当活动被创建时被调用。所有静态设定应当在此进行,包括创建view、对列表的数据绑定等等。会有一个Bundle对象传递给该方法,它包含了活动前一个状态的信息(如果该状态被记录的话)(参见之后的 保存活动状态)。
始终后续 | 不可 | onStart() | ||
| 当活动停止时被调用,仅发生于被再次启动之前
始终后续 | 不可 | onStart() | ||
| 仅在活动可见之前被调用。
当活动来到前台时,后续 | 不可 | onResume() 或 onStop() | ||
| 仅在活动开始用户交互前被调用。这时该活动处于活动栈的顶部,可以接受用户输入。
始终后续 | 不可 | onPause() | ||
| 当系统将要恢复另一个活动时被调用。该方法主要用于保存尚未保存的修改、停止动画,或者其他需要消耗处理器资源的工作。该方法应当迅速完成,不然下一个活动将一直等待至其完成才恢复。
当活动回到前台时后续 | 可以 | onResume() 或 onStop() | ||
| 当活动不再对用户可见时被调用。当该活动被销毁或有其他活动(已存在的或是新的活动)被恢复来覆盖此活动时会使用该方法。
当活动回到前台与用户发生交互时后续 | 可以 | onRestart() 或 onDestroy() | ||
| 当活动被销毁时被调用。这时一个活动能收到的最后一次调用。当活动完成(调用了 )或是系统为节省空间而销毁了该活动的实例时会调用该方法。可以通过 来区别这两种情况。 | 可以 | 无
|
注意上表中的可杀一栏。它说明了系统是否可以在方法返回后的任一时刻杀死运行该活动的进程而不必执行更多的活动代码。三种方法(onPause(),onStop()和onDestroy())被标记为“可以”。由于onPause()是三者中的第一个,因此它是唯一可以保证在进程被杀之前被调用的——onStop()和onDestroy()就不一定了。因此,应该使用onPause()来执行保存长期数据(比如用户编辑的内容)的工作。
可杀一栏中标记为“不可”的方法在它们被调用时会保护运行活动的进程不被杀除。因此一个活动在,比如说,onPause()返回直到onResume()被调用之间,是可杀的。在onPause()再次返回之前,它不会再次变为可杀的。
之后的“进程和生命周期”一节中将会提到,一个在定义上“技术性不可杀”的活动仍有可能被系统杀除——不过这只会发生在资源缺乏极端严重的情况下。
保存活动状态
当系统而不是用户关闭一个活动来获得内存时,用户会希望能再次回到活动并处于之前的状态。
为获取活动被杀之前的状态,可以在活动中使用onSaveInstanceState()状态。Android会在杀死活动之前调用这个方法——也就是在onPause()被调用前。会有一个Bundle对象传递给该方法,其中以名称-值对(name-value pair)的形式记录了活动的动态状态。当活动重新启动时,Bundle会被传递给onCreate()以及在