Android消息机制-Handler、Looper、MessageQueue

由于Andoird的UI元素采用的单一线程模型,只能在UI线程进行更新,于是我们用来更新UI,常用Handler进行更新,用法如下:

   mHandler = new Handler(Looper.getMainLooper(), new Handler.Callback() {
       public boolean handleMessage(Message msg) {
           //do ui update
           return false;
       }
   });
   Message message = new Message();
   mHandler.sendMessage(message);

用法简单明了,接着我们直接来体会一下 消息是如何被传送到主线程的。

0x00 有些什么?

Handler: 我们通过它来发送消息。
Looper: 构造Handler时,需要传入一个 Looper对象。例子中我们传入了主线程的Looper。

MessageQueue:当我们使用 Handler将一条Message sendMessageDelayed 出去,在Handler源码中追踪,调用路径如下:

Handler.sendMessage
   sendEmptyMessage
       sendEmptyMessageDelayed
           sendMessageAtTime
               enqueueMessage
                   msg.target = handler;// set target -> Handler
                   MessageQueue.enqueueMessage(msg, uptimeMillis)

这边出现了我们的新的对象 MessageQueue。很明显的,sendMessage的操作,其实是将Message 放入了MessageQueue中。

我们先来大致梳理一下3者的关系:

img

简言之,我们在子线程,生成了一个持有主线程 Looper的Handler。

  1. 我们通过Handler将Message 传递到了主线程,加到了对应的MessageQueue中。
  2. 主线程的Looper读取Message,并且处理。

其中,有几个关键点:

  1. 如何找到 目标线程?
  2. MessageQueue如何对新增Message进行处理的?
  3. Message如何被消费的?

我们一个一个看。

0x01 Handler如何找到目标线程,并且发送Message?

从上面的sendXXX调用堆栈可以看出,最终调用到MessageQueue.enqueueMessage(msg, uptimeMillis)。那么,Handler中的MessageQueue 是怎么来的呢?

我们可以从构造器中找到答案:

public Handler(Looper looper, Callback callback, boolean async) {
        mLooper = looper;
        mQueue = looper.mQueue;
        mCallback = callback;
        mAsynchronous = async;
    }

来自Looper,看记得那个Looper么?-> Looper.getMainLooper().
我们来看看Looper:

 public static Looper getMainLooper() {
        synchronized (Looper.class) {
            return sMainLooper;
        }
    }
 ...
 sMainLooper = myLooper();
 ...
 public static @Nullable Looper myLooper() {
        return sThreadLocal.get();
    }

这里出现了一个很重要的对象:sThreadLocal->ThreadLocal<T> 我们来看看它。

ThreadLocal 原理

这是一个用来做线程内部存储的类,通过它可以再指定的线程中获取、存储数据,并且只有在该线程里才能获取,其他线程无法获取。而我们的不同线程的Looper 就是放在ThreadLocal中。对外提供的方法很简单:

  1. public T get()
  2. public void set(T value)

我们大致看看实现:

    public T get() {
        // Optimized for the fast path.
        Thread currentThread = Thread.currentThread();
        Values values = current.localValues;
        if (values != null) {
            Object[] table = values.table;
            int index = hash & values.mask;
            if (this.reference == table[index]) {
                return (T) table[index + 1];
            }
        } else {
            values = initializeValues(currentThread);
        }
        return (T) values.getAfterMiss(this);
    }
    public void set(T value) {
        Thread currentThread = Thread.currentThread();
        Values values = current.localValues;
        if (values == null) {
            values = initializeValues(currentThread);
        }
        values.put(this, value);
    }
    
    class Values {
        ...
        void put(ThreadLocal<?> key, Object value) {
            for (int index = key.hash & mask;; index = next(index)) {
                Object k = table[index];

                if (k == key.reference) {
                    // Replace existing entry.
                    table[index + 1] = value;
                    return;
                }
                ...
            }
        }
        ...
    }

主要思路是将数据结构放在了Thread.localValues,用某种算法进行了下标计算,得出 put、get的index。

Ok,那么现在,我们的Looper、以及它的MessageQueue都非常eazy的获取到了。

0x02 MessageQueue如何对新增Message进行处理的?

首先,我们需要了解一下MessageQueue的结构。最重要的一个变量是:Message mMessages;

而在Message也有几个重要的变量:

  1. Message next; 标志下一个Message
  2. Handler target 目标Handler是什么,用于做消息派发(还记得上面代码部分,有设置过么? msg.target)。

