花了几天研究Handler消息机制的成果

最近花了几天研究了一下Android的Handler机制。也阅读了网上很多的资料,于是在此写篇文章整理一下,想形成自己的体系。有一些别人总结的很好的干货我这边就不再总结了(但是会给出推荐链接),我只是把自己认为特别重要的写在这里。

一.Handler机制是干啥的

我认为可以从两方面来理解

1 从侠义上来说,handler(Android消息机制的主要成员)是用来解决子线程无法访问更改UI的问题的
2 从广义上来说,所有的代码都是在handler基础上运行的,handler是Android的app运行的整个框架。Android系统框架内,Activity生命周期的通知等功能也是通过Handler消息机制来实现的

1好理解,因为这是handler与我们距离最近的使用。但是为什么我认为有2这么一点呢?下面我来解释下:

关于 2 的解释

我们知道,安卓应用程序作为一个控制类程序,跟Java程序类似,都是有一个入口的,而这个入口就是ActivityThread,ActiviyThread也有一个main方法,这个main方法其实是安卓应用程序真正的入口。所以

①我们进入ActivityThreadmain方法看一下
public static void main(String[] args) {
		...
        Looper.prepareMainLooper();
        ...
        Looper.loop();
		
        throw new RuntimeException("Main thread loop unexpectedly exited");
}

省略无关代码,我们不难发现,在main函数这里进行了两个关于Looper的至关重要的操作,第一个是
Looper.prepareMainLooper();

②我们追踪进入
public static void prepareMainLooper() {
        prepare(false);
        synchronized (Looper.class) {
            if (sMainLooper != null) {
                throw new IllegalStateException("The main Looper has already been prepared.");
            }
            sMainLooper = myLooper();
        }
    }

发现这里是对sMainLooper 进行赋值,就是给主线程的looper赋值(looperhandler机制里面一个非常重要的角色)也就是创建了主线程的Looper

③然后Looper.loop(),我们追踪进入loop方法
public static void loop() {
        ...

        for (;;) {
           ...
        }
    }

发现在loop方法里面,除了开头几行的一些赋值等等,后面就会进入一个死循环,我们整个app的启动,可以说都是在这个死循环里面进行的。可以这么理解,就是可以把我们的app比喻成人,这个死循环比喻成自然环境。只有自然环境能够一直运行下去,正常运转,那么人才能有生活的物资,才不至于灭亡。所以app就依赖这个死循环才能在你不退出的情况下一直运行。所以我说2 不过分吧?

二.Handler机制的几个角色及其相互关系

上面我们解决了两个问题,其一是Handler机制是啥(主线程中更新UI),其二是Handler机制的重要性(Android的framework层的极端重要的部分)。那么下面就来详细地介绍下Handler机制涉及的几个角色Handler,Looper,MessageQueue,Message及其相互关系

1.基本介绍

  • Handler: 发送消息和处理消息

  • Looper: 从MessageQueue中获取Message,并将其交给 HandlerdispatchMessage(Message) 方法

  • MessageQueue: 消息队列,存放Handler发送过来的消息

  • Message: 消息的载体

2.他们之间的关系

他们之间的关系,我从两个方面来分析

①其一,从基本功能的角度

首先,看这么一张图
花了几天研究Handler消息机制的成果
(1)MessageQueue就像履带。它上面装载着一个个Message
(2)Thread就像传送带的动力,对应到程序就是我们通信都是基于线程而来的。
(3)传送带的滚动需要一个开关给电机通电,那么就相当于我们的Looperloop函数,而这个loop里面的for循环就会带着传送带不断的滚动,去轮询messageQueue上面装载的每一个Message
(4)Message就是 我们的货物了。

②其二,从设计思想的角度

花了几天研究Handler消息机制的成果
其实构成了线程模型中的经典问题 生产者-消费者模型

生产者-消费者模型:生产者和消费者在同一时间段内共用同一个存储空间,生产者往存储空间中添加数据,消费者从存储空间中取走数据。

