进程和线程

在一个程序的仅有的一个应用程序组件启动时(除此之外没有其他组件在运行),Android系统会为该程序开启一个包含一个单独执行线程的新的Linux进程。默认来说,同一个程序的所有组件都运行于同一个进程的同一个线程中(被称为“主”线程(main thread))。如果一个组件在启动时该程序已经有一个进程存在的话(这是因为该程序存在有其他的组件),那么组件将运行于这个进程中且共用线程。不过,可以安排程序中的不同组件运行于不同的线程,以及为任何进程创建新的线程。

本文档将讨论Android应用程序中的进程和线程是如何工作的。

进程

默认地,同一个应用程序的所有组件都运行于同一个进程,大部分的程序不应该改变这一点。不过,如果认为有必要控制某个特定的组件所属的进程,可以在manifest文件中进行设定。

每一种类型的组件元素——<activity>、<service>、<receiver>和<provider>的manifest条目都支持android:process属性来指定组件应当运行于哪一个进程之中。可以设置该属性以使组件运行于其自有的进程或让一些组件共用进程的同时另一些使用独立进程。还可以通过设置android:process来事不同程序的组件运行于统一进程——假设这些程序共享同样的Linux用户ID并被相同的权限标记。

<application>元素也有android:process属性,用来给所有的组件设定默认值。

Android在某些时候(比如内存不足而用户又马上需要更多的内存来运行其他的进程)会做出关闭某个进程的决定。运行于该进程的应用程序组件也就相应地会被销毁。当再次需要它们时将会重启一个进程。

在决定终止哪一个进程时,Android会权衡它们对于用户的重要性。比如,系统更有可能关闭一个活动在屏幕上不可见的进程而不是一个活动仍然可见的进程。因此,是否终止一个进程取决于进程中组件的运行状态。具体决定终止哪一个进程的规则将在之后讨论。

进程生命周期

Android系统总是试图尽可能长时间地保持一个程序进程,不过旧的进程最终将被移除来为更为重要的进程提供内存。为了决定保留哪个进程,杀除哪个进程,系统将每一个进程置于一个基于进程内运行的组件及其状态的“重要性层级”之中。最低重要性的进程将被最先去除,之后是次低重要性的,以此类推直至能够提供足够的系统资源。

重要性层级中有五个等级。下面根据重要性顺序列出了不同类型的进程(第一种进程是最为重要的,将被最后杀除):

1. 前台进程

一个用户当前正在使用的进程。如果以下任一条件被满足,进程就被划归为该类型:

  • 它包含一个用户正在进行交互操作的活动(该活动的onResume()方法已被返回)。
  • 它包含一个与用户正在进行交互操作的的活动相绑定的服务。
  • 它包含一个“在前台运行的”服务——服务调用了startForeground()。
  • 它包含了一个正在执行其生命周期回馈方法之一(onCreate(),onStart()或onDestroy())的服务。
  • 它包含了一个正在执行其onReceive()方法的广播接收者。

通常,在某一时刻只会存在少数的前台进程。它们将被最后杀除——在内存极端不足的情况下。通常此时设备处于内存分页状态,需要杀除前台进程来保持用户界面响应。

2. 可见进程

这类进程没有任何前台组件,但是可以影响用户在屏幕上看到的内容。如果以下任一条件被满足,进程就被划归为可见:

  • 它包含一个不在前台但是对用户可见的活动(其onPause()方法已被调用)。比如,前台活动是一个对话框,而前一个活动仍可以被看到存在于其后方的情况。
  • 它包含一个与可见活动(或前台活动)相绑定的服务。

3. 服务进程

这是一类运行着由startService()方法启动的服务但不属于前两种进程的进程。尽管服务进程不与任何用户直接看到的内容绑定,它们确实在执行用户所希望的工作(比如背景播放mp3或是从网络下载数据),因此系统只有在亟需内存维持所有的前台和可见进程时才会杀除他们。

4. 后台进程

这是一类包含一个当前不对用户可见的活动(活动的onStop()方法已被调用)。这些进程对用户体验没有直接影响,当前台、可见或服务进程需要内存时它们会被随时杀除。通常有着大量的后台进程在运行,它们被保存在一个LRU(Least recently used)表中以确保最后看到的活动所在的进程会被最后杀除。如果一个活动正确地执行其生命周期方法并保存其状态,杀死它的进程不会影响用户体验。这是因为当用户返回活动时,所有的可见状态将被恢复。参见“活动(Activities)”文档以了解关于保存与恢复状态的更多信息。

