Android | 面试必问的 Handler,你确定不看看?

前言

  • 在 Android 中,Handler 是贯穿于整个应用的消息机制,在面试中出现的概率为:100%
  • 在这篇文章里,我将带你梳理 Handler 的使用攻略 & 设计原理。追求简单易懂又不失深度,如果能帮上忙,请务必点赞加关注!

延伸文章


目录

1. 概述

在 Android 中,很多地方是通过消息机制驱动的,例如线程间通信、四大组件的启动等等。消息机制中主要涉及 的类有:Handler & Looper & MessageQueue & Message,其中 Handler 可以说是消息机制提供给 Java 层的上层接口。

1.1 概念模型

问:Handler 怎么进行线程通信,原理是什么?

消息机制其实并不是 Android 系统独有的设计,在很多系统设计中都可以看到消息机制的身影,例如 IOS 的 runLoop、Web 的 Ajax 和 Spring 的消息队列等。在所有系统设计的消息机制里,都会有生产者与消费者的概念,如以下模型:

消息机制概念模型

其中消息缓冲区的具体实现可以是栈 & 队列,因为队列(特别是优先级队列)是最常见的,所以很多情况都会直接将消息缓冲区称为消息队列

1.2 架构图

【类图】

  • Looper 里组合了 MessageQueue 消息队列,创建 Looper 的同时也创建了 MessageQueue
  • MessageQueue 里组合了待处理的 Message 链表
  • Message 持有用于处理消息的 Handler(target)
  • Handler 被创建时需要聚合 Looper 与 MessageQueue,默认使用的是当前线程的 Looper

1.3 消息的享元模式

消息机制里需要频繁创建消息对象(Message),因此消息对象需要使用享元模式来缓存,以避免重复分配 & 回收内存。具体来说,Message 使用的是有容量限制的、无头节点的单链表的对象池:

Message 的数据结构

2. Handler 核心源码分析

这一节我们来分析 Handler 的核心源码:

2.1 启动消息循环

  • 问:Looper 如何在子线程中创建?(字节、小米)

要在哪个线程启动消息循环,就需要在该线程执行Looper.prepare() & Looper.loop()。只有调用Looper.loop()之后,消息循环才算真正运转起来了。具体来说,启动消息循环的分为两种情况:主线程消息循环 & 子线程消息循环,前者由 Framework 启动,而后者需要我们自己启动:

  • 主线程消息循环

可以看到,在应用启动时,Framework 已经为主线程开启了消息循环,后续我们熟悉的startActivity & startService都是通过主线程消息循环来驱动的。

  • 子线程消息循环

在子线程开启消息循环,我们需要自己调用Looper.prepare() & Looper.loop();。可以直接创建线程,或者使用 HandlerThread,后者主要考虑的多线程中获取 Looper 的同步问题,见 第 5.1 节

小结一下:创建 Handler 的代码需要放在Looper.prepare(); & Looper.loop();中间执行,这是因为创建 Handler 对象时需要聚合 Looper 对象(默认使用的是当前线程的 Looper),而只有执行Looper.prepare();之后,才会创建该线程私有的 Looper 对象,否则创建 Handler 会抛异常。

2.2 Looper 线程唯一性

  • 问:说一下 Looper、handler、线程间的关系。例如一个线程可以对应几个 Looper、几个Handler?

  • 问:ThreadLocal 的原理,以及在 Looper 是如何应用的?

每个线程只允许调用一次Looper.prepare(),否则会抛异常。这样设计是因为一个 Looper 对应了一个消息循环,而一个线程进行多个消息循环是没有意义的(一个线程不可能同时进行两个死循环)。那么,Handler 是如何保证 Looper 线程唯一的呢?

答:首先,Handler 主要利用了 ThreadLocal 在每个线程单独存储副本的特性,保证了一个ThreadLocal<Looper>在不同线程存取的Looper对象相互独立;其次,ThreadLocal 是 Looper 的一个static final变量,这样就保证了整个进程中 sThreadLocal对象不可变;第三,Looper.prepare()判断在一个线程里重复调用,则会抛出异常。

关于 ThreadLocal 的原理分析,在这篇文章里,我们详细讨论:《Java | ThreadLocal 用法解析》,请关注!

2.3 消息发送

  • 问:Handler#post(Runnable) 是如何执行的?(字节、小米)

  • 问:Handler#sendMessage() 和 Handler#postDelay() 的区别?(字节)

  • 问:多个 Handler 发消息时,消息队列如何保证线程安全?

  • 问:为什么 MessageQueue 不设置消息上限?

消息发送的 API 非常多,最终它们都会调用到Handler#sendMessageAtTime(Message msg, long uptimeMillis),内部会交给MessageQueue#enqueueMessage(Message msg, long when)处理,梳理如下:

消息发送调用链

消息入队关键源码

