看完这篇还不明白Handler你砍我

8,257 阅读30分钟

前言

Handler可以说小伙伴们用的非常多了,可以说Handler是支撑整个Android系统运行的基础,本质上Android系统都是由事件驱动的。而处理事件的核心就在于Handler。接下来我们就从简单的使用,到源码分析让你彻彻底底明白Handler的本质。不会再让你发出为什么Looper.loop不会堵塞主线程,Handler是如何切换线程等这类疑惑。

简单使用

一般是在主线程中实现一个Handler,然后在子线程中使用它。

class HandlerActivity: AppCompatActivity() {

    private val mHandler = MyHandler()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // 在子线程中通过自定义的 Handler 发消息
        thread {
            mHandler.sendEmptyMessageDelayed(1, 1000)
        }
    }

    // 自定义一个 Handler
    class MyHandler: Handler() {
        override fun handleMessage(msg: Message) {
            Log.i("HandlerActivity", "主线程:handleMessage: ${msg.what}")
        }
    }
}

或者有时候需要在子线程中创建运行在主线程中的Handler

class HandlerActivity: AppCompatActivity() {
    private var mHandler: Handler? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        thread {
            //获得main looper 运行在主线程
            mHandler = MyHandler(Looper.getMainLooper())
            mHandler!!.sendEmptyMessageDelayed(1, 1000)
        }
    }
 	// 自定义一个 Handler
    class MyHandler(): Handler() {
        override fun handleMessage(msg: Message) {
            Log.i("HandlerActivity", "子线程:handleMessage: ${msg.what}")
        }
    }
}

这就是小伙伴们一般常用的两个用法。大家注意到了在第二个用法中出现了一个Looper.getMainLooper(),使用它作为参数,即使MyHandler是在子线程中定义的,但是它的handleMessage方法依然运行在主线程。我们看一下这个参数究竟是什么东东~

public Handler(@NonNull Looper looper) {
       this(looper, null, false);
}

可以看到这个Looper就是我们上面传入的参数Looper.getMainLooper(),也就说明了handleMessage方法具体运行在哪个线程是和这个Looper息息相关的。那么这个Looper究竟是何方神圣,它是怎么做到线程切换的呢?

概述

我们先来看一张图

这就是整个Handler在Java层的流程示意图。可以看到,在Handler调用sendMessage方法以后,Message对象会被添加到MessageQueue中去。而这个MessageQueue就是被包裹在了Looper中。那么Looper对象是干什么的呢?它和Handler是什么关系呢?我们来看一下他们具体的职责把~

  • Handle 消息机制中作为一个对外暴露的工具,其内部包含了一个 Looper 。负责Message的发送及处理

    • Handler.sendMessage() :向消息队列发送各种消息事件
    • Handler.handleMessage()处理相应的消息事件
  • Looper 作为消息循环的核心,其内部包含了一个消息队列 MessageQueue ,用于记录所有待处理的消息;通过Looper.loop()不断地从MessageQueue中抽取Message,按分发机制将消息分发给目标处理者,可以看成是消息泵。注意,线程切换就是在这一步完成的。

  • MessageQueue 则作为一个消息队列,则包含了一系列链接在一起的 Message ;不要被这个Queue的名字给迷惑了,就以为它是一个队列,但其实内部通过单链表的数据结构来维护消息列表,等待Looper的抽取。

  • Message 则是消息体,内部又包含了一个目标处理器 target ,这个 target 正是最终处理它的 Handler

哦?原来他们的职责是这样啊,可我还是不懂他们到底是怎么运行起来的。就像你告诉我医生是负责治病,警察是抓坏人的,他们具体是如何去做的呢?

Handler

从我们大家最熟悉的sendMessage方法说起。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);
}

继而调用sendMessagAtTime方法:

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);
}

眼尖的小伙伴就会发现,等等不对,这代码中出了一个叛徒,啊不对,出了一个奇怪的东西。没错,就是刚才流程图中出现的这个MessageQueue。你看,我没有胡说吧,这个MessageQueue是实打实存在的,并且被作为参数一起传给了enqueueMessage方法。其实无论你是如何使用Handler发送消息,结果都会走到enqueueMessage方法中。

这是方法的调用链:

可以看到无论如何,最后都会走到enqueueMessage方法中。这个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);
}

enqueueMessage一共做了两件事情,一个是给Message赋值,一个是调用传进来的这个MessageQueueenqueueMessage方法。注意啊,最后这个enqueueMessage方法是在MessageQueue中的,已经不再是Handler的方法了,也就是说,调用走到了这里。事件的流向已经不归Handler管了。

Handler::enqueueMessage方法中第一行msg.target = this;,这个this是什么呢?这个thishandler方法中自然是handler本身了,也就是说这一行代码将handler自身赋值给了Message对象的target字段。我们可以看以下这个target字段的定义:

//简化后的代码
public final class Message implements Parcelable{
    @UnsupportedAppUsage
    /*package*/ Handler target;
}

啊,这样明白了,也就是说每个发出去的Message都持有把它发出去的Handler的引用,对不对?

