Handler源码学习(三)MessageQueue入队插队

Handler源码学习(一)流程
Handler源码学习(二)Message对象池
Handler源码学习(三)MessageQueue入队插队

1.消息入队

消息队列与Message对象池的结构很像,也是通过对象之间通过next指向形成链表结构

这时候加入一个msg消息,先来看如果消息队列为空的情况

//判断消息队列为空时,会直接将这个msg赋值给mMessage,并将p赋值给msg.next,这时next当然时null
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 {
    // Inserted within the middle of the queue.  Usually we don't have to wake
    // up the event queue unless there is a barrier at the head of the queue
    // and the message is the earliest asynchronous message in the queue.
    needWake = mBlocked && p.target == null && msg.isAsynchronous();
    //首先声明了一个message对象prev,从字面意思理解是前一个message的意思
    Message prev;
    for (;;) {
      //接着进入一个死循环,将p赋值给prev,看看前面的代码可以知道,p指向的是mMessages,所以这里
      //是将prev指向了mMessage,再下一次循环的时候,prev则是指向了第二个message,依次类推
        prev = p;
      //接着将p指向p.next也就是mMessages.next,也就是消息链表中的第二个message
        p = p.next;
      //接下来判断两件事
      //1.p==null,说明没有下一个消息了,跳出循环
      //2.p!=null,并且当前需要入队的这个message的执行时间是小于队列中这个任务的执行时间的
      //也就是说这个入队的message需要比队列中的这个message先执行,也跳出循环
      //3.如果这两个条件都不满足的话,则继续跟队列中的下一个消息进行对比,直到满足条件,或者到
      //队列的末尾
        if (p == null || when < p.when) {
            break;
        }
        if (needWake && p.isAsynchronous()) {
            needWake = false;
        }
    }
  //跳出循环后做了两件事情
  // 1.将入队的这个消息的next指向循环中获取到的应该排在这个消息之后的message
    msg.next = p; // invariant: p == prev.next
  //将msg前面那个message.next指向了msg
    prev.next = msg;
  //到这里就将一个message完成了入队
  //入队的过程是线程安全的
}
MessageQueue入队.jpg
MessageQueue入队(2).jpg

2.取出消息

这里贴的不是完整代码,而是取出message的核心逻辑代码,这里其实分了两个部分,第一个部分是消息插队,这个在第三节叙述,第二个部分是正常的消息,其实取出消息的部分比较简单,注释也比较清晰

