Android 消息机制Handler完全解析(一)

ops/2024/9/25 23:24:48/

提到Handler相信即使你是刚入行的android开发也会用过,可能你会觉得很简单,但是Handler中包含的内容真的不是你理解的那么简单,可能有些工作3-5年的同学也没有对其有很深入的了解。但Handler在android中的地位非常重要,并且几乎是面试必问问题,鉴于此我决定写一个系列全面的讲解Handler的相关知识,相信通过本系列的学习足以应对日常的工作以及面试。

什么?你说我对Handler了解不深?可能有些同学表示不服,那么我们先来几个大厂真实的面试题,如果你都能很清晰的回答,那你可以跳过本系列,说明你对Handler的了解还是比较深入的,不多逼逼先看面试题

  • 1.一个线程中有几个Handler
  • 2.一个线程有几个Looper?如何保证
  • 3.Handler是怎么进行线程间通讯的,原理是什么?
  • 4.Handler的callback存在但返回true,handleMessage是否会执行?
  • 5.既然多个Handler往MessageQueue中添加数据(发消息时各个Handler可能处于不同线程),那它内部是如何确保线程安全的?
  • 6.Handler内存泄漏的原因?
  • 7.Looper死循环为什么不会导致应用卡顿?
  • 8.IdleHandler是什么?怎么使用?能解决什么问题?
  • 9.ThreadLocal的原理,以及在Looper中是如何应用的?
  • 10.请你谈谈消息屏障
  • 11.对epoll机制有了解吗?

。。。。。。。。

上面的问题你都能对答如流吗?相信通过本系列的学习你对Handler定会有更深的认识

1. Handler介绍

Handler是Android消息机制的上层接口,这使得在开发过程中只需要和Handler交互即可。Handler的使用过程很简单,通过它可以轻松地将一个任务切换到Handler所在的线程去执行。很多人认为Handler的作用是更新UI,这的确没错,但是更新UI仅仅是Handler的一个特殊使用场景。具体来说是这样的:有时候需要在子线程中进行耗时的I/O操作,可能是读取文件或者访问网络等,当耗时操作完成以后可能需要在UI上做一些改变,由于Android开发规范的限制,我们并不能在子线程中访问UI控件,否则就会触发异常,这个时候通过Handler就可以将更新UI的操作切换到主线程执行。因此,本质上来说,Handler并不是专门用于更新UI的,它只是常被开发者用来更新UI。

在Android源码中ViewRootImpl中对UI操作做了验证

void checkThread() {if (mThread != Thread.currentThread()) {throw new CalledFromWrongThreadException("Only the original thread that created a view hierarchy can touch its views.");}}

Only the original thread that created a view hierarchy can touch its views.这个异常我相信很多初学者都遇到过,这个报错的意思就是:只有创建视图层次结构的原始线程才能接触其视图,创建视图层次的线程就是主线程,也就是说只有在主线程中才能修改UI。

大家有没有想过为什么不允许在子线程中访问UI呢?这是因为Android的UI控件不是线程安全的,如果在多线程中并发访问可能会导致UI控件处于不可预期状态,那为什么系统不对UI控件的访问加上锁机制呢?缺点有两个

  • 加上锁机制会让UI访问的逻辑变得复杂
  • 锁机制会降低UI访问的效率,因为锁机制会阻塞某些线程的执行。

所以最简单且高效的方法就是采用单线程模型来处理UI操作,对于开发者来说也不是很麻烦,只需要通过Handler切换一下UI访问的执行线程即可。

2. Android消息机制相关的几个对象

Message:消息体

Handler:消息处理器,发送、处理消息

MessageQueue:消息队列

Looper:循环器,整个机制的动力

3.Handler机制源码解析

在此之前先来看下整体的运行流程,以下图片来自享学课堂

在这里插入图片描述

我们一般用Handler的时候一般是使用handler.post或handler.send系列的方法发送一条消息,此时这条消息会被加入到MessageQueue,MessageQueue中的消息随着时间的流逝会被消费掉即调用handler.dispatchMessage方法进行分发,有点类似于生产者和消费者模式,handler.post和handler.send系列的方法发送消息相当于生产者,handler.dispatchMessage相当于消费者,那么接下来我们从handler.post和handler.send开始分别对Handler、MessageQueue、Looper的原理进行讨论之后再把这三者串联起来充分理解Handler消息的机制。

