Android中的Looper , Handler , MessageQueue有什么关系

一、Looper

每个线程只能有一个Looper,主线程创建Looper,在ActivityThread的main方法里

public static void main(String[] args) {
  ……
  Looper.prepareMainLooper();
  ……
  Looper.loop();
  ……
}

Looper通过prepare进行创建,构造函数中new出来mQueue,Looper创建后保存进了线程的ThreadLocalMap里。prepare方法只能调用一次,prepare之后调用loop()方法开始消息的遍历。

问1:prepare方法传进去的quitAllowed是干什么用的?
答1:标记Looper是否允许放弃,Looper的quit方法是调用mQueue的quit将消息移除,如果设置quitAllowed为false,则不能调用quit,比如主线程就不允许quit,quit有安全和非安全,非安全移除所有的消息,安全是移除(消息触发时间大于当前时间)的消息。quit后,mQueue就不能在添加消息了。

问2:ThreadLocal原理是什么?
答2:查看其set方法,发现是获取了当前线程的ThreadLocalMap对象,将ThreadLocal本身为key,Looper为value进行存储。
在查看ThreadLocal源码,内部有一个数组private Entry[] table,具体的存放:
根据key(ThreadLocal)计算出数组索引值,然后将key和value封成Entry(弱引用)放入数组中,ThreadLocal本质是通过每个线程单独一份存储空间,牺牲空间来解决多线程冲突。细节可点击此处

二、Handler

Handler创建时,会创建Looper

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

问3:Handler发送消息和接收消息进行分发。
答3:可以通过sendMessageXXX和postXXX方法来发送消息,使用post方法发送消息时,会在msg中传入一个runnable给callback。最终会调用enqueueMessage方法将消息放入消息队列中,会设置Handler为消息的target。

    private static Message getPostMessage(Runnable r) {
        Message m = Message.obtain();
        m.callback = r;
        return m;
    }

在Looper的loop()方法会遍历获取消息,调用消息target的dispatchMessage方法,即Handler的dispatchMessage方法,该方法即消息的分发:

    public void dispatchMessage(Message msg) {
        //1.首先判断如果消息自带callback,则执行runnable的run进行消息处理。
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            //2.如果消息没有callback,则判断Handler是否有mCallback(构造函数中传入)
            //如果有,则执行callback里的handleMessage,且根据返回值判断是否继续
            //执行Handler中的handleMessage,返回true则结束,返回false 则继续
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            //走到了此处,执行handleMessage
            handleMessage(msg);
        }
    }

