Android必知必会——消息机制

1,263 阅读11分钟

概述

相信不管是出入Android,还是已开发多年的老司机们,肯定都对Android的Handler不会陌生,而它就是今天要介绍的Android消息机制中的一部分。

在Android系统中,有两大特色利剑:Binder IPC机制和消息机制。Android也由大量的消息驱动方式来交互,大到四大组件的工作流程,小到异步回调更新UI等等,各处都有消息机制的存在。

角色

在对消息机制进行分析之前,先来看一下消息机制中,都含有哪些角色以及他们各自的作用又是什么:

  • Message

    消息本体,一切逻辑都围绕它来展开。

  • MessageQueue

    消息队列,管理消息的入队和出队。

  • Handler

    消息机制的两端,可作为消息产生端,也可作为消息消费端。

  • Looper

    消息机制运转的动力,不断的循环执行,取出消息、分发消息。

他们之间的关系,可以通过一个简单图来表示一下:

Handler

在消息机制的四个角色中,我们经常使用和见到的就是Handler了,那就先从Handler看起。

构造

Handler有很多构造方法,但是可用开发中使用的只有如下几个:

  • Handler()

  • Handler(@Nullable Callback callback)

  • Handler(@NonNull Looper looper)

  • Handler(@NonNull Looper looper, @Nullable Callback callback)

这样来看无非是可设置两个参数:Looper和Callback。

如果不指定Looper,在构造时会通过Looper.myLooper()获取当前线程的Looper,如果当前线程没有Looper那么会抛出异常。

mLooper = Looper.myLooper();
if (mLooper == null) {
    throw new RuntimeException(
        "Can't create handler inside thread " + Thread.currentThread()
        + " that has not called Looper.prepare()");
}

至于Callback,后面会进行分析。

作为生产者生产Message

Handler的sendEmptyMessage、sendEmptyMessageAtTime、sendEmptyMessageDelayed、sendMessage和sendMessageDelayed,最终都会调用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);
}

这其中的uptimeMillis = SystemClock.uptimeMillis() + delayMillis,之所以采用SystemClock.uptimeMillis(),是因为它是已开机时间,而如果使用System.currentTimeMillis()在用户修改手机时间时,该值就会发生变化。

除了上面的sendMessageAtTime,还有一个特殊的方法sendMessageAtFrontOfQueue:

public final boolean sendMessageAtFrontOfQueue(@NonNull Message msg) {
        MessageQueue queue = mQueue;
        if (queue == null) {
            RuntimeException e = new RuntimeException(
                this + " sendMessageAtTime() called with no mQueue");
            Log.w("Looper", e.getMessage(), e);
            return false;
        }
        //固定uptimeMillis为0
        return enqueueMessage(queue, msg, 0);
    }

可以发现这两类方法最终都会通过调用方法:

private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
            long uptimeMillis) {
    //将msg的target属性指向当前Handler
    msg.target = this;
    msg.workSourceUid = ThreadLocalWorkSource.getUid();

    if (mAsynchronous) {
        msg.setAsynchronous(true);
    }
    //转到MessageQueue,消息入队
    return queue.enqueueMessage(msg, uptimeMillis);
}

作为消费者消费Message

当Looper在通过MessageQueue读取到下一条消息时,就会通过handler的dispatchMessage分发给目标Handler来消费这条消息:

public void dispatchMessage(@NonNull Message msg) {
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        if (mCallback != null) {
            //如果配置了Callback,就不再走Handler的handleMessage
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        handleMessage(msg);
    }
}

Looper

既然在创建Handler时需要制定或从当前线程获取Looper,那么接下来就看一下Looper。

构造

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

从构造方法可以看出:

1.私有方法,不允许外部直接通过构造方法创建

2.初始化时会初始化MessageQueue

3.初始化时会记录当前线程

在线程中创建Looper,可以使用prepare:

public static void prepare() {
    //必须可退出
    prepare(true);
}

