Android 消息机制:Handler、Looper、Message源码 详细版解析 ------从入门到升天

时间:2021-01-13 05:37:11

简介

首先复习一下大多数人都知道的一个流程

Android 消息机制:Handler、Looper、Message源码 详细版解析 ------从入门到升天

最外框是一个主线程,它在运行的时候,内部已初始化一个轮循器Looper,而Looper类中有消息队列MessageQueue,接着会调用loop()方法不断去读取消息Message,该方法是一个死循环,读取消息Message后会将消息交给其对应的handler(只要Message存在,handler也存在)。最后对应的handler会调用handlerMessage()方法。

如此一来,子线程想要与主线程进行交互就易如反掌,比如说左边的子线程做完耗时操作,需要更新UI,它就可以调handler,使用sendMessage(message)方法去发一个消息Message。发送的消息Message就存储到消息队列MessageQueue中。此时轮循器Looper会读取到该信息,取出来交给handler去调用handlerMessage()方法处理。(Looper在主线程中初始化,在主线程调loop()loop()中再调用handlerMessage(),所以handlerMessage()肯定是在主线程中执行!)

这就是子线程与主线程交互的原理子线程消息队列MessageQueue中存入一个消息Message,由轮循器Looper不断从MessageQueue读取消息Message,交给handler,此时就在主线程了。


以上只是简单概述流程,关于Handler消息机制的大致流程,大家都不陌生,但是考虑几个底层的问题

1. 消息池是什么?是如何进行维护的?
2. Looper轮循器是如何创建的?它与线程的关系?
3. 消息队列MessageQueue是如何创建的?与Looper的联系?
4. 程序的主线程到底是如何运行的?在不对屏幕进行任何操作的情况下,主线程会停止吗?
5.发送消息Message,插入消息队列MessageQueue中,插入方法是如何?
6. 主线程睡眠唤醒是如何触发的?底层原理?
7. 每次创建Handler时,需要重写handleMessage()方法,为何?底层实现?

若能将以上问题详细答出来,自认为对这个Handler源码部分算是了解了,若有不足欢迎讨论,下面开始从源代码分析:






消息Message的创建(消息池)

1. Message的创建

首先查看Handler类,第一步获取消息Message,注意!这里的Message不是new出来的,而是调用Handler类的obtainMessage方法获取的。因此,来查找HandlerobtainMessage方法,可以发现,obtainMessage方法重载有好几个(举例一个):

public final Message obtainMessage(int what, int arg1, int arg2, Object obj){
return Message.obtain(this,what,arg1,arg2,obj);
}

