Handler一般用于线程间通信,如常用的子线程使用handler让主线程更新UI。那么这是怎么实现的呢?

我们先把这个大问题分解成多个小问题:

  1. post();postDelayed();sendMessage();sendEmptyMessage();等方法有什么不同?
  2. Handler为什么需要一个Looper,为什么它不能为空?
  3. Handler为什么可以做到线程间通信?
  4. postDelayed()为什么可以让线程延迟执行?

接下来带着这些疑惑去寻找答案。

post();postDelayed();sendMessage();sendEmptyMessage();等方法有什么不同?

它们最终都是调用同一个方法:sendMessageAtTime(),只是参数不同,Handler帮我们进行了一下封装。

先看post();postDelayed();这两个方法,查看源码可以发现,这两个方法都是调用sendMessageDelayed(Message, long)。只是post()的时间这个参数是0。

代码如下:

public final boolean post(@NonNull Runnable r) {
return sendMessageDelayed(getPostMessage(r), 0);
}
public final boolean postDelayed(@NonNull Runnable r, long delayMillis) {
return sendMessageDelayed(getPostMessage(r), delayMillis);
}
private static Message getPostMessage(Runnable r) {
Message m = Message.obtain();
m.callback = r;
return m;
}

关键是将Runable任务封装成Message的这个getPostMessage()

这里并不是简单地将Runable封装成Message,这里还有一个Message回收池机制的实现。将在下文展开介绍。

再看sendMessage();sendEmptyMessage();这两个方法:

public final boolean sendMessage(@NonNull Message msg) {
return sendMessageDelayed(msg, 0);
}
public final boolean sendEmptyMessage(int what){
return sendEmptyMessageDelayed(what, 0);
}
public final boolean sendEmptyMessageDelayed(int what, long delayMillis) {
Message msg = Message.obtain();
msg.what = what;
return sendMessageDelayed(msg, delayMillis);
}

可以看到这四个方法最终都是调用sendMessageDelayed():

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

上面sendMessageDelayed()的实现都很简单,但需要注意的是这里使用了SystemClock.uptimeMillis(),它返回的是设备的开机时间(不包括息屏睡眠时间),这个时间和Handler的消息可以延迟触发有关。将在后面详细介绍。

Handler为什么需要一个Looper,为什么它不能为空?

因为MessageQueue是通过Looper获取到的。

而Message需要通过MessageQueue来等待执行。

在创建Handler时如果检测到Looper为空,将会抛出NullPointerException错误

如果不是通过构造函数传入的Looper,比如Handler#Callback,构造方法会通过Looper.myLooper()获取到当前线程的Looper。

Looper.myLooper()可以获取到当前线程的Looper是因为ThreadLocal的特性。

代码如下:

public Handler(@Nullable Callback callback, boolean async) {
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;
}

Handler为什么可以做到线程间通信?

Handler最长使用的大概是子线程通知UI线程更新UI吧。

我们通过post();sendMessage();等方法提交一个Message时,这个Message会被放入MessageQueue。在创建Handler时会得到一个Looper,Looper会循环从MessageQueue取出Message处理,而每个Looper属于一个线程,如果该Looper是UI线程的,那Message就是在UI线程处理。

知其然亦应知其所以然。

我们从Handler#post()这个方法开始研究。

在上面已经讲过,post()最终调用的是sendMessageAtTime(),这个方法首先是获取了与该Handler绑定Looper的MessageQueue对象,然后通过一些参数设置,最后执行MessageQueue#enqueueMessage()方法。

MessageQueue#enqueueMessage()

看方法名就知道这个方法主要工作是对Message排队处理:

