进程和线程概览

当应用组件启动时,如果应用没有运行任何其他组件,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() 将应用绑定到服务。如需了解详情,请参阅服务概览