android 进程与线程

sancaiodm Android应用 2021-09-23 1478 0

 进程和线程概览

当应用组件启动且该应用未运行任何其他组件时,Android 系统会使用单个执行线程为应用启动新的 Linux 进程。默认情况下同一应用的所有组件(android四大组件)会在相同的进程和线程(称为“主”线程)中运行。(博主加:android的组件都是运行在主线程中的),如果某个应用组件启动且该应用已存在进程(因为存在该应用的其他组件),则该组件会在此进程内启动并使用相同的执行线程。但是,您也可以安排应用中的其他组件在单独的进程中运行,并为任何进程创建额外的线程。

当用户启动您的应用时,Android 会创建新的 Linux 进程以及执行线程。这个主线程也称为界面线程,负责屏幕上发生的一切活动。

主线程的设计非常简单:它的唯一工作就是从线程安全工作队列获取工作块并执行,直到应用被终止。框架会从多个位置生成部分工作块。这些位置包括与生命周期信息、用户事件(例如输入)或来自其他应用和进程的事件相关的回调。此外,应用也可以不使用框架而自行对块进行明确排队。

应用执行的任何代码块几乎都与事件回调(例如输入、布局扩充或绘制)相关联。当某个操作触发事件时,发生了事件的线程会将事件从线程本身里推送到主线程的消息队列中。然后,主线程可以为事件提供服务。

当有动画或屏幕更新正在进行时,系统会每隔 16ms 左右尝试执行一个工作块(负责绘制屏幕),从而以每秒 60 帧的流畅速度进行渲染。要使系统达到此目标,界面/视图层次结构必须在主线程上更新。但是,如果主线程的消息队列中的任务太多或太长,导致主线程无法足够快地完成更新,那么应用应将此工作移至工作线程。如果主线程无法在 16ms 内执行完工作块,则用户可能会察觉到卡顿、延迟或界面对输入无响应。 如果主线程阻塞大约 5 秒,系统会显示“应用无响应”(ANR) 对话框,允许用户直接关闭应用。

将大量或冗长的任务从主线程中移出,使其不影响流畅渲染和快速响应用户输入,这是您在应用中采用线程处理的最大原因。


默认情况下,系统会将线程的优先级设置为与生成它的线程具有相同的优先级和组成员资格。但是,您的应用可以使用 setThreadPriority() 明确调整线程优先级。

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

各类组件元素(<activity><service><receiver> 和 <provider>)的清单文件条目均支持 android:process 属性,此属性可指定该组件应在哪个进程中运行。您可以设置此属性,使每个组件均在各自的进程中运行,或者使某些组件共享一个进程,而其他组件则不共享。您也可设置 android:process,以便不同应用的组件在同一进程中运行,但前提是这些应用共享相同的 Linux 用户 ID 并使用相同的证书进行签署。

此外,<application> 元素还支持 android:process 属性,用来设置适用于所有组件的默认值。

当内存不足,而其他更急于为用户提供服务的进程又需要内存时,Android 可能会决定在某一时刻关闭某个进程。正因如此,系统会销毁在被终止进程中运行的应用组件。当这些组件需再次运行时,系统将为其重启进程。

决定终止哪个进程时,Android 系统会权衡其对用户的相对重要性。例如,相较于托管可见 Activity 的进程而言,系统更有可能关闭托管屏幕上不再可见的 Activity 的进程。因此,是否终止某个进程的决定取决于该进程中所运行组件的状态。

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

启动应用时,系统会为该应用创建一个称为“main”(主线程)的执行线程。此线程非常重要,因为其负责将事件分派给相应的界面微件,其中包括绘图事件。此外,应用与 Android 界面工具包组件(来自 android.widget 和 android.view 软件包的组件)也几乎都在该线程中进行交互。因此,主线程有时也称为界面线程。但在一些特殊情况下,应用的主线程可能并非其界面线程,相关详情请参阅线程注解

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

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

