当前位置: 首页 > news >正文

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

提到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方法会根据执行的时间取消息,如果链表的第一个节点执行时间还未到则会进行阻塞等待,等到执行的时间点到达时取出链表的第一个消息返回。

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

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 【Qt】常用控件QProgressBar
  • C++题解(23) 信息学奥赛一本通:1026:空格分隔输出
  • 初识redis:Zset有序集合
  • ESP RainMaker OTA 自动签名功能的安全启动
  • ssrf漏洞复现
  • 《机器学习》—— 通过下采样方法实现逻辑回归分类问题
  • 基于Java的小区物业管理系统APP的设计与实现(论文+源码)_kaic
  • Lambda 表达式的使用案例
  • openGauss之系统隐藏列
  • Vue路由—进阶篇
  • Spring Boot + MyBatis-Plus 实现 MySQL 主从复制动态数据源切换
  • 鸿蒙HarmonyOS开发:如何使用第三方库,加速应用开发
  • MAML算法详解
  • Oracle RAC 集群启动顺序
  • C语言——位运算
  • 《微软的软件测试之道》成书始末、出版宣告、补充致谢名单及相关信息
  • Android单元测试 - 几个重要问题
  • Codepen 每日精选(2018-3-25)
  • happypack两次报错的问题
  • HTTP中GET与POST的区别 99%的错误认识
  • Java知识点总结(JDBC-连接步骤及CRUD)
  • js操作时间(持续更新)
  • Less 日常用法
  • Python - 闭包Closure
  • SpiderData 2019年2月25日 DApp数据排行榜
  • Vue全家桶实现一个Web App
  • WePY 在小程序性能调优上做出的探究
  • 从地狱到天堂,Node 回调向 async/await 转变
  • 电商搜索引擎的架构设计和性能优化
  • 高程读书笔记 第六章 面向对象程序设计
  • ------- 计算机网络基础
  • 技术攻略】php设计模式(一):简介及创建型模式
  • 验证码识别技术——15分钟带你突破各种复杂不定长验证码
  • Oracle Portal 11g Diagnostics using Remote Diagnostic Agent (RDA) [ID 1059805.
  • Android开发者必备:推荐一款助力开发的开源APP
  • 从如何停掉 Promise 链说起
  • 智能情侣枕Pillow Talk,倾听彼此的心跳
  • #include
  • #NOIP 2014# day.1 生活大爆炸版 石头剪刀布
  • $ is not function   和JQUERY 命名 冲突的解说 Jquer问题 (
  • %3cli%3e连接html页面,html+canvas实现屏幕截取
  • (2)关于RabbitMq 的 Topic Exchange 主题交换机
  • (22)C#传智:复习,多态虚方法抽象类接口,静态类,String与StringBuilder,集合泛型List与Dictionary,文件类,结构与类的区别
  • (70min)字节暑假实习二面(已挂)
  • (aiohttp-asyncio-FFmpeg-Docker-SRS)实现异步摄像头转码服务器
  • (el-Transfer)操作(不使用 ts):Element-plus 中 Select 组件动态设置 options 值需求的解决过程
  • (NO.00004)iOS实现打砖块游戏(九):游戏中小球与反弹棒的碰撞
  • (STM32笔记)九、RCC时钟树与时钟 第二部分
  • (ZT)薛涌:谈贫说富
  • (附源码)计算机毕业设计SSM基于java的云顶博客系统
  • (没学懂,待填坑)【动态规划】数位动态规划
  • (四) 虚拟摄像头vivi体验
  • (一) storm的集群安装与配置
  • (转)大道至简,职场上做人做事做管理
  • (转)我也是一只IT小小鸟