好处:能够保证数据生产消费的顺序(通过MessageQueue,先进先出) 不管是生产者(子线程)还是消费者(主线程)都只依赖缓冲区(handler),生产者消费者之间不会相互持有,使他们之间没有任何耦合

③再总结下,就是下面这张图

花了几天研究Handler消息机制的成果

三.关于Handler机制的一些必要知识点

1.Handler有哪些发送消息的方法

sendMessage(Message msg)
sendMessageDelayed(Message msg, long uptimeMillis)
post(Runnable r)
postDelayed(Runnable r, long uptimeMillis)
sendMessageAtTime(Message msg,long when)

//下面这些不怎么重要
sendEmptyMessage(int what)
sendEmptyMessageDelayed(int what, long uptimeMillis)
sendEmptyMessageAtTime(int what, long when)

2.从Handler发送消息到消息入队,其内部是如何实现的?

①首先在子线程调用sendMessage方法发送消息
public final boolean sendMessage(@NonNull Message msg) {
    return sendMessageDelayed(msg, 0);
}
②追踪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);
}
④发现调用了HandlerenqueueMessage,继续追踪
private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,long uptimeMillis) {
     //将当前的Handler赋值给Message的target属性
     msg.target = this;
     msg.workSourceUid = ThreadLocalWorkSource.getUid();

     if (mAsynchronous) {
         msg.setAsynchronous(true);
     }
     return queue.enqueueMessage(msg, uptimeMillis);
}
⑤发现最终调用了MessageQueueenqueueMessage方法,将消息入队

2.MessageQueue的必要知识

(1)基本介绍

MessageQueue维护了一个优先级队列(以时间为顺序进行排列),而且是以链表的形式实现的。其他线程若想发送消息到此线程,则就要把消息发送到此线程的MessageQueue

(2)排序依据when的介绍

MessageQueue不仅满足先进先出,而且还有顺序。其顺序就是根据每一个消息的when属性来排序的。还记得一开始列出来的提交信息的那些方法嘛。不管用哪个方法去提交消息,最终一般都会走到sendMessageDelayed方法,而这个方法就可以设置when,我们进去看一下

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

可以看到这个when值是 SystemClock.uptimeMillis()delayMillis 之和。代表从系统启动开始再加上你给它设置的延迟时间数。很好理解的。when就是用于表示 Message 期望被分发的时间,就是啥时候执行。

(3)在哪根据when来排序的
boolean enqueueMessage(Message msg, long when) {
		...
        synchronized (this) {
          	...
            msg.when = when;
            ...
            if (p == null || when == 0 || when < p.when) {
               ...
            } else {
                ...
                for (;;) {
                    prev = p;
                    p = p.next;
                    if (p == null || when < p.when) {
                    	//***************************
                    	//***************************
                    	//看这里看这里
						//***************************
						//***************************
                        break;
                    }
                    if (needWake && p.isAsynchronous()) {
                        ...
                    }
                }
               ...
            }

           ...
        }
      ...
    }

我们把相对无关的代码都省去了。在代码标注的位置,我们可以看到这个for死循环跳出的条件有两个。其一是一直遍历走到MessageQueue的队尾,才退出循环,其二是发现有符合要求的地方可以插入message了。这个符合要求是啥意思。比如待插入的messagewhen是2,而MessageQueue队列中的message分别是1,3,4,5.那么待插入的message就会插入到when13message之间,新队列的when就变成了1,2,3,4,5。很好理解吧。

特殊情况,如果when相同,则按代码执行时间先后顺序插入。

(4)MessageQueue核心算法enqueueMessage的完整分析

上面我们简简单单地分析了下enqueueMessage,下面我们就较为完整地分析下这个算法

