Java教程

Handler机制源码分析笔记

本文主要是介绍Handler机制源码分析笔记,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

前言

Handler机制作为Android Framework层的基础,很多问题需要研究一下源码才可以弄清楚,如果只是停留在对于一些面试答案的背诵上是没有更好的代码理解的。所以我想结合面试问题来研究Handler源码。

文章内容主要分成以下几个方面:

  1. Handler机制大家多多少少都使用过,所以先分析Handler发送消息的尽头,也就是MessageQueue#enqueueMessage方法
  2. 在分析MessageQueue中处理消息的类型(同步,异步,屏障消息)以及next方法
  3. 最后以结合面试问题的形式,再对Handler机制中的细节进行分析。

添加消息 MessageQueue#enqueMessage

Handler中所有发送消息的方法都都最终会调用到Handler#enqueueMessage:

//Handler#enqueueMessage
private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
        long uptimeMillis) {
        // 指定需要发送的Message的接收对象Target为当前的Handler
    msg.target = this;
    msg.workSourceUid = ThreadLocalWorkSource.getUid();

    if (mAsynchronous) {
        msg.setAsynchronous(true);
    }
    //会调用 MessageQueue#enqueMessage
    return queue.enqueueMessage(msg, uptimeMillis);
}

我们一般在写Handler时,都会复写Handler中的空handleMessage方法。在Handler#enqueueMessage通过写message的target对象是当前的Handler就可以保证发送的Message被我们复写的方法处理。

// MessageQueue#enqueMessage

boolean enqueueMessage(Message msg, long when) {
    // ....

    // 线程同步操作
    synchronized (this) {
        //....

        // mMessages是一个没有头节点的单链表,enqueMessage时,通过Message中时间顺序(when)
        // 将Message加入mMessage中
        Message p = mMessages;
        boolean needWake;

        // ------- 插入到开头 --------
        if (p == null || when == 0 || when < p.when) {
            // Message加入
            // 在以下情况下,使用头插法,建立新的头结点
            // 1\. 队列(单链表)为空
            // 2\. when=0,时间最前面
            // 3\. 当前队列的头结点的时间小于待插入的Message

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

        // 注意这个点:nativeWake 和 nativePollOnce相互配合实现被阻塞的线程苏醒
        if (needWake) {
            nativeWake(mPtr);
        }
    }
    return true;
}

从MessageQueue的关键方法enqueMessage中,我们可以得到以下关键信息 mMessage在数据结构上是一个单链表的形式,但是MessageQueue会根据Message的时间进行排序。使用的是头插法,when越小,时间越提前

消耗消息 MessageQueue#next

消息类型

Message消息分成三种:同步、异步、屏障消息。

同步消息和异步消息

我们通常使用的消息都是同步消息:

// Handler#constructor
public Handler(@NonNull Looper looper) {
    //指定async字段为false
    this(looper, null, false);
}

我们常规构建的Handler都会指定async字段为false,这样就会间接修改全局变量mAsynchronous ,一条同步消息被添加到队列中。

// Handler#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);
}

发送异步消息的话就需要指定async为true。

屏障消息

同步和异步消息都会指定message.target处理对象,但是屏障消息会设置target为空。所以屏障消息可以理解为是消息队列中一个特殊的节点。

屏障消息会妨碍 MessageQueue#next取出同步消息。但是不会阻止取出异步消息。异步消息就是在存在消息屏障的时候有更搞的优先级。

既然MessageQueue在形式上可以看做是一个根据时间排序的优先级队列,同步消息根据时间排队势必会影响消息的处理,下面引用Handler之消息屏障你应该知道的中的一句话来说明同步消息的阻塞对于Android系统的影响:

在Android系统中存在一个VSync消息,它主要负责每16ms更新一次屏幕展示,如果用户同步消息在16ms内没有执行完成,那么VSync消息的更新操作就无法执行在用户看来就出现了掉帧或卡顿的情况,为此Android开发要求每个消息的执行需要限制在16ms之内完成。但是消息队列中可能会包含多个同步消息,假如当前主线程消息队列有10个同步消息,每个同步消息要执行10ms,总共也就需要执行100ms,这段时间内就会有近7帧无法正常刷新展示,应用执行过程中遇到这种情况还是很普遍的。

MessageQueue#next