boolean enqueueMessage(Message msg, long when) {
//...
synchronized (this) {
if (msg.isInUse()) {
throw new IllegalStateException(msg + " This message is already in use.");
}
//...
msg.markInUse();
msg.when = when;
Message p = mMessages;
boolean needWake;
if (p == null || when == 0 || when < p.when) {
msg.next = p;
mMessages = msg;
needWake = mBlocked;
} else {
Message prev;
for (;;) {
prev = p;
p = p.next;
if (p == null || when < p.when) {
break;
}
//...
}
msg.next = p; // invariant: p == prev.next
prev.next = msg;
}
//...
}
return true;
}

我去掉了一些和排队无关的代码,上面这段代码并不难,就是以链表的形式将Message进行排列。

首先判断这个Message是否正在被使用。

Message什么情况下是被使用状态呢?

其实这个也和Message回收池有些关系。

我们new的对象和回收池中取出的Message默认状态是0,当Message进入MessageQueue等待处理时就是被使用状态。从MessageQueue取出,被处理完成回收的Message其状态又会被重置为0 。

回到Message排队问题,然后判断三个条件:

  1. Message链表是否为空
  2. when == 0
    1. 这种情况只有调用Handler#sendMessageAtFrontOfQueue()才会出现
  3. when < p.when
    1. when表示消息将在什么时候执行,数字越小的排在前面

当其中一个条件满足时,将当前Message插入链表头部。

既然Message时怎么放入MessageQueue这块已经弄清楚了,那接着看一下Looper。看看是怎么取出Messages,又是怎么处理的

Looper

如果使用过Looper就应该知道,这是一个循环。

查看这个类的注释,可以看到它提供了一个简单的用法。

Looper.prepare();
Looper.loop();

主要有两个方法:

  1. 第一个在当前线程创建Looper对象并放入ThreadLocal中,
  2. 第二个循环MessageQueue,取出其中的Message在当前线程处理

Looper实现在这里不进行深入,只讲一下和MessageQueue有关的。

先看loop():

//Looper
public static void loop() {
final Looper me = myLooper();
//...
for (;;) {
if (!loopOnce(me, ident, thresholdOverride)) {
return;
}
}
}
private static boolean loopOnce(final Looper me,
final long ident, final int thresholdOverride) {
Message msg = me.mQueue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return false;
}
//。。。
msg.target.dispatchMessage(msg);
//。。。
msg.recycleUnchecked();
return true;
} //Handler
public void dispatchMessage(@NonNull Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}

可以看到loop()里面用for写了一个死循环,它执行了loopOnce(),它是真正取出Message并执行的方法。

dispatchMessage()方法内,首先判断callback是否为空,它是Message的Runable,就是我们使用post();postDelayed();提交到Runable。

然后判断Handler#Callback,是否为空,这个是在Handler构造方法传入的Callback,这里也解释了当我们实现了callback时可以跨线程通信的原因。

next()返回null会结束for循环。我们Android主线程没有消息为什么还可以继续运行?

我们创建Looper对象都是通过其静态方法来创建的,而Looper的构造方法有一个参数quitAllowed,这个参数为True时MessageQueue不会因为消息为空而退出。

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

postDelayed()为什么可以让线程延迟执行?

让我们回到上面Looper#loopOnce()这个方法,我在上面没有介绍怎么从MessageQueue取出Message就是留给这个问题的。

线上关键代码:

//MessageQueue
Message next() {
for (;;) {
synchronized (this) {
//获取当前的开机时间
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;
if (msg != null) {
//判断当前开机时间是否小于msg的开机时间
//如果为false表示这条消息应该被拿出去处理了
if (now < msg.when) {
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
// Got a message.
if (prevMsg != null) {
prevMsg.next = msg.next;
} else {
mMessages = msg.next;
}
msg.next = null;
if (DEBUG) Log.v(TAG, "Returning message: " + msg);
msg.markInUse();
return msg;
}
}
//。。。
}
}
}

这块代码还是很多的,我去掉了和该问题无关的代码。

这里先获取了当前开机时间,然后和Message的when对比,如果当前开机时间比when大,表示这条消息到了处理时间,直接return。