重载方法的参数不同,但是方法体中调用的方法相同 —— Message类的obtain() 方法,所以说消息的创建还是由Message类,Handler类并不能直接new出消息Message查看该方法具体实现:(同样,Message类的该方法由于参数不同也有重载,但是相同的是方法体中都调用了无参的obtain()方法,查看:

Message.java

public static Message obtain(){
synchronized(mPoolSync){
if(mPool != null){
Message m = mPool;
mPool = m.next;
m.next = null;
return m;
}
}
return new Message();
}

if判断如果消息池不为空的话,将消息池中的第一条消息Message赋值给m,返回出去,此时消息池中的第二条消息就为第一条了。若消息池为空,则new一个Message返回出去。来讲解消息池原理:

2. 消息池原理

Android 消息机制:Handler、Looper、Message源码 详细版解析 ------从入门到升天

提到“消息池”这个概念首先想到的是一个消息Message集合维护,可是if方法体中第一行代码将mPool赋值给一个Message对象,所以mPool就是“消息池”里面的第一条消息!既然不是用集合维护,一个Message对象mPool如何保存下一条消息?

单链表的形式进行维护!以上图示部分一个框代表一个对象,在Message消息对象中,有一个属性是next(也是Message类型),指向消息池中的下一个消息对象。其实单链表就是通过next来维护!一个框代表一个对象,那么此对象名就是next。所以消息池看似只有mPool一个消息对象,实则是它已经维护了一个单链表

而以上几行代码的作用就是:把消息池里的第一条数据取出来,然后把第二条消息设置为第一条!首先
Message m = mPool;
在栈空间声明了变量m,将mPool赋值给m,我们知道一个变量是无法保存一个对象!所以这里变量m保存的是对象的首地址(一个对象在内存中占有一部分空间,只需要拿到首地址,即可访问该对象剩余地址)。赋值后代表m指向了消息池的第一条消息
mPool = m.next;
m.next是消息池中第二条消息,赋值给mPool,而mPool是维护消息池的,代表此时消息池中第二条消息升为了第一条。
m.next = null;
里将消息池中第一条消息的next属性设置为null,代表切断与其他消息的连接,它为一个独立的消息,最后返回出去。

所以以上代码含义就是:把消息池里的第一条数据取出来,然后把第二条消息设置为第一条!

———————————————————————————————
【问题一解决! —————— 消息池是什么?是如何进行维护的? 】
———————————————————————————————






Handler 创建流程

1. Handler创建

以上步骤结束后,获取到消息Message,接下来需要通过Handler发送消息。关于Handler的获取通常是new出来的,来查看Handler的构造方法:(仅查看无参的构造方法即可)

Handler.java

public Handler() {
...
//拿到looper
mLooper = Looper.myLooper();

if(mLooper == null){
throw new RuntimeException(
"can't creat handler inside thread that has not called Looper.prepare()")
}

//拿到消息队列
mQueue = mLooper.mQueue;
mCallback = null;
}

Handler构造方法中:
mLooper = Looper.myLooper();
获取到了轮循器Looper,后面还有if判断Looper是否为空。所以在new一个Handler时,该线程必须已经存入一个Looper,可是Looper是如何存入的?

Looper.prepare(),后面再讲。所以创建Handler对象时,在构造方法中会获取LooperMessageQueue的对象。若在主线程中创建,拿到的是主线程Looper,若在子线程,则是子线程Looper!(如果new一个Thread,在此创建Handler对象,Looper还未存储,轮循器获取不了,为空,肯定会抛出异常)暂不分析在子线程创建Handler的情况,一般都是在主线程new 一个Handler。现在来查看何时存储Looper?点进去Looper类的myLooper() 方法:


2. Looper的创建与存储

Looper.java
//Return the Looper object associated with the current thread.
public static final Looper myLooper(){
return (Looper)sThreadLocal.get();
}

首先简单介绍ThreadLocal类,功能是实现线程内的单例。

Android 消息机制:Handler、Looper、Message源码 详细版解析 ------从入门到升天

如上图所示,假设用户访问服务器,服务器保持会话Session,会话中有某个方法getUser(),需要保持单例的形式。此时有两个不同的子线程需要访问会话中的getUser()方法,注意:不同线程获取的对象不同!同一线程不同阶段时获取的都是同一对象!这就是所谓线程的单例

上面的代码return (Looper)sThreadLocal.get();中:在哪个线程中执行,就会返回对应线程的Looper对象。既然有Looperget方法,必然有set方法(即Looper的存入方法),之前说过,是Looper.prepare():

Looper.java

public static final void prepare(){
if(sThreadLocal.get() != null){
throe new RuntimeException("only one Looper may be created per thread");
}

sThreadLocal.set(new Looper());
}

Looper存入到当前线程中,所以在哪个线程调用get方法,返回对应线程的Looper一个线程只允许存在一个Looper。了解后回到发消息原理来,这个主线程Looper何时保存?即prepare()方法何时调用?

Looper.java

public static final void prepareMainLooper(){
prepare(); //★★★
setMainLooper(myLooper());
if(Process.supportsProcess()){
myLooper().mQueue.mQuitAllowed = false;
}
}

如方法名,该方法是主线程Looper的创建方法,在方法prepareMainLooper中调用了prepare()后,已存入了一个new出来的Looper

———————————————————————————————
【问题二解决!————Looper轮循器是如何创建的?它与线程的关系?】
———————————————————————————————


3. MessageQueue的创建

可是消息队列MessageQueue是如何创建的?查看Looper的构造方法:

Looper.java

private Looper() {
mQueue = new MessageQueue();//new出来了
mRun = true;
mThread = Thread.currentThread();
}

可以看见出在Looper构造方法中,new出了消息队列对象。

———————————————————————————————
【问题三解决!————消息队列MessageQueue是如何创建的?与Looper的联系?】
———————————————————————————————


【Handler、Looper、Message创建逻辑图 V1】:
Android 消息机制:Handler、Looper、Message源码 详细版解析 ------从入门到升天


继续回到prepareMainLooper() 方法,该方法调用后,主线程Looper会创建并保存,所以该方法不可能在子线程中调用,只能在主线程!可是这个主线程是怎么执行起来的?它的入口?



4. ActivityThread 类中的main方法

在java中有个main方法,一个程序执行起来首先走main方法,它就是主线程,无需人为代码调用,java虚拟机会主动创建一个线程执行,而拥有main方法的这个类就是ActivityThread,查找其main方法:

public static final void main(String[] args) {
...
//创建Looper和MessageQueue
Looper.prepareMainLooper();

if(sMainThreadHandler != null){
sMainThreadHandler = new Handler();
}
ActivityThread thread = new ActivityThread();
...

//轮询器开始轮询
Looper.loop();
...
}

这个main方法就是主线程!在方法中Looper.prepareMainLooper();
存入了主线程Looper;然后
Looper.loop();
轮循器Looper死循环不断执行获取消息的方法。所以清楚了handler执行handlerMessage()方法为何在主线程中执行?

因为调用该方法的loop方法是在主线程中执行的!

【Handler、Looper、Message创建逻辑图 最终版】:
Android 消息机制:Handler、Looper、Message源码 详细版解析 ------从入门到升天


理清以上思路后,来查看Looper.loop()方法的具体实现:

Looper.java

public static final void loop(){
Looper me = myLooper();
MessageQueue queue = me.mQueue;

while (true) {
//取出消息队列的消息,可能会阻塞
Message msg = queue.next(); // might block

if(msg != null){
if(msg.target == null){
return;
}
if(me.Loggingc!= null)
...
//解析消息,分发消息
msg.target.dispatchMessage(msg);
...

}
}
}

Looper.loop()方法中有一个while循环,很明显是一个死循环所以说主线程中的方法并非一执行完就结束,因为调用了loop方法,一直在死循环,主程序main.java走到loop()里就不会再执行下面的代码了,不会被销毁。

其实我们对手机屏幕的操作,这种交互也是通过发消息来进行反应的,然后loop()中就是不停地在轮询获取消息再做出相应的反应。所以为什么主程序在不点击即不做任何操作的时候也不会退出?

是因为loop()方法中的死循环!先了解大概,详细部分后面讲。

———————————————————————————————
【问题四解决!———— 程序的主线程到底是如何运行的?在不对屏幕进行任何操作的情况下,主线程会停止吗?】
———————————————————————————————






Handler发消息

以上:
1.消息的创建;
2.new一个Handler后,获取到轮询器Looper和消息队列MessageQueue

两点已分析完。接下来需要发送消息,sendMessage()有很多重构的方法,可是方法体中都调用了另一个重构的方法:sendMessageDelayed(),最后它的方法体中都调用了一个唯一的方法:sendMessageAtTime(Message msg, long uptimeMillis)

Handler.java

public boolean sendMessageAtTime(Message msg, long uptimeMillis)
{
boolean sent = false;
MessageQueue queue = mQueue;
if(queue != null){
msg.target = this;//★★★★★
//把消息放到消息队列中☆☆☆☆☆
sent = queue.enqueueMessage(msg, uptimeMillis);
}
else{
...
}
return send;
}

参看该方法的两个参数,一个是消息Message对象,一个是该消息何时执行的时间(比如说延迟的消息)。if判断如果队列不等于空,就往消息队列MessageQueue中插入一条消息。

msg.target = this;
这个很重要,此处的this代表当前的handler类,将它赋值给msg对象的target。所以说Message对象中保存了对应处理的handler,之后获取到消息时,即可了解该消息应当交给哪个handler处理!

sent = queue.enqueueMessage(msg, uptimeMillis);
接着调用消息队列MessageQueue的方法,将传过来的参数添加到消息队列中,来具体查看enqueueMessage方法的实现:

1. 消息插入消息队列enqueueMessage()方法解析

MessageQueue.java

final boolean enqueueMessage(Message msg, long when) {
...
//判断主线程是否需要被唤醒
final boolean needWake;
synchronized (this) {
...
//对消息的重新排序,通过判断消息队列里是否有消息以及消息的时间对比
msg.when = when;

Message p = mMessages;
if (p == null || when == 0 || when < p.when) {
// 如果消息队列中没有消息,或者当前消息的时候比队列中的消息的时间小,则让当前消息成为队列中的第一条消息
msg.next = p;
mMessages = msg;
needWake = mBlocked; // new head, might need to wake up
} else {
// 代码能进入到这里说明消息队列中有消息,且队列中的消息时间比当前消息时间小,说明当前消息不能做为队列中的第一条消息
Message prev = null; // 当前消息要插入到这个prev消息的后面
// 这个while循环用于找出当前消息(msg)应该插入到消息列表中的哪个消息的后面(应该插入到prev这条消息的后面)
while (p != null && p.when <= when) {
prev = p;
p = p.next;
}

// 下面两行代码
msg.next = prev.next;
prev.next = msg;
needWake = false; // still waiting on head, no need to wake up
}
}
//唤醒主线程
if (needWake) {
nativeWake(mPtr);
}
return true;
}

直接分析synchronized 里的代码,其作用为:对消息的重新排序,通过判断消息队列里是否有消息以及消息的时间对比。

先将该消息Message何时执行的事件赋值给自身属性,又将mMessages赋值给Message对象,注意:这里的mMessages指的是消息队列中的第一条消息。然后通过if、else判断该消息的执行时间,将它插入到队列里的合适位置(注意:消息队列MessageQueue是由单链表形式进行维护的!)。具体分析if判断:


Android 消息机制:Handler、Looper、Message源码 详细版解析 ------从入门到升天

此判断的情况是:若消息队列中没有消息或者该消息比队列中第一条消息的执行时间小(代表执行顺序应先执行),则让该消息成为队列中的第一条消息。
msg.next = p;
即msg当前消息的next属性指向原消息队列的第一条消息,
mMessages = msg;
而mMessages指向msg,因为mMessages代表的是消息队列的第一条消息,而现在msg成为第一条消息了。


再来查看eles块中的判断实现:

Android 消息机制:Handler、Looper、Message源码 详细版解析 ------从入门到升天

走到else语句中的情况为:消息队列中有消息,且队列中的消息时间比当前消息时间小,说明当前消息不能做为队列中的第一条消息,只能成为中间部分或末尾部分。

既然走到此模块,代表队列中有消息比当前消息的执行时间短,执行顺序更加靠前,但也要找到当前消息应插入的位置。就通过while循环遍历队列,若该消息的执行时间小于队列中某个消息的则跳出循环,此时也找到了队列中的prev,当前消息就要插入到prev消息的后面!下面为其插入步骤:
msg.next = prev.next;
prevnext变量(即原本在prev后面的一个消息地址)赋值给当前消息的next变量,因为当前消息msg插入在prev的后面。
prev.next = msg;
prevnext变量指向当前消息。

———————————————————————————————
【问题五解决! ——————发送消息Message,插入消息队列MessageQueue中,插入方法是如何? 】
———————————————————————————————



Android 消息机制:Handler、Looper、Message源码 详细版解析 ------从入门到升天

分析完后,回顾发消息流程:Handler去发消息sendMessage,最核心的还是调用sendMeassageAtTime(Message msg, long updateMillis),而此方法的作用只是将消息放入到消息队列MessageQueue中。而在主线程main方法中调用了Looper.loop(),它的作用是死循环不停的获取消息来详细分析loop()方法:
(上面出现过以下代码)

Looper.java

public static final void loop(){
Looper me = myLooper();
MessageQueue queue = me.mQueue;

while (true) {
//取出消息队列的消息,可能会阻塞
Message msg = queue.next(); // might block

if(msg != null){
if(msg.target == null){
return;
}
if(me.Loggingc!= null)
...
//解析消息,分发消息
msg.target.dispatchMessage(msg);
...

}
}
}

首先取出线程对应的Looper和消息队列MessageQueue,而后进入死循环,在方法体中取出消息队列的消息,
Message msg = queue.next();
往消息队列中取消息是有可能阻塞的,当主线程无任何操作即队列中无消息,主线程会进入睡眠,无任何响应,那么何时醒?先讲解主线程睡眠的原理:

2. 主线程的睡眠与唤醒

Android 消息机制:Handler、Looper、Message源码 详细版解析 ------从入门到升天

Linux的一个进程间通信机制:管道(pipe)原理:在内存中有一个特殊的文件,这个文件有两个句柄(引用),一个是读取句柄,一个是写入句柄。

主线程Looper从消息队列读取消息,当读完所有消息时,进入睡眠,主线程阻塞。子线程往消息队列发送消息,并且往管道文件写数据,主线程即被唤醒,从管道文件读取数据,主线程被唤醒只是为了读取消息,当消息读取完毕,再次睡眠


查看MessageQueuenext方法:

MessageQueue.java

final Message next(){
int PendingIdleHandlerCount = -1;
int nextPoolTimeoutMillis = 0
For(;;){
if(nextPoolTimeoutMillis != 0){
Binder.flushPendingCommands();
}
nativePoolonce(mPtr, nextPoolTimeoutMillis );

synchronized(this){
final long now = SystemColock.uptimeMiills();
final Message msg = mMessage;
if(msg != null){
final long when = msg.when;
if(now >= when){
mBlocked = false;
mMessage = msg.next;
msg.next = null;

return msg;
}else{...}
}else{...}
}
}
}

很明显,next方法中也是维护了一个死循环,首先if判断nextPoolTimeoutMillis 不等于0,代表队列中已无消息,调用:
Binder.flushPendingCommands();
其实就是一个暂停的命令,代表主线程已阻塞,进入睡眠状态。那么何时被唤醒

之前说过发消息sendMessage()实质是将消息添加到消息队列中,再次查看Message类中的enqueueMessage方法的代码(上面有,只截取有关部分)

MessageQueue.java

final boolean enqueueMessage(Message msg, long when) {
...
//判断主线程是否需要被唤醒
final boolean needWake;
synchronized (this) {
...
if (p == null || when == 0 || when < p.when) {
...
needWake = mBlocked; // new head, might need to wake up
} else {
...
needWake = false; // still waiting on head, no need to wake up
}
}
//唤醒主线程
if (needWake) {
nativeWake(mPtr);
}
return true;
}

这里的needWake变量代表主线程是否需要被唤醒,synchronized 中首先if判断代表当前插入的消息要成为队首,将mBlocked赋值给needWakeelse则代表不需要唤醒主线程,将状态设置为false。最后判断状态,为true则调用nativeWake(mPtr)方法,查看其实现:

private native void nativeWake(int mPtr);

它是一个本地的方法,原理就是上面画图的那个管道,子线程去往管道中写数据,一写主线程就可以读到,同时也就被唤醒,获取到消息返回。

———————————————————————————————
【问题六解决! ——————主线程的睡眠和唤醒是如何触发的?底层原理? 】
———————————————————————————————


再次回到next方法体中,若队列中有消息,则走到synchronized方法体中,先if判断判断消息执行时间,若当前消息的执行时间小于,则返回该消息,回到loop()方法中,继续下面逻辑:

再判断该消息是否为空,若不为空则再判断该消息的target是否为空,target是什么对象还记得吗?

之前在介绍Handler发送消息时的sendMessageAtTime方法中,将当前的线程保存到了传过来的参数msg的target中!所以target保存了一个Handler对象,代表这个消息该交由哪个handler去进行处理,用于记录该消息由哪个Handler创建。如果它不为空的话,则继续调用
msg.target.dispatchMessage(msg);
所以dispatchMessage方法是属于Handler里的,查看详细实现:


3. Handler类处理消息的方法 ——dispatchMessage()

Handler.java

public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg); // 第一优先Runnable对象
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) { // 第二优先Callback对象
return;
}
}
handleMessage(msg);
}
}

