首页 > 其他分享 >Handler机制实现原理总结

Handler机制实现原理总结

时间:2023-06-14 13:34:00浏览次数:37  
标签:总结 MessageQueue Handler Looper msg 原理 Message null

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

标签:总结,MessageQueue,Handler,Looper,msg,原理,Message,null
From: https://www.cnblogs.com/VoidCom/p/17479950.html

相关文章

  • 编译原理动手实操,用java实现编译器-算术表达式及其语法解析器的实现
     本节代码下载地址:http://pan.baidu.com/s/1sjWiwPn代码的理解和运行是吃透编译原理的关键,如果我们看的是爱情动作片,自然选择无码的好,但如果看得是计算机课程,则必须有码,无码的计算机理论都是耍流氓。当前,java所实现的简易编译器目的是将一条或一组含有加号和乘号的算术表达式编译......
  • java构建TCP/IP协议:DNS,域名解析协议的基本原理介绍
    从本节开始,我们研究和实现一个体系较为复杂的协议,也就是域名解析协议,简写为DNS。该协议几乎也是我们”日用而不知“的幕后英雄,没有它肯定就没有现在的互联网繁荣。当我们在浏览器上输入网址,例如www.baidu.com时,浏览器先通过DNS协议找到与该网址对应的IP地址,然后再使用IP去向服务器......
  • java开发C语言编译器:JVM 的基本操作指令介绍及其程序运行原理
    更详细的讲解和代码调试演示过程,请参看视频用java开发C语言编译器更详细的讲解和代码调试演示过程,请参看视频如何进入google,算法面试技能全面提升指南如果你对机器学习感兴趣,请参看一下链接:机器学习:神经网络导论更详细的讲解和代码调试演示过程,请参看视频LinuxkernelHacker,......
  • 编译原理动手实操,用java实现一个简易编译器-语法解析
    语法和解析树:举个例子看看,语法解析的过程。句子:“我看到刘德华唱歌”。在计算机里,怎么用程序解析它呢。从语法上看,句子的组成是由主语,动词,和谓语从句组成,主语是“我”,动词是“看见”,谓语从句是”刘德华唱歌“。因此一个句子可以分解成主语+动词+谓语从句:句子-->主语+动词+谓语......
  • 深度学习应用篇-元学习[13]:元学习概念、学习期、工作原理、模型分类等
    #深度学习应用篇-元学习[13]:元学习概念、学习期、工作原理、模型分类等1.元学习概述1.1元学习概念元学习(Meta-Learning)通常被理解为“学会学习(Learning-to-Learn)”,指的是在多个学习阶段改进学习算法的过程。在基础学习过程中,内部(或下层/基础)学习算法解决由数据集和......
  • 电路原理图简明入门
    元件类型符号R:电阻C:电容L:电感D:二极管Q:晶体管X:晶体J:连接器,跳线器U:半导体(集成芯片)大的半导体一般有更具体的名称,如存储芯片,命名RAM0,RAM1。元件常见标记顶部横线:表示低电平有效CS:片选信号三角:边缘触发输入GND:公共端,接地(假设的地,一般是电源的负极,有时候是接地)VCC:C=circuit......
  • 模块知识点总结
    当讲解Python模块时,可以按照以下详细的内容和示例进行讲解:一、什么是模块?A.定义模块:模块是一个包含了Python代码的文件,可以包含变量、函数、类和可执行的代码。模块通过将代码组织成单独的文件,方便重复使用和管理。B.模块的作用和意义:提供了代码的重用性,避免重复编写相......
  • [ARM汇编]计算机原理与数制基础—1.1.4 逻辑运算
    在计算机中,逻辑运算是对二进制数据进行操作的基础。逻辑运算主要包括以下几种:与(AND)、或(OR)、非(NOT)和异或(XOR)。接下来,我们将详细介绍这几种逻辑运算的原理及其应用。与(AND)运算与运算的规则如下:0AND0=00AND1=01AND0=01AND1=1两个二进制数进行与运算时,从最低......
  • Java_memcached-release 安装 原理 实例
     Java_memcached-release安装原理实例  一、了解和使用使用安装memcached在这一块已经有车了,就不再造了。一个日本君写的:长野雅广memcached-全面剖析.pdfheiyeluren(黑夜路人) Memcached-原理和使用详解.pdf  二、javamemcached客启端的调用   2.1下载客户端jar包......
  • 20230612刷题总结
    2023/06/12刷题总结A-DoubleCola如果n在1到5之间先单独判断是谁.如果大于5之后,用一个cnt记录当前这一组由几个人排在一起,然后使用循环每次动态的删除人数,直到找到n在那一组,然后将剩下的n直接整除pow(2,cnt)就可以了.#include<bits/stdc++.h>#defineintlonglong#d......