Android 的消息机制(UI线程的Looper 为啥不会阻塞?答案在后面)

2024-06-07 20:08

本文主要是介绍Android 的消息机制(UI线程的Looper 为啥不会阻塞?答案在后面),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

说道 Android 的消息机制,其实说的就是 Handler 的运行机制以及 Handler 所附带的 MessageQueue 和 Looper 的工程过程。

一、 Handler 的运行机制

当 Handler 创建的时候,会采用当前线程的 Looper 来构建消息循环系统,如果当前线程没有 Looper 则会报错,当然,开始是 UI 线程,所以不用担心。
当然,当Looper 被创建的时候, MessageQueue 也被创建好了,这样 Looper 和 MessageQueue 就可以跟 Handler 一起工作了。

这里,先做一下 Handler 的工作流程:
当我们使用 Handler 的send 或者 post 的时候,它会调用 MessageQueue 的 qnqueueMessage 方法,将这个 消息放到消息队列中,然后 Looper 发现有新消息到来时,就会处理这个消息了;然后 Handler 的 handleMessage 方法就会被调用。

注意 Looper 是运行在创建 Handler 所在的线程中的,这样一来,Handler 中的业务逻辑就会被切换到创建 Handler 所在的县城中去执行了

过程可以用下图表示(图片来源):
在这里插入图片描述
接下来继续深入它。

1.2、消息队列的工作原理

消息队列在 Android 中指的是 MessageQueue,它其实不是队列,里面实现的单链表结构,它主要包含两个操作:插入和读取。
插入对应 enqueueMessage ,读取则对应 next,其中 enqueueMessage 表示往队列中插入一条消息,而 next 则从队列中取出消息,并将消息从队列中删除。

