一张图总结Handler消息机制

文字总感觉很难描述,还不如来一张图总结一下来得清晰。


Handler

Handler#dispatchMessage,消息分发的核心代码

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

1.Looper.prepare()

创建Looper,并保存到sThreadLocal中,创建了一个MessageQueue并赋值给Looper#mQueue,同时在Native层页创建了MessageQueue

private static void prepare(boolean quitAllowed) {
    if (sThreadLocal.get() != null) {
        throw new RuntimeException("Only one Looper may be created per thread");
    }
    //prepare只要就是创建Looper对象,放在当前线程的ThreadLocalMap中,待后续Looper.loop()从线程中取出来
    sThreadLocal.set(new Looper(quitAllowed));
}

private Looper(boolean quitAllowed) {
    mQueue = new MessageQueue(quitAllowed); //消息队列,构造中调用了nativeInit(),这里会在native层也创建消息队列
    mThread = Thread.currentThread();
}

# MessageQueue.java
MessageQueue(boolean quitAllowed) {
    mQuitAllowed = quitAllowed;
    mPtr = nativeInit(); //调用nativeInit,这里会创建native层的消息队列
}

2.创建Handler

从ThreadLocal中获取线程的Looper赋值给Handler的mLooper,Looper的mQueue赋值给Handler的mQueue,所以Handler和其关联的Looper共同持有同一个MessageQueue。

public Handler(Callback callback, boolean async) {
    if (FIND_POTENTIAL_LEAKS) {
        final Class<? extends Handler> klass = getClass();
        if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
                (klass.getModifiers() & Modifier.STATIC) == 0) {
            Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
                klass.getCanonicalName());
        }
    }

    mLooper = Looper.myLooper(); //Looper赋值给Handler#mLooper
    if (mLooper == null) {
        throw new RuntimeException(
            "Can't create handler inside thread that has not called Looper.prepare()");
    }
    mQueue = mLooper.mQueue; //Looper#mQueue赋值给Handler#mQueue
    mCallback = callback;
    mAsynchronous = async;
}

3.Looper.loop()开启消息循环

进入一个for(;;)循环,mQueue.next()取出消息后,交给Handler#dispatchMessage()来处理,当取出的Message为null时退出循环。以下为关键性源码。

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;

    // Make sure the identity of this thread is that of the local process,
    // and keep track of what that identity token actually is.
    Binder.clearCallingIdentity();
    final long ident = Binder.clearCallingIdentity();

    for (;;) {
        Message msg = queue.next(); // might block
        //MessageQueue的next方法取出的消息为null,退出循环
        if (msg == null) {
            // No message indicates that the message queue is quitting.
            return;
        }
        ...
        final long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;

        final long traceTag = me.mTraceTag;
        if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
            Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
        }
        final long start = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
        final long end;
        try {
            //取出消息后交给Handler的dispatchMessage来处理
            msg.target.dispatchMessage(msg); 
            end = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
        } finally {
            if (traceTag != 0) {
                Trace.traceEnd(traceTag);
            }
        }
        ...
        msg.recycleUnchecked();
    }
}

从注释可以看到,MessageQueue#next()有可能导致线程阻塞,实际上是next()方法中的nativePollOnce()方法会阻塞线程。当处理完成一个Message后调用msg.recycleUnchecked()来回收消息,回收的消息将被缓存到sPool单链表的表头,最大缓存消息数量是50。当调用Handler#obtainMessage()时,就会从这个缓存链表中获取一个Message对象。

4.对外提供的消息回收接口:Message#recycle()

public void recycle() {
    if (isInUse()) {
        if (gCheckRecycle) {
            throw new IllegalStateException("This message cannot be recycled because it "
                    + "is still in use.");
        }
        return;
    }
    recycleUnchecked();
}

5.MessageQueue