5. 空进程

这是一类没有任何活动的(active)应用程序组件的进程。保持这样一个进程的唯一理由是作为缓存来提高下一次有组件要在其内运行时的启动速度。系统为了在进程缓存和底层核心缓存间平衡整体系统资源而常常杀除这些空进程。

Android根据进程中当前活动着的组件的重要性来尽可能高地给一个进程定级别。比如,如果一个进程包含一个服务和一个可见活动,那么进程会被分级为可见进程而不是服务进程。

另外,进程的层级可能会因为其他进程依赖于它而被提升。服务于另一个进程的进程不可能比那个进程的层级低。比如,进程A中的一个内容提供者服务于进程B中的一个客户端,又或是进程A中的一个服务与进程B中的一个组件绑定,那么进程A被认为至少要和进程B同样重要。

由于运行服务的进程比运行后台活动的进程层级高,一个执行长时间操作的活动应该开启一个服务来处理该工作而不是简单地创建一个工作线程——特别是该操作可能比活动本身持续更久时。这样的例子有个后台播放音乐或是上传一张拍摄的照片到网上。使用服务至少可以确保该操作有服务进程的优先级,而不必在意活动本身怎样。正如之前在广播接收者生命周期一节中提到的,这也是为什么广播接收者应当用服务而不是用线程来执行花费时间的工作的原因。

线程

当一个程序被启动后,系统将创建一个被称为“主线程(main)”线程来执行该程序。这个线程非常重要,它负责正确地向UI控件分发事件,包括绘图事件。它同时还与Android UI toolkit中的组件(即android.widget和android.view包中的组件)进行交互。因此,主线程有时也被称作为UI线程。

系统不会为每一个组件的实例创建新的线程。所有运行于同一进程的组件都在UI线程中被实例化,系统对它们的调用也都是从该线程中分发。因此,要响应系统回馈的方法(比如回馈用户操作的onKeyDown()或是生命周期回馈方法)总是要运行于进程的UI线程中。

例如,当用户点击屏幕上的一个按钮,程序的UI线程将分发触摸事件至控件,控件将设置其点击状态并向事件队列发送invalidate请求。UI线程会将此请求出列并通知控件进行重绘。

当程序在响应用户交互操作中需要处理密集型工作时,这样的单线程模式可能会在没有很好的设计程序的情况下降低程序表现。特别是如果所有的任务都由UI线程执行,网络连接或是数据请求等长时间操作将会阻塞整个UI。当线程被阻塞,事件就无法被分发,包括绘图事件。从用户的角度来看,程序似乎就停止响应了。更糟糕的情况下,如果UI线程被阻塞的时间过长(目前来说大约是5秒)用户就会看到糟糕的“程序没有相应”(application not responding,ANR)对话框。用户之后就会考虑退出程序,甚至不满地卸载它。

另外,Android UI toolkit不是线程安全的。因此,不能在工作线程中进行UI处理——必须将所有用户交互操作在UI线程中实行。于是,对于Android的单线程模式有两条规则:

  1. 不要阻塞UI线程
  2. 不要在UI线程外读取Android UI toolkit

工作线程

由上面所描述的单线程模式可知,不阻塞UI线程对于程序UI的响应性是很重要的。如果不得不执行无法立即完成的工作,就必须确保它们处于不同的线程之中(“后台”或“工作”线程)。

例如,下面是一个点击监听器(click listener)的代码,它将从另一个线程中下载一张图片并将其显示在一个ImageView中:

最初这看起来能很好的运行,因为它创建了一个新的线程来处理网络操作。不过,它违反了单线程模式的第二条规则:不要在UI线程外读取Android UI toolkit——这个范例在工作线程中修改了ImageView而不是在UI线程中。这可能导致未定义的不被希望的行为,并且可能要花费很长时间来解决这个问题。

要解决这个问题,Android提供了几种不同的方法来从其他线程中接入UI线程。这里列出了有用的一些方法:

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

例如,可以用View.post(Runnable)方法来解决上面的问题:

现在代码是线程安全的了:网络操作在另一个线程中完成,同时ImageView是由UI线程处理的。

