【面试官爸爸】继续给我讲讲Handler?

前言

大家好,我是方木,一名在帝都干饭的程序员。本篇文章已经收录到【面试官爸爸】系列,欢迎各位大兄弟捧场

欢迎关注我的微信公众号 「方木 Rudy」,里面不仅有技术干货,也记录了一位北漂程序员挣扎向上的点点滴滴~

熟悉的黑影

一团黑影缓缓向我逼近

他走路不紧不慢,每一步都坚定而沉稳,像沉重的鼓槌一下一下敲在我的心上。稀疏的头顶上闪耀着高 P 的光芒,锐利的眼神仿佛一下看穿了我的心虚

少顷,他坐在我对面

“来面试的?”

”对...对对对“

”那好,Handler 这东西你会吧?来给我讲讲吧“

什么是 Handler?

Handler 是 Android 的一种消息处理机制,与 Looper,MessageQueue 绑定,可以用来进行线程的切换。常用于接收子线程发送的数据并在主线程中更新 UI

你刚说 Handler 可以切换线程,它是怎么实现的?

“切换线程”其实是“线程通信”的一种。为了保证主线程不被阻塞,我们常常需要在子线程执行一些耗时任务,执行完毕后通知主线程作出相应的反应,这个过程就是线程间通信。

Linux 有一种进程间通信的方式叫消息队列,简单来说当两个进程想要通信时,一个进程将消息放入队列中,另一个进程从这个队列中读取消息,从而实现两个进程的通信。

Handler 就是基于这一设计而实现的。在 Android 的多线程中,每个线程都有一个自己的消息队列,线程可以开启一个死循环不断地从队列中读取消息。

当 B 线程要和 A 线程通信时,只需要往 A 的消息队列中发送消息,A 的事件循环就会读取这一消息从而实现线程间通信

呦呵,不错嘛~ 你刚提到了事件循环和消息队列,他们是怎么实现的呢?

Android 的事件循环和消息队列是通过 Looper 类来实现的

Looper.prepare() 是一个静态方法。它会构建出一个 Looper,同时创建一个 MessageQueue 作为 Looper 的成员变量。MessageQueue 是存放消息的队列

当调用 Looper.loop() 方法时,会在线程内部开启一个死循环,不断地从 MessageQueue 中读取消息,这就是事件循环

每个 Handler 都与一个 Looper 绑定,Looper 包含 MessageQueue

那这个 Looper 被存放在哪里呢?

Looper 是存放在线程中的。但如何把 Looper 存放在线程中就引入了 Android 消息机制的另一个重点 --- ThreadLocal

前面我们提到。Looper.prepare() 方法会创建出一个 Looper,它其实还做了一件事,就是将 Looper 放入线程的局部变量 ThreadLocal 中。

// Looper.java
private static void prepare(boolean quitAllowed) {
        if (sThreadLocal.get() != null) {
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        // sThreadLocal是一个静态对象,类型是ThreadLocal<Looper>
        sThreadLocal.set(new Looper(quitAllowed));
 }

那么问题来了,什么是 ThreadLocal 呢?

ThreadLocal 又称线程的局部变量。它最大的神奇之处在于,一个 ThreadLocal 实例在不同的线程中调用 get 方法可以取出不同的值。 用一个例子来表示这种用法:

fun main() {
    val threadLocal = ThreadLocal<Int>()
    threadLocal.set(100)

    Thread {
        threadLocal.set(20)
        println("子线程1 ${threadLocal.get()}")
    }.start()

    Thread {
        println("子线程2 ${threadLocal.get()}")
    }.start()

    println("主线程: ${threadLocal.get()}")

}

// 运行结果:
子线程1 20
主线程: 100
子线程2 null

ThreadLocal 的核心是 set 方法,它的作用总结成一句话就是:

ThreadLocal.set 可以将一个实例变成线程的成员变量

看一下源码

// ThreadLocal.java
public void set(T value) {
        // ① 获取当前线程对象
        Thread t = Thread.currentThread();
        // ② 获取线程的成员属性map
        ThreadLocalMap map = getMap(t);
        // ③ 将value放入map中,如果map为空则创建map
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
}

方法很简单,就是根据当前线程获取线程的一个 map 对象,然后把 value 放入 map 中,达到将 value 变成线程的成员变量的目的

多个 Theadlocal 将多个变量变成线程的成员变量。于是线程就用 ThreadlLocalMap 来管理,key 就是 threadLocal

知道了它 set 方法的奥秘,get 方法也就很简单啦

//ThreadLocal.java
public T get() {
        // ① 获取当前线程对象
        Thread t = Thread.currentThread();
        // ② 获取线程对象的Map
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                 // ③ 获取之前存放的value
                return result;
            }
        }
        return setInitialValue();
    }

和 set 方法差不多,区别就是一个将 value 写入 map,一个从 map 中读取 value。哼哼