当应用执行繁重的任务以响应用户交互时,除非您正确实现应用,否则这种单线程模式可能会导致性能低下。具体地讲,如果界面线程需要处理所有任务,则执行耗时较长的操作(例如,网络访问或数据库查询)将会阻塞整个界面线程。一旦被阻塞,线程将无法分派任何事件,包括绘图事件。从用户的角度来看,应用会显示为挂起状态。更糟糕的是,如果界面线程被阻塞超过几秒钟时间(目前大约是 5 秒钟),用户便会看到令人厌烦的“应用无响应”(ANR) 对话框。如果引起用户不满,他们可能就会决定退出并卸载此应用。

此外,Android 界面工具包(自 android.widget 和 android.view 软件包的下的所有类)并非线程安全工具包。所以您不得通过工作线程操纵界面,而只能通过界面线程操纵界面。因此,Android 的单线程模式必须遵守两条规则:

  1. 不要阻塞 UI 线程

  2. 不要在 UI 线程之外访问 Android UI 工具包

根据上述单线程模式,如要保证应用界面的响应能力,关键是不能阻塞界面线程。如果执行的操作不能即时完成,则应确保它们在单独的线程(“后台”或“工作”线程)中运行。

但请注意,除了界面线程或“主”线程,您无法更新任何其他线程的界面。

为解决此问题,Android 提供了几种途径,以便您从其他线程访问界面线程。以下列出了几种有用的方法:

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

上述实现属于线程安全型:在单独的线程中完成后台操作,同时始终在界面线程中操纵 ImageView

但是,随着操作日趋复杂,这类代码也会变得复杂且难以维护。如要通过工作线程处理更复杂的交互,可以考虑在工作线程中使用 Handler 处理来自界面线程的消息。当然,最好的解决方案或许是扩展 AsyncTask 类,此类可简化与界面进行交互所需执行的工作线程任务。

AsyncTask 允许对界面执行异步操作。它会先阻塞工作线程中的操作,然后在界面线程中发布结果,而无需您亲自处理线程和/或处理程序。

如要使用该类,您必须创建 AsyncTask 的子类并实现 doInBackground() 回调方法,该方法会在后台线程池中运行。如要更新界面,您应实现 onPostExecute()(该方法会传递 doInBackground() 返回的结果并在界面线程中运行),以便安全更新界面。然后,您可以通过从界面线程调用 execute() 来运行任务。

如要全面了解如何使用此类,请阅读 AsyncTask 参考文档。

在某些情况下,系统可能会从多个线程调用您实现的方法,因此编写这些方法时必须确保其满足线程安全的要求。

这一点主要适用于可以远程调用的方法,如绑定服务中的方法。如果对 IBinder 中所实现方法的调用源自运行 IBinder 的同一进程,则系统会在调用方的线程中执行该方法。但是,如果调用源自其他进程,则系统会选择线程池中的某个线程,并在此线程中(而不是在进程的界面线程中)执行该方法,线程池由系统在与 IBinder 相同的进程中进行维护。例如,即使服务的 onBind() 方法通过服务进程的界面线程调用,在 onBind() 所返回对象中实现的方法(例如,实现 RPC 方法的子类)仍会通过线程池中的线程调用。由于服务可以有多个客户端,因此多个池线程可同时使用相同的 IBinder 方法。因此,IBinder 方法必须实现为线程安全方法。

同样,内容提供程序也可接收来自其他进程的数据请求。尽管 ContentResolver 和 ContentProvider 类隐藏了如何管理进程间通信的细节,但系统会从内容提供程序进程的线程池(而非进程的界面线程)调用响应这些请求的 ContentProvider 方法(query()insert()delete()update() 和 getType() 方法)。由于系统可能会同时从任意数量的线程调用这些方法,因此它们也必须实现为线程安全的方法。

