Android 消息分发机制解读

前言

想必大家都知道Android系统有自己的一套消息分发机制,,从App启动那一刻起,App就创建了主线程的消息分发实例:Looper.sMainLooper,并开始无限循环,也就是App的心脏,一直跳动,负责协调分配来自各方的事件,让App不断响应用户操作,如果主线程出现了异常,也就是心脏跳动异常停止,那么App的生命随之终止,也就是常见的‘进程已停止运行’。那么,你有没有想过,既然他在一直无限循环,为什么没有卡死呢?为什么能看到“应用无响应”?怎么保证界面刷新不受其他事件影响,怎么做到有条不理的处理每一条消息等等这些问题呢,作为一名Android开发者,我想我们有必要对其结构进行简单了解。

思路整理

基于消息分发机制,我们可以从以下几个方面由深到浅去解惑:

  1. Message
  2. MessageQueue 的核心逻辑;
  3. Looper的核心逻辑;
  4. Handler机制;
    在阅读前,你可能需要对数据结构单链表有一定的了解。

源码基于 Android API 33

Message

消息对象,部分源码:


public final class Message implements Parcelable {///*** The targeted delivery time of this message. The time-base is* {@link SystemClock#uptimeMillis}.* @hide Only for use within the tests.*/@UnsupportedAppUsage@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)public long when;//@UnsupportedAppUsage/*package*/ Handler target;//是否为异步消息/*** Returns true if the message is asynchronous, meaning that it is not* subject to {@link Looper} synchronization barriers.** @return True if the message is asynchronous.** @see #setAsynchronous(boolean)*/public boolean isAsynchronous() {return (flags & FLAG_ASYNCHRONOUS) != 0;}
}

关于此类,需要知道的是,我们外部创建的target一般不为空,为空一般是系统内部创建的消息,比如执行Viewinvalidate()就是发送了target为空的异步消息,具体看 消息队列中的分发逻辑。

MessageQueue 消息队列

顾名思义,是一个存放消息Message的队列,主要负责管理消息的插入和取出。每个消息都有对应的创建时间,插入队列中的消息会按时间排序,当调用next时会从队列中取出一条符合条件的Message,如果没有,则next函数会进入休眠状态,直到被唤醒。为了方便理解,下面对该类的核心方法,核心变量分开分析。
大致结构如下:

//源码位于 android.os.MessageQueue @UnsupportedAppUsage
@SuppressWarnings("unused")
private long mPtr; // used by native code @UnsupportedAppUsage
Message mMessages;boolean enqueueMessage(Message msg, long when){}
Message next() {}
@UnsupportedAppUsage
@TestApi
public int postSyncBarrier(){}@UnsupportedAppUsage
@TestApi
public void removeSyncBarrier(int token) {}

mPtr

看起来就是一个指针,源码中注释为: used by native code,也就是说是在Native层用的代码,我们只需知道他是一道闸,堵塞next方法用的。

mMessages

队列的头部消息。

postSyncBarrier ,removeSyncBarrier

这两个方法是发送和移除同步屏障消息,我们可以把它想象成为一道栅栏,能拦截所有同步消息,只允许异步信息通过。当向队列中插入该消息后,消息分发会主动跳过所有同步消息,即使队列中没有异步消息,直至屏障被移除。
不难发现,调用postSyncBarrier时,会返回一个int类型,也就是屏障的标识,自然移除屏障的时候需要传入这个标识。
至于为什么加入这个机制,大致因为,加入队列中有很多消息,此时用户点击了一下界面,如果没有这个机制,那么响应用户点击的操作需要等待其他事件被分发完后才轮到,容易让用户觉得不流畅,所以系统为了UI相关的消息要优先被分发,在队列中插入同步屏障信号,之后响应点击的消息会被优先处理,处理完成后,再把信号移除,继续执行其他分发。
这个机制如果处理不好,屏障没有正常移除,就会出现进程假死的问题,这也就是官方为何把此方法标记成@UnsupportedAppUsage,不给开发者调用。
*那么这样就可以避免不会出问题吗?*不可能,这只是减少了问题的出现概率,还是有机会出现的,屏障信号是系统在更新UI时发送的,如果我们操作不当,频繁在子线程操作UI,可能某一瞬间,发送了超过两个屏障信号,但是只记录到最后一个token,更新完成后,自然只移除了最后添加的屏障,结果就是之前插入的一直挂在队列中,堵塞主线程所有同步消息,也就引发了同步屏障泄露 的问题,App就直接出现了明明有些控件明明还在刷新,但是怎么点都没反应。
这也是为什么官方只允许我们只能在主线程执行更新UI操作原因之一。