这里首先if判断msgcallback属性是否为空,暂时忽略,直接看最后执行的方法是handleMessage(msg);

来查看其具体实现:

public void handlerMessage(Message msg){
}

方法体中并无任何实现,这也证明了每次使用Handler,将它new出来后,需要覆盖重写handlerMessage方法。

———————————————————————————————
【问题七解决! ——————每次创建Handler时,需要重写handleMessage()方法,为何?底层实现? 】
———————————————————————————————


到这里,Handler的消息机制已经理解完了,包括消息如何创建、发送,如何获取消息并处理。



再来补充一点细节,上述dispatchMessage方法中首先判断了msgcallback属性是否为空,从而调用了handleCallback(msg);
方法,查看其实现:

private final void handleCallback(Message message){
message.callback.run();
}

其实这个callback就是一个Runnable对象,在Message消息创建中可以看出来:

Message.java

pulic Runnable callback;

而我们在new一个Message对象时,也可以传一个Runnable 对象的,查看:

public static Message obtain(Handler h, Runnable callback){
Message m = obtain();
m.target = h;
m.callback = callback;
return m;
}

所以说在创建消息Message时,如果传入callback参数的话,在dispatchMessage方法中,会优先调用handleCallback方法,而不是handlerMessage方法,而这个runnable也可以直接运行在主线程了!

应用场景:所以可以直接将需要在主线程做的逻辑(比如UI更新)放到Runnable中,在创建Message时传入参数callback即可!







以上就是全部部分,其实关于这个Handler消息机制大致的流程,大家都可以说出一二,但是从源代码底层的角度去解读,你会了解的更加通透!我们总说Java的三大特性——封装、继承、多态,但是并不能真正去体会为何是一大特性,将代码分析后,自认为将这三大特性用的淋漓尽致!也更加深自我理解!


这个国庆假期玩玩写写,就专门在弄这个,墨迹墨迹,终于写完了,希望对你们有帮助 :)