Message next() {

    // ... 
    // ------------- 取消息循环 --------------
    int nextPollTimeoutMillis = 0;
    for (;;) {

       //....
       // ------------- 休眠 ----------------
        nativePollOnce(ptr, nextPollTimeoutMillis);

        synchronized (this) {

           // ....
           // ------------- 取消息的逻辑 ----------------
    }
}

MessageQueue#next的逻辑还是非常清楚的,主要分成这样几个部分。

取出消息

### MessageQueue#next
Message next() {

    // .....
    int nextPollTimeoutMillis = 0;
    for (;;) {

       //....
        nativePollOnce(ptr, nextPollTimeoutMillis);

        synchronized (this) {

            Message prevMsg = null;
            Message msg = mMessages;
            // msg.target == null 是Message为屏障消息的特征
            // 当前的msg 是MessageQueue中的第一个对象
            // 如果第一个对象是屏障消息,找异步消息
            if (msg != null && msg.target == null) {
                do {
                    prevMsg = msg;
                    msg = msg.next;
                    // 找异步消息
                } while (msg != null && !msg.isAsynchronous());
            }

            // 不管是同步还是异步消息
            if (msg != null) {
                // 如果可以取出这个消息
                if (now < msg.when) {
                   // 阻塞线程 直到时间
                   nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                } else {
                    // Got a message.
                    mBlocked = false;
                    // 取出消息
                    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;
                }
            } else {
                // No more messages.
                nextPollTimeoutMillis = -1;
            }

           // 和Idlehandler有关
    }
}

Message#next在取出消息时会检查是否有屏障消息的存在,如图所示,如果存在屏障消息就不会优先处理同步消息,会先出处理异步消息。

当取出了消息,但是消息没有到时的话,会通过nativePollOnce方法暂时阻塞线程。这个方法和MessageQueue#enqueMessage方法中的nativeWake相互配合。这两个方法都是native方法,ativePollOnce使用Linux中的epoll机制监听文件描述符,而nativeWake就写入这个文件描述符。

复用消息

Message在Looper#loop中被取出来之后,会被复用:

msg.recycleUnchecked();
// MessageQueue#recycleUnchecked
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的字段被重新初始化并且被放入了复用池sPool中:

private static Message sPool;

Message本身也可以被理解为一个链表的结构,sPool就是Message链表的头结点。我们在使用Message时,应该尽量使用obtain方法复用在Message池中的对象,这样一来可以尽量避免创建、销毁对象带来的内存抖动。

// Message#obtain
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();
}

带着问题看源码

Q1: 同一线程中创建的Handler共用一个MessageQueue吗?

A1: 是的。 关于这个问题的回答,需要查看一下Looper#prepare:

Looper#prepare

private static void prepare(boolean quitAllowed) {
    if (sThreadLocal.get() != null) {
        // 在ThreadLocal中找,如果重复创建了Looper会抛出异常
        throw new RuntimeException("Only one Looper may be created per thread");
    }
    // 创建一个新的Looper并且设置到ThreadLocal中
    sThreadLocal.set(new Looper(quitAllowed));
}

ThreadLocal可以认为是一个这样的Map结构: Map<Thread, Looper> 不同的线程对应的Looper对象(如果创建了)是不同的实例。通过Looper的构造方法,每个Looper创建时都会实例化新的的MessageQueue对象。

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

因此Looper、MessageQueue和Thread三者都是一一对应的关系。一个线程只有一个Looper。 更重要的是,Looper#prepare中会检查,如果重复创建了Looper,会抛出异常。这样一来可以保证一个Thread对应一个Looper。

扩展问题:MessageQueue如何保证线程安全?

  1. 如果是没有指定Looper来创建Handler,每一个Handler都会使用当前线程相关的Looper,从而有了自己独立的MessageQueue。这样同一个子线程中创建的所有Handler都共用了一个MessageQueue,串行的情况下不用考虑线程安全的问题。
  2. 如果指定了Looper,但是Handler是在不同的子线程中创建的,这个时候需要考虑线程安全。主要分析MessageQueue#enqueueMessage和MessageQueue#next:
//MessageQueue#enqueueMessage
boolean enqueueMessage(Message msg, long when) {

    // .....
    synchronized (this) {
    // .....
    }
    return true;
}

// MessageQueue#next
Message next() {

    // ....
    for (;;) {
       //...
        synchronized (this) {
            // ...
        }
    }
}

这两个方法都使用了synchronized,锁的对象是MessageQueue自身。因为这些Handler都共用了同一个Looper,也就是共用了同一个MessageQueue,这样添加Message时可以保证写入和读取按照设置的时间先后顺序。

扩展思考:保证了线程安全的同时,消息即时性很难保证

Handler中发送消息在某个delay之后到达的方法都是使用了当前的时间+delay时间:

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

如果没有指定具体的时间,就会设置delay为0:

public final boolean post(@NonNull Runnable r) {
   return  sendMessageDelayed(getPostMessage(r), 0);
}

MessageQueue会根据时间先后排序这些消息。但是因为MessageQueue可能会被不同线程的,持有了同一Looper对象的Handler访问,因为锁机制,后到的访问者需要排队,所以这种即时性是不能保证的。会在具体设置的时间稍后。

Q2: Handler的内存泄漏及其处理?

A1: Handler造成的内存泄漏应该从两个方面来思考

  1. Handler如果是Activity中的一个非静态的内部类,就回隐式持有外面Activity的引用
  2. Handler#postDelayed等方法发送了很长时间之后执行的Message。

主要是说一下第二种问题:

// Handler#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);
}