小结一下:

  • 每个消息的处理时间(when)不一样(SystemClock.uptimeMillis() + delayMill)
  • 消息入队时,根据消息的处理时间(when)插入排序,队头的消息就是最需要执行的消息
  • 当消息队列为空时(无消息时线程会阻塞),消息入队需要唤醒线程
  • 当消息队列不为空时(一般不需要唤醒),只有当开启同步屏障后第一个异步消息需要唤醒(开启同步屏障时会在队首插入一个占位消息,此时消息队列不为空,但是线程可能是阻塞的),关于同步屏障的内容见第 3 节

2.4 消息获取

  • 问:消息队列无消息会怎么样?为什么 block 不会 ANR?

  • 问:Looper 死循环为什么不会 ANR?(B站)

  • 问:Looper 死循环为什么不阻塞主线程?

  • 问:Handler内存泄漏的原因?

上一节我们说到,消息入队后 Looper 所在线程就会被唤醒(如果被阻塞),以继续消息循环。在消息循环中,Looper.loop()会死循环从 MessageQueue 获取队首的消息,因为消息已经按照处理时间(when)排序,所以每次获取的都是when最小的消息:

【图】 loop next

至于 Looper 死循环为什么不会 ANR?

消息队列中无消息怎么处理 block
nativePollOnce值为-1表示无限等待,让出cpu时间片给其线程,本线程等待
0表示无须等待直接返回
nativePollOnce -> epoll(linux) ->linux层的messagequeue
msg -> 5s -> ANRmsg

ANR:
5秒内没有响应输入事件,比如按键、屏幕触摸
10秒内没有处理广播
本质:消息队列中其他消息耗时,按键或广播消息没有及时处理

根本原因不是线程在睡眠,而是消息队列被其他耗时消息阻塞,导致按键或广播消息没有及时处理

Handler内存泄漏的原因
MessageQueue持有Message,Message持有activity
delay多久,message就会持有activity多久
方法:静态内部类、弱引用

取到一个消息时,如果when还不到,则有限等待(nextPollTimeoutMills)nativePoll()
如果消息队列没有消息,则无限等待nativePoll(-1,),而消息入队时,会执行nativeWake()

quit也会nativeWake,唤醒Looper所在线程 => messagequeue返回null => Looper退出

2.5 消息分发

  • 问:Message.callback 与 Handler.callback 哪个优先?

  • 问:Handler.callback 和 handlemessage() 都存在,但 callback 返回 true,handleMessage() 还会执行么?(字节、小米)

获取需要执行的消息之后,将调用msg.target.dispatchMessage(msg);处理消息,具体如下:

【图】

public void dispatchMessage(Message msg) {
    if (msg.callback != null) {
        // 1. 设置了Message.Callback(Runnable)
        handleCallback(msg);
    } else {
        if (mCallback != null) {
            // 2. 设置了 Handler.Callback(Callback )
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        // 3. 未设置 Handler.Callback 或 返回 false
        handleMessage(msg);
    }
}
public interface Callback {
    public boolean handleMessage(Message msg);
}

可以看到,除了在Handler#handleMessage(...)中处理消息外,Handler 机制还提供了两个 Callback 来增加消息处理的灵活性。具体来说,若设置了Message.Callback则优先执行,否则判断Handler.Callback的返回结果,如果返回false,则最后分发到Handler.handleMessage(...)

2.6 终止消息循环

quit:
mQuitting = true
removeAllMessage()
nativeWake() 唤醒,程序从nativePollOnce(-1)开始执行

主线程Looper不允许退出 quit() 抛异常 mQuitAllowed = false
ActivityThead#main looper.loop() 之后抛异常 
原因:是handler驱动的机制,所有的事件都需要Handler处理,例如LAUNCH_ACTIVITY等

3. Handler 同步屏障机制

同步屏障(SyncBarrier)是 Handler 用来筛选高低优先级消息的机制,即:当开启同步屏障时,高优先级的异步消息优先处理。

3.1 开启同步屏障

3.2 关闭同步屏障

3.3 同步屏障下的消息循环


4. IdleHandler 机制

  • 问:IdleHandler 是什么?怎么使用,能解决什么问题?

5. Handler 应用场景

4.1 HandlerThread

Handler 都是在 Looper 所在线程创建的,但是有时候我们需要在其他线程中创建 Looper 所在线程的 Handler,就需要考虑同步问题,使用 HandlerThread 可以简化这种同步处理:

既然涉及多个线程的通信,会有同步的问题,Android为了简化Handler的创建过程,提供了HandlerThread类

wait - notifyAll - 避免prepare之前调用getLooper()

【重点 锁的机制】

4.2 IntentService

处理完 service 自动停止 内存释放

4.3 Fragment 生命周期管理

attach -> commit
Glide生命周期管理 RequestManagerFragment 双重检查(避免连续两次with()重复创建Fragment,因为commit会发到Handle消息队列的)

Handler是贯穿于Android的消息管理机制

所有的代码都是在Handler上运行的(loop()死循环)


参考资料

推荐阅读

感谢喜欢!你的点赞是对我最大的鼓励!欢迎关注彭旭锐的简书!

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