//quitAllowed是否允许退出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));
}

Android系统在创建主线程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();
    }
}

Looper在创建成功后会通过ThreadLocal与当前线程绑定,并且从源码中可以看出,每个线程只能存在一个Looper。

开启死循环

Looper既然是通过死循环,为消息机制提供运转动力,那么在创建Looper之后,就要适时的开启死循环:

public static void loop() {
    final Looper me = myLooper();
    if (me == null) {
        throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
    }
    final MessageQueue queue = me.mQueue;
    //省略部分代码
    for (;;) {
        //从queue中取消息,可能会阻塞当前线程
        Message msg = queue.next();
        if (msg == null) {
            // 取出null,说明消息机制退出,那么跳出循环
            return;
        }
        final Printer logging = me.mLogging;
        if (logging != null) {
            logging.println(">>>>> Dispatching to " + msg.target + " " +
            msg.callback + ": " + msg.what);
        }
        
        //省略部分代码
        
        //取到了消息,分发消息
        msg.target.dispatchMessage(msg);
        
        //省略部分代码
        
        if (logging != null) {
            logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
        }
        
        //省略部分代码
        
        //回收这条消息
        msg.recycleUnchecked();
    }
}

MessageQueue

在Looper初始化时,会初始化MessageQueue,接下来就看一它有哪些内容。

构造

//quitAllowed是否允许退出
MessageQueue(boolean quitAllowed) {
    mQuitAllowed = quitAllowed;
    mPtr = nativeInit();
}

不允许退出(quitAllowed传false)会怎样:

void quit(boolean safe) {
    if (!mQuitAllowed) {
        throw new IllegalStateException("Main thread not allowed to quit.");
    }
    
    //省略部分代码
}

消息入队

在前面分析Handler时,最终发送消息都会通过MessageQueue的enqueueMessage:

boolean enqueueMessage(Message msg, long when) {
    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.");
    }

    synchronized (this) {
        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;
        }

        msg.markInUse();
        msg.when = when;
        //下一个准备要分发的消息
        Message p = mMessages;
        boolean needWake;
        if (p == null || when == 0 || when < p.when) {
            //没有准备要分发的消息
            //或者这条消息是sendMessageAtFrontOfQueue发送的 
            //或者这条消息要发送的时间比下一条要早
            //那么下一条就是你了
            msg.next = p;
            mMessages = msg;
            needWake = mBlocked;
        } else {
        
            //省略部分代码
            
        }

        //省略部分代码
        
    }
    return true;
}

消息出队

在Looper中,会通过死循环的方式调用queue.next()来获取下一条消息:

Message next() {

    //省略部分代码

    for (;;) {
        
        //省略部分代码
        
        synchronized (this) {
            final long now = SystemClock.uptimeMillis();
            Message prevMsg = null;
            Message msg = mMessages;
            
            //省略部分代码
            
            if (msg != null) {
                if (now < msg.when) {
                    
                    // 省略部分代码
                        
                } else {
                    // 取到了一个消息
                    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();
                    //返给looper
                    return msg;
                }
            }
            
            //省略部分代码
            
        }
    }
}

Handler引起的内存泄漏

众所周知,如果在Activity内部使用非静态内部类的形式声明Handler,就会默认持有Activity的引用,而进一步导致内存泄漏。

但这里有一个点需要知道,引起内存泄漏的真正原因是——Activity间接被GCRoots持有。当一个对象,被GCRoots直接或间接引用时,是不会被GC回收的。

那Handler背后的GCRoots又是哪位大仙呢?我们知道,当通过handler发送消息时,会将msg.target指向handler,在looper中循环遍历得到msg后,会通过msg.target.hanleMessage分发消息。而Looper是跟具体的某一个线程绑定的,如果我们在初始化Handler时,不传或传入mainLooper,那么looper绑定的就是主线程,主线程在程序运行过程中是不会退出的,那么就意味着如果队列中还存在此Handler发送出去的消息(可能是延迟消息还没到分发时间),消息就存在于messagequeue队列中,被Looper绑定的线程即主线程间接持有,而msg.target又指向了handler,handler持有Activity的引用,最终导致内存泄漏。

