进程和线程概览

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

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

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

进程

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

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

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

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

当其他进程需要资源来更直接地为用户提供服务时,Android 可能会决定在某个时间点关闭某个进程。在关闭的进程中运行的应用组件也会随之被销毁。 当这些组件有工作要做时,系统会再次为它们启动一个进程。

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

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

线程

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

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

例如,当用户触摸屏幕上的按钮时,应用的界面线程会将触摸事件分派给微件,该微件会设置其按下状态并将失效请求发布到事件队列。界面线程会将请求出列,并通知微件自行重绘。

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

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

请注意,Android UI 工具包不是线程安全的。 因此,请勿从工作器线程操作界面。请从界面线程对界面进行所有操作。Android 的单线程模型有两条规则:

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

工作器线程

由于此单线程模型,请务必不要阻塞界面线程,以确保应用界面的响应能力。如果您有非即时操作要执行,请务必在单独的后台线程或工作器线程中执行这些操作。 请记住,您无法从界面线程(或主线程)以外的任何线程更新界面。

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

以下示例演示了如何将任务分流到后台线程,并在任务完成后更新界面线程:

Kotlin

// Kotlin coroutines implementation.
fun onClick(v: View) {
    // Launch a coroutine in the lifecycle scope (e.g., in an Activity or Fragment).
    lifecycleScope.launch {
        // Run the blocking task on the IO dispatcher.
        val bitmap = withContext(Dispatchers.IO) {
            BitmapFactory.decodeFile("image.png")
        }
        // Back on the main thread, update the UI.
        imageView.setImageBitmap(bitmap)
    }
}

Java

// Java Executor implementation.
// (executorService is assumed to be defined elsewhere).
public void onClick(View v) {
    executorService.execute(() -> {
        // Run the heavy task on a background thread.
        Bitmap bitmap = BitmapFactory.decodeFile("image.png");

        // Update the View on the UI thread.
        imageView.post(() -> imageView.setImageBitmap(bitmap));
    });
}

此实现是线程安全的,因为后台操作是从单独的线程完成的,而 ImageView 始终是从界面线程操作的。

不过,随着操作复杂性的增加,此类代码可能会变得复杂且难以维护。如需处理与工作器线程的更复杂互动,您可以考虑在工作器线程中使用 Handler 来处理从界面线程传送的消息。如需全面了解如何在后台线程中调度工作以及如何将结果传回界面线程,请参阅后台工作概览。

线程安全方法

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

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

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

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

进程间通信

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

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

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