其中,需要注意的是它的 next 方法:

    Message next() {...for (;;) {if (nextPollTimeoutMillis != 0) {Binder.flushPendingCommands();}...}...

可以发现 next 方法是一个无线循环的方法,如果消息队列没有消息,那么next 方法会一直阻塞在这里。当有新消息到来时,next 方法会返回这条消息并将它从单链表中删除。

1.3 Looper 的工作原理

Looper 在 Android 的消息机制中扮演者消息队列的角色,具体来说,你会不停地从 MessageQueue 中查看是否有新的消息,如果有则取出消息,如果没有,则一直阻塞在那里。

在 Looper 的构造方法中,会创建一个 MessageQueue ,然后将当前线程保存起来。

    private Looper(boolean quitAllowed) {mQueue = new MessageQueue(quitAllowed);mThread = Thread.currentThread();}

然后,Handler 的创建是需要 Looper 的,没有Looper 是会报错的,那 Looper 如何创建呢?
很简单,在线程中,通过 Looper.prepare() 为线程开启一个 Looper,接着使用 Looper.loop() 来开启消息循环模式,如下所示:

new Thread(){@Overridepublic void run() {Looper.prepare();super.run();Looper.loop();}
};

当然,这个是普通线程的,如果是 UI 线程,还提供了 prepareMainLooper 方法,这个方法主要是给 ActivityThread 创建Looper 使用,由于主线程比较特殊,也可以使用 Looper.getMainLooper() 来获取主线程Looper。

比如常用在共用类中:

 public static Handler HANDLER = new Handler(Looper.getMainLooper());

1.3.1 Looper 的退出

Looper 也是可以退出的,有 quit() 和 quitSafely() 两个方法;quit() 会直接退出,二 quitSalely() 则是已有消息都处理完毕,才会退出。

如果是自己定义的 Looper ,则建议要执行退出操作,否则这个子线程就会一直处于等待状态。容易造成内存泄漏,当然主线程就不用我们操心了。

Looper 的重要方法是 loop 方法,只有调用了 loop 后,消息循环系统才会真正起作用,它的实现如下:

    public static void loop() {final Looper me = myLooper();if (me == null) {throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");}final MessageQueue queue = me.mQueue;// Make sure the identity of this thread is that of the local process,// and keep track of what that identity token actually is.Binder.clearCallingIdentity();final long ident = Binder.clearCallingIdentity();...boolean slowDeliveryDetected = false;for (;;) {Message msg = queue.next(); // might blockif (msg == null) {// No message indicates that the message queue is quitting.return;}// This must be in a local variable, in case a UI event sets the loggerfinal Printer logging = me.mLogging;if (logging != null) {logging.println(">>>>> Dispatching to " + msg.target + " " +msg.callback + ": " + msg.what);}final long traceTag = me.mTraceTag;long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;long slowDeliveryThresholdMs = me.mSlowDeliveryThresholdMs;if (thresholdOverride > 0) {slowDispatchThresholdMs = thresholdOverride;slowDeliveryThresholdMs = thresholdOverride;}final boolean logSlowDelivery = (slowDeliveryThresholdMs > 0) && (msg.when > 0);final boolean logSlowDispatch = (slowDispatchThresholdMs > 0);final boolean needStartTime = logSlowDelivery || logSlowDispatch;final boolean needEndTime = logSlowDispatch;if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {Trace.traceBegin(traceTag, msg.target.getTraceName(msg));}final long dispatchStart = needStartTime ? SystemClock.uptimeMillis() : 0;final long dispatchEnd;try {msg.target.dispatchMessage(msg);dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;} finally {if (traceTag != 0) {Trace.traceEnd(traceTag);}}... ...

可以看到,只有 queue.next() 为null 时,才会退出这个循环;当执行了 quit 或者 quitSafely 来通知 Looper 退出,它就会调用 MessageQueue 来退出,这样 queue.next() 就会返回null,looper 也就退出了。
否则就会一直阻塞在这里,一直等到 next 有新的消息,则 msg.target.dispatchMessage(msg); 就会被执行,这样 Handler 的dispatchMessage 就会被执行了。可以看到 Handler 的 dispatchMessage 方法如下:

    public void dispatchMessage(Message msg) {if (msg.callback != null) {handleCallback(msg);} else {if (mCallback != null) {if (mCallback.handleMessage(msg)) {return;}}handleMessage(msg);}}

首先,先检查 msg.callback 是否为null,Message 的callback 是一个 RUnnable 对象,实际上就是 Hnadler 的post 方法所传递的 Runnable 。所以,如果我们使用 了 Handler.post ,则不会走 handleMessage(msg) ;
如果不是,则检查 mCallback 是否为null,mCalback 其实就是个接口:

    public interface Callback {/*** @param msg A {@link android.os.Message Message} object* @return True if no further handling is desired*/public boolean handleMessage(Message msg);}

如果不为空,最终就会调用到我们熟悉的 handleMessage(msg) 方法了。

这样,Handler 的消息机制就分析完了。

这里,面试官就会问了,既然 looper 这里是个无线循环,为啥不会阻塞UI线程?

为了回答这个问题,首先,我们先去到主线程的消息队列

二、主线程的消息队列

Android 的主线程就是 ActivityThread,主线程的入口为 main 方法:

public static void main(String[] args) {Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ActivityThreadMain");.... Looper.prepareMainLooper();// Find the value for {@link #PROC_START_SEQ_IDENT} if provided on the command line.// It will be in the format "seq=114"long startSeq = 0;if (args != null) {for (int i = args.length - 1; i >= 0; --i) {if (args[i] != null && args[i].startsWith(PROC_START_SEQ_IDENT)) {startSeq = Long.parseLong(args[i].substring(PROC_START_SEQ_IDENT.length()));}}}ActivityThread thread = new ActivityThread();thread.attach(false, startSeq);if (sMainThreadHandler == null) {sMainThreadHandler = thread.getHandler();}if (false) {Looper.myLooper().setMessageLogging(newLogPrinter(Log.DEBUG, "ActivityThread"));}// End of event ActivityThreadMain.Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);Looper.loop();throw new RuntimeException("Main thread loop unexpectedly exited");}

可以看到,main() 方法会通过 Looper.prepareMainLooper() 来创建 Looper 和 MessageQueue,然后创建了 ActivityThread线程,最后通过 Looper.loop() 开启循环。

而它的 Handler 就是 ActivityThread.H ,里面对应着 Activity 的启动等消息:
在这里插入图片描述

那这还是不能说明UI线程为啥不会阻塞啊?!!

别急,除了这个 ActivityThread 这个线程,其实还有个 ApplicationThread 线程,它里面创建了 Binder 方法,当 ApplicationThread 与 AMS 进行进程间通信完成后,就会通过 ApplicationThread 会向 H 发送消息,H 收到消息之后,就会将 ApplicationThread 中的逻辑切换到 ActivityThread 中取执行,即切换到主线程去执行了。
比如 Activity 的启动最后就通过 ApplicationThread 发送 LUANCHER_ACTIVITY 给 H 去启动的。

所以,UI线程并不会因为looper而阻塞,但如果我们主线程中做了一些耗时操作,导致导致 MessageQueue 的消息过多,等待执行,造成 ANR。

自此,我们的 Android 消息机制就分析完了。

参考 Android 艺术开发 第10章

这篇关于Android 的消息机制(UI线程的Looper 为啥不会阻塞?答案在后面)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1040171

相关文章

JAVA保证HashMap线程安全的几种方式

《JAVA保证HashMap线程安全的几种方式》HashMap是线程不安全的,这意味着如果多个线程并发地访问和修改同一个HashMap实例,可能会导致数据不一致和其他线程安全问题,本文主要介绍了JAV... 目录1. 使用 Collections.synchronizedMap2. 使用 Concurren

C++如何通过Qt反射机制实现数据类序列化

《C++如何通过Qt反射机制实现数据类序列化》在C++工程中经常需要使用数据类,并对数据类进行存储、打印、调试等操作,所以本文就来聊聊C++如何通过Qt反射机制实现数据类序列化吧... 目录设计预期设计思路代码实现使用方法在 C++ 工程中经常需要使用数据类,并对数据类进行存储、打印、调试等操作。由于数据类

Android实现在线预览office文档的示例详解

《Android实现在线预览office文档的示例详解》在移动端展示在线Office文档(如Word、Excel、PPT)是一项常见需求,这篇文章为大家重点介绍了两种方案的实现方法,希望对大家有一定的... 目录一、项目概述二、相关技术知识三、实现思路3.1 方案一:WebView + Office Onl

Android实现两台手机屏幕共享和远程控制功能

《Android实现两台手机屏幕共享和远程控制功能》在远程协助、在线教学、技术支持等多种场景下,实时获得另一部移动设备的屏幕画面,并对其进行操作,具有极高的应用价值,本项目旨在实现两台Android手... 目录一、项目概述二、相关知识2.1 MediaProjection API2.2 Socket 网络

Redis消息队列实现异步秒杀功能

《Redis消息队列实现异步秒杀功能》在高并发场景下,为了提高秒杀业务的性能,可将部分工作交给Redis处理,并通过异步方式执行,Redis提供了多种数据结构来实现消息队列,总结三种,本文详细介绍Re... 目录1 Redis消息队列1.1 List 结构1.2 Pub/Sub 模式1.3 Stream 结

Android实现悬浮按钮功能

《Android实现悬浮按钮功能》在很多场景中,我们希望在应用或系统任意界面上都能看到一个小的“悬浮按钮”(FloatingButton),用来快速启动工具、展示未读信息或快捷操作,所以本文给大家介绍... 目录一、项目概述二、相关技术知识三、实现思路四、整合代码4.1 Java 代码(MainActivi

Android Mainline基础简介

《AndroidMainline基础简介》AndroidMainline是通过模块化更新Android核心组件的框架,可能提高安全性,本文给大家介绍AndroidMainline基础简介,感兴趣的朋... 目录关键要点什么是 android Mainline?Android Mainline 的工作原理关键

如何解决idea的Module:‘:app‘platform‘android-32‘not found.问题

《如何解决idea的Module:‘:app‘platform‘android-32‘notfound.问题》:本文主要介绍如何解决idea的Module:‘:app‘platform‘andr... 目录idea的Module:‘:app‘pwww.chinasem.cnlatform‘android-32

SpringRetry重试机制之@Retryable注解与重试策略详解

《SpringRetry重试机制之@Retryable注解与重试策略详解》本文将详细介绍SpringRetry的重试机制,特别是@Retryable注解的使用及各种重试策略的配置,帮助开发者构建更加健... 目录引言一、SpringRetry基础知识二、启用SpringRetry三、@Retryable注解

Android实现打开本地pdf文件的两种方式

《Android实现打开本地pdf文件的两种方式》在现代应用中,PDF格式因其跨平台、稳定性好、展示内容一致等特点,在Android平台上,如何高效地打开本地PDF文件,不仅关系到用户体验,也直接影响... 目录一、项目概述二、相关知识2.1 PDF文件基本概述2.2 android 文件访问与存储权限2.