【Android】深入解析 Handler 源码

官方文档:
https://developer.android.google.cn/reference/android/os/Handler

目录(简书Markdown页内跳转不好用。。。)

  1. 工作流程
  2. 线程问题
  3. 内存泄漏原因
  4. 关于 new Handler 的问题
  5. 消息队列无消息时的问题
  6. 线程安全
  7. 如何创建 Message
  8. Looper 死循环为什么不会导致应用卡死的问题

一、工作流程

Handler 从子线程发送消息(sendMessage)到主线程接收消息(handleMessage)到底都干了些什么?

Handler 通信五大类:
Handler、Message、Looper、MessageQueue、Thread

这五大类是怎么工作的?
可以把 Handler 的工作流程想象成一个生产线上的传送带,如下图:


Handler 工作流程

首先是发送消息,发送消息可以理解为工人将产品放到传送带的过程。
Handler 发送消息可以是 send 也可以是 post。
如:sendMessage、sendEmptyMessage、sendEmptyMessageDelayed、postDelayed 等。

以 sendEmptyMessage 举例,查看源码是如何发送消息的。

  1. Handler 的 sendEmptyMessage 会调用 Handler 的 sendEmptyMessageDelayed
public final boolean sendEmptyMessage(int what) {
    return sendEmptyMessageDelayed(what, 0);
}
  1. Handler 的 sendEmptyMessageDelayed 会调用 Handler 的 sendMessageDelayed
public final boolean sendEmptyMessageDelayed(int what, long delayMillis) {
    ...
    return sendMessageDelayed(msg, delayMillis);
}
  1. Handler 的 sendMessageDelayed 会调用 Handler 的 sendMessageAtTime
public final boolean sendMessageDelayed(Message msg, long delayMillis) {
    ...
    return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}
  1. Handler 的 sendMessageAtTime 会调用 Handler 的 enqueueMessage
public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
    ...
    return enqueueMessage(queue, msg, uptimeMillis);
}
  1. Handler 的 enqueueMessage 会调用 MessageQueue 的 enqueueMessage
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
    ...
    return queue.enqueueMessage(msg, uptimeMillis);
}

所以,所有的消息,不管调用的是哪个 send 或 post,最终都是将消息发送到消息队列里。


问:MessageQueue 的 enqueueMessage 干了什么事情?
答:MessageQueue 的 enqueueMessage 就是存储了 Message ,也就是把消息放到消息队列里,而 MessageQueue 的 enqueueMessage 存储消息的方式是一个优先级队列(根据时间进行排序的队列)。

优先级队列是用链表结构构成的,链表是数据的保存方式,只不过链表里面加入了队列算法(先进先出),而在先进先出的算法基础之上又加入了根据时间进行排序,就变成了优先级队列。

通过源码看具体逻辑实现:

boolean enqueueMessage(Message msg, long when) {
    ...
 
    synchronized(this) {
        ...
 
        // 根据时间进行排序(你添加的 when ,和消息队列的时间节点进行排序)
        if (p == null || when == 0 || when < p.when) {
            msg.next = p;
            mMessages = msg;
            needWake = mBlocked;
        } else {
            ...
 
            // 排序完了之后,找到插入的点,把这个消息放进去
            for (;;) {
                prev = p;
                p = p.next;
                if (p == null || when < p.when) {
                    break;
                }
                if (needWake && p.isAsynchronous()) {
                    needWake = false;
                }
            }
            msg.next = p;
            prev.next = msg;
        }
 
        ...
    }
    return true;
}

可以看到所有的消息都是根据时间进行排序,这样就形成了一个消息队列,队头的消息最早执行,队尾的消息最晚执行。也叫优先级排序。


问:这个时间怎么理解?
答:以 Handler 的 sendMessage 举例

public final boolean sendMessage(Message msg) {
    return sendMessageDelayed(msg, 0);
}

如果没有设置时间,那么就是当前时间,意味着立刻执行。那么给 sendMessageDelayed 传入的参数就是 0。

public final boolean sendMessageDelayed(Message msg, long delayMillis) {
    if (delayMillis < 0) {
        delayMillis = 0;
    }
    return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}

那么时间就是 SystemClock.uptimeMillis(系统时间)+ delayMillis 时间。所以每一个消息都会有一个执行时刻。

那么这样我们就可以知道消息一旦发送出去之后就会存在于我的 MessageQueue 里面,那么有消息进来了自然就会有消息出去。


问:这个消息怎么出去?
答:还是拿传送带举例,工人(sendMessage )把产品(Messgae) 放到了传送带上 ,传送带可以理解为 MessageQueue ,传送带滚动需要提供动力,Looper 就是给它提供动力的函数。提供动力的开关是 Looper.loop() ,或者说动力通上电是通过线程(Thread)调用线程所对应的 Looper 的函数。