Android 利用远程过程调用 (RPC) 提供了一种进程间通信 (IPC) 机制,在此机制中,系统会(在其他进程中)远程执行由 Activity 或其他应用组件调用的方法,并将所有结果返回给调用方。因此,您需将方法调用及其数据分解至操作系统可识别的程度,并将其从本地进程和地址空间传输至远程进程和地址空间,然后在远程进程中重新组装并执行该调用。然后,返回值将沿相反方向传输回来。Android 提供执行这些 IPC 事务所需的全部代码,因此您只需集中精力定义和实现 RPC 编程接口。

本文出自: https://developer.android.google.cn/guide/components/processes-and-threads


       当程序启动的时候Android会自动创建一个进程和一个线程,这个线程负责界面更新,收集系统事件和用户的操作事件等并分配给对应的组件,所以这个线程非常重要被称为主线程,因为所的和UI有关的操作都是在这个线程当中进行的所以也被称作UI线程。所以说默认情况下主线程和UI线程指的是同一个线程.

     Android 的UI系统是非线程安全的,意思是说只有创建UI的线程(也就是主线程)才可以对UI进程操作。

    很多Java起步的程序员对UI线程中的"消息循环"会感觉陌生。其实就是在线程内部有一个死循环一直监听系统事件(用户的操作等)并把任务分发给对应的组件(有兴趣的可以看看NDK附带的native-activity的一个sample,      在c中的实现方式就是一个while(1)的死循环)。主线程通过Looper实现了消息的循环处理。


       如果一个线程里边有一个死循环,那么这个循环就会一直在死循环里边循环,并且这个线程不会过多的cpu资源,那么这个线程肯定的阻塞的。如果线程只是一直循环这样没有什么意义,实际情况通常需要线程根据不同的条件运行不同的方法。我们通常的作法可能会加一个switch开关,根据条件的不同去调用不同的方法。

      线程间通信(最常见的就是,worker线程需要更新UI),这个时候实际是包含两个内容:一,数据的传递;二,方法的传递; Android中的Message用于数据传递,而Handler就是方法传递(其实是方法的执行者,在handleMessage方法内获取Message信息数据后,再执行调用一个方法 ,且Handler是把这个方法放到Handler所在的线程当中去执行);MessageQueue是Message的容器和Java中的Queue一样都是容器,只不过Message Queue是专门用于装载Message的容器。Looper则是一个线程中的死循环用于管理MessageQueue,它负责阻塞读取MessageQueue中的信息(如果MessageQueue中没有信息会一直在那里,处于等待(堵塞状态)状态),挨个读取并把这个Message分发给对应的Handler去执行。

       当我们调用msg.sendToTarget()的时候,我们的msg对象应付被压入MessageQueue的尾部。Looper在MessageQueue的另一端一个一个的读取信息并处理。根据msg的target属性把cpu分配给对应的Handler去执行任务,这时Handler会根据msg中的属性调用msg.handleMsg()或者msg.callback.run();当一个msg中的消息处理完返回之后Looper会把这个msg放入msgPool当中方便下次再重复利用。从而减少对象的创建。Looper再从MessageQueue中读取下一个信息,如此反复。

      ANR是如何实现:

      理解了这些其实就不难想象ANR是如何实现的了: 当用户执行操作(比如点击了一个按钮)系统会生成一个Message对象,把用户操作的信息写入Message对象,并把这个Message对象压入MessageQueue队列的尾部。系统过一段时间(一般是五秒)后会再来检查,刚刚放入的信息是不是已经被处理了,如果信息还在队列中就表明处理前面信息的过程当中发生的阻塞,用户的操作没有及时得到响应。系统弹出ANR对话框。


此文出自:https://blog.csdn.net/jiaoheshang/article/details/20227777



另两篇很好的文章:

为什么Looper中的Loop()方法不能导致主线程卡死?

Handler中有Loop死循环,为什么没有阻塞主线程,原理是什么



评论