3.1 Handler相关源码解析

我们先从源头开起,即Handler对象的post和send系列方法,有了发送才会有接下来的一系列流程

Handler post系列相关源码

    public final boolean post(@NonNull Runnable r) {return  sendMessageDelayed(getPostMessage(r), 0);}

可以看到调用post方法时会调用Handler的sendMessageDelayed方法

    public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {if (delayMillis < 0) {delayMillis = 0;}return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);}

sendMessageAtTime方法的源码如下

   public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) {MessageQueue queue = mQueue;if (queue == null) {RuntimeException e = new RuntimeException(this + " sendMessageAtTime() called with no mQueue");Log.w("Looper", e.getMessage(), e);return false;}return enqueueMessage(queue, msg, uptimeMillis);}

可以看到调用了sendMessageAtTime方法,sendMessageAtTime方法里具体做了什么我们先不管。

send相关的方法我们就看一个sendMessage

    public final boolean sendMessage(@NonNull Message msg) {return sendMessageDelayed(msg, 0);}

可以看到它也是调用了sendMessageDelayed方法最终调用sendMessageAtTime方法。

总结:无论我们调用handler的send系列的相关方法(sendMessage、)还是调用post系列的相关方法,最终都会调用到sendMessageAtTime方法

我们来看下Handler中的sendMessageAtTime的源码

public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) {MessageQueue queue = mQueue;if (queue == null) {RuntimeException e = new RuntimeException(this + " sendMessageAtTime() called with no mQueue");Log.w("Looper", e.getMessage(), e);return false;}return enqueueMessage(queue, msg, uptimeMillis);}

然后看下Handler中的enqueueMessage方法

private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,long uptimeMillis) {msg.target = this;msg.workSourceUid = ThreadLocalWorkSource.getUid();if (mAsynchronous) {msg.setAsynchronous(true);}return queue.enqueueMessage(msg, uptimeMillis);}

可以发现这里msg.target=this即把当前的handler对象赋值给msg的target,最终会调用MessageQueue的enqueueMessage方法。

在上述sendMessageAtTime这个方法里有个mQueue,这个mQueue是哪里来的呢?看Handler的构造函数可以看到,当我们new Handler的时候构造函数传递的参数可以分为两种(废弃的就不看了):一种带Looper,一种不带Looper

在这里插入图片描述

当参数中有Looper时,其实最终会调用到如下方法(注意这个方法是隐藏的)

    @UnsupportedAppUsagepublic Handler(@NonNull Looper looper, @Nullable Callback callback, boolean async) {mLooper = looper;mQueue = looper.mQueue;mCallback = callback;mAsynchronous = async;}

也就是说这个mQueue其实是Looper中的MessageQueue对象,如果不传Looper对象最终会进入到如下方法(此方法也是隐藏的)

    @hidepublic Handler(@Nullable Callback callback, boolean async) {if (FIND_POTENTIAL_LEAKS) {final Class<? extends Handler> klass = getClass();if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&(klass.getModifiers() & Modifier.STATIC) == 0) {Log.w(TAG, "The following Handler class should be static or leaks might occur: " +klass.getCanonicalName());}}mLooper = Looper.myLooper();if (mLooper == null) {throw new RuntimeException("Can't create handler inside thread " + Thread.currentThread()+ " that has not called Looper.prepare()");}mQueue = mLooper.mQueue;mCallback = callback;mAsynchronous = async;}
    public static @Nullable Looper myLooper() {return sThreadLocal.get();}

从构造函数中可以看出,这里首先会调用Looper.myLooper()方法获取到Looper对象,然后再将mLooper的MessageQueue对象赋值给Handler的MessageQueue对象

总结:无论我们通过Handler发送的何种消息最终都会调用sendMessageAtTime方法,并最终调用MessageQueue中的enqueueMessage方法

3.2 MessageQueue源码