从上述代码中可以看出,Message会指定Target为当前的Handler。所以Message持有Handler的引用,并且被放入了MessageQueue中等待唤醒。结合Q1和Q2两个问题,Handler处理机制中重要的几个类,以及它们之间对应的关系如图所示:

Handler机制设计的重要方法也归纳在下图中:

MessageQueue中可以有多个Message,Message在被放入MessageQueue(Handler#enqueMessage)时会指定handler对象。 一个Thread在ThreadLocal的管理下,只会有一个Looper,Looper通过Looper#prepare创建了一个MessageQueue。Looper通过loop开始循环,取出消息(MessageQueue#next)。

处理方式

  1. 将Handler独立出来成一个类,使用static修饰,并且使用弱引用修饰Activity
  2. 在Activity生命周期中移除所有的Message和Callback

具体的代码可以参考:

private CurHandler handler = new CurHandler(this);

private static class CurHandler extends Handler {
    WeakReference<Activity> outer;
    public CurHandler(@NonNull Activity outer){
        super(outer.getMainLooper());
        this.outer = new WeakReference<Activity>(outer);
    }

    @Override
    public void handleMessage(@NonNull Message msg) {
        super.handleMessage(msg);
    }
}

@Override
protected void onDestroy() {
    super.onDestroy();
    handler.removeCallbacksAndMessages(null);
}

Q3: 子线程中如何创建Handler?

这个是一个老生常谈的问题,我们在Activity中使用Handler时,如果使用匿名内部类继承Handler,使用不写任何构造函数的方法(这个构造函数Android Studio会显示不推荐,最好是使用指定Looper的方式,避免错误),会获取当前线程的Looper。如果是在Activity中,Activity的Looper已经在ActivityThread#main中进行了prepare和loop。所以在子线程中也要prepare和loop。

// 开启一个子线程,使用Kotlin自带的方法,会自动创建并start thread
 thread {
        // Case#1
        // 运行在mainLooper上
        val handlerMain = object: Handler(mainLooper){
            override fun handleMessage(msg: Message) {
                when(msg.what){
                    TEST_HANDLER -> Log.d("TEST Main", "收到通知")
                    else -> {}
                }
            }
        }

        // Case#2 运行在线程指定的Looper上
        Looper.prepare()

        val handlerOther = object: Handler(Looper.myLooper()!!){
            override fun handleMessage(msg: Message) {
                when(msg.what){
                    TEST_HANDLER -> Log.d("TEST other", "收到通知")
                    else -> {}
                }
            }
        }
        handlerOther.sendEmptyMessage(TEST_HANDLER)
        Looper.loop()

        Looper.myLooper()!!.quitSafely()
    }
}

这个地方还要记得退出Looper,因为Looper#loop线程主要负责消息循环,其中使用了for(;;)维持运转,在没有消息的时候进入阻塞状态。在这里使用quit退出for循环,释放子线程的相关资源。

Q4: loop的for循环为什么不会造成ANR?

A4: 首先看看官网对于ANR的定义

虽然Looper中的loop是死循环,回忆之前说过的Message#next,在线程没有接收到消息时进入阻塞状态,但是如果用户继续有时间输入,消息队列还是会进行处理:

for (;;) {
    Message msg = queue.next(); // might block
    if (msg == null) {
        // No message indicates that the message queue is quitting.
        return;
    }
    //.....
}

总结来说,Looper#loop中的for循环不会造成主线程无法处理消息,反倒是Handler的消息机制需要for循环来不断尝试从消息队列中取出消息。

这篇关于Handler机制源码分析笔记的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!