大致可以用下图示意:

解决办法:

  • 在Activity声明周期onDestroy的时候,通过handler.removeCallbacks(myRunnable),可以将msg.target为当前Handler,并且msg.callback == myRunnable的msg从队列中移除:

    //handler
    public final void removeCallbacks(@NonNull Runnable r) {
          mQueue.removeMessages(this, r, null);
    }
    
    //MessageQueue
    void removeMessages(Handler h, Runnable r, Object object) {
          if (h == null || r == null) {
              return;
          }
    
          synchronized (this) {
              Message p = mMessages;
    
              // Remove all messages at front.
              while (p != null && p.target == h && p.callback == r
                     && (object == null || p.obj == object)) {
                  Message n = p.next;
                  mMessages = n;
                  p.recycleUnchecked();
                  p = n;
              }
    
              // Remove all messages after front.
              while (p != null) {
                  Message n = p.next;
                  if (n != null) {
                      if (n.target == h && n.callback == r
                          && (object == null || n.obj == object)) {
                          Message nn = n.next;
                          n.recycleUnchecked();
                          p.next = nn;
                          continue;
                      }
                  }
                  p = n;
              }
          }
      }
    
  • 在Activity声明周期onDestroy的时候,通过handler.removeMessages(int what, @Nullable Object object),还可以将msg.what = what && msg.obj == object的消息移除

  • 如果发送的Msg比较多且情况各异(有的是runnable,有的有特定what,有的有特定的obj),那么可以通过静态内部类的方式,不让Handler持有外部Activity强引用,改用弱引用,这里不再进行代码示意

我是主线程,我要做的事很多,但有优先级机制

我们知道,在Android中,主线程不只是要完成开发者写代码逻辑需求,还要完成系统对它的指示,比如刷新页面。

对于同一个Looper来说,是可以同时存在多个Handler,可以同时向Looper中发送消息,这其中既有Android系统中定义的各种Handler,又有开发者编写的Handler,那么如何才能让MessageQueue首先将系统发布的msg分发出来,能够被率先执行呢?

MessageQueue.postSyncBarrier(long when)

往消息队列头部,放入一个系统的“告示”,告知MessageQueue接下来我会发送一些优先级高的指令,务必先执行我接下来的优先指令。

//类似于enqueueMessage,根据when合理的插入这个“告示”
//此方法被hide标记
//添加成功后会返回一个唯一的token,标识该屏障
public int postSyncBarrier() {
    return postSyncBarrier(SystemClock.uptimeMillis());
}

private int postSyncBarrier(long when) {
    synchronized (this) {
        final int token = mNextBarrierToken++;
        final Message msg = Message.obtain();
        //未给该msg设置target,是“告示”的身份特征
        msg.markInUse();
        msg.when = when;
        msg.arg1 = token;

        Message prev = null;
        Message p = mMessages;
        if (when != 0) {
            while (p != null && p.when <= when) {
                prev = p;
                p = p.next;
            }
        }
        if (prev != null) {
            msg.next = p;
            prev.next = msg;
        } else {
            msg.next = p;
            mMessages = msg;
        }
        return token;
    }
}

再来MessageQueue是怎么识别“告示”的:

Message next() {

    //省略部分代码
    
    for (;;) {
    
        //省略部分代码
    
        synchronized (this) {
            final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                Message msg = mMessages;
                if (msg != null && msg.target == null) {
                    // msg.target == null 说明是系统“告示”,让我先进行优先消息的分发
                    do {
                        prevMsg = msg;
                        msg = msg.next;
                        //当找到第一个msg.isAsynchronous() = true的消息时,就会跳出循环,首先分发这个消息
                    } while (msg != null && !msg.isAsynchronous());
                }
                
                //省略部分代码
                
        }
    
    //省略部分代码
    
    }

}