boolean enqueueMessage(Message msg, long when) {
    // Hanlder为空则抛异常
    if (msg.target == null) {
        throw new IllegalArgumentException("Message must have a target.");
    }
    //当前消息如果已经已经被执行则抛异常
    if (msg.isInUse()) {
        throw new IllegalStateException(msg + " This message is already in use.");
    }

    // 对MessageQueue进行加锁
    synchronized (this) {
        // 判断目标thread是否已经死亡
        if (mQuitting) {
            IllegalStateException e = new IllegalStateException(
                    msg.target + " sending message to a Handler on a dead thread");
            Log.w(TAG, e.getMessage(), e);
            msg.recycle();
            return false;
        }
        // 标记Message正在被执行,以及需要被执行的时间
        msg.markInUse();
        msg.when = when;
        // p是MessageQueue的链表头
        Message p = mMessages;
        // 判断是否需要唤醒MessageQueue
        boolean needWake;
        // 如果有新的队头,同时MessageQueue处于阻塞状态则需要唤醒队列
        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; 
            prev.next = msg;
        }

        // 如果需要则唤醒队列
        if (needWake) {
            nativeWake(mPtr);
        }
    }
    return true;
}

需要注意的是:当新插入的Message在链表头时,如果messageQueue是空的或者正在等待下个延迟消息,换句话说就是处于阻塞状态的时候,则需要唤醒MessageQueue

3.Looper的必要知识

(1)简单介绍

Looper是个什么角色呢?从一个简单的例子来说吧,我们在子线程创建Handler实例的时候,必须要先创建一个Lopper,并开启循环读取消息。在主线程中我们ActivityThread已经帮我们创建了(在本文最开始的地方就有介绍),所以我们不需要再次创建。但是在子线程中新建Handler的时候就需要创建咯!比如这样

public static class MyThread extends Thread {
    public Handler mHandler;
 
        public void run() {
            Looper.prepare();
            mHandler = new Handler() {
                public void handleMessage(@NonNull Message msg) {
                                        //处理接收的消息
                }
            };
            Looper.loop();
        }
}

一个线程对应一个Looper,但是可以有多个HandlerLooper是消息轮询的基础。比如本文最开始的传送带图片,没有Looper那么整个Handler机制就无法运转,而且Looper还不能多,否则传送带就会崩掉,违背单线操作的概念,造成不合适的并发操作。

  • 值得注意的是,我们看下Looper的构造方法,发现我们在创建Looper的时候,也会捎带着创建MessageQueue
 private Looper(boolean quitAllowed) {
        mQueue = new MessageQueue(quitAllowed);
        mThread = Thread.currentThread();
    }
(2)Looper的工作介绍(消息是怎么被读取的)

刚刚说了用Loop来轮询消息,那么具体是怎么个询法呢?没错,就是Looperloop方法。我们要想搞懂Looper的消息轮询机制,就要从loop方法开始。(本文开始也有一丢丢对loop方法的分析,不过这里才是更全面的)

public static void loop() {
      	...
        for (;;) {
            Message msg = queue.next(); // might block
            ...
        }
    }

我们可以看到,在开启死循环后,获取每一个消息的方式是调用queuenext方法,而且值得注意的是,在源码的注释中给了两个单词:might block,即有可能是阻塞的。好,下面追踪queuenext方法看看