没错事实就是这样,每个发出去的Message对象内部都会有个把它发出去的Handler对象的引用,也可以理解Message这么做的目的,毕竟Handler把它发射出去了,它不得知道是谁干的,好随后找它报仇么。那么我们继续下一步,msg.setAsynchronous(true)这一行代码是设置异步消息的,这里暂时先不管它。我们先看queue.enqueueMessage(msg, uptimeMillis)这行代码。也就是从这行代码,Message就可以和Handler说拜拜了您讷。

MessageQueue

Handler这个mQueue就是上文我们提到过的MessageQueue对象,在上面的介绍说也说了,这货就是个骗子,明明起名是Queue,却是单链表。你可能误会Google工程师了,名字也确实没什么错了,从机制上看确实很像队列。队列是什么特性啊,先进先出对吧。这个先后就是按时间来划分的,时间靠前的就在前面时间靠后的就在后面。而在这个单链表中也确实是这样实现的,按照时间的先后排序。这个就先不多讲了,一会讲如何实现的消息延时发送的时候会讲到这个。

到这里你可能有疑惑了,这个MessageQueue是什么鬼,从哪里冒出来的。你可能还记得,在上面的sendMessageAtTime方法中有这么一行:

MessageQueue queue = mQueue;

那么这个mQueue是在哪里被赋值的呢?当然是在构造方法中啦~

public 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;
}

不对啊, 你TM骗我,在最开始你继承的Handler可没有这几个参数。哎呀,小伙子别心急,你看这个无参构造方法不也调用的这个方法么。

public Handler() {
        this(null, false);
}

在这个有参数的构造方法中呢,可以看到有这么两行:

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;

我们在Handler中使用的mQueue就是在这里赋值的。这里的赋值可不简单,它拿的是人家LooperMessageQueue作为自己的MessageQueue,**而且在上面的代码中有一个很关键的点,就是调用Looper.myLooper()方法中获取这个Looper对象,如果是空的话就要抛出异常。**这一点非常关键,我们先做个记号,一会回过头来会看这一行代码。你就会明白它的作用了。

现在先不研究Looper,我们继续看我们的MessageQueue。上面说到,最后发送消息都调用的是MessageQueuequeue.enqueueMessage(msg, uptimeMillis)方法。现在我们已经拿到了queue,进去看看这个方法它做了什么。

// MessageQueue.java
//省略部分代码
boolean enqueueMessage(Message msg, long when) {

    synchronized (this) {
        if (mQuitting) {
            IllegalStateException e = new IllegalStateException(
                    msg.target + " sending message to a Handler on a dead thread");
            msg.recycle();
            return false;
        }

        msg.markInUse();
        msg.when = when;
        
        //【1】拿到队列头部
        Message p = mMessages;
        boolean needWake;
        
        //【2】如果消息不需要延时,或者消息的执行时间比头部消息早,插到队列头部
        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 {
            //【3】消息插到队列中间
            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.next
            prev.next = msg;
        }

        if (needWake) {
            nativeWake(mPtr);
        }
    }
    return true;
}

主要分为3个步骤(见以上代码标注)。

  1. mMessages 是队列的第一消息,获取到它
  2. 判断消息队列是不是空的,是则将当前的消息放到队列头部;如果当前消息不需要延时,或当前消息的执行时间比头部消息早,也是放到队列头部。
  3. 如果不是以上情况,说明当前队列不为空,并且队列的头部消息执行时间比当前消息早,需要将它插入到队列的中间位置。

如何判断这个位置呢?依然是通过消息被执行的时间。

通过遍历整个队列,当队列中的某个消息的执行时间比当前消息晚时,将消息插到这个消息的前面。

可以看到,消息队列是一个根据消息【执行时间先后】连接起来的单向链表。

想要获取可执行的消息,只需要遍历这个列表,对比当前时间与消息的执行时间,就知道消息是否需要执行了。

好了,MessageQueueJava层的分析到这里就结束了。

等等,这就结束了?

没错,到这一步,消息已经添加到了这个名为队列实为单链表的队列中。

不对啊,我handleMessage方法如何被调用呢?消息添加进去就完了?说好的线程切换呢?

其实到这一步真的就结束了,最起码在Java层是结束了。消息到这一步被添加到了队列中,HandlerMessageQueue在发送的过程中做的工作已经做完了。但是既然有队列,那么不可能说光添加不读取把。不然我添加了有什么用?

是的,接下来就是Looper大展神威的时候到了。

Looper

在上面提到了,Handler中的MessageQueue对象其实就是Handler中的Looper它的MessageQueueHandlerMessageQueue中添加消息,其实就是往HandlerLooper所持有的MessageQueue中添加对象。可能有点绕,但是需要明白的是这个MessageQueueLooper的,不是Handler的。明白了这一点,你就能很好的理解后面发生的事情。

可能有的小伙伴会说了,这个Looper哪来的,我创建Handler的时候从没看见过它出现啊。没错,在使用Handler的时候它确实没出现过,但是大家还记得Handler中两个参数的那个构造方法嘛?就是下面这个:

//Handler.java
//省略部分代码
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通过Looper.myLooper方法获取到了Looper对象,当然,也有可能没获取到。不过,你如果没获取到就惨了,就要抛异常了。

在职责分析中我们提到了, 这个Looper对象作为消息循环的核心,不断从它的MessageQueue中取出消息然后进行分发。

说人话可以不?

