进程和线程概览

当应用组件启动时,如果应用没有运行任何其他组件,Android 系统会使用单个执行线程为应用启动新的 Linux 进程。默认情况下,同一应用的所有组件都在同一进程和线程(称为主线程)中运行。

如果某个应用组件已启动,且该应用已有进程,由于应用中的其他组件已启动,则该组件会在该进程内启动并使用同一执行线程。不过,您可以安排应用中的其他组件在单独的进程中运行,并为任何进程创建额外的线程。

本文档介绍进程和线程在 Android 应用中的工作方式。

进程

默认情况下,应用的所有组件均在同一进程中运行,大多数应用都不会改变这一点。不过,如果您发现需要控制某个组件属于哪个进程,可以在清单文件中执行此操作。

每种组件元素(<activity><service><receiver><provider>)的清单条目均支持 android:process 属性,该属性可指定运行组件的进程。您可以设置该属性,让每个组件都在自己的进程中运行,或者使一些组件共享一个进程,而其他组件不共享。

您还可以设置 android:process,以便不同应用的组件在同一进程中运行,前提是这些应用共用相同的 Linux 用户 ID 并使用相同的证书进行签名。

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

当其他更迅速地为用户提供服务的进程需要资源时,Android 可能会决定在某个时刻关闭某个进程。因此,在关闭的进程中运行的应用组件也会被销毁。当这些组件需要执行某些工作时,系统会再次为其启动一个进程。

在决定关闭哪些进程时,Android 系统会权衡它们对用户的相对重要性。例如,与托管可见 activity 的进程相比,它更容易关闭托管屏幕上不再可见的 activity 的进程。因此,是否终止某个进程的决定取决于该进程中运行的组件的状态。

如需详细了解进程生命周期及其与应用状态的关系,请参阅进程和应用生命周期

Threads

应用启动时,系统会为应用创建一个执行线程,称为“主线程”。此线程非常重要,因为它负责将事件分派给相应的界面 widget,包括绘图事件。它也几乎始终是应用与 Android 界面工具包的 android.widgetandroid.view 软件包中的组件进行交互的线程。因此,主线程有时称为界面线程。不过,在特殊情况下,应用的主线程可能并不是其界面线程。如需了解详情,请参阅线程注解

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

例如,当用户轻触屏幕上的按钮时,应用的界面线程会将触摸事件分派给 widget,而 widget 反过来会设置其按下状态并将失效请求发布到事件队列。界面线程将请求移出队列并通知 widget 重新绘制自身。

除非您正确实现应用,否则当应用为了响应用户互动而执行密集工作时,此单线程模式可能会导致性能不佳。在界面线程中执行长时间操作(例如网络访问或数据库查询)会阻塞整个界面。当线程被阻塞时,无法分派任何事件,包括绘图事件。

从用户的角度来看,应用似乎会挂起。更糟糕的是,如果界面线程被阻塞超过几秒钟,用户就会看到“应用无响应”(ANR) 对话框。然后,用户可能会决定退出甚至卸载您的应用。

请注意,Android 界面工具包并非线程安全工具包。因此,请不要在工作线程中操纵界面。您可以通过界面线程对界面进行所有操作。Android 的单线程模型有以下两条规则:

  1. 请勿阻塞界面线程。
  2. 请勿从界面线程外部访问 Android 界面工具包。

工作线程

鉴于这种单线程模式,确保应用界面的响应能力至关重要,即不能阻塞界面线程。如果您执行的操作不能即时完成,请确保在单独的后台工作器线程中执行操作。请记住,您不能从界面或主线程之外的任何线程更新界面。

为了帮助您遵循这些规则,Android 提供了多种从其他线程访问界面线程的方式。以下列出了几种有用的方法:

以下示例使用 View.post(Runnable)

Kotlin

fun onClick(v: View) {
    Thread(Runnable {
        // A potentially time consuming task.
        val bitmap = processBitMap("image.png")
        imageView.post {
            imageView.setImageBitmap(bitmap)
        }
    }).start()
}

Java

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 处理从界面线程传送的消息。如需详细了解如何在后台线程上调度工作并与界面线程进行通信,请参阅后台工作概览

线程安全方法

在某些情况下,系统会从多个线程调用您实现的方法,因此编写的方法必须保证线程安全。

这主要适用于可以远程调用的方法,如绑定服务中的方法。如果对 IBinder 中实现的方法的调用源自运行 IBinder 的同一进程,则系统会在调用方的线程中执行该方法。不过,当调用源自另一个进程时,该方法会在从系统与 IBinder 在同一进程中维护的线程池中选择的线程中执行。它不会在进程的界面线程中执行,

例如,虽然从服务进程的界面线程调用服务的 onBind() 方法,但在 onBind() 返回的对象中实现的方法(例如实现远程过程调用 (RPC) 方法的子类)是从池中的线程调用的。由于一项服务可以有多个客户端,因此多个池线程可同时使用同一 IBinder 方法,因此 IBinder 方法必须实现为线程安全的。

同样,content provider 可以接收源自其他进程的数据请求。ContentResolverContentProvider 类会隐藏关于进程间通信 (IPC) 如何管理的详细信息,但响应这些请求的 ContentProvider 方法(query()insert()delete()update()getType() 方法)是从 content provider 进程中的线程池调用的,而不是从进程的界面线程中调用的。由于这些方法可能会同时从任意数量的线程调用,因此它们也必须实现为线程安全方法。

进程间通信

Android 提供了一种使用 RPC 的 IPC 机制。在这种机制中,一个方法被 activity 或其他应用组件调用,但在另一个进程中远程执行,系统会将所有结果返回给调用方。这需要将方法调用及其数据分解到操作系统可以理解的级别,将其从本地进程和地址空间传输到远程进程和地址空间,然后在远程进程和地址空间中重新组装和执行该调用。

然后,返回值将沿相反方向传输。Android 提供了执行这些 IPC 事务的所有代码,因此您可以专注于定义和实现 RPC 编程接口。

如需执行 IPC,必须使用 bindService() 将应用绑定到服务。如需了解详情,请参阅服务概览