enqueueMessage

分发消息,把用户发的Message插入到队列中。关键源码:

boolean enqueueMessage(Message msg, long when){//target为空为屏障消息,屏障信息只能由系统代码发送,用户端发送的消息target不可能是空//这里就解析了为什么用户不能发送没有target的消息if (msg.target == null) {//    throw new IllegalArgumentException("Message must have a target.");}//......省略synchronized (this) {//......省略//1.把当前的对头赋值给pMessage p = mMessages;boolean needWake;//是否需要唤醒next函数if (p == null || when == 0 || when < p.when) { //2.A 如果对头为空(队列没有任何消息)或者when为0(只有刚开机才会存在)或者msg的时间比队头的时间早//把待分发消息插到对头// New head, wake up the event queue if blocked.   msg.next = p;   //把当前消息的next指向p 尽管p为空mMessages = msg; //把当前消息放到对头needWake = mBlocked;//?} else {   //2.B 队列中已有消息,这时需要把当前消息按时间顺序插到队列中// Inserted within the middle of the queue.  Usually we don't have to wake  // up the event queue unless there is a barrier at the head of the queue  // and the message is the earliest asynchronous message in the queue. //3、是否需要唤醒(正在堵塞、队头是屏障信号、当前消息是异步消息 )needWake = mBlocked && p.target == null && msg.isAsynchronous();   Message prev;   for (;;) {   //4、开始循环队列,和队列中消息when做比较,找出当前消息的when值在队列中最适合的位置prev = p;    //此时p的角色为当前遍历的消息,先赋值给上一条消息,保证每次循环能拿到上一条消息和下一条消息,方便插入p = p.next;     //取下一条消息,赋值到pif (p == null || when < p.when) {  //5、如果下一条消息是空(说明已到达队尾),或者当前消息的时刻比下一条消息的时刻小,说明此时的位置最适合,结束循环break;      }//队头符合步骤3处条件,说明有同步屏障信号,并且当前p是异步消息,根据步骤5,能走到这里说消息分发时机还没到,所以不需要唤醒nextif (needWake && p.isAsynchronous()) {   needWake = false;  }   }    //把当前消息入队,插入到上一条消息和p之间msg.next = p; // invariant: p == prev.next  prev.next = msg;}// We can assume mPtr != 0 because mQuitting is false.if (needWake) {   //唤醒next函数,取 消息 分发nativeWake(mPtr);}}return true;
}

next 取消息

从队列中取出一条可用消息(when时机合适),如果有就返回,没有则堵塞,直到mPtr被唤醒。