刚才说到MessageQueue那个队列中那么多的消息没人拿,MessageQueue的老板Looper看不下去了,说你这也太浪费了,来我拿吧,然后它专门负责一个个拿,然后看这是谁发的,然后让谁去处理。

那我们看看这个Looper.myLooper()方法做了什么事情呢。它是如何返回一个Looper对象的呢?

public static @Nullable Looper myLooper() {
        return sThreadLocal.get();
}

sThreadLocal又是什么鬼?咱们看一下它的定义

//sThreadLocal.get() will return null unless you've called prepare().
@UnsupportedAppUsage
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

可以看到这个sThreadLocal是一个ThreadLocal类,并且它的泛型是Looper对象。ThreadLocal提供了线程的局部变量,每个线程都可以通过set()get()来对这个局部变量进行操作,但不会和其他线程的局部变量进行冲突,实现了线程的数据隔离。简要言之:往ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的。

呀呵,源代码中还有行注释,这行注释的意思是除非您已调用prepare(),否则sThreadLocal.get()将返回null。这行注释就有趣了,刚才我还寻思这个ThreadLocal的get方法得有数据才能返回,可这个数据是啥时候塞进去的呢?你这注释就告诉我了,只有我调用了prepare()方法,才有值啊。那我们就去看看这个方法做了些什么。

public static void prepare() {
        prepare(true);
}

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));
}

可以看得出,最后调用了是prepare(boolean quitAllowed)方法,而这个方法首先判断,如果sThreadLocal有值,就抛异常,没有值才会塞进去一个值。其实很好理解,就是说prepare方法必须调用但也只能调用一次,不调用没有值,抛异常,调用多次也还抛异常。我好难哦~不过大家还记得上面我们重点关注的一个内容吗,在Handler中的有参构造函数中有这么一行代码会报异常:

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;

如果Looper为空就抛异常,现在我们知道了,什么时候Looper为空呢?没有调用prepare方法的时候会为null,也就是说在构造Handler之前,必须得有Looper对象,换言之,在构造Handler之前,必须调用Looperprepare方法创建Looper这句话非常重要,所以我又是下划线又是加粗的,一定要记住这句话。在后面自定义一个Looper的时候会用到。

接下来再看看这行sThreadLocal.set(new Looper(quitAllowed));做了什么吧,它是如何塞进去的呢

public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
}

set方法首先获取到了当前的线程,然后获取一个map。这个map是以键值对形式存储内容的。如果获取的map为空,就创建一个map。如果不为空就塞进去值。要注意的是,这里面的key是当前的线程,这里面的value就是Looper。也就是说,线程和Looper是一一对应的。也就是很多人说的Looper线程绑定了,其实就是以键值对形式存进了一个map中。没什么高大上的。你来你也行。

而这个Looper的构造方法我们也得去看一下:

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

Looper的构造方法中,可以看到它创建了一个MessageQueue,没错就是那个被Handler无耻使用的MessageQueue。需要注意的一点,上面的分析中提到了prepare方法必须调用但也只能调用一次,调用以后就会创建Looper对象,也就是说一个线程中只会创建一个Looper对象,而一个Looper对象也只会创建一个MessageQueue对象。

现在我们来梳理一下这个流程哈~

首先创建一个无参数的Handler,在这个Handler的构造方法中又去获取Looper对象,当然获取Looper对象其实是为了它的MessageQueueHandler巴结上了人家Looper对象的MessageQueue以后,发送消息的时候,把要发送的消息给了MessageQueue,添加到了队列中。是不是感觉缺少了什么?没错,好像在这个里面Looper的作用没体现出来,说好的分发消息呢?而且你刚刚说了得调用prepare()方法才会创建Looper,可我没调用过这个方法啊。那这个Looper谁创建的?

刚才提到了,Looper在创建的时候会被当成value塞入到一个map中去,这个mapThreadLocal。而key就是创建Looper时所在的线程。也就是所谓的Looper线程绑定。我们一般在用的时候从没创建过Looper,但是我们知道handle中的回调handleMessage方法是运行在主线程中的。Looper的职责不就是分发消息么,也就是说Looper对象在主线程中把消息分发给了Handler。那么这下就明白了,在我们创建Looper的时候,Looper所在的线程是主线程,换言之,与这个Looper绑定的线程就是主线程。

明白了,我这就去和面试官对线。

既然是主线程,那么大家应该知道,主线程是谁创建的?ActivityThread类。ActivityThread类也正是整个app的入口。以前我也很好奇,既然Android是用Java写的,按理说Java不应该是有个什么main方法么?怎么我写Android没用过这个main方法呢?其实呢,在ActivityThread中就有这个main方法,它是程序的入口,也就说当你点开app以后,首先会进入到这个main方法中,然后做了一大堆事情,这里就不分析了。你只需要知道,这个main方法才是真正的入口。

那我们来看看这个main方法到底干了什么事情:

//ActivityThread.java
//省略部分代码
public static void main(String[] args) {
        Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ActivityThreadMain");
        Process.setArgV0("<pre-initialized>");
    	//1 敲黑板,划重点,就是这一句!
        Looper.prepareMainLooper();
        ActivityThread thread = new ActivityThread();
        thread.attach(false, startSeq);

        if (sMainThreadHandler == null) {
            sMainThreadHandler = thread.getHandler();
        }

        if (false) {
            Looper.myLooper().setMessageLogging(new
                    LogPrinter(Log.DEBUG, "ActivityThread"));
        }

        // End of event ActivityThreadMain.
        Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
    	//2 敲黑板,划重点,这一句!
        Looper.loop();

        throw new RuntimeException("Main thread loop unexpectedly exited");
    }