Looper.loop() 源码

public static void loop() {
    ...
 
    // 死循环
    for (;;) {
        // 从消息队列里取消息
        Message msg = queue.next();
        if (msg == null) {
           return;
        }
        
        ...
 
        try {
            msg.target.dispatchMessage(msg);
              ...
        } catch (Exception exception) {
            ...
        } finally {
            ...
        }
        
        ...
    }
}

MessageQueue.next() 源码

Message next() {
    ...
 
    for (;;) {
        ...
      
        synchronized (this) {
            ...
 
            if (msg != null) {
                if (now < msg.when) {
                    ...
                } else {
                    ...
 
                    return msg;
                }
            } else {
                ...
            }
 
            ...
        }
 
        ...
    }
}

next 返回值是一个 msg。next 函数就是从这个优先级队列(传送带)里取消息。也就是通上电之后从队头取消息。

Handler.dispatchMessage() 源码

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

工作流程:传送带通电(Looper.loop),传送带滚轮滚动(MessageQueue.next),滚动之后后就会从消息队列里取消息(Handler.dispatchMessage)。


问:那么怎样让它停下来呢?
答:查看 Looper.loop() 源码

public static void loop() {
    ...
 
    // 死循环
    for (;;) {
        // 从消息队列里取消息
        Message msg = queue.next();
        
        ...
    }
}

如果消息队列为空,那么 queue.next() 就会被 block 住。那么 Message msg = queue.next() 就会有一个睡眠,外层的 for 死循环就会停下来。


二、线程问题

一个线程有几个 Handler ?一个线程有几个 Looper ?该如何保证?

1.一个线程有几个 Handler ?为什么?
答:一个线程有多个 Handler 。
解:在一个 Activity 里 new 一个 Handler ,在另一个 Activity 里又 new 了一个 Handler 。只要内存够用想 new 多少个 Handler 都可以。


2.一个线程有几个 Looper ?该如何保证?
答:一个线程有一个 Looper 。
解:关键字:ThreadLocal。实际上 ThreadLocal 就是一个 Key Value 键值对。

先看源码对 Looper 是怎样进行初始化的:

private Looper(boolean quitAllowed) {
    mQueue = new MessageQueue(quitAllowed);
    mThread = Thread.currentThread();
}

Looper 的构造函数是一个私有的构造函数。也就意味着不可能在其他类里进行初始化。

public static void prepare() {
    prepare(true);
}
 
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));
}

内部定义了一个 prepare 函数来进行初始化。在 ThreadLocal.set() 的时候进行初始化。


问:初始化的 Looper 是如何和线程进行绑定的?
答:得看 ThreadLocal.set() 的过程:

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) map.set(this, value);
    else createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

set 函数传入的 value 就是 Looper ,set 函数第一行用来获取当前线程。第二行根据当前线程来获取对应的 ThreadLocalMap 。

根据前两行代码可知一个线程对应一个 Map 。

map.set() 函数源码:

private void set(ThreadLocal < ?>key, Object value) {
    Entry[] tab = table;
    ...
    tab[i] = new Entry(key, value);
    ...
}

ThreadLocal 本身作为 Key ,Looper 作为 Value。并且是一一对应。

所以在 prepare 的时候,一个 ThreadLocal 对应了一个 Looper 。而且这个 ThreadLocal 是跟线程进行绑定的。

那么就会有一个疑问 Looper 里面存在多个 ThreadLocal 那不就完蛋了吗?
查看 Looper 源码:

static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

发现 ThreadLocal 是 final ,所以他只能有一个。

那就会有一个疑问 Key 可以不变,Value 可以有多个,那程序不就乱套了吗?
查看 prepare 函数源码:

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));
}

每当执行 prepare 的时候,会根据唯一的 ThreadLocal 来 get 一下。如果发现 ThreadLocal 已经有值了那就会发一个 Exception 。也就彻底保证了唯一性。

综上所述:
一个线程(Thread)对应一个 Map(ThreadLocalMap),一个 Map 里面会有多个 Entry ,但是 Entry 里面会有一个键(ThreadLocal)对应一个值(Looper)。也就是说一个线程有一个 Looper 。


三、内存泄漏原因

Handler 内存泄漏原因,以及为什么其他的内部类没有说过有这个问题。

答:因为 Handler 会持有 Activity 。
解:跟匿名内部类有关,举个匿名内部类写法的例子:
定义一个 Handler ,然后 new 一个 Handler 。

Handler handler = new Handler() {
 
    @Override public void handleMessage(@NonNull Message msg) {
        super.handleMessage(msg);
    }
};