@UnsupportedAppUsageMessage next() {//......省略//1、下一次唤醒时间int nextPollTimeoutMillis = 0;for (;;) {//2、开始死循环取消息if (nextPollTimeoutMillis != 0) {Binder.flushPendingCommands();}//3、堵塞,时长为nextPollTimeoutMillis,或者主动调用唤醒函数`nativeWake`nativePollOnce(ptr, nextPollTimeoutMillis);//被唤醒后,开始取消息synchronized (this) {//同步锁,保证线程安全// Try to retrieve the next message.  Return if found.//记下当前系统时间,后面判断消息是否达到时间条件final long now = SystemClock.uptimeMillis();Message prevMsg = null;Message msg = mMessages;if (msg != null && msg.target == null) {//4、关键点,target为空,说明当前队头消息为屏障信号,需要优先处理离信号最近的异步消息// Stalled by a barrier.  Find the next asynchronous message in the queue.do {//一直在队列中寻找离信号最近的异步消息,直到没找到或者到达队尾prevMsg = msg;msg = msg.next;//如果一直找到队尾,msg.next是空,结束循环} while (msg != null && !msg.isAsynchronous());}if (msg != null) {//5、此时,如果有屏障信号的话,步骤4肯定走了,msg不为空肯定是异步消息,否则msg必为空//检查该消息是否以达到分发时机if (now < msg.when) {//当前时间还没达到消息的时机,计算还差多久,赋值给nextPollTimeoutMillis,进入下一次堵塞,直到达到时间nextPollTimeoutMillis,再取该消息// Next message is not ready.  Set a timeout to wake up when it is ready.nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);} else {//已达到取消息时机// Got a message.mBlocked = false;//把消息出队if (prevMsg != null) {//说明此消息在队列里面(同步屏障会优先执行里面的异步消息),要把消息出队,把该消息的上一条消息的next直接指向该消息的下一条消息prevMsg.next = msg.next;} else {//说明此消息是队头,直接把当前消息的next放到队头mMessages = msg.next;}//删除出队消息的next指向msg.next = null;if (DEBUG) Log.v(TAG, "Returning message: " + msg);//标注消息正在使用msg.markInUse();//把该消息返回return msg;}} else {//6、没有符合条件消息,继续下一次循环,取符合条件的消息//如果屏障信号没有被移除,又没有异步消息进入队列,那么next函数将陷入死循环,循环线路 3--》4--》6//导致APP所有同步消息无法被处理,表现为软件部分界面卡死(如文本正常刷新,点击事件无法响应)并且不会引发ANR// No more messages.nextPollTimeoutMillis = -1;}// Process the quit message now that all pending messages have been handled.if (mQuitting) {dispose();return null;}// If first time idle, then get the number of idlers to run.// Idle handles only run if the queue is empty or if the first message// in the queue (possibly a barrier) is due to be handled in the future.if (pendingIdleHandlerCount < 0&& (mMessages == null || now < mMessages.when)) {pendingIdleHandlerCount = mIdleHandlers.size();}if (pendingIdleHandlerCount <= 0) {// No idle handlers to run.  Loop and wait some more.mBlocked = true;continue;}if (mPendingIdleHandlers == null) {mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];}mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);}// Run the idle handlers.// We only ever reach this code block during the first iteration.for (int i = 0; i < pendingIdleHandlerCount; i++) {final IdleHandler idler = mPendingIdleHandlers[i];mPendingIdleHandlers[i] = null; // release the reference to the handlerboolean keep = false;try {keep = idler.queueIdle();} catch (Throwable t) {Log.wtf(TAG, "IdleHandler threw exception", t);}if (!keep) {synchronized (this) {mIdleHandlers.remove(idler);}}}// Reset the idle handler count to 0 so we do not run them again.pendingIdleHandlerCount = 0;// While calling an idle handler, a new message could have been delivered// so go back and look again for a pending message without waiting.nextPollTimeoutMillis = 0;}}

Looper

一个无限循环的类,他内部持有消息队列MessageQueue的引用,当Looper.loop()后,将会一直调用MessageQueue.next函数获取消息,next在没有消息的时候又会堵塞,让出CPU资源,这就是为什么死循环却没有占满CPU的原因。关键源码:


public final class Looper {public static void prepare() {prepare(true);}public static Looper getMainLooper() {synchronized (Looper.class) {return sMainLooper;}}/*** 循环一次的逻辑* Poll and deliver single message, return true if the outer loop should continue.*/@SuppressWarnings("AndroidFrameworkBinderIdentity")private static boolean loopOnce(final Looper me,final long ident, final int thresholdOverride) {//从队列中取一条消息Message msg = me.mQueue.next(); // might blockif (msg == null) {//由于next没消息会堵塞,所以没消息的时候这里不会执行,除非强制退出// No message indicates that the message queue is quitting.return false;}try {//开始分发取到的消息msg.target.dispatchMessage(msg);if (observer != null) {observer.messageDispatched(token, msg);}dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;} catch (Exception exception) {if (observer != null) {observer.dispatchingThrewException(token, msg, exception);}throw exception;} finally {ThreadLocalWorkSource.restore(origWorkSource);if (traceTag != 0) {Trace.traceEnd(traceTag);}}//省略,,,,,,,,,,,,,,,,,,,,,,,// Make sure that during the course of dispatching the// identity of the thread wasn't corrupted.final long newIdent = Binder.clearCallingIdentity();if (ident != newIdent) {Log.wtf(TAG, "Thread identity changed from 0x"+ Long.toHexString(ident) + " to 0x"+ Long.toHexString(newIdent) + " while dispatching to "+ msg.target.getClass().getName() + " "+ msg.callback + " what=" + msg.what);}msg.recycleUnchecked();return true;}/*** 开始无限循环* Run the message queue in this thread. Be sure to call* {@link #quit()} to end the loop.*/@SuppressWarnings("AndroidFrameworkBinderIdentity")public static void loop() {final Looper me = myLooper();if (me == null) {throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");}if (me.mInLoop) {Slog.w(TAG, "Loop again would have the queued messages be executed"+ " before this one completed.");}me.mInLoop = true;// 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();// Allow overriding a threshold with a system prop. e.g.// adb shell 'setprop log.looper.1000.main.slow 1 && stop && start'final int thresholdOverride =SystemProperties.getInt("log.looper."+ Process.myUid() + "."+ Thread.currentThread().getName()+ ".slow", 0);me.mSlowDeliveryDetected = false;for (;;) {//无限循环核心if (!loopOnce(me, ident, thresholdOverride)) {return;}}}
}

Handler

通过Handler,我们可以把消息发送到对应的消息队列中,是用户代码操作消息队列的入口。关键源码:

public class Handler {/*** Use the provided {@link Looper} instead of the default one.** @param looper The looper, must not be null.*/public Handler(@NonNull Looper looper) {this(looper, null, false);}/*** Use the provided {@link Looper} instead of the default one and take a callback* interface in which to handle messages.** @param looper The looper, must not be null.* @param callback The callback interface in which to handle messages, or null.*/public Handler(@NonNull Looper looper, @Nullable Callback callback) {this(looper, callback, false);}
}

上面只列举了几个构造函数,其他一些post消息函数就不一一说明了,主要是Handler创建需要关联一个Looper,发消息的时候又调用了Looper 内部的消息队列的分发消息函数,把消息插入到队列中,完成用户对消息队列的操作。

总结

至此,相信你对消息分发机制也有大概的理解。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/216535.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

CompletableFuture使用小结

为什么需要CompletableFuture CompletableFuture继承了CompletionStage接口和Future接口&#xff0c;在原有Future的基础上增加了异步回调、流式处理以及任务组合&#xff0c;成为JDK8多任务协同场景下一个有效利器。 CompletableFuture使用示例 提交有返回值的异步任务 通…

AI全栈大模型工程师(二十四)常用的超参

文章目录 七、先介绍几个常用的超参7.1、过拟合与欠拟合7.2、学习率调整策略八、自然语言处理常见的网络结构8.1、文本卷积神经网络 TextCNN8.2、循环神经网络 RNN8.3、Attention (for RNN)后记七、先介绍几个常用的超参 7.1、过拟合与欠拟合 奥卡姆剃刀: 两个处于竞争地位的…

6000字告诉你内向的程序员该如何工作比较合适

本文首发于我的个人掘金博客&#xff0c;看到很多人都比较喜欢这篇文章&#xff0c;分享给大家。 个人博客主页&#xff1a;https://www.aijavapro.cn 个人掘金主页&#xff1a;juejin.cn/user/2359988032644541/posts 个人的知识星球: 觉醒的新世界程序员 一、背景 之前写了…

【离散数学】——期末刷题题库( 二元关系)

&#x1f383;个人专栏&#xff1a; &#x1f42c; 算法设计与分析&#xff1a;算法设计与分析_IT闫的博客-CSDN博客 &#x1f433;Java基础&#xff1a;Java基础_IT闫的博客-CSDN博客 &#x1f40b;c语言&#xff1a;c语言_IT闫的博客-CSDN博客 &#x1f41f;MySQL&#xff1a…

在HarmonyOS上使用ArkUI实现计步器应用

介绍 本篇Codelab使用ArkTS语言实现计步器应用&#xff0c;应用主要包括计步传感器、定位服务和后台任务功能&#xff1a; 通过订阅计步器传感器获取计步器数据&#xff0c;处理后显示。通过订阅位置服务获取位置数据&#xff0c;处理后显示。通过服务开发实现后台任务功能。…

什么是Scss

Sass(Syntactically Awesome Style Sheets) ,是一种 css 预处理器和一种语言, 它可以用来定义一套新的语法规则和函数&#xff0c;以加强和提升 CSS. 它有很多很好的特性&#xff0c;但是它有类似 Ruby 的语法&#xff0c;没有花括号&#xff0c;没有分号&#xff0c;遵循严格的…

Ubuntu系统使用快速入门实践(七)——软件安装与使用(6)

Ubuntu系统使用快速入门实践系列文章 下面是Ubuntu系统使用系列文章的总链接&#xff0c;本人发表这个系列的文章链接均收录于此 Ubuntu系统使用快速入门实践系列文章总链接 下面是专栏地址&#xff1a; Ubuntu系统使用快速入门实践系列文章专栏 文章目录 Ubuntu系统使用快速…

外贸获客引流详细教程,小白也能懂!

一、引子 随着全球化的不断发展&#xff0c;国际贸易日益频繁&#xff0c;许多外贸企业为了拓展市场&#xff0c;获取更多客户&#xff0c;都在努力提高营销策略。 然而&#xff0c;对于许多外贸新手来说&#xff0c;如何高效地进行获客引流仍然是一个难题&#xff0c;今天&a…

手动搭建koa+ts项目框架(ts项目实现开发阶段实时查看)

文章目录 前言优化脚本如有启发&#xff0c;可点赞收藏哟~ 前言 上篇文章记录了手动简单搭建 koats项目步骤 虽然可以直接编译后并开启服务&#xff0c;但如果修改./src内的文件&#xff0c;没法实时编译 以下介绍使用其他方法实现实时效果 优化脚本 咱使用以下依赖可实现边写…

机械中常用的一些术语

目录 一、OEMSOP:SOP编写指南 WI(标准作业指导书):标准作业程序 &#xff08;SOP&#xff09;:SOP和WI的区别&#xff1a;一、PFC、FMEA、PCP、WIPPAP、PSW&#xff1a;APQP&#xff1a;BOM&#xff08;Bill of Material&#xff09;物料清单DV&#xff08;设计验证&#xff09…

“产学研用”深度融合,校企合作助力烟花产业数字化发展

为推动烟花行业数字化转型升级&#xff0c;充分发挥科教资源优势&#xff0c;技术成果及创新资源&#xff0c;推动构建产学研用高效协同&#xff0c;加快提升烟花产业创新能力&#xff0c;助力企业在国内外复杂的市场环境下提升发展能力及竞争能力。12月6日&#xff0c;烟花生产…

前端知识(十)———JavaScript 使用URL跳转传递数组对象数据类型的方法

目录 首先了解一下正常传递基本数据类型 JavaScript跳转页面方法 JavaScript路由传递参数 JavaScript路由接收参数传递对象、数组 在前端有的时候会需要用链接进行传递参数&#xff0c;基本数据类型的传递还是比较简单的&#xff0c;但是如果要传递引用数据类型就比较麻烦了…

final,finally,finalize的区别

final&#xff0c;finally&#xff0c;finalize的区别 在Java中&#xff0c;final、finally和finalize是三个不同的关键字&#xff0c;它们具有不同的作用和用法。 1、final&#xff1a; final是一个修饰符&#xff0c;可以用于修饰类、方法和变量。 用于修饰类时&#xff0…

python sort函数

得开个新坑学一下sort的用法了。 a [a, b, c, d] b [0, 1, 2, 3]sorted_list [x for _, x in sorted(zip(b, a))] 参数 sorted(iterable, cmpNone, keyNone, reverseFalse)菜鸟教程的例子&#xff1a; >>>a [5,7,6,3,4,1,2] >>> b sorted(a) #…

如何让弹出的.py文件窗口缩小

原图比例如下&#xff0c;很明显实际大小已经超出屏幕显示范围&#xff1a; 参考这篇文章的代码&#xff0c;新定义一个函数&#xff1a; def get_scaling():user32 windll.user32# 获取现在的尺寸&#xff08;缩放后now_width user32.GetSystemMetrics(0)now_height user…

Day07 Liunx高级系统设计8-线程

概述 进程与线程 进程 : 系统分配资源的基本单位 , 可以简单理解为一个正在进行的程序 线程 : 操作系统调度的最小单位 , 就是一段代码的执行顺序 注意&#xff1a; 1, 一个进程必须要有一个线程 , 该线程被称为主线程 2, 一个进程可以有多个线程 , 除主线程外的其他线程都是…

我的创作三周年纪念日

今天收到CSDN官方的来信&#xff0c;创作三周纪念日到了。 Dear: Hann Yang &#xff0c;有幸再次遇见你&#xff1a; 还记得 2020 年 12 月 12 日吗&#xff1f; 你撰写了第 1 篇技术博客&#xff1a; 《vba程序用7重循环来计算24》 在这平凡的一天&#xff0c;你赋予了它…

MacOS 12 开放指定端口 指定ip访问

MacOS 12 开放指定端口 指定ip访问 在 macOS 上开放一个端口&#xff0c;并指定只能特定的 IP 访问&#xff0c;你可以使用 macOS 内置的 pfctl&#xff08;Packet Filter&#xff09;工具来实现。 以下是一些基本的步骤&#xff1a; 1、 编辑 pf 配置文件&#xff1a; 打开 /…

Android开发之长log的打印

在开发过程中&#xff0c;我们想要打印长的log&#xff0c;这个时候我们就需要做特殊的处理&#xff0c;下面这段代码就是处理的长log的打印&#xff1a; public static void LogLong(String tag, String msg) { //信息太长,分段打印//因为String的length是字符数量不是字节数…

JavaScript中如何向URL添加参数

设url为https://yungot.com/ 求:向url中添加参数 ?page=1&limit=10 变成 https://yungot.com/?page=1&limit=10 解决方案:调用addUrlParams函数 解:addUrlParams({“page”: 1, “limit”: 10}) // url处理 function getRequestParams() {const requestParams = …