从中我们可以看出,MessageQueue是个单链表结构。

此时,我们再回到那个函数:MessageQueue.enqueueMessage(msg, uptimeMillis)

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

可以很明显看出,这是一个根据 msg.when 为依据,做的链表插入。这样,我们的Message就能按照正确时间的顺序,进行排列。

之外:

MessageQueue还有其他的操作,都是对单链表的操作。如:

  1. private void removeAllFutureMessagesLocked()
  2. void removeMessages(Handler h, Runnable r, Object object)

0x03 Message如何被消费的?

我们有一块很重要的东西,那就是Looper,来看看前面那张图,Looper 会从Message中,不断的读取 Message。是如何做到的呢? 我们试着来看看源码。

现在我们没有了头绪,转过来,我们先来看看主线程是怎么启动的。

主线程的启动过程中对Looper的处理。

我们知道,主线程启动,是通过ActivityThead.main(String[] args) 方法进行启动,我们一起看看:

public static void main(String[] args) {
        ..
        Looper.prepareMainLooper();
        ...
        if (sMainThreadHandler == null) {
            sMainThreadHandler = thread.getHandler();
        }
        ...
        Looper.loop();
        ...
}

里面有几行对于looper的处理,有2个关键的操作:

  1. Looper.prepareMainLooper
  2. Looper.loop();

Looper.prepareMainLooper

prepareMainLooper会接着调用到prepare(boolean quitAllowed)

prepare的过程比较简单。我们大致看一眼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));
   }

还记得 上面的 ThreadLocal吗?prepare的过程,其实就是生成一个Looper,往ThreadLocal 中塞。

Looper.loop();

这个是最核心的部分,我们依然上源码:

 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 (;;) {
           Message msg = queue.next(); // might block
           ...
           msg.target.dispatchMessage(msg);
           ...
       }
   }

第一行获取myLooper()的过程,也就解释了为什么要先做“prepare”操作。

接着,会是一个死循环,核心步骤2步:

  1. 不断的从MessageQueue.next 中读取Message。
  2. 并且将消息进行派发-msg.target.dispatchMessage(msg);

我们接着看这2步:

MessageQueue.next

这个方法会返回一个Message,但是是一个阻塞方法,在适当的时候才会返回,这也就是为什么我们可以使用sendxxxDelay(...)在一段时间后才执行的原因。

我们来看源码:

Message next() {
        ...
        for (;;) {
            ...
            synchronized (this) {
                final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                Message msg = mMessages;
                if (msg != null && msg.target == null) {
                    // Stalled by a barrier.  Find the next asynchronous message in the queue.
                    do {
                        prevMsg = msg;
                        msg = msg.next;
                    } while (msg != null && !msg.isAsynchronous());
                }
                if (msg != null) {
                    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;
                }
                ...
            }
            ...
            // Reset the idle handler count to 0 so we do not run them again.
            pendingIdleHandlerCount = 0;

            // While calling an idle handler, a new message could have been delivered
            // so go back and look again for a pending message without waiting.
            nextPollTimeoutMillis = 0;
        }
    }

为了好看,缩减了一些片段。next() 方法,里面也有一个死循环,当message.when到了之后,才会返回。

消息派发:msg.target.dispatchMessage(msg);

这步能够找到正确的target,完全依赖于在 Handler.sendMessage() 做 enqueueMessage 时设置的:msg.target = handler;

所以,能够用正确的Handler进行派发,Handler派发逻辑:

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

注意哦,这里进行消息处理,是Looper调用过来的,因此是在 目标线程。

0x04 总结

我们再来回忆一下这张图:

img

发送消息流程:

  1. 新建Handler,指定Looper。
  2. 调用sendXXXX方法。
  3. Handler将消息发送到 Looper.MessageQueue中。

注意几个细节:

  1. Looper通过ThreadLocal找到。
  2. 当一个线程需要用Looper时,要先调用Looper.prepare(),目的就是为了在ThreadLocal中创建当前线程的Looper。

消息处理过程:

  1. 每个线程调用Looper.loop(),是个死循环,会不断的从MessageQueue.next读消息进行处理。
  2. MessageQueue.next也是个阻塞函数,会等到有可处理的消息时返回Message对象。
  3. 对Message消息,会调用message.target.dispatchMessage进行消息传递。

注意几个细节:

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

推荐阅读更多精彩内容