一个内部类会有一个特征:默认会持有外部类的对象。


Handler 处理原理:
发送消息(handler.send)和处理消息(handler.dispatch)是同一个 Handler 。


问:为什么在子线程里通过 Handler 发送的消息,在主线程里还能用同一个 Handler ?
答:查看 Handler 源码
Handler 调用 sendMessageAtTime:

public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
    ...
    return enqueueMessage(queue, msg, uptimeMillis);
}

sendMessageAtTime 调用:

private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
    msg.target = this;
    ...
}

会把这个 this (也就是 Handler) 赋值给 Message 的 target。

也就是说 MessageQueue 会持有 Message ,Message 会包含一个 target(Handler),这个 target 就会持有 Activity 。

Message 会放到队列里,在队列里 delay 一定时间,意味着 Message 一直存在于队列里面。也就意味着 MessageQueue 要在 delay 时间后处理这个 Message ,Message 会在 delay 时间后解除和 Handler 的关系,也就意味着 delay 时间后解除和 Activity 的关系。如果 Message 没有得到及时的处理,意味着 Activity 会一直被 Message 持有,而 Message 就会一直放在 MessgaeQueue 里面。MessageQueue 属于内存。

所以内存该释放的时候没释放,就会导致内存泄漏。

问:如何解决这个问题?
答:1⃣️Handler 使用静态变量 2⃣️弱引用


四、关于 new Handler 的问题

为何主线程可以 new Handler ,如果想要在子线程中 new Handler 要做些什么准备。

问:为何主线程可以 new Handler
答:以 Launcher 界面点击图标跳转到 App 举例。触发AMS启动过程。

Launcher -> Zygote -> 给每一个应用创建一个虚拟机(ART),也可以说成为每一个进程创建一个虚拟机,也就是说每一个应用会有一个独立的虚拟机,也就是每一个应用会有一个独立的进程 -> ActivityThread(SDK -> android-29 -> android -> app -> ActivityThread)

ActivityThread 源码:

public static void main(String[] args) {
    ...
 
    Looper.prepareMainLooper();
 
    ...
 
    Looper.loop();
 
    ...
}
public static void prepareMainLooper() {
    prepare(false);
    ...
}
private static void prepare(boolean quitAllowed) {
    ...
    sThreadLocal.set(new Looper(quitAllowed));
}

在 main 函数里,首先对 Looper 进行 prepare ,然后调用 Looper.loop() 。

所以正是因为这个原因(主线程的 Looper 初始化工作已经由系统帮我们完成),所以这个开关已经由主线程帮我们打开,正是因为这个原因我们在主线程里面使用的时候可以直接 new Handler。


问:如果想要在子线程中 new Handler 要做些什么准备?
答:为线程准备一个 Looper 。所以在子线程中创建 Handler ,一定要对他进行 Looper.prepare() 和 Looper.loop() 。


五、消息队列无消息时的问题

子线程中维护的 Looper ,消息队列无消息的时候的处理方案是什么。有什么用。

问:子线程中维护的 Looper ,消息队列无消息的时候的处理方案是什么?
答:首先要知道 Looper 干了些什么事情。消息队列里面每一个消息都会经过 loop() 去轮询

public static void loop() {
    ...
 
    for (;;) {
        Message msg = queue.next(); // might block
        ...
    }
}

如果消息队列里面没有消息就会被阻塞。

Message next() {
    ...
 
    for (;;) {
        ...
 
        nativePollOnce(ptr, nextPollTimeoutMillis);
 
        synchronized(this) {
            ...
 
            if (msg != null) {
                ...
 
            } else {
                // No more messages.
                nextPollTimeoutMillis = -1;
            }
 
            ...
 
            if (pendingIdleHandlerCount <= 0) {
                ...
 
                continue;
            }
 
            ...
        }
 
        ...
    }
}

如果队列里没有消息, nextPollTimeoutMillis 就为 -1 ,然后调用 continue 。跳出这次循环进行下一次循环。

nativePollOnce 就会传一个 -1 下去。

private native void nativePollOnce(long ptr, int timeoutMillis);

如果为 -1,则表示无限等待,直到有事件发生为止。如果值为0,则无需等待立即返回。

如果队列里面没有消息,当前的线程就处于阻塞状态。这个时候想退出,就要调用 Looper.quit()

public void quit() {
    mQueue.quit(false);
}
void quit(boolean safe) {
    if (!mQuitAllowed) {
        throw new IllegalStateException("Main thread not allowed to quit.");
    }
 
    synchronized(this) {
        if (mQuitting) {
            return;
        }
        mQuitting = true;
 
        if (safe) {
            removeAllFutureMessagesLocked();
        } else {
            removeAllMessagesLocked();
        }
 
        // We can assume mPtr != 0 because mQuitting was previously false.
        nativeWake(mPtr);
    }
}