synchronized (this) {
    // Try to retrieve the next message.  Return if found.
    final long now = SystemClock.uptimeMillis();
    Message prevMsg = null;
  //队列的第一个message
    Message msg = mMessages;
  //正常取出消息
    if (msg != null) {
      //1.首先判断当前时间是否小于了msg的执行时间,
        if (now < msg.when) {
            // Next message is not ready.  Set a timeout to wake up when it is ready.
          //(翻译)需要执行的消息还没有到执行时间,设置一个唤醒时间,当到了执行时间时唤醒
            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;
    }

3.消息插队

首先需要知道的是整个android系统其实主要是依赖消息机制来处理事件,比如说点击事件等等,都是通过handler发送消息进行处理,但是这些系统消息相较于程序自己发送的消息,应该要优先执行,所以就涉及到了消息插队

  • isAsynchronous() — 如何将系统消息和程序发送的消息区分开来

在阅读Hanlder的源码的时候,可以看到一个@hide的构造方法,传入一个布尔值,从字面意思理解是异步

public Handler(boolean async) {
    this(null, async);
}
mAsynchronous = async;//赋值给mAsychronous
//消息入队的时候,msg.setAsynchronous(true);将这个msg标记为异步
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
        msg.target = this;
        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        return queue.enqueueMessage(msg, uptimeMillis);
    }

为什么要标记为异步呢?是要新开一个线程来执行么?可是最后也是发送到looper所绑定的消息队列中啊,貌似这里并没有异步,接着看上一节提到的消息插队的源码,在这里看到调用了一个方法msg.isAsynchronous(),貌似这个布尔值并不是跟异步有关系,而是将这个消息做了一个标记而已

前面的消息入队分析可以知道,虽然被标记了系统消息,但是还是按照消息队列的规则去入队,那么如何做到优先取出这个任务呢,一步步看

Message prevMsg = null;
Message msg = mMessages;
//1.判断当前第一个消息不为空
//2.重点来了,msg.target == null,如果这个消息的target为空,看过handler源码可以知道,
//只要是通过handler发送消息,就会将这个msg的target指向发送消息的handler,否则也无法处理这个消息
//那么为什么这里会出现target为空呢?先看if代码块里面的代码
if (msg != null && msg.target == null) {
    // Stalled by a barrier.  Find the next asynchronous message in the queue.
    //进入一个循环,注意while中的判断条件
    do {
      
      //不断地取下一个消息来匹配判断条件
        prevMsg = msg;
        msg = msg.next;
    } while (msg != null && !msg.isAsynchronous());
  //只有当msg不为空 并且当前的这个消息是异步的,也就是说是系统消息,则跳出循环。
  //跳出循环后就走到了正常取消息的代码中,取出的正是这个系统消息,发现插队就成功了
}

上面的分析已经插队成功,但是还有疑点,到底是怎么进入到if代码块中的

// Stalled by a barrier. Find the next asynchronous message in the queue.

作者在这里给了一条注释,通过一个阻塞块来停止正常的队列,找到队列中的第一个系统消息

在MessageQueue中试图搜索barrier,发现一个方法,

public int postSyncBarrier() {
        return postSyncBarrier(SystemClock.uptimeMillis());
    }
private int postSyncBarrier(long when) {
    // Enqueue a new sync barrier token.
    // We don't need to wake the queue because the purpose of a barrier is to stall it.
    synchronized (this) {
        final int token = mNextBarrierToken++;
        final Message msg = Message.obtain();
        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) { // invariant: p == prev.next
            msg.next = p;
            prev.next = msg;
        } else {
            msg.next = p;
            mMessages = msg;
        }
        return token;
    }
}

加入队列的代码已经很熟悉了,可以看到在这段代码中其实入队了一个没有target的消息,而最上面那个public方法中可以看到传入的when是当前的系统时间,也就是说如果调用这个方法会在消息队列的头部插入一个没有target的message,到这里思路就比较清晰了,但是这个消息肯定不能一直在队列中,否则整个队列的正常消息就永远无法处理,所以相对应还有一个remove。注释中说明,这个方法应该跟插入的方法匹配使用。

public void removeSyncBarrier(int token) {
    // Remove a sync barrier token from the queue.
    // If the queue is no longer stalled by a barrier then wake it.
    synchronized (this) {
        Message prev = null;
        Message p = mMessages;
        while (p != null && (p.target != null || p.arg1 != token)) {
            prev = p;
            p = p.next;
        }
        if (p == null) {
            throw new IllegalStateException("The specified message queue synchronization "
                    + " barrier token has not been posted or has already been removed.");
        }
        final boolean needWake;
        if (prev != null) {
            prev.next = p.next;
            needWake = false;
        } else {
            mMessages = p.next;
            needWake = mMessages == null || mMessages.target != null;
        }
        p.recycleUnchecked();

        // If the loop is quitting then it is already awake.
        // We can assume mPtr != 0 when mQuitting is false.
        if (needWake && !mQuitting) {
            nativeWake(mPtr);
        }
    }
}

当发送系统消息时 会在消息队列中插入一个target为空的message,在取出消息时如果发现了这个消息,就跳过所有的正常消息,返回最近的一个系统消息,然后将这个标记消息从队列中remove。

整个Handler源码学习系列笔记就完结了,当然现在也没有到特别深入的程度,但了解了整套Handler的消息机制后,相信对于源码的阅读能力和编程思路上也有挺大的提高。

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

推荐阅读更多精彩内容