一、前言
应用启动时,Android 系统会启动新的 Linux 进程。默认情况下,同一应用的所有组件在相同的进程和线程(称为“主”线程)中运行。但是,也可以安排应用中的组件在单独的进程中运行,以及为任何进程创建额外的线程。
二、进程
清单文件中各组件元素:<activity>
、<service>
、<receiver>
和 <provider>
均支持 android:process
属性,此属性可以指定该组件应在哪个进程运行。
你可以设置此属性,使每个组件在各自的进程中运行,或者使一些组件共享一个进程,而其他组件则不共享。
此外,还可以设置
android:process
,使不同应用的组件在相同的进程中运行,但前提是这些应用共享相同的 Linux 用户 ID 并使用相同的证书进行签署。
此外,<application>
元素也支持 android:process
属性,用来设置所有组件该属性的默认值。
进程生命周期
系统会根据进程中正在运行的组件以及这些组件的状态,将每个进程放入 “重要性层次结构” 中。必要时,系统会首先消除重要性最低的进程,然后是重要性略低的进程,依此类推,以回收系统资源。
重要性层次结构一共有 5 级。以下列表按照重要程度列出了各类进程(第一个进程最重要,将是最后一个被终止的进程):
1. 前台进程
用户当前操作所必需的进程。如果一个进程满足以下任一条件,即视为前台进程:
托管用户正在交互的
Activity
(已调用Activity
的onResume()
方法)托管某个
Service
,后者绑定到用户正在交互的Activity
托管正在 “前台” 运行的
Service
(服务已调用startForeground()
)托管正执行一个生命周期回调的
Service
(onCreate()
、onStart()
或onDestroy()
)托管正执行其
onReceive()
方法的BroadcastReceiver
只有在内存不足的情况下,系统才会终止它们。 此时,设备往往已达到内存分页状态,因此需要终止一些前台进程来确保用户界面正常响应。
2. 可见进程
没有任何前台组件、但仍会影响用户在屏幕上所见内容的进程。如果一个进程满足以下任一条件,即视为可见进程:
托管不在前台、但仍对用户可见的
Activity
(已调用其onPause()
方法)。例如,如果前台Activity
启动了一个对话框,允许在其后显示上一Activity
,则有可能会发生这种情况。托管绑定到可见(或前台)
Activity
的Service
。
可见进程被视为是极其重要的进程,除非为了维持所有前台进程同时运行而必须终止,否则系统不会终止这些进程。
3. 服务进程
正在运行使用 startService()
方法启动的服务且不属于上述两个更高类别进程的进程。尽管服务进程与用户所见内容没有直接关联,但是它们通常在执行一些用户关心的操作(例如,在后台播放音乐或从网络下载数据)。因此,除非内存不足以维持所有前台进程和可见进程同时运行,否则系统会让服务进程保持运行状态。
4. 后台进程
包含目前对用户不可见的 Activity 的进程(已调用 Activity 的 onStop() 方法)。这些进程对用户体验没有直接影响,系统可能随时终止它们,以回收内存供前台进程、可见进程或服务进程使用。 通常会有很多后台进程在运行,因此它们会保存在 LRU (最近最少使用)
列表中,以确保包含用户最近查看的 Activity 的进程最后一个被终止。如果某个 Activity 正确实现了生命周期方法,并保存了其当前状态,则终止其进程不会对用户体验产生明显影响,因为当用户导航回该 Activity 时,Activity 会恢复其所有可见状态。
5. 空进程
不含任何活动应用组件的进程。保留这种进程的的唯一目的是用作缓存,以缩短下次在其中运行组件所需的启动时间。 为使总体系统资源在进程缓存和底层内核缓存之间保持平衡,系统往往会终止这些进程。
Android 会将进程评定为它可能达到的最高级别。例如,如果某进程托管服务和可见 Activity,则会将此进程评定为可见进程,而不是服务进程。
此外,一个进程的级别可能会因其他进程对它的依赖而有所提高,即服务于另一进程的进级别永远不会低于其所服务的进程。例如,如果进程 A 中的内容提供程序为进程 B 中的客户端提供服务,或者如果进程 A 中的服务绑定到进程 B 中的组件,则进程 A 始终被视为至少与进程 B 同样重要。
由于服务进程其级别高于后台进程,因此启动长时间运行操作的 Activity 最好为该操作启动服务,而不是创建工作线程,当操作有可能比 Activity 更加持久时尤要如此。例如,正在将图片上传到网站的 Activity 应该启动服务来执行上传,这样一来,即使用户退出 Activity,仍可在后台继续执行上传操作。使用服务可以保证,无论 Activity 发生什么情况,该操作至少具备 “服务进程” 优先级。 同理,广播接收器也应使用服务,而不是将耗时冗长的操作放入线程中。
三、线程
应用启动时,系统会为应用创建一个名为 “主线程” 的执行线程。 此线程非常重要,因为它负责将事件分派给控件,其中包括绘图事件。 此外,它也是应用与 Android UI 工具包组件(android.widget
和 android.view
软件包的组件)进行交互的线程。因此,主线程也称为 UI 线程。
运行于同一进程的所有组件均在 UI 线程中实例化,并且对每个组件的系统调用均由该线程进行分派。 因此,响应系统回调的方法始终在 UI 线程中运行。
如果 UI 线程执行耗时很长的操作(例如,网络访问或数据库查询)将会阻塞整个 UI。 一旦线程被阻塞,将无法分派任何事件,包括绘图事件。如果 UI 线程被阻塞超过几秒钟时间(目前大约是 5 秒钟),用户就会看到一个 “应用无响应”(ANR) 对话框。
此外,Android UI 工具包不是线程安全工具包,所以不能通过子线程操纵 UI,而只能通过 UI 线程操纵用户界面。 因此,Android 的单线程模式必须遵守两条规则:
不要阻塞 UI 线程
不要在 UI 线程之外访问 Android UI 工具包
1. 工作线程
如果要执行耗时操作,则应确保它们在单独的线程中运行。
例如,以下代码演示了一个点击侦听器从单独的线程下载图像并将其显示在 ImageView 中:
public void onClick(View v) {
new Thread(new Runnable() {
public void run() {
Bitmap b = loadImageFromNetwork("http://example.com/image.png");
mImageView.setImageBitmap(b);
}
}).start();
}
上面的代码有问题,因为它违反了单线程模式的第二条规则:不要在 UI 线程之外访问 Android UI 工具包。此示例在子线程中修改了 ImageView。 这可能导致出现不明确、不可预见的行为。
为解决此问题,Android 提供了几种途径来从其他线程访问 UI 线程。 以下列出了几种有用的方法:
Activity.runOnUiThread(Runnable)
View.post(Runnable)
View.postDelayed(Runnable, long)
例如,可以通过使用 View.post(Runnable)
方法修复上述代码:
public void onClick(View v) {
new Thread(new Runnable() {
public void run() {
final Bitmap bitmap =
loadImageFromNetwork("http://example.com/image.png");
mImageView.post(new Runnable() {
public void run() {
mImageView.setImageBitmap(bitmap);
}
});
}
}).start();
}
现在,上述实现属于线程安全型:在单独的线程中完成网络操作,而在 UI 线程中操纵 ImageView。
也可以考虑使用 Handler 处理来自 UI 线程的消息。当然,最好的解决方案或许是扩展 AsyncTask 类。
例如,可以通过以下方式使用 AsyncTask 来实现上述示例:
public void onClick(View v) {
new DownloadImageTask().execute("http://example.com/image.png");
}
private class DownloadImageTask extends AsyncTask<String, Void, Bitmap> {
/** The system calls this to perform work in a worker thread and
* delivers it the parameters given to AsyncTask.execute() */
protected Bitmap doInBackground(String... urls) {
return loadImageFromNetwork(urls[0]);
}
/** The system calls this to perform work in the UI thread and delivers
* the result from doInBackground() */
protected void onPostExecute(Bitmap result) {
mImageView.setImageBitmap(result);
}
}
现在 UI 是安全的,代码也得到简化,因为任务分解成了两部分:一部分应在工作线程内完成,另一部分应在 UI 线程内完成。
注意:使用工作线程时可能会遇到一个问题,即:运行时配置变更(例如,用户更改了屏幕方向)导致 Activity 意外重启,这可能会销毁工作线程。
2. 线程安全方法
在某些情况下,方法可能会在多个线程中调用,因此必须确保其线程安全。
这一点主要适用于可以远程调用的方法,如绑定服务中的方法。如果调用 IBinder
中的方法的进程和运行 IBinder
的进程一样,则该方法在调用方的线程中执行。但是,如果是从其他进程中调用,则该方法将在从线程池选择的某个线程中执行(而不是在进程的 UI 线程中执行),线程池由系统在与 IBinder
相同的进程中维护。
例如,即使服务的 onBind()
方法将从服务进程的 UI 线程调用,在 onBind()
返回的对象中实现的方法仍会从线程池中的线程调用。 由于一个服务可以有多个客户端,因此可能会有多个线程在同一时间使用同一 IBinder
方法。因此,IBinder
方法必须实现为线程安全方法。
同样,内容提供程序也可以接收来自其他进程的数据请求。尽管 ContentResolver
和 ContentProvider
类隐藏了如何管理进程间通信的细节,但响应这些请求的 ContentProvider
方法(query()
、insert()
、delete()
、update()
和 getType()
方法)将从内容提供程序所在进程的线程池中调用,而不是从进程的 UI 线程调用。 由于这些方法可能会同时在任意数量的线程中调用,因此它们也必须实现为线程安全方法。
四、进程间通信
Android 利用远程过程调用 (RPC) 提供了一种进程间通信 (IPC) 机制,通过这种机制,由 Activity 或其他应用组件调用的方法将(在其他进程中)远程执行,而所有结果将返回给调用方。Android 提供了执行这些 IPC 事务所需的全部代码,因此我们只需定义和实现 RPC 接口即可。
要执行 IPC,必须使用 bindService()
将应用绑定到服务上。