了解 Android 的进程和线程

Posted by on 2017-04-16

前言:本文所写的是博主的个人见解,如有错误或者不恰当之处,欢迎私信博主,加以改正!demo链接

当某个应用组件启动且未运行其他组件时, Android 系统会使用单个执行线程为应用启动新的 Linux 进程。默认情况下,同一应用的所有组件在相同的进程和线程(主线程)中运行。如果某个应用组件启动且应用已存在进程(存在其他组件),则该组件会在此进程内启动并使用相同的执行线程,但是你可以安排应用的其他组件在单独的进程中运行,并为任何进程创建额外的线程。

进程

默认情况下,同一应用的所有组件均在相同的进程中运行,且大多数的应该都是如此。但是,如果需要控制某个组件所属的进程,则可以在清单文件中执行此操作。

各类组件元素的清单文件条目 <activity><service><receiver><provider> 均支持 android:process 属性,该属性可以指定组件应该运行在哪个进程。可以设置此属性,使每个组件在各自的进程中运行,或者使一些组件共享一个进程,而其他组件则不共享。此外,还可以设置 android:process ,应用共享具有相同的 Linux 用户 ID 和相同的证书签署,使不同应用的组件在相同的进程中运行。

此外,<application> 元素还支持 android:process 属性,可以设置适用于所有组件的默认值。

如果内存不足,而其他为用户提供更紧急服务的进程又需要内存时,Android 可能会决定在某一时刻关闭某一进程。在被终止进程中运行的应用组件也会随之销毁。当这些组件需要再次运行时,系统将为它们重启进程。

决定终止哪个进程时,Android 系统将权衡它们对用户的相对重要程度。例如,相对于托管可见 Activity 的进程而言,它更有可能关闭托管屏幕上不再可见的 Activity 进程。因此,是否终止某个进程取决于该进程所运行组件的状态。

进程的生命周期

Android 系统将尽量长时间的保持应用进程,但为了新建进程或者运行更重要的进程,最终需要移除旧的进程来回收内存。为了确定保留或终止哪些进程,系统会根据正在运行的组件及这些组件的状态,将每个进程按重要性层次结构进行划分,必要时,系统会首先清除重要性最低的进程,然后是重要性略逊的进程,以此类推,来回收系统资源。

重要性层次结构一共有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 会恢复其所有可见的状态。可以参考浅谈Android Activity的生命周期

  5. 空进程
    不含任何活动应用组件的进程。 保留这种进程的唯一目的是用作缓存,以缩短下次在其运行组件所需的启动时间。为使总体系统资源在进程缓存和底层内了缓存之间保持平衡,系统往往会终止这些进程。

根据进程中当前活动组件的重要程度, Android 会将进程评定为它可能达到的最高级别。例如,如果某些进程托管这服务和可见 Activity ,则会将进程评定为可见进程,而不是服务进程。

此外,一个进程的级别可能会因其他进程对他的依赖而有所提高,即服务于另一个进程的进程,它的级别永远不会低于它所服务的进程。例如,如果进程 A 中的内容提供程序为 B 进程的客户端提供服务,或者进程 A 中的服务绑定到进程 B 中的组件,则进程 A 始终被视为至少与 B 同样重要。

由于运行服务的进程其级别高于托管在后台的 Activity 的进程,因此启动长时间运行操作的 Activity 最好为该操作启动服务,而不是简单的创建工作线程,当操作有可能比 Activity 更加持久时尤要如此。例如,正在将图片上传到网站的 Activity 应该启动服务来执行上传,这样一来,即使用户退出 Activity ,仍可在后台继续执行上传操作。使用服务可以保证,无论 Activity 发生什么情况,该操作至少具备“服务进程”优先级。同理,广播接收器也可以使用服务,而不是简单的将耗时操作放入工作线程中。

线程

应用启动时,系统会为应用创建一个名为“主线程”的执行线程。此线程非常重要,它负责将事件分派给相应的用户界面小部件,其中包括绘图事件。此外,他也是应用于 Android UI 工具包组件(来自 android.widget 和 android.view 软件包的组件)进行交互的线程。因此,主线程也有时被称为 UI 线程。

系统不会为每个组件创建单独的线程。运行在同一进程的所有组件均在 UI 线程中实例化,并且对每个组件的系统调用均由该线程进行分派。因此响应系统回调的方法(例如,报告用户操作的 onKeyDown() 或者生命周期回调方法)始终在进程的 UI 线程中运行。

例如,当用户触摸屏幕上的按钮时,应用的 UI 线程将会将触摸时间分派给小部件,而小部件反过来又设置其按下状态,并将失效请求发布到事件队列中。UI 线程从队列中取消该请求并通知小部件重绘自身。

在应用执行繁重的任务响应用户交互时,除非正确实现应用,否则这种单线程模式可能会导致性能低下。如果 UI 线程需要处理所有任务,则执行耗时操作(如网络访问或数据库查询)将会阻塞整个 UI 。一旦线程被阻塞,将无法分派任何事件,包括绘图事件。从用户角度来看,应该显示为挂起。更糟糕的情况是如果 UI 线程被阻塞超过几秒钟(目前大约是5秒钟),用户就会看到一个“应用无响应”( ANR )对话框。如果引起用户不满,他们可能会决定退出并卸载应用。