不错不错,ThreadLocal 就是这么简单直接。那你说说为什么要将 ThreadLocal 作为 Looper 的设置和获取工具呢?

因为 Looper 要放在线程中的,每个线程只需要一个事件循环,只需要一个 Looper。事件循环是个死循环,多余的事件循环毫无意义。ThreadLocal.set 可以将 Looper 设置为线程的成员变量

同时为了方便在不同线程中获取到 Looper,Android 提供了一个静态对象 Looper.sThreadLocal。这样在线程内部调用 sThreadLocal.get 就可以获取线程对应的 Looper 对象

综上所述,使用 ThreadLocal 作为 Looper 的设置和获取工具是十分方便合理的

好,你刚说 Looper 是个死循环是吧,如果消息队列中没有消息了,这个死循环会一直“空转”吗?

当然不会!如果事件循环中没有消息要处理但仍然执行循环,相当于无意义的浪费 CPU 资源!Android 是不允许这样的

为了解决这个问题,在 MessageQueue 中,有两个 native 方法,nativePollOnce 和 nativeWake。

nativePollOnce 表示进行一次轮询,来查找是否有可以处理的消息,如果没有就阻塞线程,让出 CPU 资源

nativeWake 表示唤醒线程

所以这两个方法的调用时机也就显而易见了

// MessageQueue.java
boolean enqueueMessage(Message msg, long when) {
    ···
    if (needWake) {
        nativeWake(mPtr);
    }
    ···
}

在 MessageQueue 类中,enqueueMessage 方法用来将消息入队,如果此时线程是阻塞的,调用 nativeWake 唤醒线程

// MessageQueue.java
Message next() {
    ···
    nativePollOnce(ptr, nextPollTimeoutMillis);
    ···
}

next() 方法用来取出消息。取之前调用 nativePollOnce() 查询是否有可以处理的消息,如果没有则阻塞线程。等待消息入队时唤醒。

不错,看来你对 Looper 循环的一些边界处理也注意到了。既然 Looper 是个死循环,为什么不会导致 ANR 呢?

首先要明确一下概念。ANR 是应用在特定时间内无法响应一个事件时抛出的异常。

典型例子的是在主线程中执行耗时任务。当一个触摸事件来临时,主线程忙于处理耗时任务而无法在 5s 内响应触摸事件,此时就会抛出 ANR。

但 Looper 死循环是事件循环的基石,本身就是 Android 用来处理一个个事件的。正常情况下,触摸事件会加入到这个循环中被处理。但如果前一个事件太过耗时,下一个事件等待时间太长超出特定时间,这时才会产生 ANR。所以 Looper 死循环并不是产生 ANR 的原因。

好的,看来这个小陷阱没能误导你。那你说说消息队列中的消息是如何进行排序的呢?

这个就要看 MessageQueue 的 enqueueMessage 方法了

enqueueMessage 是消息的入队方法。Handler 在进行线程间通信时,会调用 sendMessage 将消息发送到接收消息的线程的消息队列中,消息队列调用 enqueueMessage 将消息入队。

// MessageQueue.java
boolean enqueueMessage(Message msg, long when) {
    synchronized (this) {
        // ① when是消息入队的时间
        msg.when = when;
        // ② mMessages是链表的头指针,p是哨兵指针
        Message p = mMessages;
        boolean needWake;
        if (p == null || when == 0 || when < p.when) {
            msg.next = p;
            mMessages = msg;
            needWake = mBlocked;
        } else {
            needWake = mBlocked && p.target == null && msg.isAsynchronous();
            Message prev;
            for (;;) {
                prev = p;
                p = p.next;
                // ③ 遍历链表,比较when找到插入位置
                if (p == null || when < p.when) {
                    break;
                }
                if (needWake && p.isAsynchronous()) {
                    needWake = false;
                }
            }
            // ④ 将msg插入到链表中
            msg.next = p;
            prev.next = msg;
        }

        if (needWake) {
            nativeWake(mPtr);
        }
    }
    return true;
}

消息入队分为 3 步:

① 将入队的时间绑定在 when 属性上

② 遍历链表,通过比较 when 找到插入位置

③ 将 msg 插入到链表中

这就是消息的排序方式

好的,假如我有一个消息,想让它优先执行,如何提高它的优先级呢?

根据上个问题,最容易想到的是修改 Message 的 when 属性。这确实不失为一种方法,但 Android 为我们提供了更科学简单的方式,异步消息和同步屏障。

在 Android 的消息机制中,消息分为同步消息、异步消息和同步屏障三种。(没错,同步屏障是 target 属性为 null 的特殊消息)。通常我们调用 sendMessage 方法发送的是同步消息。异步消息需要和同步屏障配合使用,来提升消息的优先级。

