进程和线程概述

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

如果应用程序组件启动并且该应用程序已有一个进程(因为该应用程序的另一个组件已启动),则该组件在该进程中启动并使用相同的执行线程。但是,您可以安排应用程序中的不同组件在单独的进程中运行,并且可以为任何进程创建其他线程。

本文档讨论了进程和线程在 Android 应用程序中的工作方式。

进程

默认情况下,应用程序的所有组件都在同一个进程中运行,大多数应用程序都不会更改此设置。但是,如果您发现需要控制某个组件所属的进程,则可以在清单文件中执行此操作。

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

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

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

当其他进程需要资源以更直接地为用户服务时,Android 可能会在某个时候决定关闭一个进程。因此,在被关闭的进程中运行的应用程序组件会被销毁。当这些组件有工作要做时,系统会为它们重新启动一个进程。

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

进程生命周期及其与应用程序状态的关系的详细信息在 进程和应用程序生命周期 中讨论。

线程

当启动应用程序时,系统会为应用程序创建一个执行线程,称为主线程。此线程非常重要,因为它负责将事件分派到相应的用户界面小部件,包括绘制事件。它也几乎总是应用程序与 Android UI 工具包中的组件交互的线程 android.widgetandroid.view 包。因此,主线程有时被称为UI 线程。但是,在特殊情况下,应用程序的主线程可能不是其 UI 线程。有关更多信息,请参阅 线程注释

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

例如,当用户触摸屏幕上的按钮时,您的应用程序的 UI 线程将触摸事件分派到小部件,该小部件依次设置其按下状态并将无效请求发布到事件队列。UI 线程出队请求并通知小部件重新绘制自身。

除非您正确实现应用程序,否则此单线程模型在您的应用程序响应用户交互执行密集型工作时可能会导致性能下降。在 UI 线程中执行长时间操作,例如网络访问或数据库查询,会阻塞整个 UI。当线程被阻塞时,无法分派任何事件,包括绘制事件。

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

请记住,Android UI 工具包不是线程安全的。因此,不要从工作线程操作您的 UI。从 UI 线程执行对用户界面的所有操作。Android 的单线程模型有两个规则

  1. 不要阻塞 UI 线程。
  2. 不要从 UI 线程外部访问 Android UI 工具包。

工作线程

由于这种单线程模型,对于应用程序 UI 的响应能力至关重要的是不要阻塞 UI 线程。如果您有要执行的操作不是即时的,请确保在单独的后台工作线程中执行它们。请记住,您不能从 UI 线程以外的任何线程更新 UI,或主线程。

为了帮助您遵循这些规则,Android 提供了几种从其他线程访问 UI 线程的方法。以下是一些可以帮助您的方法列表

以下示例使用 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 始终从 UI 线程进行操作。

但是,随着操作复杂度的增加,这种代码可能会变得复杂且难以维护。要处理与工作线程更复杂的交互,您可能会考虑在工作线程中使用 Handler 来处理从 UI 线程传递的消息。有关如何在后台线程上安排工作以及如何与 UI 线程通信的完整说明,请参阅 后台工作概述

线程安全方法

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

这主要适用于可以远程调用的方法,例如 绑定服务 中的方法。当对 IBinder 中实现的方法的调用源自 IBinder 正在运行的同一进程时,该方法将在调用者的线程中执行。但是,当调用源自另一个进程时,该方法将在系统在与 IBinder 相同的进程中维护的线程池中选择的线程中执行。它不会在进程的 UI 线程中执行。

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

类似地,内容提供程序可以接收源自其他进程的数据请求。该 ContentResolverContentProvider 类隐藏了如何管理进程间通信 (IPC) 的详细信息,但响应这些请求的 ContentProvider 方法(方法 query()insert()delete()update()getType())是从内容提供程序进程的线程池中调用的,而不是该进程的 UI 线程。因为这些方法可能同时从任意数量的线程调用,所以它们也必须实现为线程安全的。

进程间通信

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

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

要执行 IPC,您的应用程序必须使用 bindService() 绑定到服务。有关更多信息,请参阅 服务概述