线程处于阻塞状态,如果想退出就必须先唤醒。先把所有的消息 remove 掉。

private void removeAllMessagesLocked() {
    Message p = mMessages;
    while (p != null) {
        Message n = p.next;
        p.recycleUnchecked();
        p = n;
    }
    mMessages = null;
}
void recycleUnchecked() {
    // Mark the message as in use while it remains in the recycled object pool.
    // Clear out all other details.
    flags = FLAG_IN_USE;
    what = 0;
    arg1 = 0;
    arg2 = 0;
    obj = null;
    replyTo = null;
    sendingUid = UID_NONE;
    workSourceUid = UID_NONE;
    when = 0;
    target = null;
    callback = null;
    data = null;
 
    synchronized(sPoolSync) {
        if (sPoolSize < MAX_POOL_SIZE) {
            next = sPool;
            sPool = this;
            sPoolSize++;
        }
    }
}

这里实际上并没有把消息赋值为 null ,而是说把消息里面的变量赋值为 null 。

然后在 MessageQueue.quit() 里面调用 nativeWake 。nativeWake 和 nativePollOnce 是对立关系。


问:有什么用?
答:释放线程的好处是子线程又可以在别的地方使用(因为释放了资源)。


问:主线程需要释放吗?
答:不能
解:查看 MessageQueue.quit() 源码:

void quit(boolean safe) {
    if (!mQuitAllowed) {
        throw new IllegalStateException("Main thread not allowed to quit.");
    }
 
    ...
}

如果发现是主线程,会抛出异常。


问:为什么这么设计?
答:查看 ActivityThread 源码,所有 Activity 操作,包括四大组件,他们的生命周期都是在 Handler 里处理。


六、线程安全

既然可以存在多个 Handler 往 MessageQueue 中添加数据(发消息时各个 Handler 可能处于不同线程),那它内部是如何确保线程安全的。

一个线程 对应一个 Looper ,一个 Looper 对应一个 MessageQueue 。所以一个线程只有一个 MessageQueue 。

问:如何保证线程安全?
答:锁
解:往 MessageQueue 存消息和取消息的时候都会加锁。

void quit(boolean safe) {
    ...
 
    synchronized(this) {
        ...
    }
}
Message next() {
        ...        
 
        for (;;) {
            ...
 
            synchronized (this) {
                ...
            }
 
            ...
        }
}
boolean enqueueMessage(Message msg, long when) {
    ...
 
    synchronized(this) {
        ...
    }
    
    ...
}

这样就保证了线程安全。


问:为什么是 synchronized (this) ?
答:synchronized 是一个内置锁。传 this 的原因是它是对整个 MessageQueue 。一个线程只有一个 MessageQueue 。对 MessageQueue 有整个访问过程。也就是说对 MessageQueue 的访问,所有的线程都是去访问同一个消息队列。所以当你在一个线程访问这个锁的时候其他线程都不能去访问。所以这个时候传的是 this 。这个 this 就代表着我们对一个线程的 MessageQueue 去访问的时候,当它访问这个锁,锁住这块代码块的时候,其他的对象只要访问的都是目前这个 this 所对应的 MessageQueue ,那么就都不能访问。


问:为什么是内置锁?
答:因为这个锁是 JVM 完成的,它的锁的动作和解锁的动作都是由 JVM 完成的。所以叫内置锁。


七、如何创建 Message

使用 Message 时应该如何创建它。

查看 Message 源码,

Message 源码包含两个变量:next ,sPool。

享元设计模式。


八、Looper 死循环为什么不会导致应用卡死的问题

AMS 和多线程的问题。

卡死就是 ANR 。

既然 Handler 的消息全都是 loop 来的,为什么我们没有 ANR 问题?之前不是说5秒钟不响应就会出现阻塞问题吗,为什么休眠个好长时间也并不会被 ANR 呢?

唤醒线程的方法:1⃣️looper 中添加 message 。通过 nativeWait() -> loop 运作 2⃣️输入事件

产生 ANR 的问题不是因为主线程睡眠了,而是因为输入事件没有响应,输入事件没有响应他就没有办法唤醒这个 Looper ,才加了这个5秒的限制。

真正的 ANR 是消息没有及时处理。按键事件、广播 都是以 Message 形式传递的。

结论:因为应用卡死 ANR 压根与这个 Looper 没有关系,应用在没有消息需要处理的时候,它是在睡眠,释放线程;卡死是 ANR ,而 Looper 是睡眠。


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