Message next() {
    // Return here if the message loop has already quit and been disposed.
    // 源码中的注释表示:如果looper已经退出了,这里就返回null
    final long ptr = mPtr;
    if (ptr == 0) {
        return null;
    }
    //...
    // 定义阻塞时间赋值为0
    int nextPollTimeoutMillis = 0;
    //死循环
    for (;;) {
        if (nextPollTimeoutMillis != 0) {
            Binder.flushPendingCommands();
        }
        // 阻塞对应时间 这个方法最终会调用到linux的epoll机制
        nativePollOnce(ptr, nextPollTimeoutMillis);
        // 对MessageQueue进行加锁,保证线程安全
        synchronized (this) {
            final long now = SystemClock.uptimeMillis();
            Message prevMsg = null;
            Message msg = mMessages;
            //...
            if (msg != null) {
                if (now < msg.when) {
                    // 将要获取的下一个消息(也就是队头)还没开始,计算等待时间
                    nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                } else {
                    // 获得消息且现在要执行,标记MessageQueue为非阻塞
                    mBlocked = false;
                    // 链表操作
                    if (prevMsg != null) {
                        prevMsg.next = msg.next;
                    } else {
                        mMessages = msg.next;
                    }
                    msg.next = null;
                    msg.markInUse();
                    return msg;
                }
            } else {
                // 没有消息,进入阻塞状态
                nextPollTimeoutMillis = -1;
            }
            //退出
            if (mQuitting) {
                 dispose();
                 return null;
             }
            
    }
}

总结下:先看下Looper是否已经退出(怎么退出后面讲),然后进入死循环,首先判断是否要进行阻塞(判断是依据是上一次循环得到的某个值:nextPollTimeoutMillis ),阻塞最终会调用到linuxepollpipe/epoll)机制,阻塞结束后会进入中,得到队首的Message对象,然后进入if判断,如果有消息,即把队头消息取出,然后进行判断,决定是当下立即返回还是delay一下再返回。如果没有消息,则无限期进入阻塞状态,直到有消息进入。

  • nextPollTimeoutMillis表示阻塞的时间,其中,-1表示无限时间,直到有消息传入为止,0表示不阻塞。
(3)Looper的退出

刚刚说了Looper的工作原理,那么工作完了怎么退出呢?
Looper提供了quitquitSafely两个方法来退出一个Looper,二者的区别是: quit会直接退出Looper,而quitSafely只是设定一个标记,然后把消息队列中的已有消息处理完毕后才安全退出。这个好理解。

public void quit() {
    mQueue.quit(false);
}

public void quitSafely() {
    mQueue.quit(true);
}

其实两个方法最终都是调用mQueuequit方法。让我们来分析一下这个方法

// 最终都是调用到了这个方法
void quit(boolean safe) {
    // 如果!mQuitAllowed为true即不能退出则抛出异常。
    if (!mQuitAllowed) {
        throw new IllegalStateException("Main thread not allowed to quit.");
    }

    synchronized (this) {
        if (mQuitting) {
            return;
        }
        mQuitting = true;
        // 执行不同的退出逻辑方法
        if (safe) {
            removeAllFutureMessagesLocked();
        } else {
            removeAllMessagesLocked();
        }
        // 唤醒MessageQueue
        nativeWake(mPtr);
    }
}