此外,Android UI 工具包并非线程安全工具包,所以不能通过工作线程操纵 UI,只能通过 UI 线程操纵用户界面。因此 Android 的单线程模式必须遵守两条规则:

  1. 不要阻塞 UI 线程
  2. 不要在 UI 线程之外访问 Android UI 工具包

工作线程

在单线程模式下,要保证应用 UI 的响应能力,关键是不能阻塞 UI 线程。如果执行的操作不能很快完成,则应该确保他们在单独的线程(后台或者工作线程)中运行。

例如,下面演示了一个点击监听器从单独的线程下载图片并将其显示在 ImageView 中:

1
2
3
4
5
6
7
8
9
10
@Override
public void onClick(View v) {
new Thread(new Runnable() {
@Override
public void run() {
final Bitmap bitmap = loadImageFromNetwork(url);
showImage.setImageBitmap(bitmap);
}
}).start();
}

这段代码看起来似乎运行良好,因为它创建了一个新的线程来处理网络操作。但是它违背了单线程模式的第二条规则:不要在 UI 线程之外访问 Android UI 工具包,此示例从工作线程(而不是 UI )线程修改了 ImageView 。这个可能导致出现不明确,不可预见的行为,但是要跟踪这个行为既困难又费时。

未解决此问题,Android 提供了几种途径从其他线程访问 UI 线程。以下列出几种有用的方法:

  • Activity.runOnUiThread(Runnable)
  • View.post(Runnable)
  • View.postDelayed(Runnable,long)

例如你可以通过使用 Activity.runOnUiThread(Runnable) 方法修复上面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
public void onClick(View v) {
new Thread(new Runnable() {
@Override
public void run() {
final Bitmap bitmap = loadImageFromNetwork(url);
MainActivity.this.runOnUiThread(new Runnable() {
@Override
public void run() {
showImage.setImageBitmap(bitmap);
}
});
}
}).start();
}

上面的实现属于线程安全型:在单独的线程中完成网络操作,而在 UI 线程中操作 ImageView。

但是随着操作的复杂,这类代码变得难以维护,要通过工作线程实现更复杂的交互,可以考虑在工作线程中使用 Handler 处理来自 UI 线程的消息。当然更好的解决方案是扩展 AsyncTask 类,此类简化了与 UI 线程进行交互所需要执行的工作线程任务。

使用 AsyncTask

AsyncTask允许用户对用户界面执行异步操作。他会先阻塞工作线程中的操作,然后在 UI 线程中发布结果,而你无需亲自处理线程和处理程序。

要使用它,必须创建 AsyncTask 的子类,并实现 odInBrackground() 回调方法,该方法将在后台线程池中运行,需要更新 UI 则要实现 onPostExecute(),传递 odInBrackground() 返回的结果并在 UI 线程中运行,使之更安全地更新 UI,可以通过从 UI 线程中调用 execute() 来运行任务。

例如,下面使用 AsyncTask 来实现上述的示例:

Activity 里调用 execute() 方法

1
2
3
4
@Override
public void onClick(View v) {
new DownloadImageTask(showImage).execute(url);
}

继承 AsyncTask 实现异步加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class DownloadImageTask extends AsyncTask<String, Void, Bitmap> {
private ImageView mImageView;
public DownloadImageTask() {
}
public DownloadImageTask(ImageView imageView) {
mImageView = imageView;
}
@Override
protected Bitmap doInBackground(String... params) {
return DownImageUtil.getInstance().loadImageFromNetwork(params[0]);
}
@Override
protected void onPostExecute(Bitmap bitmap) {
if (mImageView != null) {
mImageView.setImageBitmap(bitmap);
}
}
}

现在 UI 是安全的,代码也得到简化,任务分解成了两部分:一部分在工作线程内完成,另一部分在 UI 线程内完成。

下面简单的介绍 AsyncTask 的工作方法:

  • 可以使用泛型指定参数类型、进度值和任务最终值
  • 方法 doInBackground() 会在工作线程上自动执行
  • onPreExecute() 、onPostExecute() 和 onProgressUpdate() 均在 UI 线程中调用
  • doInBackground() 返回值将发送到 onPostExecute()
  • 可以随时在 doInBackground() 中调用 publishProgress() ,以在 UI 线程中执行 onProgressUpdate()
  • 可以随时取消任何线程中的任务

注意:使用工作线程时有可能遇到另一个问题,即:运行时配置变更(例如,用户更改了屏幕方向),导致 Activity 意外重启,这可能会销毁工作线程。

线程安全方法

在某些情况下,你实现的方法可能会从多个线程调用,因此在编写这些方法时必须确保其满足线程安全的要求。

这一点主要适用于可以远程调用的方法,如绑定服务中的方法。如果对 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() 将应用绑定到服务上,想了解详细的信息,可以参考
浅谈 Android Service