这段代码是不是很符合我们平常写的java程序呢?熟悉的main方法又回来了。main方法中可以看到,它调用了Looper的prepareMainLooper方法:

public static void prepareMainLooper() {
    	//设置不允许退出的Looper
        prepare(false);
        synchronized (Looper.class) {
            if (sMainLooper != null) {
                throw new IllegalStateException("The main Looper has already been prepared.");
            }
            sMainLooper = myLooper();
        }
}

可以看到注释1,这个方法最终还是调用了Looperprepare方法,这个方法干嘛的?创建Looper并且把它和当前线程一起塞进map中的啊。当前线程是哪个线程?主线程啊!

一切到这里就真相大白了,在APP启动的时候,入口方法中已经自动帮我们创建好了Looper,并且也自动的帮我们和主线程绑定了。也就是说我们平常用的Handler中的Looper就是主线程中创建的这个Looper。细心的小伙伴应该会发现,这个prepareMainLooper方法你是不能调用的。为啥?因为这个方法在入口的时候执行了一次,所以里面的sMainLooper不为Null了,如果你在调用一次,不就要抛异常了么~

现在Looper也有了,LooperMessageQueue也有了。接下来该分发消息了吧?我Handler发送消息可是已经很久过去了,你这里分析一大通,我还干不干活了?

好,我们现在先假设一个场景。

你买了一个快递,你知道迟早会给你送到,但是不确定到底什么时候才会送到。你想早点拿到快递应该怎么做?

你会不停的问快递公司,我的快递到哪了,到哪了。当然,现实中一般都是等快递员打电话才去拿快递~问题在于,这是程序。

Looper虽说要分发消息,但是它又不知道你什么时候会发送消息,只能开启一个死循环,不断的尝试从队列中拿数据。这个死循环在哪里开始的?没错就是注释2处,Looper.loop()开启了一个死循环,然后不断的尝试去队列中拿消息。

// Looper.java
public static void loop() {

    //拿到当前线程的Looper
    final Looper me = myLooper();
    
    if (me == null) {
        throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
    }
    
    //拿到Looper的消息队列
    final MessageQueue queue = me.mQueue;

    // 省略一些代码...
    //1 这里开启了死循环
    for (;;) {
        Message msg = queue.next(); // might block
        if (msg == null) {
            // No message indicates that the message queue is quitting.
            return;
        }

        try {
            msg.target.dispatchMessage(msg);
            //省略一些代码...
        } catch (Exception exception) {
            //省略一些代码...
            throw exception;
        } finally {
            //省略一些代码...
        }
        //省略一些代码...
        msg.recycleUnchecked();
    }
}

在循环中Looper不停的取出消息,拿到Message对象以后,会去调用Messagetarget字段的dispatchMessage方法,这个target字段还有印象吗?没错,就是发送它的Handlermessage在被发送出去的时候就已经暗暗记下了是谁发送出去的。现在轮到它报仇了~

我们可以跟进看一下这个dispatchMessage方法:

//Handler.java
public void dispatchMessage(@NonNull Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
}

可以看到,消息会先分发给Meesgaecallback,我们没有定义这个callback,那我们接下来看,还有一个mCallback。这个mCallback是创建Handler的时候可以选择传一个CallBack回调,里面依然是handleMessage方法。也就是说你可以自定义一个类继承Handler,重写handleMessage方法,也可以直接new一个Handler传一个回调。当然,这个都很简单,我就不再赘述了,大家可以自行尝试体验。

我们关注重点,当Looper拿到Message以后,并且根据Messagetarget字段找到了发送消息的Handler,紧接着调用了HandlerhandleMessage方法。重点来了,这个Looper是在哪个线程运行的?主线程,它调用方法是在哪个线程运行的?依然是主线程!handleMessage方法此时在哪个线程运行的?依然是主线程!不知不觉中,线程已经切换过来了,神奇不?其实并不神奇,其实就是主线程中的Looper不断的尝试调用handleMessage方法,如果有消息就调用成功了,此时handleMessage方法就是在主线程中调用的。而handler在哪个线程,Looper并不关心,我反正只在主线程调用你的handleMessage方法。这就是线程切换的本质。就是没有线程切换,主线程的Looper不断的尝试调用而已。

可能有的小伙伴已经懵逼了,我们再次从头到尾梳理一下哈~

  1. mainThreadActivityThread首先创建了一个运行在主线程的Looper,并且把它和主线程进行了绑定。
  2. Looper又创建了一个MessageQueue,然后调用Looper.loop方法不断地在主线程中尝试取出Message
  3. Looper如果取到了Message,那么就在主线程中调用发送这个MessageHandlerhandleMessage方法。
  4. 我们在主线程或者子线程中通过Looper.getMainLooper为参数创建了一个Handler
  5. 在子线程中发送了Message,主线程中的Looper不断循环,终于收到了Message,在主线程中调用了这个HandlerhandleMessage方法。