这个方法首先根据mQuitAllowed参数判断是否能退出,如果可以退出则进入中执行退出逻辑。如果mQuitting==true,那么这里会直接return掉,而且mQuitting这个变量只有在这里被执行了赋值,所以一旦looper退出,则无法再次运行了。也就是说Looper只能创建一次,一旦quit之后此线程就无法再复活了。

  • 而且同一个线程,只能创建一个Looper。因为Looper的创建是通过Looper.prepare方法实现的,而在prepare方法中就判断了,当前线程是否存在Looper对象,如果已经存在则抛出异常。所以同一个线程,只能创建一个Looper,多次创建会报错。
    private static void prepare(boolean quitAllowed) {
        if (sThreadLocal.get() != null) {
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        sThreadLocal.set(new Looper(quitAllowed));
    }

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

之后执行不同的退出逻辑,然后唤醒MessageQueue,之后MessageQueuenext方法会退出,Looperloop方法也会跟着退出,那么线程也就停止了。

(4)关于Looper的几个骚操作
①如何获取当前线程的looper
Looper.myLooper()

内部原理就是通过 ThreadLocal<Looper>sThreadLocal对象的get方法来获取 Looper
sThreadLocal对象在整个app中只有一个,因为它是由static final修饰的


public static @Nullable Looper myLooper() {
    return sThreadLocal.get();
}
②如果在任意线程获取主线程的 Looper
Looper.getMainLooper()
③如何判断当前线程是不是主线程

方法一:

Looper.myLooper() == Looper.getMainLooper()

方法二:

Looper.getMainLooper().getThread() == Thread.currentThread()

方法三: 方法二的简化版

Looper.getMainLooper().isCurrentThread()

4.关于ThreadLocal

ThreadLocalJava中一个用于线程内部存储数据的工具类,并不是Android所特有的。
值得注意的是,在不同的线程中访问同一个threadLocal对象,但是它们获取的值却是不一样的,这就是ThreadLocal的奇妙之处。那么问题来了,原理是什么?
我们进入其set方法看一下

public void set(T value) {
		//得到当前线程
        Thread t = Thread.currentThread();
        //得到当前线程的ThreadLocalMap 
        ThreadLocalMap map = getMap(t);
        if (map != null)
        	//将当前的ThreadLocal变量作为key,传进来的泛型作为value进行存储
            map.set(this, value);
        else
        	//没有map则创建,并且将当前的ThreadLocal变量作为key,传进来的泛型作为value进行存储
            createMap(t, value);
    }

我们可以发现,每一个线程都维护着一个Map集合,其名字为ThreadLocalMap 。我们可以通过
getMap方法来获得。此集合是以键值对的方式来存储数据的。键为ThreadLocal对象,也就是调用set方法的那个ThreadLocal对象。这个值就是set()里面传入的参数了。每一个线程都有一个ThreadLocalMap 。虽然键相同,但是取出来的值是不相同的。

  • 我们可以这样类比,就是ThreadLocal是一个人,不同的线程即不同的Thread和不同的ThreadLocalMap,是不同的环境。相同的一个人(key),在学校(学校的Thread,里面有学校特有的ThreadLocalMap)里面他的身份(value)就是学生,在家庭里面他的身份(value)可能是父亲,在社会上他的身份(value)就是中国公民。即同一个人在不同的环境下有不同的身份,同一个ThreadLocal在不同的线程下可能映射着不同的值,就是这个道理。
  • 前面在Looper中提到的的 ThreadLocal<Looper>sThreadLocal,它的get方法就是返回每个线程所特有的Looper。其也是从ThreadLocalMap中取值,虽然整个App中sThreadLocal是唯一的,但是每个线程ThreadLocalMap是不同的,所以不同的线程能得到不同的Looper

5.同步屏障

Handler机制中,有三种消息类型:

①同步消息。也就是普通的消息。
②异步消息。通过setAsynchronous(true)设置的消息。
③同步屏障消息。通过postSyncBarrier方法添加的消息,特点是target为空,也就是没有对应的handler

同步屏障是什么意思?可以这么理解,比如救护车,在正常情况下,没有拉病人的情况下是可以和其他车一样在公路上正常行驶的,但是如果拉上病人就不一样了。不仅需要加急,不用等红灯,而且前面还有一辆警车帮忙带路。其实这个救护车就可以理解成异步消息,警车就可以理解成同步屏障消息。在后方有承载着病人的救护车的时候,其他车辆都会被警车“屏障”,救护车优先通行。这就是同步屏障机制存在的意义。当有要紧的异步消息需要立马被执行的时候,同步屏障消息就来了。具体过程我们可以看一下源码

Message next() {
        ...
        for (;;) {				
			...
            synchronized (this) {
                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());
                }
				...
            }
			...
        }
    }