我们使用postDelayed(Runnable,0)时,执行到sendMessageDelayed()后会加上SystemClock.uptimeMillis()最后变成when值,也就是SystemClock.uptimeMillis()+0

因此MessageQueue取出消息时,该Message会被立即执行,而延迟xxx时间是一样的道理。

Message回收池是怎么回事?

让我们回到上面展示的Looper#loopOnce()这个方法,可以看到当消息被取出来处理后,调用了msg.recycleUnchecked();回收当前Message:

void recycleUnchecked() {
// Mark the message as in use while it remains in the recycled object pool.
// Clear out all other details. flags = FLAG_IN_USE;
what = 0;
arg1 = 0;
arg2 = 0;
obj = null;
replyTo = null;
sendingUid = UID_NONE;
workSourceUid = UID_NONE;
when = 0;
target = null;
callback = null;
data = null; synchronized (sPoolSync) {
if (sPoolSize < MAX_POOL_SIZE) {
next = sPool;
sPool = this;
sPoolSize++;
}
}
}

这个方法会重置Message参数,然后判断当前回收池有没有达到上限,上限是50个,没有达到会把这个Message插入链表等待再次使用。

public static Message obtain() {
synchronized (sPoolSync) {
if (sPool != null) {
Message m = sPool;
sPool = m.next;
m.next = null;
m.flags = 0; // clear in-use flag
sPoolSize--;
return m;
}
}
return new Message();
}

Message.obtain()会检查回收池,如果回收池不为空,从链表头部取出一个对象并返回。

在Google官方文档也能看到,Google建议我们通过Message.obtain()获取一个新的Message对象,而不是直接new。

总结

handler的这个机制是由几个类一起协作共同实现的。它们分别是:

  1. Handler

    • Handler:负责协调各个类的工作,以达到这个机制的功能。
  2. Looper
    • Looper:一个Looper对象属于一个线程,由它来管理此线程里的MessageQueue(消息队列)
  3. MessageQueue
    • MessageQueue:消息队列,负责管理Message,以及延迟Message处理
  4. Message
    • Message:用于存放需要发送的数据,将数据包装为消息对象。管理回收池等。

扩展知识

  1. MessageQueue

Handler机制实现原理总结的更多相关文章

  1. 为什么要有handler机制?handler机制的原理

    为什么要有handler机制? 在Android的UI开发中,我们经常会使用Handler来控制主UI程序的界面变化.有关Handler的作用,我们总结为:与其他线程协同工作,接收其他线程的消息并通过 ...

  2. Android 为什么要有handler机制?handler机制的原理

    为什么要有handler机制? 在Android的UI开发中,我们经常会使用Handler来控制主UI程序的界面变化.有关Handler的作用,我们总结为:与其他线程协同工作,接收其他线程的消息并通过 ...

  3. Android 异步通信:图文详解Handler机制工作原理

    前言 在Android开发的多线程应用场景中,Handler机制十分常用 今天,我将图文详解 Handler机制 的工作原理,希望你们会喜欢 目录 1. 定义 一套 Android 消息传递机制 2. ...

  4. 【转载】Android 的 Handler 机制实现原理分析

    handler在安卓开发中是必须掌握的技术,但是很多人都是停留在使用阶段.使用起来很简单,就两个步骤,在主线程重写handler的handleMessage( )方法,在工作线程发送消息.但是,有没有 ...

  5. handler机制的原理(转)

      Handler概述   andriod提供了Handler 和 Looper 来满足线程间的通信.Handler先进先出原则.Looper类用来管理特定线程内对象之间的消息交换(MessageEx ...

  6. handler机制的原理

    andriod提供了Handler 和 Looper 来满足线程间的通信.Handler先进先出原则.Looper类用来管理特定线程内对象之间的消息交换(MessageExchange). 1)Loo ...

  7. [转]Android中handler机制的原理

    Andriod提供了Handler 和 Looper 来满足线程间的通信.Handler先进先出原则.Looper类用来管理特定线程内对象之间的消息交换(MessageExchange). 1)Loo ...

  8. Handler系列之原理分析

    上一节我们讲解了Handler的基本使用方法,也是平时大家用到的最多的使用方式.那么本节让我们来学习一下Handler的工作原理吧!!! 我们知道Android中我们只能在ui线程(主线程)更新ui信 ...

  9. Android的Handler机制

    Handler机制的原理 Android 的 Handler 机制(也有人叫消息机制)目的是为了跨线程通信,也就是多线程通信.之所以需 要跨线程通信是因为在 Android 中主线程通常只负责 UI ...

  10. Handler机制原理图、源码、使用!!!!!

    android的消息处理机制——Looper,Handler,Message  (原理图.源码) 转自:http://my.oschina.net/u/1391648/blog/282892 在开始讨 ...

