当某个应用组件启动且该应用没有运行其他任何组件时,Android 系统会使用单个执行线程为应用启动新的 Linux 进程。默认情况下,同一应用的所有组件在相同的进程和线程(称为主线程)中运行。 如果某个应用组件启动且该应用已存在进程(因为存在该应用的其他组件),则该组件会在此进程内启动并使用相同的执行线程。 但是,您可以安排应用中的其他组件在单独的进程中运行,并为任何进程创建额外的线程。
本文档介绍进程和线程在 Android 应用中的工作方式。
默认情况下,同一应用的所有组件均在相同的进程中运行,且大多数应用都不会改变这一点。 但是,如果您发现需要控制某个组件所属的进程,则可在manifest
文件中执行此操作。
各类组件元素的manifest文件条目——activity
``、service
、`receiver` 和 `provider`均支持 `android:process`属性,此属性可以指定该组件应在哪个进程运行。您可以设置此属性,使每个组件均在各自的进程中运行,或者使一些组件共享一个进程,而其他组件则不共享。 此外,您还可以设置 `android:process`,使不同应用的组件在相同的进程中运行,但前提是这些应用共享相同的 Linux 用户 ID 并使用相同的证书进行签署。
此外,application
元素也支持 android:process
属性,以设置适用于所有组件的默认值。
如果内存不足,而其他为用户提供更紧急服务的进程又需要内存时,Android 可能会决定在某一时刻关闭某一进程。在被终止进程中运行的应用组件也会随之销毁。 当这些组件需要再次运行时,系统将为它们重启进程。
决定终止哪个进程时,Android 系统将权衡它们对用户的相对重要程度。例如,相对于托管可见 Activity 的进程而言,系统更有可能关闭托管屏幕上不再可见的 Activity 的进程。 因此,是否终止某个进程取决于该进程中所运行组件的状态。 有关进程生命周期及其与应用程序状态的关系的更多内容,请参阅进程和应用程序的生命周期
应用启动时,系统会为应用创建一个名为“主线程”的执行线程。 此线程非常重要,因为它负责将事件分派给相应的用户界面中的小部件,其中包括绘图事件。 此外,它也是应用与 Android UI 工具包组件(来自 android.widget
和 android.view
软件包的组件)进行交互的线程。因此,主线程有时也称为 UI 线程。
系统不会为每个组件实例创建单独的线程。运行于同一进程的所有组件均在 UI 线程中实例化,并且对每个组件的系统调用均由该线程进行分派。 因此,响应系统回调的方法(例如,报告用户操作的 onKeyDown()
或生命周期回调方法)始终在进程的 UI 线程中运行。
例如,当用户触摸屏幕上的按钮时,应用的 UI 线程会将触摸事件分派给小部件,而小部件反过来又设置其按下状态,并将失效请求发布到事件队列中。 UI 线程从队列中取消该请求并通知小部件应该重绘自身。
在应用执行繁重的任务以响应用户交互时,除非正确实现应用,否则这种单线程模式可能会导致性能低下。 具体地讲,如果 UI 线程需要处理所有任务,则执行耗时很长的操作(例如,网络访问或数据库查询)将会阻塞整个 UI。 一旦线程被阻塞,将无法分派任何事件,包括绘图事件。 从用户的角度来看,应用显示为挂起。 更糟糕的是,如果 UI 线程被阻塞超过几秒钟时间(目前大约是 5 秒钟),用户就会看到一个让人厌烦的“应用无响应”(ANR) 对话框。如果引起用户不满,他们可能就会决定退出并卸载此应用。
此外,Android UI 工具包并非线程安全工具包。因此,您不得通过工作线程操纵 UI,而只能通过 UI 线程操纵用户界面。 因此,Android 的单线程模式必须遵守两条规则:
- 不要阻塞 UI 线程
- 不要在 UI 线程之外访问 Android UI 工具包
根据上述单线程模式,要保证应用 UI 的响应能力,关键是不能阻塞 UI 线程。 如果您要执行的操作不是即时的,则应确保在单独的线程(“后台”或“工作线程”线程)中执行它们。
但是,请注意,您无法从UI线程或“主”线程以外的任何线程更新UI。
为了解决这个问题,Android提供了几种从其他线程访问UI线程的方法。以下列出了可以提供帮助的方法:
Activity.runOnUiThread(Runnable)
View.post(Runnable)
View.postDelayed(Runnable, long)
示例代码:
-
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
中:
public void onClick(View v) {
new Thread(new Runnable() {
public void run() {
Bitmap b = loadImageFromNetwork("http://example.com/image.png");
mImageView.setImageBitmap(b);
}
}).start();
}
此实现是线程安全的:后台操作是在单独的线程完成的,而ImageView始终是在UI线程操作的。
但是,随着操作复杂性的增加,这种代码变得复杂且难以维护。要处理与工作线程的更复杂的交互,您可以考虑在工作线程中使用Handler来处理从UI线程传递的消息。也许最好的解决方案是继承AsyncTask类,它简化了需要与UI交互的工作线程任务的执行。
AsyncTask允许您在用户界面上执行异步工作。它在工作线程中执行阻塞操作,然后在UI线程上发布结果,而不需要您自己处理线程和/或处理程序。
要使用它,必须创建 AsyncTask
的子类并实现 doInBackground()
回调方法,该方法将在后台线程池中运行。 要更新 UI,应该实现 onPostExecute()
以传递 doInBackground()
返回的结果并在 UI 线程中运行,以便您安全地更新 UI。 之后,您可以通过从 UI 线程调用 execute()
来运行任务。
您应该阅读AsyncTask文档,以全面了解如何使用此类。
在某些情况下,您实现的方法可能会从多个线程调用,因此编写这些方法时必须确保其满足线程安全的要求。
这一点主要适用于可以远程调用的方法,如绑定Service中的方法。如果对 IBinder
中所实现方法的调用源自运行 IBinder
的同一进程,则该方法在调用方的线程中执行。但是,如果调用源自其他进程,则该方法将在从线程池选择的某个线程中执行(而不是在进程的 UI 线程中执行),线程池由系统在与 IBinder
相同的进程中维护。 例如,即使Service的 onBind()
方法将从服务进程的 UI 线程调用,在 onBind()
返回的对象中实现的方法(例如,实现 RPC 方法的子类)仍会从线程池中的线程调用。 由于一个Service可以有多个客户端,因此可能会有多个池线程在同一时间使用同一 IBinder
方法。因此,IBinder
方法必须实现为线程安全方法。
同样,内容提供程序也可接收来自其他进程的数据请求。尽管 ContentResolver
和 ContentProvider
类隐藏了如何管理进程间通信的细节,但响应这些请求的 ContentProvider
方法(query()
、insert()
、delete()
、update()
和 getType()
方法)将从内容提供程序所在进程的线程池中调用,而不是从进程的 UI 线程调用。 由于这些方法可能会同时从任意数量的线程调用,因此它们也必须实现为线程安全方法。
Android 利用远程过程调用 (RPC) 提供了一种进程间通信 (IPC) 机制,通过这种机制,由 Activity 或其他应用组件调用的方法将(在其他进程中)远程执行,而所有结果将返回给调用方。 这就要求把方法调用及其数据分解至操作系统可以识别的程度,并将其从本地进程和地址空间传输至远程进程和地址空间,然后在远程进程中重新组装并执行该调用。 然后,返回值将沿相反方向传输回来。 Android 提供了执行这些 IPC 事务所需的全部代码,因此您只需集中精力定义和实现 RPC 编程接口即可。
要执行 IPC,必须使用 bindService()
将应用绑定到服务上。如需了解详细信息,请参阅Service开发者指南。