同步屏障理解起来其实很简单。刚才说同步屏障是一种特殊的消息,当事件循环检测到同步屏障时,之后的行为不再像之前那样根据 when 的值一个个取消息,而是遍历整个消息队列,查找到异步消息取出并执行。

这个特殊的消息在消息队列中像一个标志,事件循环探测到它时就改变原来的行为,转而去查找异步消息。表现上看起来像一个屏障一样拦住了同步消息。所以形象地称为同步屏障。

源码实现非常非常简单:

//MessageQueue.java
Message next() {
    ···
    // ① target为null表明是同步屏障
    if (msg != null && msg.target == null) {
        // ② 取出异步消息
       do {
            prevMsg = msg;
            msg = msg.next;
       } while (msg != null && !msg.isAsynchronous());
    }
    ···
}

了解的挺透彻的嘛。那假如说我插入了一个同步屏障,不移除,会发生什么事呢?

同步屏障是用来“拦住”同步消息,处理异步消息的。如果同步屏障不移除,消息队列里的异步消息会一个一个被取出处理,知道异步消息被取完。如果此时队列中没有异步消息了,则线程会阻塞,队列中的同步消息永远不会执行。所以同步屏障要及时移除。

那你知道同步屏障有哪些应用场景吗?

同步屏障的核心作用是提高消息优先级,保证 Message 被优先处理。Android 为了避免卡顿,应用在了 view 绘制中。具体可以看之前关于 view 绘制的总结~

为什么使用 Handler 会有内存泄漏问题呢?该如何解决呢?

内存泄漏归根到底其实是生命周期“错位”导致的:一个对象本来应该在一个短的生命周期中被回收,结果被一个长生命周期的对象引用,导致无法回收。 Handler 的内存泄漏其实是内部类持有外部类引用导致的。
形成方式有两种:

(1)匿名内部类持有外部类引用

class Activity {
    var a = 10
    fun postRunnable() {
        Handler(Looper.getMainLooper()).post(object : Runnable {
            override fun run() {
                this@Activity.a = 20
            }
        })
    }
}

Handler 在发送消息时,message.target 属性就是 handler 本身。message 被发送到消息队列中,被线程持有,线程是一个无比“长”生命周期的对象,导致 activity 无法被及时回收从而引起内存泄漏。

解决办法是在 activity destory 时及时移除 runnable

(2)非静态内部类持有外部类引用

//非静态内部类
protected class AppHandler extends Handler {
    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {

        }
    }
}

解决方案是用静态内部类,并将外部引用改为弱引用

private static class AppHandler extends Handler {
    //弱引用,在垃圾回收时,被回收
    WeakReference<Activity> activity;

    AppHandler(Activity activity){
        this.activity = new WeakReference<Activity>(activity);
    }

    public void handleMessage(Message message){
        switch (message.what){
        }
    }
}

好的,Handler,Looper 和 MessageQueue 的基础知识我基本问完了,最后一个问题,你知道 HandlerThread 和 IdleHandler 吗?它们是用来干什么的?

HandlerThread 顾名思义就是 Handler+Thread 的结合体,它本质上是一个 Thread。

我们知道,子线程是需要我们通过 Looper.prepare()和 Looper.loop()手动开启事件循环的。HandlerThread 其实就帮我们做了这件事,它是一个实现了事件循环的线程。我们可以在这个线程中做一些 IO 耗时操作。

IdleHandler 虽然叫 Handler,其实和同步屏障一样是一种特殊的”消息"。不同于 Message,它是一个接口

public static interface IdleHandler{
    boolean queueIdle();
}

Idle 是空闲的意思。与同步屏障不同,同步屏障是提高异步消息的优先级使其优先执行,IdleHandler 是事件循环出现空闲的时候来执行。

这里的“空闲”主要指两种情况

(1)消息队列为空

(2)消息队列不为空但全部是延时消息,也就是 msg.when > now

利用这一特性,我们可以将一些不重要的初始化操作放在 IdleHandler 中执行,以此加快 app 启动速度;由于 View 的绘制是事件驱动的,我们也可以在主线程的事件循环中添加一个 IdleHandler 来作为 View 绘制完成的回调,等等。 但应该注意的是,如果主线程中一直有任务执行,IdleHandler 被执行的时机会无限延后,使用的时候要注意哦~

本篇是【面试官爸爸】系列第三篇,后续我还会继续更新这个系列,包括面试最常考的 Activity 启动,编译打包流程及优化,Java 基础,设计模式,组件化等面试常问的问题。如果不想错过,欢迎点赞,收藏,关注我!球球兄弟萌辣,这个对我真的很重要!!

我是方木

一个在互联网世界挣扎向上的打工人

努力生活,努力向前

微信搜公众号 方木 Rudy 第一时间获取我的更新!里面不仅有技术,还有故事和感悟

搜索它,带走我!我们下期见~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容