随机推荐

  1. jq-ajax-get

    get有三个参数,第一个时候url,第二个是数据,第三个是回调函数(可以用返回值表示,如下所示) 执行正确时,返回的依次是res,type,xhr. 执行错误连接不上的的依次是xhr,type,res ...

  2. 四月二十八号Java基础知识

    1.由于Thread类位于java.lang包中,因而程序的开头不用import导入任何包就可直接使用try{ sleep((int)(1000*Math.random()));//sleep()方法 ...

  3. 【FAQ】关于华为推送服务因营销消息频次管控导致服务通讯类消息下发失败的解决方案

    一. 问题描述 使用华为推送服务下发IM消息时,下发消息请求成功且code码为80000000,但是手机总是收不到消息: 在华为推送自助分析(Beta)平台查看发现,消息发送触发了频控. 二. 问题原 ...

  4. AI降临,前端启用面壁计划

    作者:京东零售 郑炳懿 开篇: "在我们有生之年,你觉得会看到AI兵临城下的那一天吗?就像电影黑客帝国里面演的一样",Barry从红色的烟盒里取出一根烟发问道. "不可能 ...

  5. 《HelloGitHub》第 85 期

    兴趣是最好的老师,HelloGitHub 让你对编程感兴趣! 简介 HelloGitHub 分享 GitHub 上有趣.入门级的开源项目. https://github.com/521xueweiha ...

  6. [Pytorch框架] 5.3 Fashion MNIST进行分类

    文章目录 5.3 Fashion MNIST进行分类 Fashion MNIST 介绍 数据集介绍 分类 格式 数据提交 数据加载 创建网络 损失函数 优化器 开始训练 训练后操作 可视化损失函数 保 ...

  7. 【11个适合毕设的Python可视化大屏】用pyecharts开发拖拽式可视化数据大屏

    你好,我是@马哥python说,一枚10年程序猿. 一.效果演示 以下是我近期用Python开发的原创可视化数据分析大屏,非常适合毕设用,下面逐一展示:(以下是截图,实际上有动态交互效果哦) 以下大屏 ...

  8. 2020-03-02:在无序数组中,如何求第K小的数?

    2020-03-02:在无序数组中,如何求第K小的数? 福哥答案2021-03-02: 1.堆排序.时间复杂度:O(N*lgK).有代码. 2.单边快排.时间复杂度:O(N).有代码. 3.bfprt ...

  9. GRPC与 ProtoBuf 的理解与总结

    转载请注明出处: 1.GRPC 官网:https://www.grpc.io/ gRPC 官方文档中文版:http://doc.oschina.net/grpc RPC 框架的目标就是让远程服务调用更 ...

  10. 一个线上全文索引BUG的排查:关于类阿拉件数字的分词与检索

    说到全文检索的分词,多半讲到的是中(日韩)文分词,少有英文等拉丁文系语言,因为英语单词天然就是分词的. 但更少讲到阿拉伯数字.比如金额,手机号码,座机号码等等. 以下不是传统的从0开始针对mysql全 ...