不过,当操作的复杂性提升时,这样的代码会变得很复杂而难以维护。要处理更为复杂的与工作线程的交互,应该考虑在工作线程中使用Handler,来处理接收自UI线程的消息。不过,最好的解决方案或许是继承一个AsyncTask类,来简化工作线程与UI间交互任务的执行。

使用AsyncTask

AsyncTask允许在用户界面中执行异步工作。它在一个工作线程中执行阻塞性操作并把结果发布于UI线程而不需要处理线程关系。

要使用它,必须继承AsyncTask并使用doInBackground()回馈方法(它运行于后台线程池中)。要更新UI,需要使用onPostExecute()以传递来自doInBackground()的结果来安全地更新UI。之后可以通过在调用execute()来运行UI线程中的任务。

例如,可以像这样使用AsyncTask来实现上一个范例:

现在不但UI是安全的,代码也变得更简单了,因为工作被分成了需要在工作线程中完成的部分和应当在UI线程中完成的部分。

应该阅读AsyncTask的参考文档来了解关于如何使用这个类的完整信息,不过这里有关于它工作方式的简介:

  • 可以使用generic来指定参数类型,进度值(progress value)和任务的最终值
  • doInBackground()方法自动在工作线程中执行
  • onPreExecute()、onPostExecute()和onProgressUpdate()都在UI线程中
  • onPreExecute()所返回的值被发送至onPostExecute()
  • 可以在onInBackground()执行的任意时刻调用publishProgress()以在UI线程中执行onProgressUpdate()
  • 可以在任一时刻,在任意线程中,取消任务

注意:在使用工作线程时可能遇到的另一个问题是由于运行配置的改变而活动自动重启(比如用户改变了屏幕方向),这可能会销毁该工作线程。要防止任务在这种情况下重启,并在活动被销毁时正确地取消任务,请参见Shelves范例程序的源代码。

线程安全方法(Thread-safe methods)

在某些情况下,所使用的方法可能会被不止一个线程调用,因此这些方法必须是线程安全的。

对于可以被远程调用的方法——比如在绑定的服务中的那些方法——来说,这是非常必要的。当在一个IBinder所运行的进程中调用一个在该IBinder中使用的方法时,这个方法会在调用者的线程中被执行。然而,如果这个调用是在另一个进程中的,那么这个方法将在从线程池中选择的一个系统保留的与IBinder处于同一进程的线程中执行(并不是在进程的UI线程中被执行)。例如,尽管一个服务的onBind()方法会被在服务的进程的UI线程中调用,对象中onBind()所返回的方法(比如,使用了RPC方法的一个子类)会在线程池中的线程中被调用。因为一个服务有不止一个客户端,所以多个池线程可以同时和同一个IBinder方法相关联。因此IBinder()方法必须是线程安全的。

线程间通信(Interprocess Communication,IPC)

Android提供了一种使用远程过程调用(remote procedure call,RPC)的进程间通信(IPC)机制,使得一个被某一活动或其他应用程序组件调用的方法将被(于另一进程中)远程执行,而所有的结果将被返回给调用者。这将会分解一个方法的调用及其数据至操作系统可以理解的级别,将其从本地进程及地址空间传输至远程进程及地址空间,然后在远程进程中重新组装激活这个调用。之后返回值将被反向传输。Android提供了所有执行这样的IPC事务所需的代码,因此只需关注如何定义和使用RPC编程接口即可。

要进行IPC,程序必须通过bindService()和一个服务相绑定。更多信息请参阅“服务开发者指南”。

本作品采用知识共享 署名-非商业性使用-禁止演绎 3.0 Unported许可协议进行许可。

《进程和线程》有2个想法

  1. 我参加了Android中文翻译组,看到了这里的精彩文章,在翻译进程与线程时参考了这一篇。根据许可协议,希望联系到您啦。欢迎您的加入,http://code.taobao.org/project/view/404/
    可是没有找到联系方式,twitter在国内是不通的,只好留言啦。

    1. 你好,很高兴我的翻译能对你有帮助。在遵守版权协议的前提下欢迎使用这些文档翻译。看了一下你们的工作,很有意义。很抱歉我暂时因为种种原因暂不考虑加入你们。我也是看到了很多人或组织尝试过翻译Android开发文档,可惜大多半途而废。希望你们可以坚持到底,加油!

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注