这里需要注意的是,Looper.loop方法中取到的Looper对象并不一定就是主线程的,因为它是取出当前线程的Looper对象。只不过在ActivityThread这里是主线程,所以拿到的是主线程的Looper对象。所以如果我们要在子线程中创建一个Looper也是可以的,一会我们就实现一下。

到这里可能有的小伙伴还是懵逼,我还是不太明白怎么切换的线程。我们通过一个比喻很好的解释一下

首先有一个小学生小明,小明的任务是写作业。然后有一个老师,老师的任务是批改作业。这个班里还有一个学习委员,学习委员的任务就是负责收作业然后交给老师去批改。

一般情况下,老师是学校已经聘请好的,我们不需要自己去聘请老师。老师一般也就只在办公室批改作业,办公室我们可以理解为主线程。学校就是我们的app。老师就是Looper对象。而小明同学就是Handler,学习委员就是MessageQueue,作业就是Message。老师(Looper)在办公室(主线程)不断的从学习委员(MessageQueue)那里拿到下一本要批改的作业(Message),老师突然发现作业里有错误,老师很生气,于是就从作业本上的姓名知道了是谁写的这个作业(对应Messagetarget字段),于是老师把小明(Handler)叫到办公室(主线程),让小明在办公室(主线程)把作业改好(handleMessage)。在这个例子中,小明作为Handler,他可以在任何地方写作业(sendMessage),也就是说他可以在家里写作业,可以在教室写作业,也可以在小公园写作业,这里的各个地方就是不同的线程。但是写完作业以后一定要交给学习委员,学习委员手里有一摞作业,这一摞作业就是消息的队列,而学习委员就是MessageQueue,他负责收集作业,也就是收集Message。老师在办公室批改作业,发现出错了,就把小明叫到了办公室,让小明在办公室改错。在这里,办公室就是主线程,老师不会管小明是在哪里写的作业,老师只是关心作业出错了,需要小明在办公室里改错。小明在办公室里改错这就是handleMessage方法运行在了主线程。但是也有个问题,不能说你小明在办公室改错改个没完没了,那岂不是影响了后面同学作业的批改?如果小明真的改错改的没完没了,也就是在主线程上作耗时操作很久,那么老师也无法进行下一个同学的作业批改,时间一长,教学就没法进行了。这就是著名的ANR问题。不知道这样比喻,小伙伴们能不能理解线程切换的意思和ANR的意思。如果还不能理解,那么你来砍我吧~

Looper和ANR

很多面试官喜欢问,Looperloop方法是个死循环,而loop方法又是运行在主线程的,主线程上有死循环为什么不会导致ANR存在呢?

其实这里面很有趣的一个点就是,很多小伙伴把Looperloop方法当做一个普通方法来看待,所以才会有这样的疑问。但是这个loop方法并不是一个普普通通的方法。

我们先思考一点,如果我们写一个app,里面一行代码也不写的话,app会不会崩溃?

答案显而易见,是不会的。

可是在上面提到了,本质上App就是一个Java程序,Java程序就有main方法,在ActivityThread类中也确实有这个main方法。我们一般写java程序的时候,是不是main方法中的代码执行完,程序也就结束了。但是app并没有,只要你不退出,它一直运行。那这是为什么呢?

很多小伙伴应该想到了,没错,让程序不退出的话,写一个死循环,那么main方法中的代码永远不会执行完,这样程序就不会自己退出了。Android当然也是这么干的,而且不止Android,基本上所有的GUI程序都是这么干的。正是因为Looper.loop方法这个死循环,它阻塞了主线程,所以我们的app才不会退出。那你可能有疑问了,那既然这里有死循环了,那我其他的代码怎么运行?界面交互怎么办?你问到点子上了。

本质上Android就是事件驱动的程序,界面刷新也好,交互也好,本质上都是事件,这些事件最后通通被作为了Message发送到了MessageQueue中。由Looper来进行分发,然后在进行处理。用人话来说就是,我们的Android程序就是运行在这个死循环中的。一旦这个死循环结束,app也就结束了。

那么ANR是什么呢?ANRApplication Not Responding也就是Android程序无响应。为什么没响应呢?因为主线程做了耗时操作啊。可我还是不明白,明明Looperloop方法就是阻塞了主线程,为什么不ANR呢。那我们就来说道说道,什么是响应?响应就是界面的刷新,交互的处理等等对吧。那么这个响应是谁来响应的?没错,就是loop方法中进行响应的。没响应什么意思?就是loop方法中被阻塞了,导致无法处理其他的Message了。

所以结论就来了,主线程做耗时操作本质上不是阻塞了主线程,而是阻塞了Looperloop方法。导致loop方法无法处理其他事件,导致出现了ANR事件。对比小明这个比喻的话,就是因为小明在办公室里没完没了的改作业,占用了老师的时间,让老师没法批改下一个同学的作业,才导致了教学活动无法正常进行。而老师不断的批改作业,这本身就是正常的教学活动,也正是因为老师不断批改作业,同学们才有提高,教学才能继续。