接下来我们看看MessageQueue的方法的源码,上述我们看到handler发送的消息会走到MessageQueue中的enqueueMessage方法,首先来看下这个方法的源码

 boolean enqueueMessage(Message msg, long when) {。。。。。。省略部分代码msg.markInUse();msg.when = when;Message p = mMessages;boolean needWake;if (p == null || when == 0 || when < p.when) {// New head, wake up the event queue if blocked.msg.next = p;mMessages = msg;needWake = mBlocked;} else {// 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.needWake = mBlocked && p.target == null && msg.isAsynchronous();Message prev;for (;;) {prev = p;p = p.next;if (p == null || when < p.when) {break;}if (needWake && p.isAsynchronous()) {needWake = false;}}msg.next = p; // invariant: p == prev.nextprev.next = msg;}// We can assume mPtr != 0 because mQuitting is false.if (needWake) {nativeWake(mPtr);}}return true;}

从这里我们看出MessageQueue其实不是一个队列,它是一个单链表,为什么用队列不行而用单链表呢?分析完这个方法你就会明白,我们拆开来看

(1) 先看if语句

if (p == null || when == 0 || when < p.when) {// New head, wake up the event queue if blocked.msg.next = p;mMessages = msg;needWake = mBlocked;
} 

这里其实就是把最新的消息插入到链表的表头,先看第一个条件因为

Message p = mMessages;

  • 如果p==null,说明当前单链表中没有元素,此时的结构如下

在这里插入图片描述

执行语句之后的格式如下

在这里插入图片描述

即新的消息的next指向null,并把此消息赋值给mMessages

也就是说单链表中插入了一条最新的数据此时最新的数据指向null

  • 如果when==0 说明要插入的消息delay的时间是0,此时肯定为第一个要执行的消息所以也要放到单链表的表头,它的数据结构的形式的变化可能如下

    在这里插入图片描述

    这也不难理解,因为我消息的延时为0所以肯定要排到最前面。

  • 如果when < p.when,说明要插入的消息的执行的时间点比较早,所以要插入到mMessages这个消息的前面,图跟上面这个差不多,我就不再画了

(2)接着看下else分支的代码

else分支代码如下,不要想着每一行代码都要搞懂,这一点很很很重要,我们看源码就是要了解核心流程,如果太钻牛角尖,看源码会很浪费时间,我们只看主流程的代码

else {// 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.needWake = mBlocked && p.target == null && msg.isAsynchronous();Message prev;for (;;) {prev = p;p = p.next;if (p == null || when < p.when) {break;}if (needWake && p.isAsynchronous()) {needWake = false;}}msg.next = p; // invariant: p == prev.nextprev.next = msg;}// We can assume mPtr != 0 because mQuitting is false.if (needWake) {nativeWake(mPtr);}
}

这个是一个典型的单链表的插入操作,我们来看下它具体的执行流程

首选定义一个Message prev,代表链表的前驱节点

接着开启一个for循环,for循环里的各条件都是空的说明这个循环会一直执行,直到它被打断(比如break return等),

首先执行prev = p,这里的prev就代表要插入节点的前驱节点,p最开始的时候代表链表的第一个节点

然后执行p = p.next,让p指向它的下一个节点,接着重点来了,开启了一个if判断

if (p == null || when < p.when) {break;
}

这个语句的作用是什么呢?我们知道Handler发送消息时可以设置延时消息,正常情况下具体的消息的执行顺序是按照时间进行排序的(暂不考虑消息屏障)这个if语句的意思就是根据将要插入的消息A的执行时间跟链表中的数据挨个比较,直到找到晚于A执行的第一条消息B,说明消息A应该插入到消息B的前面,此时就找到了A要插入的位置,然后break退出for循环。接下来就是将消息A插入到消息B前面的过程,这也是典型的单链表的插入。如果有点看不明白,我画个图你就清楚了,假如目前的MessageQueue的情况如下
在这里插入图片描述

此时要插入一条如下的消息

在这里插入图片描述

首先第一步找到新消息要插入的位置,首先执行prev = p ,p = p.next执行之后如下

在这里插入图片描述

然后判断p== null || when < p.when

因为新消息的when为1500,而p.when为1008,这里的when可以理解为什么时候执行,可以这么理解新消息要在1500这个时刻执行,而p指向的这消息需要在1008这个时刻执行,即p指向的消息要先执行所以新插入消息的位置还要继续往后找,执行下一轮循环

在这里插入图片描述

if (p == null || when < p.when) {break;
}

此时在执行这个语句时会发现新消息的执行时刻(1500)小于P指向的消息的执行时刻(2000),满足if条件找到了新消息的插入位置,此时break退出for循环执行如下语句

msg.next = p; 
prev.next = msg;

即将新消息插入过程如下图所示

在这里插入图片描述

此时就按照执行的时间顺序将新的消息插入到了单链表中。
在开始讨论MessageQueue我问了一个问题为什么MessageQueue的数据结构不用队列而用单链表呢?
这里答案应该很明显:
(1)队列不能满足业务需要,因为涉及到数据的插入操作而队列只能先进先出,每次只能将新的数据放到队列末尾,它不支持将数据插入到某个位置这种操作

(2)单链表的插入操作效率很高时间复杂度为O(1),只要找到合适的插入位置就能迅速将心的消息插入到链表中。

有插入消息的方法就有与之对应的取消息方法,MessageQueue除了enqueueMessage方法之外存储方法,还有一个取消息的方法next(),我们来看下它的源码

为了看起来更加方便我删除了一些无用的代码

 @UnsupportedAppUsageMessage next() {。。。。。。。for (;;) {if (nextPollTimeoutMillis != 0) {Binder.flushPendingCommands();}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) {// Stalled by a barrier.  Find the next asynchronous message in the queue.do {prevMsg = msg;msg = msg.next;} while (msg != null && !msg.isAsynchronous());}if (msg != null) {if (now < msg.when) {// 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) {prevMsg.next = msg.next;} else {mMessages = msg.next;}msg.next = null;return msg;}} else {// No more messages.nextPollTimeoutMillis = -1;}。。。。。。。。。}}

首先可以看到调用next()方法会开启一个死循环for(;;)然后会去取一个消息,先看if

if (msg != null && msg.target == null) {// Stalled by a barrier.  Find the next asynchronous message in the queue.do {prevMsg = msg;msg = msg.next;} while (msg != null && !msg.isAsynchronous());
}

这里其实是一个消息屏障如果有遇到消息屏障会一直返回插入消息屏障的异步消息,这个后面专门讲。

后面还有个if语句

if (msg != null) {if (now < msg.when) {// 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) {prevMsg.next = msg.next;} else {mMessages = msg.next;}msg.next = null;return msg;}
} 

进入到这个if语句说明msg != null && msg.target != null,先看

if (now < msg.when) {// 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);
}

此时会把MessageQueue中最前面的那个Message.when与当前时刻做比较如果now<msg.when说明链表表头的那个消息还未到执行时间

在这里插入图片描述

也就是说这里msg1还未到执行的时间,此时需要设置一个阻塞的时间,msg.when - now得到的值就是单链表中表头的那个Message再过多久会执行。举个例子

当前时间是16:13:15,单链表表头的元素的when=16:13:30,循环到此之后发现此消息的执行时刻还没到,所以要等待,等多久呢?16:13:30 - 16:13:15 = 15s即等15s后处理此消息。

else {// Got a message.mBlocked = false;if (prevMsg != null) {prevMsg.next = msg.next;} else {mMessages = msg.next;}msg.next = null;return msg;
}

这里分两种情况一种是prevMsg != null 一种是prevMsg == null, prevMsg等于null很好理解因为mMessages是指向表头的,mMessages = msg.next这个就是单链表的删除操作把当前节点删除

在这里插入图片描述

这里其实是就是把当前链表中最先执行的消息也就是(单链表表头的那个消息)取出并返回,prevMsg!=null的情况跟消息屏障有关,后面再详细看

总结:MessageQueue的底层实现是一个单链表,主要包含两个操作
(1)插入 会把新消息按照时间顺序(p.when)插入到单链表中

(2)取消息 因为MessageQueue是按照时间顺序排序的,表头的消息是最先执行的,所以每次取MessageQueue的第一个消息进行执行(屏障消息除外)

总结一下:本篇主要讲解了Handler和Message的相关源码,Handler主要用来发送消息主要有post和send系列的相关方法,无论采用哪种形式最终都会调用sendMessageAtTime,在sendMessageAtTime中会调用MessageQueue的enqueueMessage方法此方法会按照执行的时间的顺序将Message进行排序,MessageQueue的next方法会根据执行的时间取消息,如果链表的第一个节点执行时间还未到则会进行阻塞等待,等到执行的时间点到达时取出链表的第一个消息返回。

篇幅原因,这篇就先写到这里吧,后面会详细讲解每一个知识点确保满足工作和面试的需要,文中有错误欢迎留言讨论我会在第一时间改正。锁定本台下节更精彩。


http://www.ppmy.cn/ops/102351.html

相关文章

Aiseesoft Data Recovery for Mac:专业级数据恢复解决方案

在数字时代&#xff0c;数据的安全与恢复成为了我们不可忽视的重要议题。对于Mac用户而言&#xff0c;Aiseesoft Data Recovery无疑是一款值得信赖的专业级数据恢复软件。它以其强大的恢复能力、简洁的操作界面以及广泛的兼容性&#xff0c;在众多数据恢复工具中脱颖而出&#…

大模型企业应用落地系列五》基于大模型的对话式推荐系统》大模型管理层

注&#xff1a;此文章内容均节选自充电了么创始人&#xff0c;CEO兼CTO陈敬雷老师的新书《自然语言处理原理与实战》&#xff08;人工智能科学与技术丛书&#xff09;【陈敬雷编著】【清华大学出版社】 文章目录 大模型企业应用落地系列五基于大模型的对话式推荐系统》大模型管…

★ 算法OJ题 ★ 力扣1089 - 复写零

Ciallo&#xff5e;(∠・ω< )⌒☆ ~ 今天&#xff0c;我将和大家一起做一道双指针算法题--复写零~ 目录 一 题目 二 算法解析 2.1 算法思路 2.2 算法流程 三 编写算法 一 题目 1089. 复写零 - 力扣&#xff08;LeetCode&#xff09; 二 算法解析 2.1 算法思路 …

行为型设计模式-访问者(visitor)模式

设计模式汇总&#xff1a;查看 通俗示例 想象一下你正在开发一个动物园管理系统。在动物园里&#xff0c;有多种动物&#xff0c;如狮子、老虎、长颈鹿等&#xff0c;每种动物都有不同的行为&#xff0c;比如吼叫、吃东西和移动。如果你想要为每种动物添加新的行为&#xff0c…

内存管理篇-14kmalloc机制实现分析

引入这个kmalloc的目的&#xff0c;是因为前面的slab接口太过于复杂&#xff0c;因此需要一个全新的封装kmalloc接口&#xff0c;内存申请编程接口实现。kmalloc底层起始也是基于slab缓存实现的。 1.kmalloc 调用流程 参数解析: 解析 gfp_mask 参数&#xff0c;确定分配时是否…

android交叉编译报错no input files的解决方法

问题描述 安装NDK后&#xff0c;make报错"clang-18: error: no input files"&#xff0c;即使直接使用clang命令&#xff08;例如clang -c test.c&#xff09;仍然报错。 开发环境 操作系统&#xff1a;win11 虚拟机&#xff1a;WSL ubuntu22.04 NDK版本&#x…

Visio po解版的详细介绍

一、Visio简介 Visio是一款流程图、组织结构图、地平图、工程图等各类专业图表的制作软件。自问世以来&#xff0c;凭借其友好的用户界面、丰富的图形库和强大的编辑功能&#xff0c;已成为行业内使用最广泛的图形设计软件之一。无论是初学者还是专业人士&#xff0c;都能在Vi…

[易聊]软件项目测试报告

一、项目背景 随着互联网发展&#xff0c;各种各样的软件&#xff0c;比如游戏、短视频、购物软件中都有好友聊天功能&#xff0c;这是一个可在浏览器中与好友进行实时聊天的网页程序。“ 易聊 ”相对于一般的聊天软件&#xff0c;可以让用户免安装、随时随地的通过浏览器网页…