问4: 消息是有同步和异步的,具体区别表现在哪。
问5:正常情况下,消息都是同步,Handler设置了async为true时,则所有该Handler发送的消息都是异步,消息也可以单独设置为异步。区别主要还是看是否设置了同步消息屏障。
在MessageQueue中有postSyncBarrier 和 removeSyncBarrier 来发送和移除同步屏障消息,postSyncBarrier 不会唤醒线程, removeSyncBarrier 会唤醒线程(当队列里面有消息时。在遍历消息中,根据target是否为null来判断是否是同步屏障消息。同步屏障消息和普通消息一样可以根据时间来插入到消息队列中的适当位置,并且只会挡住它后面的同步消息的分发,postSyncBarrier方法是私有的,如果我们想调用它就得使用反射。

Handler的构造函数会初始化looper,如果在异步线程中直接创建一个Handler,在获取looper时会抛异常,所以需要先初始化一个looper,调用looper的prepare()方法即可,然后调用loop()方法开始循环消息。如果传入主线成的looper,Looper.getMainLooper(), 这样就可以实现了异步线程中发送消息让主线程收到的了,最常见的就是利用异步线程做耗时的网络请求,请求回来后通知主线程更新UI。

MessageQueue是一个单链表,入列的消息是按照触发时间的先后顺序排列,可以看下enqueueMessage的源码,注释了一堆

enqueueMessage

boolean enqueueMessage(Message msg, long when) {
       ……
        synchronized (this) {
//quit后,无法再添加消息
            if (mQuitting) {
               ……
                msg.recycle();
                return false;
            }

//标记消息正在被使用
            msg.markInUse();

//设置消息触发时间
            msg.when = when;
//mMessages:是下一个即将要出队列的消息
            Message p = mMessages;
//needWake:是否要唤醒
            boolean needWake;

            if (p == null || when == 0 || when < p.when) {
//满足插入队列头部的情况,将刚插入的消息置为队列头
//1.p == null 表示消息队列为空,自然就是插入队列头
//2.when == 0 ,通过Handler的XXXAtFrontOfQueue方法,设置when == 0,将消息置为队列头
//3.时间比队列头消息的时间要早

                msg.next = p;
                mMessages = msg;
//mBlocked为false,没有睡眠,needWake为false表示不需要唤醒。
//mBlocked为true,正在睡眠,needWake为true,表示需要唤醒。
                needWake = mBlocked;
            } else {
//满足插入队列中部的情况
//正常情况下,插入消息队列中消息的不需要唤醒事件队列,
//除了有“屏障消息”在队列头,并且插入的消息是时间最早的异步消息

//mBlocked为false,没有睡眠,needWake为false表示不需要唤醒。
//mBlocked为ture,正在睡眠,并且p是没有目标发送对象(屏障消息),并且插入的消息是异步的,则需要唤醒
                needWake = mBlocked && p.target == null && msg.isAsynchronous();

//根据when < p.when,查找当前msg插入的位置
                Message prev;
                for (;;) {
                    prev = p;
                    p = p.next;

                    if (p == null || when < p.when) {
                    //遍历到最后或者要插入消息的时间小于当前消息的时间
                        break;
                    }

//假如正在睡眠,且前面满足唤醒的条件。即needWake为true,
//如果msg前面的消息存在异步,则当前的msg就算为异步,则不需要唤醒。
//正常情况下,插入消息队列中消息的不需要唤醒事件队列,
//除了有“屏障消息”在队列头,并且插入的消息是时间最早的异步消息
                    if (needWake && p.isAsynchronous()) {
                        needWake = false;
                    }
                }
//插入msg
                msg.next = p; // invariant: p == prev.next
                prev.next = msg;
            }
 // 唤醒判断
            if (needWake) {
                nativeWake(mPtr);
            }
        }
        return true;
    }

问5:总结下当消息阻塞时,什么情况需要唤醒消息队列:
答5:

  1. 插入的消息在队列头部
  2. 消息队列的头部消息是屏障消息,且插入的消息是异步消息,且是队列中最早的异步消息。

第二点不太好理解,可以看下这两行代码(注释),

//
//如果队列头消息p是同步屏障消息,且插入消息msg是异步消息,就暂定唤醒
needWake = mBlocked && p.target == null && msg.isAsynchronous();
//下面假设needWake为true,也就是(头部是同步屏障消息,插入的消息是异步消息)
                Message prev;
                for (;;) {
                    prev = p; // prev 变成了同步屏障消息
                    p = p.next; // p变成了队列中的下一条
//when < p.when 表示插入的消息msg比p早,就是紧跟在prev(同步屏障消息)之后的消息,这样 needWake 就不变
                    if (p == null || when < p.when) {
                        break;
                    }----
//插入的消息msg比p晚,且p如果是异步消息,怎不用唤醒,所以插入的异步消息需要是最早的异步消息
                    if (needWake && p.isAsynchronous()) {
                        needWake = false;
                    }
                }


if (needWake && p.isAsynchronous()) {
          needWake = false;
}
//在遍历的过程中,发现队列中已经有异步消息,比要插入的异步消息要早触发,则不唤醒。

next

 Message next() {
// 判断是否初始化,mPtr = nativeInit(); 初始化后,mPtr不为0,获取是否quit,quit后,mPtr 为0
        final long ptr = mPtr;
        if (ptr == 0) {
            return null;
        }
//挂起的闲置Handler数,通过addIdleHandler 可以添加IdleHandler
//IdleHandler的优先级最低,在消息队列中没有正常的消息后,才开始处理IdleHandler
        int pendingIdleHandlerCount = -1; // 
//下次唤醒系统的时间
        int nextPollTimeoutMillis = 0;

        for (;;) {
            if (nextPollTimeoutMillis != 0) {
//调用native方法做些刷新命令
                Binder.flushPendingCommands();
            }

//这里就是开始“睡眠”, nextPollTimeoutMillis大于0,则“睡眠”直到过了nextPollTimeoutMillis时间后醒来
//enqueueMessage方法中调用的nativeWake也可以唤醒
//这里可以回答为什么不会造成anr,
            nativePollOnce(ptr, nextPollTimeoutMillis);

            synchronized (this) {
// 开始检查下一条消息,如果有,则返回
                final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
//mMessages是上次消息发送出去后,获取的next消息,即这次要发出去的消息
                Message msg = mMessages;
                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 {
// 获取了消息,设置非阻塞
                        mBlocked = false;

                        if (prevMsg != null) {
//prevMsg非空,说明同步屏障消息出现,获取了第一个异步消息
//将这次本该发出去的消息最为这个异步消息的下一条消息
                            prevMsg.next = msg.next;
                        } else {
//更新下一次要发送的消息
//结果是这次要发送的消息是msg,下一次要发送的消息是mMessages
                            mMessages = msg.next;
                        }
                        msg.next = null;
                        if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                        msg.markInUse();
                        return msg;
                    }
                } else {
// 没有更多消息了
                    nextPollTimeoutMillis = -1;
                }
// quit后,所有挂起的闲置消息不处理
                if (mQuitting) {
                    dispose();
                    return null;
                }
 //没有更多消息后,开始处理所有的闲置消息
                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);
            }

// 遍历所有的闲置消息,调用其queueIdle方法
            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 {
                    keep = idler.queueIdle();
                } catch (Throwable t) {
                    Log.wtf(TAG, "IdleHandler threw exception", t);
                }
                if (!keep) {
                    synchronized (this) {
                        mIdleHandlers.remove(idler);
                    }
                }
            }
            pendingIdleHandlerCount = 0;
            nextPollTimeoutMillis = 0;
        }
    }

从上面的代码中可以看到一个问题,当同步屏障消息发送后,再发送一条延迟的消息,那么直到延迟消息的时间到了为止,都是在睡眠,让本应该执行的同步消息都不能执行,所有同步屏障消息后,异步消息一定不要是延迟的异步消息。

addIdleHandler可以解决页面卡顿的问题,在这里处理不要紧的操作

  Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
            @Override
            public boolean queueIdle() {
                //处理不要紧的操作
                return false;
            }
        });

Looper中有一个消息队列MessageQueue,通过loop()是对进行遍历,是一个for的死循环,

Q3:为什么这个死循环不会发生ANR
A3:所谓ANR是消息事件没有得到及时处理,如果没有消息,会调用了linux层的代码实现在适当时会睡眠主线程,当消息来了会进行唤醒,即Linux的epoll机制。

epoll机制

https://www.jianshu.com/p/7bc2b86c4d89
https://www.jianshu.com/p/02e4327b7e02

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

推荐阅读更多精彩内容