Android 消息队列机制

在 Android 系统中,非主线程不可更新 UI。因而经常在自线程中使用 mHandler.post{} 的方式,将 UI 更新操作 post 到主线程中去执行。这个 post 操作,则是所谓的 Android 消息队列机制。

Handler —— 方便 api 调用的入口

通过查看 Handler() 构建方法的源码,可以发现有以下成员变量的初始化:

// Looper
mLooper = Looper.myLooper();
// MessageQueue
mQueue = mLooper.mQueue;
// 自定义 callback,用于处理 sendMessage 接口发送的 Message
mCallback = callback;
// boolean 类型
mAsynchronous = async;

Handler 常用的几个方法: boolean post(@NonNull Runnable r)boolean sendMessage(@NonNull Message msg)void removeCallbacksAndMessages(@Nullable Object token) 。追踪这几个方法的调用链,发现最后都是形成一个 Message,并将其加入 MessageQueue(或从中移除)

至此,消息队列中,最重要的类出现了:

HandlerMessage

以及隐藏在handler 中的: LooperMessageQueue

实际功能上,MessageQueue 来源于 Looper (mQueue = mLooper.mQueue),而 Message 由是被 enqueue 到 MessageQueue 中。那么消息队列的基础应该是 Looper。

Looper —— 与线程一对一绑定的传送带

在 Handler 中初始化 Looper时,还伴随着一个判空的 Exception

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

此处提到了 Looper 的两个方法 —— Looper.myLooper 和在 Exception 提示中提到的 Looper.prepare

但细看两个方法,也都很简单。

public static @Nullable Looper myLooper() {
    // 从 sThreadLocal 中取出了当前 thread 对应的 Looper
    return sThreadLocal.get();
}


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

// 主线程对应的 mainLooper 不可退出;其他线程对应的 Looper 都可退出
private static void prepare(boolean quitAllowed) {
    if (sThreadLocal.get() != null) {
        throw new RuntimeException("Only one Looper may be created per thread");
    }
    // 新建一个 Looper 并存储
    sThreadLocal.set(new Looper(quitAllowed));
}

private Looper(boolean quitAllowed) {
    // 初始化 MessageQueue
    mQueue = new MessageQueue(quitAllowed);
    // 绑定 thread
    mThread = Thread.currentThread();
}

由此可见,Looper.prepare() 绑定的是当前线程。且如果已经绑定过 Looper,再次绑定,会抛异常

但除此之外,Looper 也没有明显的逻辑了?

可以通过 HandlerThread 这个 Android 系统自带类,来查看 Looper 的使用

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

在 prepare 之后,还调用了 Looper.loop()

以下代码省略了大部分非主线逻辑

public static void loop() {
  final Looper me = myLooper();
  ...
  for (;;) {
    // 死循环调用 loopOnce
    if (!loopOnce(me, ident, thresholdOverride)) {
      return;
    }
  }
}

private static boolean loopOnce(final Looper me,
                                final long ident, final int thresholdOverride) {
  // 从 MessageQueue 中获取下一个消息
  Message msg = me.mQueue.next(); // might block
  ...
  // 消息分发执行
  msg.target.dispatchMessage(msg);
  ...
  // 消息回收,清空消息内容并放回对象池
  msg.recycleUnchecked();

  return true;
}

loop 的逻辑也很简单,不断获取消息队列(MessageQueue)中的数据(Message)并分发,重点在于,当队列中没有数据的时候,next 是个什么状态?

那么继续看下一个类:MessageQueue

MessageQueue —— 消息队列

首先看 MessageQueue 的构建函数:

MessageQueue(boolean quitAllowed) {
  // 是否允许退出
  mQuitAllowed = quitAllowed;
  // native 函数调用
  mPtr = nativeInit();
}

MessageQueue中涉及了 native 函数的调用。参考资料:Native层的Looper和MessageQueue

又或者 IT 工具 查看native层源码 这篇博文学习如何查看 native 层源码实现。

此处就不粘贴 native 层源码了,只进行接口的简单说明

// 创建 NativeMessageQueue 实例,并返回句柄
private native static long nativeInit();
// 销毁实例
private native static void nativeDestroy(long ptr);
// NativeMessageQueue 通过 native 的 Looper 在 timeoutMillis 时段内,实现线程挂起,不阻塞
private native void nativePollOnce(long ptr, int timeoutMillis); /*non-static for callbacks*/
// 唤醒被挂起的线程
private native static void nativeWake(long ptr);