Message.setAsynchronous(true)

通过postSyncBarrier,系统告知MessageQueue接下来先执行的事,那么哪些才是要先执行的事呢?就是通过msg.setAsynchronous(true)方法,标记为true的事。

MessageQueue.removeSyncBarrier(int token)

系统除了可以在特定的场合(如刷新屏幕)添加同步屏障,告知MessageQueue先执行特定的优先级消息之外,还可以取消同步屏障,让MessageQueue回复正常排队执行。比如本来需要刷新下一帧,但是页面在下一帧刷新时间前被关闭了,那么就移除之前的“告知”。

一些思考

没有这个“告知”,始终在MessageQueue.next中判断msg.isAsynchronous为true,那么就优先分发它行不行?

如果没有这个添加“告知”和移除“告知”的存在,那么有些消息,包括系统发出的普通消息(msg.isAsynchronous = false),就可能永远不会被执行了(开发者把所有消息都进行msg.setAsynchronous(true))。必须要在合适的时机,让queue按照时间顺序,依次执行消息的分发,而不是始终将isAsynchronous标志放在第一位。

这里放出一个问题,可以思考一下:如果将Message的setAsynchronous进行hide处理,在MessageQueue.next中始终判断msg.isAsynchronous,优先分发msg.isAsynchronous == true的消息,又是否可行呢?

MessageQueue.IdleHandler

再来回看MessageQueue.next方法:

Message next() {

//省略部分代码

int pendingIdleHandlerCount = -1; 
int nextPollTimeoutMillis = 0;
for (;;) {

    if (msg != null) {
        if (now < msg.when) {
            //省略部分代码
        }else{
        
            //省略部分代码
        
            //如果找到了要分发的msg,并且到了分发时间,那么就返回给looper
            return msg;
        }
    }

    //省略部分代码
    
    //以下代码执行条件,是未找到下一条msg或下一条msg还未到分发时间
    
    if (pendingIdleHandlerCount < 0
                        && (mMessages == null || now < mMessages.when)) {
        pendingIdleHandlerCount = mIdleHandlers.size();
    }

    if (pendingIdleHandlerCount <= 0) {
        mBlocked = true;
        continue;
    }

    if (mPendingIdleHandlers == null) {
        mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
    }
    mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);

    for (int i = 0; i < pendingIdleHandlerCount; i++) {
        final IdleHandler idler = mPendingIdleHandlers[i];
        mPendingIdleHandlers[i] = null; // release the reference to the handler

        boolean keep = false;
        try {
            //执行idler.queueIdle,保存返回结果
            keep = idler.queueIdle();
        } catch (Throwable t) {
            Log.wtf(TAG, "IdleHandler threw exception", t);
        }

        if (!keep) {
            synchronized (this) {
                //如果不保留当前idler,那么移除
                mIdleHandlers.remove(idler);
            }
        }
    }    

}

}

再来看一下接口IdleHandler:

public static interface IdleHandler {
    boolean queueIdle();
}

使用场景:

  • 在Activity绘制完成后,做一些事情

  • 结合HandlerThread, 用于单线程消息通知器

关于使用场景,更详细的内容,可以参考这篇文章

死循环为什么不会阻塞App

阻塞App时,往往是不能在继续处理后续的逻辑,但是Android的消息机制,虽然是死循环,但是依然在有条不紊的接收和处理任务。这跟业务代码中,错误书写的一个局部小的死循环不同,正是由于存在这个死循环的存在,主线程才能一直轮询处理新的任务,保持应用的生机。

在消息机制中,取消息时,如果没有可分发消息或下一条要分发的消息还未到分发时间,就会进行适当的阻塞;在有消息传入时,会根据目标线程的阻塞状态,决定是否进行唤醒,已使其能够顺利的处理接下来的消息分发。

消息机制具体的native层机制,可跳转Gityuan大佬的文章——Android消息机制2-Handler(Native层)