我们可以看到,在next方法,每次取出消息的时候,会先进行一个if判断,即if (msg != null && msg.target == null) ,就是如果某个msg不为null而且它的targetnull,那么它就是一个同步屏障消息,那我们就要进入这个if。此时会往后逐个遍历MessageQueue中的消息,当发现下一个异步消息的时候,就退出循环,得到这个异步消息然后加急进行处理。

  • 值得注意的是:同步屏障消息只是一个标志,就像我一开始说的警车一样,它上面并没有承载病人,真正承载病人的是后面的救护车,也就是有重要信息的异步消息。
  • 而且并不是所有的异步消息都是需要加急处理的,正常情况下异步消息和同步消息都是可以根据when来决定处理顺序的,当然只是正常情况下,毕竟异步消息设计出来就是要被加急处理的,就像救护车的职能就是紧急救援病人的。

用张图表示就是这样
花了几天研究Handler消息机制的成果

那么有什么加急消息呢?比如UI的更新,这就是一个加急消息。相信不用我解释大家都能理解咯。

6.HandlerThread

HandlerThread是啥?看名字就知道,其实它继承了Thread,本质上也是一个Thread。但是它的run函数中已经帮我们完成了Looper的创建,不信你看

public void run() {
        mTid = Process.myTid();
        Looper.prepare();
        synchronized (this) {
            mLooper = Looper.myLooper();
            notifyAll();
        }
        Process.setThreadPriority(mPriority);
        onLooperPrepared();
        Looper.loop();
        mTid = -1;
}

所以它第一个好处就是方便我们使用,也就是我们使用子线程创建Handler的时候就不用再显示地创建Looper了。
但是,光知道这个还是不够的,万一在HandlerThread还没有创建好Looper的时候就调用getLooper方法怎么办呢?谷歌工程师早就想到了这个线程安全问题,所以我们看getLooper方法

public Looper getLooper() {
        ...
        // If the thread has been started, wait until the looper has been created.
        //意思是如果线程已经启动了,那么等looper创建了之后再返回
        synchronized (this) {
            while (isAlive() && mLooper == null) {
                try {
                    wait();
                } catch (InterruptedException e) {
                }
            }
        }
        return mLooper;
    }

从这个方法可以看出,人家getLooper方法并不是无脑给你返回looper,因为有可能还没有创建好,所以先wait,等前面run方法创建了looper之后notifyAll,这里getLooper才给你返回looper。这就是第二个好处,线程安全

7.IntentService

首先看源码

public abstract class IntentService extends Service {
    private final class ServiceHandler extends Handler {
        public ServiceHandler(Looper looper) {
            super(looper);
        }

        @Override
        public void handleMessage(Message msg) {
            onHandleIntent((Intent)msg.obj);
            stopSelf(msg.arg1);
        }
    }
    @Override
    public void onCreate() {
        super.onCreate();
        HandlerThread thread = new HandlerThread("IntentService[" + mName + "]");
        thread.start();

        mServiceLooper = thread.getLooper();
        mServiceHandler = new ServiceHandler(mServiceLooper);
    }

    @Override
    public void onStart(@Nullable Intent intent, int startId) {
        Message msg = mServiceHandler.obtainMessage();
        msg.arg1 = startId;
        msg.obj = intent;
        mServiceHandler.sendMessage(msg);
}

首先它是一个服务,可以借助子线程HandlerThread ,将消息一个接一个地执行,而且消息队列的特点是前面的消息执行完后面的消息才能执行,所以就可以让子任务在子线程中有条不紊地一个接一个地执行。而且消息执行完毕之后服务自动关闭stopSelf。实现了内存释放。
我们的耗时任务的逻辑就是重写onHandleIntent方法,在执行耗时任务的时候就会回调我们重写的onHandleIntent方法。
简单来说,这就是一个可以在子线程进行耗时任务,并且在任务有序执行后自动停止的Service

8.Handler内存泄漏

小题大做 | 内存泄漏简单问,你能答对吗(强烈建议大家学习此文)

参考文章

温故而知新 | 打破Handler问到底(这个强烈推荐大家学习)
你真的懂Handler吗?Handler问答
"一篇就够"系列: Handler消息机制完全解析

上一篇:Android Handler整体梳理以及热点问题解析


下一篇:WSAEventSelect IO复用模型