MessageQueue 的 next方法,除了通过 nativePollOnce 函数实现了线程的挂起,官方注释已经将逻辑阐述的很明了了,此处就不粘贴了。

队列的插入方法为 boolean enqueueMessage(Message msg, long when) ,同样的,官方有逻辑注释,需要注意的就是,在队列插入中,除了处理队列数据增删,还判断了当前队列是否在挂起中,或插入了新的队头,此时需要通过 nativeWake 唤醒阻塞

而 Handler 调用 removeCallbacksAndMessages 方法,也是调用到了MessageQueue 的 void removeCallbacksAndMessages(Handler h, Object object)。在这个方法中,对队列进行了便利,移除了对应 Handler 创建的 Message

Message 的组装发送 和 分发执行

mHandler.post{} 实质上,也是通过 getPostMessage 方法组装了一个 Message,然后拼接到 MessageQueue 中

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

// 构建 Message
private static Message getPostMessage(Runnable r) {
  // 从对象池中获取 message
  // 对象池的存在主要是为了避免内存抖动——大量的对象创建和销毁,在 java 虚拟机中会造成垃圾回收(GC)需要,GC 需要暂停所有指令(STW —— Stop the world),频繁 GC 容易产生用户能感知到的卡顿
  Message m = Message.obtain();
  // 将 post 的runnable 赋值给 message 的 callback
  m.callback = r;
  return m;
}

public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {
  if (delayMillis < 0) {
    delayMillis = 0;
  }
  // 将 delayMillis 的相对时间,转换为 SystemClock.uptimeMillis() + delayMillis 的绝对时间
  // SystemClock.uptimeMillis() 计算的是开机时间
  return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}

public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) {
  // 绑定线程的 MessageQueue
  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);
}

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);
  }
  return queue.enqueueMessage(msg, uptimeMillis);
}

从上面代码可知,Message 的 target 为 Handler。如果是调用的 mHandler.post{} 方法,message 持有 post 的 Runnable 作为 callback。

Looper.loopOnce() 方法中,消息分发执行的代码为:msg.target.dispatchMessage(msg); ,即调用 handler 的 dispatchMessage 方法中

public void dispatchMessage(@NonNull Message msg) {
  if (msg.callback != null) {
    // 如果是 post 调用,直接 run callback
    handleCallback(msg);
  } else {
    // handler 的 callback 处理
    if (mCallback != null) {
      // 如果 callback 接口方法返回了 true,则拦截
      if (mCallback.handleMessage(msg)) {
        return;
      }
    }
    // handler 的子类处理 message
    handleMessage(msg);
  }
}

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

消息队列小结

Handler 处理消息

  • post(Runnable) 通过 message 设置 callback
  • sendMessage 通过 Handler(@Nullable Callback callback, boolean async) 设置全局的 callback,或者定义 Handler 子类,并重写 private static void handleCallback(Message message) 方法

Handler 持有 Looper

  • 此 Looper 如果是入参,应传入执行线程的 Looper
  • 如果是无参构建,需要在执行线程构建(会自动获取当前线程的looper)。所以在自线程中,直接用 new Handler().post{} 并不能将消息 post 到主线程。

Looper 通过 ThreadLocal 结构,保证一对一绑定线程

  • 在已绑定 Looper 的线程中执行 Looper.prepare() 会抛异常
  • Looper.prepare() 之后,还需调用 Looper.loop() 进行启动
  • 多次在一个线程中调用 Looper.loop(),可能导致消息队列中的下一个消息,在 loop 调用的消息结束前,就被分发执行

Looper 拥有一个 MessageQueue 队列,用于存储消息

  • MessageQueue 在 enqueueMessage (插入)时,对 Message 进行了时间排序,并不是 FIFO(先进先出)

MessageQueue 中,通过 native 函数,控制挂起时长

  • 当队列为空时,mBlocked = true

  • 如果有消息,获取消息后,如果消息执行时间已经超过当前时间,立即执行,否则计算时间差,并挂起

常说 Android 的消息分发机制是基于生产者消费者模型的。可以将整个消息分发机制看成是一个车间,执行线程是消费者,发出消息的线程是生产者,Message 是需要加工的零件,MessageQueue 是承载零件的传送带,Looper 则是移动传送带的滚轮

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,723评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,003评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,512评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,825评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,874评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,841评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,812评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,582评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,033评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,309评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,450评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,158评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,789评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,409评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,609评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,440评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,357评论 2 352

推荐阅读更多精彩内容