Handler在Java层几个需要注意的点

  1. 子线程Looper

    如果要创建Handler,必须通过Looper.prepare()方法创建Looper,在主线程中ActivityThread已经帮我们创建好了,我们不需要自己去创建,但如果在子线程中创建Handler,要么使用Looper的mainLooper,要么自己调用Looper.prepare()方法创建属于这个线程的looper对象。如下是创建了一个子线程的Looper对象:

    class LooperThread extends Thread {
        public Handler mHandler;
        public void run() {
            Looper.prepare();  
            mHandler = new Handler() {  
                public void handleMessage(Message msg) {
                    //TODO 定义消息处理逻辑. 
                }
            };
            Looper.loop(); 
        }
    }
    
  2. 消息池

    在生成消息的时候,最好是用 Message.obtain() 来获取一个消息,这是为什么呢?

    // Message.java
    
    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();
    }
    

    可以看到,obtain方法是将一个Message对象的所有数据清空,然后添加到链表头中。sPool就是个消息池,默认的缓存是50个。而且在Looper的loop方法中最后一行是这样的

    msg.recycleUnchecked();
    

    Looper在分发结束以后,会将用完的消息回收掉,并添加到回收池里。

  3. Handler导致的内存泄露问题

    什么是内存泄露?简而言之就是该回收的东西没有回收。在Handler中一般是这样使用:

    class HandlerActivity: AppCompatActivity() {
    
        private val mHandler = MyHandler()
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            // 在子线程中通过自定义的 Handler 发消息
            thread {
                 mHandler.sendEmptyMessageDelayed(1, 1000)
            }
        }
    
        // 自定义一个 Handler
        class MyHandler: Handler() {
            override fun handleMessage(msg: Message) {
                Log.i("HandlerActivity", "主线程:handleMessage: ${msg.what}")
            }
        }
    }
    

    乍一看没有问题,但是有没有想过一个问题,就是说再发送延时消息之前,app推出了,那么handleMessage方法还会执行吗?答案是会的。为什么?我明明退出了,为什么还会执行呢?其实这和java有关系

    MyHandlerHandlerActivity 的内部类,会持有 HandlerActivity 的引用。

    在进入页面以后,发送了一个延时 1s 的消息,如果 HandlerActivity 在 1s 内退出了,由于 Handler 会被 Message 持有,保存在其 target 变量中,而 Message 又会被保存在消息队列中,这一系列关联,导致 HandlerActivity 在退出的时候,依然会被持有,因此不能被 GC 回收,这就是内存泄漏!当这个 1s 延时的消息被执行完以后,HandlerActivity 会被回收。

    虽然最终结果还是会被回收,但是内存泄露问题我们也必须去解决,如何解决?

    1.将 MyHandler 改为静态类,这样它将不再持有外部类的引用。可以将 HandlerActivity 作为弱引用放到 MyHandler 中使用,页面退出的时候可以被及时回收。

    2.页面退出的时候,在 onDestroy 中,调用 HandlerremoveMessages 方法,将所有的消息 remove 掉,这样也能消除持有链。

  4. 同步消息屏障

    什么是同步消息屏障?

    在Looper的loop方法中通过Message msg = queue.next();这么一行代码拿到Message进行分发,这个MessageQueue的next方法中有这么一行:

    //MessageQueue.java
    //省略部分代码
    Message 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;
                    //1 这一行很关键,同步消息屏障的关键点所在
                    if (msg != null && msg.target == null) {
                        do {
                            prevMsg = msg;
                            msg = msg.next;
                        } while (msg != null && !msg.isAsynchronous());
                    }
                   
                }
    
            }
        }
    

    注释1下面的这一行代码,首先会判断msg不为null,然后紧接着判断msgtarget为null。我们知道messagetarget就是发送它的handler,所有的message都有一个handler,这里怎么可能没有handller呢?针对同步消息还真的是所有的message都有handler,而这里是异步消息。满足target == null的消息就是异步消息同步屏障是用来阻挡同步消息执行的。说得好,那么同步屏障有什么用呢?

    似乎在日常的应用开发中,很少会用到同步屏障。那么,同步屏障在系统源码中有哪些使用场景呢?Android 系统中的 UI 更新相关的消息即为异步消息,需要优先处理。简而言之,如果在启动绘制之前,用户(开发者)插入了一个非常耗时的消息到队列中,那就会导致 UI 不能按时绘制,导致卡顿掉帧。,同步消息屏障就可以用来保证 UI 绘制的优先性。

Handler在C++层

如果你的目标是理解Handler在Java层是如何实现的,下面就不需要看了。下面主要讲解Handler在C++层是如何工作并实现的。

首先,细心的小伙伴们可能会有疑问。Looper一直处于死循环中,就像老师一直不断的问学习委员要作业批改,老师也是人,不会累么?你问对了,老师当然不会一直不断的问学习委员要作业,正常情况下,是有人交了作业以后,学习委员送过来,老师才会去批改。没有作业的时候,老师可能在休息,可能在玩游戏**。Looper也是一样,在消息队列为空的时候,Looper实际上处于休眠状态,只要当有Handler发送消息的时候,Looper才会被唤醒,去进行分发消息**。那么是怎么实现的呢?

在整个消息机制中,MessageQueue是连接Java层和Native层的纽带,换言之,Java层可以向MessageQueue消息队列中添加消息,Native层也可以向MessageQueue消息队列中添加消息。

这是MessageQueue中的Native方法:

// MessageQueue.java

private native static long nativeInit();
private native static void nativeDestroy(long ptr);
private native void nativePollOnce(long ptr, int timeoutMillis); 
private native static void nativeWake(long ptr);
private native static boolean nativeIsPolling(long ptr);
private native static void nativeSetFileDescriptorEvents(long ptr, int fd, int events);

在MessageQueue的构造方法中是这样的:

//MessageQueue.java
MessageQueue(boolean quitAllowed) {
        mQuitAllowed = quitAllowed;
        mPtr = nativeInit();
}

调用了nativeInit方法,在native层创建了native层的MessageQueue,mPtr是保存了NativeMessageQueue的指针,后续的线程挂起和线程的唤醒都要通过这个指针来完成,其实就是通过Native层的MessageQueue来完成。

//android_os_MessageQueue.cpp
static jlong android_os_MessageQueue_nativeInit(JNIEnv* env, jclass clazz) {
    //初始化native消息队列
    NativeMessageQueue* nativeMessageQueue = new NativeMessageQueue();
    nativeMessageQueue->incStrong(env); //增加引用计数
    return reinterpret_cast<jlong>(nativeMessageQueue);
}

这个是NativeMessageQueue的构造方法:

//android_os_MessageQueue.cpp
NativeMessageQueue::NativeMessageQueue()
            : mPollEnv(NULL), mPollObj(NULL), mExceptionObj(NULL) {

    mLooper = Looper::getForThread(); //功能类比于Java层的Looper.myLooper();
    if (mLooper == NULL) {
        mLooper = new Looper(false); //创建native层的Looper
        Looper::setForThread(mLooper); //保存native层的Looper到TLS,功能类比于Java层的ThreadLocal.set();
    }
}

Looper的构造方法是这样的:

//Looper.cpp
Looper::Looper(bool allowNonCallbacks) :
        mAllowNonCallbacks(allowNonCallbacks), mSendingMessage(false),
        mPolling(false), mEpollFd(-1), mEpollRebuildRequired(false),
        mNextRequestSeq(0), mResponseIndex(0), mNextMessageUptime(LLONG_MAX) {
    mWakeEventFd = eventfd(0, EFD_NONBLOCK); //构造唤醒事件的fd
    AutoMutex _l(mLock);
    rebuildEpollLocked();  //重建Epoll事件
}

void Looper::rebuildEpollLocked() {
    if (mEpollFd >= 0) {
        close(mEpollFd); //关闭旧的epoll实例
    }
    mEpollFd = epoll_create(EPOLL_SIZE_HINT); //创建新的epoll实例,并注册wake管道
    struct epoll_event eventItem;//新建唤醒监听事件
    memset(& eventItem, 0, sizeof(epoll_event)); //把未使用的数据区域进行置0操作
    eventItem.events = EPOLLIN; // 设置监听内容可读事件
    eventItem.data.fd = mWakeEventFd;
    //将唤醒事件(mWakeEventFd)添加到epoll实例(mEpollFd)
    int result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mWakeEventFd, & eventItem);

    for (size_t i = 0; i < mRequests.size(); i++) {
        const Request& request = mRequests.valueAt(i);
        struct epoll_event eventItem;
        request.initEventItem(&eventItem);
        //将request队列的事件,分别添加到epoll实例
        int epollResult = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, request.fd, & eventItem);
    }
}

等等,你上来给我这一大段C++代码,我怎么可能看得懂。还有这个epoll是什么?不是讲如何Looper怎么休眠和唤醒的么?

没错,就是讲的Looper怎么休眠和唤醒的。Looper的休眠和唤醒都是在Native层实现的,实现的原理是Linux上的epoll机制。

什么是epoll机制呢?

epoll你可以简单的理解为一个监听事件,在Linux上通过epoll机制监听一个事件,没什么事的时候我就让出CPU,进行休眠,当这个事件触发的时候我就从沉睡中唤醒开始处理。就像按钮的点击事件一样,点击了,监听到这个点击事件就会触发按钮的onClick方法。不过在LInxu上是通过文件的读写来完成的。类比于

include <sys/epoll.h>

// 创建句柄 相当于初始化onClickListener
int epoll_create(int size);
// 添加/删除/修改 监听事件  相当于addOnClicklistener
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
// 进入等待  这就相当于onCLick方法了
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

epoll_wait就类似于Java中的onCLick方法,当监听的文件有变化的时候,它就会收到结果。其实更像是Kotlin协程中的suspend方法,就一直在等着,阻塞着,有结果才会进行下一步。onClick方法是使用接口回调的形式来实现的,是非阻塞的。而epoll_wait方法是阻塞的。

在上面的Looper构造方法中,调用了rebuildEpollLocked方法,这个方法就是设置监听器的,可以理解为setOnClickListener,不过它监听的是文件的可读事件。 即eventItem.events = EPOLLIN;这行代码。什么是可读事件?就是说,文件里面有内容了是不是就可以读了,没错就是这样喵~

好了,事件也已经监听了,那么Looper是在哪沉睡的呢?

是在MessageQueue中的这行代码:

//MessageQueue.java
Message next() {
    final long ptr = mPtr;
    if (ptr == 0) {
        return null;
    }

    for (;;) {
        ...
        nativePollOnce(ptr, nextPollTimeoutMillis); //阻塞操作
        ...
}

就是通过这行代码进行阻塞操作。

调用关系是这样的:

MessageQueue::nativePollOnce ->NativeMessageQueue::pollOnce()->Looper::pollOnce()->Looper::pollInner

int Looper::pollInner(int timeoutMillis) {

   

    struct epoll_event eventItems[EPOLL_MAX_EVENTS];
    
    //1. 等待事件发生或者超时,如果nativeWake()方法中向管道写端写入字符,则该方法会返回; 
    int eventCount = epoll_wait(mEpollFd.get(), eventItems, EPOLL_MAX_EVENTS, timeoutMillis);

    mPolling = false;

    mLock.lock();

    if (mEpollRebuildRequired) {
        mEpollRebuildRequired = false;
        rebuildEpollLocked();
        goto Done;
    }

    if (eventCount < 0) {
        if (errno == EINTR) {
            goto Done;
        }
        result = POLL_ERROR;
        goto Done;
    }

    if (eventCount == 0) {
        result = POLL_TIMEOUT;
        goto Done;
    }

    //循环遍历,处理所有的事件
    for (int i = 0; i < eventCount; i++) {
        int fd = eventItems[i].data.fd;
        uint32_t epollEvents = eventItems[i].events;
        //唤醒事件
        if (fd == mWakeEventFd.get()) {
            if (epollEvents & EPOLLIN) {
                ////已经唤醒了,则读取并清空管道数据【7】
                awoken();
            } else {
                ALOGW("Ignoring unexpected epoll events 0x%x on wake event fd.", epollEvents);
            }
        } else {
            // 处理其他事件,Handler没有
            // 省略一些代码...
        }
    }
Done: ;

    //省略一些代码...
    
    // Release lock.
    mLock.unlock();
    //省略一些代码...  
    return result;
}

代码到了注释1处就开始了阻塞,也就是所谓的休眠。那么什么时候才能唤醒它呢?超时了,或者文件发生了变化,可以读了就可以唤醒了。注意,这个超时就是在Java层设置的延时发送,也就是说Java的sendMessageDelayed方法最后是通过epoll设置超时的机制实现延迟发送的。

不知道大家注意到没有,在我们发送Message的时候有这么一行代码:

// MessageQueue.java

boolean enqueueMessage(Message msg, long when) {

    // 省略一些代码...
    
    synchronized (this) {
     
        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 {
            //消息插到队列中间
            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.next
            prev.next = msg;
        }

        if (needWake) {
            // 敲黑板划重点:唤醒
            nativeWake(mPtr);
        }
    }
    return true;
}

在最后nativeWake(mPtr);这行代码进行了唤醒。不过必须neekWaketrue的时候才会唤醒,那么neekWake什么时候才是True呢?

两种情况会唤醒线程:

  1. 队列为空 || 消息无需延时 || 或消息执行时间比队列头部消息早) && (线程处于挂起状态时(mBlocked = true)
  2. 【线程挂起(mBlocked = true)&& 消息循环处于同步屏障状态】,这时如果插入的是一个异步消息,则需要唤醒。

唤醒操作具体是如何去做的?

调用链是这样的:

MessageQueue::nativeWake--->android_os_MessageQueue_nativeWake()--->NativeMessageQueue::wake()--->Looper::wake()

//Looper.cpp
void Looper::wake() {
    uint64_t inc = 1;
    // 向管道mWakeEventFd写入字符1
    ssize_t nWrite = TEMP_FAILURE_RETRY(write(mWakeEventFd, &inc, sizeof(uint64_t)));
    if (nWrite != sizeof(uint64_t)) {
        if (errno != EAGAIN) {
            ALOGW("Could not write wake signal, errno=%d", errno);
        }
    }
}

前面说了,epoll_wait在监听文件的可读事件,那么现在有消息来了,我要触发这个事件只需要往文件里随便写点什么就可以,Looper里面只是写了一个字符1。成功的唤醒了线程。然后开始轮询取出消息分发。

总结

Handler在C++层也有自己的一套消息轮询机制,和Java的基本一样,这里就不做分析了。

Handler是构成整个Android系统的基础,正是Looper的死循环才让Android程序能够不退出。所有的类似于屏幕刷新,UI互动都是一种事件,通过Handler发送给了Looper来进行分发。整个Android程序可以说就是运行在这个死循环中。

Looper就是不断批改作业的老师,MessageQueue就是催你交作业的学习委员,Message就是作业,上面写了写作业人的名字,Handler就是写作业的小明。

在一个线程中只能有一个Looper,也只能有一个MessageQueue,但是可以有多个HandlerMessageQueue也可以处理多个Handler发来的消息。

Looper的唤醒与挂起是靠Linux中的epoll机制来实现的,通过对文件的可读事件的监听来实现唤醒。

整个过程中,MessageQueue是实现Java层与C++层的互动的纽带,Native方法基本都是靠MessageQueue来实现的。

Handler与线程的绑定是依靠ThreadLocal中的map来实现的。另外,消息处理流程是先处理Native Message,再处理Native Request,最后处理Java Message。理解了该流程,也就明白有时上层消息很少,但响应时间却较长的真正原因。

参考并且致谢:

开发者的猫

gityuan