(1)MessageQueue 按Message.when降序排列,在前面的消息先出队(MessageQueue#enqueueMessage、MessageQueue#next)
(2)barrier的Message与普通Message的差别是target(类型是Handler)为null,只能通过MessageQueue#postSyncBarrier创建 barrier Message
(3)barrier的Message与普通Message以同样的规则进入队列,但是却只能通过MessageQueue#removeSyncBarrier出栈
(4)每个barrier使用独立的token(记录在Message#arg1)进行区分
(5)所有的同步消息(相对与异步消息而言,默认消息都是同步消息)如果barrier之后,都会被延后执行,直到调用MessageQueue#removeSyncBarrier通过其token将该barrier清除,当barrier在队头时,队列中的异步消息照常出队不受影响
(6)Handler中的对应构造函数被隐藏,但是可以通过调用Message#setAsynchronous指定对应的Message为asynchronous的Message。
(7)部署barrier(MessageQueue#postSyncBarrier)与清除barrier(MessageQueue#removeSyncBarrier)的相关方法都是隐藏的方法,对外不可见

6.使用Handler值得注意的事儿

(1)引起内存泄漏问题

public Handler(Callback callback, boolean async) {
    if (FIND_POTENTIAL_LEAKS) {
        final Class<? extends Handler> klass = getClass();
        if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
                (klass.getModifiers() & Modifier.STATIC) == 0) {
            Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
                klass.getCanonicalName());
        }
    }

    mLooper = Looper.myLooper();
    if (mLooper == null) {
        throw new RuntimeException(
            "Can't create handler inside thread that has not called Looper.prepare()");
    }
    mQueue = mLooper.mQueue;
    mCallback = callback;
    mAsynchronous = async;
}

异常:The following Handler class should be static or leaks might occur.

原因:
由于Handler有可能(绝大部分)会被投递到MessageQueue中的Message#target所引用,在此消息没有被处理的情况下将一直持有,此时非静态内部类的Handler持有外部类实例的引用,例如是一个Activity实例,如果此时Activity退出,将会由于被Handler强引用而无法及时GC,导致内存泄漏。

通常处理方法:
①将Handler改为静态内部类,使用弱引用来应用外部实例,只有若弱引用的对象,在GC时可以回收。关于Java中的四种引用,可以看看这篇文章《Java中的4种引用类型》
②在外部类实例销毁时,调用Handler#removeMessage()将消息从消息队列中移除回收掉,这样就能移除Message对Handler的引用,当外部实例销毁时,Handler变成可被GC的对象。

(2)创建Handler时,提示当前线程没有Looper

Can't create handler inside thread that has not called Looper.prepare()

原因:
Looper.prepare()实际上是创建一个Looper传入作为所在线程的局部变量(全局由ThreadLocal与Thread#localValues来保证,简单参考ThreadLocal#get、ThreadLocal#set即可理解),而在真正Looper#loop的时候,是需要所在线程的局部变量的Looper为载体取得所有要处理的消息以及处理的方式的。因此创建Handler的同时是需要保证所在线程已经有了局部变量Looper的实例,才能保证Handler接下来真正运作。

通常处理方法:
在创建Handler前,主动调用下Looper.prepare()

每个线程的的Looper#prepare相对所在线程只能被调用一次,否则会报"Only one Looper may be created per thread"(参见Looper#prepare),之所以主线程直接创建Handler不会抛出类似异常,是因为在程序启动时,系统已经帮我们调用了Looper#prepare(参见ActivityThread#main)

7.消息池子

Message中有一个单链表的sPool用来缓存消息,最大缓存数量是50,所以当我们要使用Handler发送消息时,可以通过Handler#obtainMessage()或者Message.obtain()来得到一个消息对象,而不是通过new Message()来得到消息。

8.epoll 阻塞/唤醒机制

Android从2.3开始,将Object#wait()/notify()的阻塞唤醒机制改为Linux的epoll机制。

java层的looper、messageQueue在c++层均有对应的类,然后通过将looper初始化时创建的 eventFd 返回的 wakeEventFd,注册到 epoll_create创建的 epoll对象里,然后通过epoll_ctl去在wakeEventFd上添加一个 ADD类型的事件监听,最后上层 调用nativePollOnce的时候,最终会调用 epoll 的 epoll_wait 通过监听fd的方式来形成阻塞。来消息后,去往wakeEventFd去写一个“1”值,此后epoll_wait监听到值了,就阻塞解除继续执行了,继续取message执行去了。具体可参考这篇文章

epoll

关于ThreadLocal可以看看这篇文章 https://www.jianshu.com/p/517f3d16ad89

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