(两百六十)学习进程和线程概览
https://developer.android.google.cn/guide/components/processes-and-threads
进程和线程概览
当应用组件启动且该应用未运行任何其他组件时,Android 系统会使用单个执行线程为应用启动新的 Linux 进程。默认情况下,同一应用的所有组件会在相同的进程和线程(称为“主”线程)中运行。如果某个应用组件启动且该应用已存在进程(因为存在该应用的其他组件),则该组件会在此进程内启动并使用相同的执行线程。但是,您可以安排应用中的其他组件在单独的进程中运行,并为任何进程创建额外的线程。
本文档介绍进程和线程在 Android 应用中的工作方式。
进程
默认情况下,同一应用的所有组件均在相同的进程中运行,且大多数应用都不应改变这一点。但是,如果您发现需要控制某个组件所属的进程,则可在清单文件中执行此操作。
各类组件元素(
<activity>
、<service>
、<receiver>
和<provider>
)的清单文件条目均支持android:process
属性,此属性可指定该组件应在哪个进程中运行。您可以设置此属性,使每个组件均在各自的进程中运行,或者使某些组件共享一个进程,而其他组件则不共享。您也可设置android:process
,以便不同应用的组件在同一进程中运行,但前提是这些应用共享相同的 Linux 用户 ID 并使用相同的证书进行签署。此外,
<application>
元素还支持android:process
属性,用来设置适用于所有组件的默认值。当内存不足,而其他更急于为用户提供服务的进程又需要内存时,Android 可能会决定在某一时刻关闭某个进程。正因如此,系统会销毁在被终止进程中运行的应用组件。当这些组件需再次运行时,系统将为其重启进程。
决定终止哪个进程时,Android 系统会权衡其对用户的相对重要性。例如,相较于托管可见 Activity 的进程而言,系统更有可能关闭托管屏幕上不再可见的 Activity 的进程。因此,是否终止某个进程的决定取决于该进程中所运行组件的状态。
如需详细了解进程生命周期及其与应用状态的关系,请参阅进程和应用生命周期。
举个例子,Settings中某些页面是跑在phone进程的
https://developer.android.google.cn/guide/components/activities/process-lifecycle
进程生命周期错误的一个常见示例是当
BroadcastReceiver
在其BroadcastReceiver.onReceive()
方法中接收到一个 Intent 时,它会启动一个线程,然后从该函数返回。一旦返回,则系统会认为 BroadcastReceiver 不再处于活动状态,因此不再需要其托管进程(除非其中有其他应用组件处于活动状态)。因此,系统可能会随时终止进程以回收内存,这样会终止在进程中运行的衍生线程。要解决这个问题,通常可以从 BroadcastReceiver 调度JobService
,这样系统就知道进程中还有处于活动状态的任务正在进行中。
进程类型
系统中只有少数此类进程,而且除非内存过低,导致连这些进程都无法继续运行,才会在最后一步终止这些进程。通常,此时设备已达到内存分页状态,因此必须执行此操作才能使用户界面保持响应。
- A 前台进程是用户目前执行操作所需的进程。在不同的情况下,进程可能会因为其所包含的各种应用组件而被视为前台进程。如果以下任一条件成立,则进程会被认为位于前台:
- 它正在用户的互动屏幕上运行一个
Activity
(其onResume()
方法已被调用)。- 它有一个
BroadcastReceiver
目前正在运行(其BroadcastReceiver.onReceive()
方法正在执行)。- 它有一个
Service
目前正在执行其某个回调(Service.onCreate()
、Service.onStart()
或Service.onDestroy()
)中的代码。- 可见进程正在进行用户当前知晓的任务,因此终止该进程会对用户体验造成明显的负面影响。在以下条件下,进程将被视为可见:
- 它正在运行的
Activity
在屏幕上对用户可见,但不在前台(其onPause()
方法已被调用)。举例来说,如果前台 Activity 显示为一个对话框,而这个对话框允许在其后面看到上一个 Activity,则可能会出现这种情况。- 它有一个
Service
正在通过Service.startForeground()
(要求系统将该服务视为用户知晓或基本上对用户可见的服务)作为前台服务运行。- 系统正在使用其托管的服务实现用户知晓的特定功能,例如动态壁纸、输入法服务等。
相比前台进程,系统中运行的这些进程数量较不受限制,但仍相对受控。这些进程被认为非常重要,除非系统为了使所有前台进程保持运行而需要终止它们,否则不会这么做。
- 服务流程包含一个已使用
startService()
方法启动的Service
。虽然用户无法直接看到这些进程,但它们通常正在执行用户关心的任务(例如后台网络数据上传或下载),因此系统会始终使此类进程保持运行,除非没有足够的内存来保留所有前台和可见进程。已经运行了很长时间(例如 30 分钟或更长时间)的服务的重要性可能会降位,以使其进程降至下文所述的缓存 LRU 列表。这有助于避免超长时间运行的服务因内存泄露或其他问题占用大量内存,进而妨碍系统有效利用缓存进程。
- 缓存进程是目前不需要的进程,因此,如果其他地方需要内存,系统可以根据需要*地终止该进程。在正常运行的系统中,这些是内存管理中涉及的唯一进程:运行良好的系统将始终有多个缓存进程可用(为了更高效地切换应用),并根据需要定期终止最早的进程。只有在非常危急(且具有不良影响)的情况下,系统中的所有缓存进程才会被终止,此时系统必须开始终止服务进程。
这些进程通常包含用户当前不可见的一个或多个
Activity
实例(onStop()
方法已被调用并返回)。只要它们正确实现其 Activity 生命周期(详情请见Activity
),那么当系统终止此类流程时,就不会影响用户返回该应用时的体验,因为当关联的 Activity 在新的进程中重新创建时,它可以恢复之前保存的状态。这些进程保存在伪 LRU 列表中,列表中的最后一个进程是为了回收内存而终止的第一个进程。此列表的确切排序政策是平台的实现细节,但它通常会先尝试保留更多有用的进程(比如托管用户的主屏幕应用、用户最后看到的 Activity 的进程等),再保留其他类型的进程。还可以针对终止进程应用其他政策:比如对允许的进程数量的硬限制,对进程可持续保持缓存状态的时间长短的限制等。
线程
启动应用时,系统会为该应用创建一个称为“main”(主线程)的执行线程。此线程非常重要,因为其负责将事件分派给相应的界面微件,其中包括绘图事件。此外,应用与 Android 界面工具包组件(来自
android.widget
和android.view
软件包的组件)也几乎都在该线程中进行交互。因此,主线程有时也称为界面线程。但在一些特殊情况下,应用的主线程可能并非其界面线程,相关详情请参阅线程注解。系统不会为每个组件实例创建单独的线程。在同一进程中运行的所有组件均在界面线程中进行实例化,并且对每个组件的系统调用均由该线程进行分派。因此,响应系统回调的方法(例如,报告用户操作的
onKeyDown()
或生命周期回调方法)始终在进程的界面线程中运行。例如,当用户轻触屏幕上的按钮时,应用的界面线程会将轻触事件分派给微件,而微件转而会设置其按下状态,并将失效请求发布到事件队列中。界面线程从队列中取消该请求,并通知该微件对其自身进行重绘。
当应用执行繁重的任务以响应用户交互时,除非您正确实现应用,否则这种单线程模式可能会导致性能低下。具体地讲,如果界面线程需要处理所有任务,则执行耗时较长的操作(例如,网络访问或数据库查询)将会阻塞整个界面线程。一旦被阻塞,线程将无法分派任何事件,包括绘图事件。从用户的角度来看,应用会显示为挂起状态。更糟糕的是,如果界面线程被阻塞超过几秒钟时间(目前大约是 5 秒钟),用户便会看到令人厌烦的“应用无响应”(ANR) 对话框。如果引起用户不满,他们可能就会决定退出并卸载此应用。
此外,Android 界面工具包并非线程安全工具包。所以您不得通过工作线程操纵界面,而只能通过界面线程操纵界面。因此,Android 的单线程模式必须遵守两条规则:
- 不要阻塞 UI 线程
- 不要在 UI 线程之外访问 Android UI 工具包
线程注释
线程注释可以检查某个方法是否从特定类型的线程调用。支持以下线程注释:
注意:构建工具会将
@MainThread
和@UiThread
注释视为可互换,因此您可以从@MainThread
方法调用@UiThread
方法,反之亦然。不过,如果系统应用有多个视图在不同的线程上,那么界面线程可能会与主线程不同。因此,您应使用@UiThread
来注释与应用的视图层次结构关联的方法,并使用@MainThread
仅标注与应用生命周期关联的方法。如果某个类中的所有方法具有相同的线程要求,您可以为该类添加一个线程注释,以验证该类中的所有方法是否从同一类型的线程调用。
线程注释的一个常见用途是验证 AsyncTask 类中的方法替换,因为此类会执行后台操作,并且仅在界面线程上发布结果。
工作线程
根据上述单线程模式,如要保证应用界面的响应能力,关键是不能阻塞界面线程。如果执行的操作不能即时完成,则应确保它们在单独的线程(“后台”或“工作”线程)中运行。
但请注意,除了界面线程或“主”线程,您无法更新任何其他线程的界面。
为解决此问题,Android 提供了几种途径,以便您从其他线程访问界面线程。以下列出了几种有用的方法:
工作线程
根据上述单线程模式,如要保证应用界面的响应能力,关键是不能阻塞界面线程。如果执行的操作不能即时完成,则应确保它们在单独的线程(“后台”或“工作”线程)中运行。
但请注意,除了界面线程或“主”线程,您无法更新任何其他线程的界面。
为解决此问题,Android 提供了几种途径,以便您从其他线程访问界面线程。以下列出了几种有用的方法:
public void onClick(View v) {
new Thread(new Runnable() {
public void run() {
// a potentially time consuming task
final Bitmap bitmap =
processBitMap("image.png");
imageView.post(new Runnable() {
public void run() {
imageView.setImageBitmap(bitmap);
}
});
}
}).start();
}
上述实现属于线程安全型:在单独的线程中完成后台操作,同时始终在界面线程中操纵
ImageView
。但是,随着操作日趋复杂,这类代码也会变得复杂且难以维护。如要通过工作线程处理更复杂的交互,可以考虑在工作线程中使用
Handler
处理来自界面线程的消息。当然,最好的解决方案或许是扩展AsyncTask
类,此类可简化与界面进行交互所需执行的工作线程任务。
使用 AsyncTask
AsyncTask
允许对界面执行异步操作。它会先阻塞工作线程中的操作,然后在界面线程中发布结果,而无需您亲自处理线程和/或处理程序。如要使用该类,您必须创建
AsyncTask
的子类并实现doInBackground()
回调方法,该方法会在后台线程池中运行。如要更新界面,您应实现onPostExecute()
(该方法会传递doInBackground()
返回的结果并在界面线程中运行),以便安全更新界面。然后,您可以通过从界面线程调用execute()
来运行任务。
如要全面了解如何使用此类,请阅读 AsyncTask
参考文档。
线程安全方法
在某些情况下,系统可能会从多个线程调用您实现的方法,因此编写这些方法时必须确保其满足线程安全的要求。
这一点主要适用于可以远程调用的方法,如绑定服务中的方法。如果对
IBinder
中所实现方法的调用源自运行IBinder
的同一进程,则系统会在调用方的线程中执行该方法。但是,如果调用源自其他进程,则系统会选择线程池中的某个线程,并在此线程中(而不是在进程的界面线程中)执行该方法,线程池由系统在与IBinder
相同的进程中进行维护。例如,即使服务的onBind()
方法通过服务进程的界面线程调用,在onBind()
所返回对象中实现的方法(例如,实现 RPC 方法的子类)仍会通过线程池中的线程调用。由于服务可以有多个客户端,因此多个池线程可同时使用相同的IBinder
方法。因此,IBinder
方法必须实现为线程安全方法。同样,内容提供程序也可接收来自其他进程的数据请求。尽管
ContentResolver
和ContentProvider
类隐藏了如何管理进程间通信的细节,但系统会从内容提供程序进程的线程池(而非进程的界面线程)调用响应这些请求的ContentProvider
方法(query()
、insert()
、delete()
、update()
和getType()
方法)。由于系统可能会同时从任意数量的线程调用这些方法,因此它们也必须实现为线程安全的方法。
进程间通信
Android 利用远程过程调用 (RPC) 提供了一种进程间通信 (IPC) 机制,在此机制中,系统会(在其他进程中)远程执行由 Activity 或其他应用组件调用的方法,并将所有结果返回给调用方。因此,您需将方法调用及其数据分解至操作系统可识别的程度,并将其从本地进程和地址空间传输至远程进程和地址空间,然后在远程进程中重新组装并执行该调用。然后,返回值将沿相反方向传输回来。Android 提供执行这些 IPC 事务所需的全部代码,因此您只需集中精力定义和实现 RPC 编程接口。
如要执行 IPC,您必须使用
bindService()
将应用绑定到服务。如需了解详细信息,请参阅服务开发者指南。