你真的懂Handler吗?Handler问答

2018年8月1日以前谢绝全文转载(已授权除外)
本文作者:@怪盗kidou
本文链接:https://www.jianshu.com/p/f70ee1765a61

周末在家有点儿无聊,不知道该干些啥,想了想开通博客这么长时间以来好像并没有些什么关于 Android 的东西,所以这次来写写Android 相关的博客 —— Handler。

为什么写 Handler

确实 HandlerAndroid 开发过程中非常非常常见的东西,讲Handler的博客也不胜枚举为什么我还要写关于Handler的内容?

起因是这样的,公司为了扩张业务准备做一个新的产品线所以给移动端这边分配了4个招聘名额(iOS和Android各两名),头一个星期我因为在忙着做需求并没有参与公司的面试,都是公司的另外两个同事在参与面试,后一个星期我也参与到其中,但是我发现一个很严重的问题:在我面试的几个人虽然工作经验都集中3~6年但都没有把 Handler 讲清楚。

与其他的博客有什么不同

市面上有太多讲 Handler 的博客了,那我的博客要如何做到让人耳目一新并且切实能让大家受益呢?

我想了一下,Handler的基本原理很简单,但细节还是蛮多的,这次发现问题也是通过面试得出的,所以我决定通过模拟面试的方式告诉你关于 Handler 的那些事儿。

约定

本文的各个问题只是我自己想出来的,并不是出自真实的面试中(除了部分我面试别人时的提问),其他的均为我为了给大家介绍 Handler 机制时想出的问题。

本文后面会出现的部分源码,为避免小伙伴们在 Android Studio 中看到代码与我博客中的不一致,这里先统一一下环境:

  • sdk版本:API 27
android{
  compileSdkVersion 27
  // ......
}
  • 源码版本:27_r03
深度截图_选择区域_20180623165118.png

深度截图_选择区域_20180623165324.png

Q:说一下 Handler机制中涉及到那些类,各自的功能是什么

A:HandlerLooperMessageQueueMessage,主要是这4个,ThreadLocal 可以不算在里面毕竟这个是JDK本身自带类不是专门为Handler机制设计的。

Handler 的作用是将 Message 对象发送到 MessageQueue 中去,同时将自己的引用赋值给 Message#target

Looper 的作用是将 Message 对象从 MessageQueue 中取出来,并将其交给 Handler#dispatchMessage(Message) 方法,这里需要主要的是:不是调用 Handler#handleMessage(Message) 方法,具体原因后面会讲。

MessageQueue 的作用负责插入和取出 Message

Q:Handler 有哪些发送消息的方法

我主要是看其知不知道 post 相关的方法,问了两个人两人都不知道有post方法

sendMessage(Message msg)
sendMessageDelayed(Message msg, long uptimeMillis)
post(Runnable r)
postDelayed(Runnable r, long uptimeMillis)
sendMessageAtTime(Message msg,long when)

下面的几个方法在我眼中可能并不是那么重要

sendEmptyMessage(int what)
sendEmptyMessageDelayed(int what, long uptimeMillis)
sendEmptyMessageAtTime(int what, long when)

Q:MessageQueue 中的 Message 是有序的吗?排序的依据是什么

是有序的。你可能会想这不是废话嘛,Queue 都是有序的,Set 才是无序的,这里想问你的是他的内部是基于什么进行的排序,排序的依据是 Message#when 字段,表示一个相对时间,该值是由 MessageQueue#enqueueMessage(Message, Long) 方法设置的。

// 见 MessageQueue.java:554,566~578
boolean enqueueMessage(Message msg, long when) {
    // ....
    synchronized (this) {
        // ....
        msg.markInUse();
        msg.when = when;
        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;
                    // 一致循环,直到找到尾巴(p == null)
                    // 或者这个 message 的 when 小于我们当前这个 message 的 when
                    if (p == null || when < p.when) {
                        break;
                    }
                    if (needWake && p.isAsynchronous()) {
                        needWake = false;
                    }
                }
                msg.next = p; // invariant: p == prev.next
                prev.next = msg;
            }
    }
    return true;
}

如果当前插入的 message#when 是介于 5~8 之间,那么for 循环结束时 prevp 指向的样子应该是下图的

prev和p的关系

由于这个特性,所以当两个 Message#when 一致时插入序按先后顺序,比如两个的 when 都是7,那么第一个进入后的样子如下图(黄):

第一个 7 入队列后

第二个进入后的样子(红):

第二个 7 入队列后

Q:Message#when 是指的是什么

Message#when 是一个时间,用于表示 Message 期望被分发的时间,该值是 SystemClock#uptimeMillis()delayMillis 之和。

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

SystemClock#uptimeMillis() 是一个表示当前时间的一个相对时间,它代表的是 自系统启动开始从0开始的到调用该方法时相差的毫秒数

Q:Message#when 为什么不用 System.currentTimeMillis() 来表示

System.currentTimeMillis() 代表的是从 1970-01-01 00:00:00 到当前时间的毫秒数,这个值是一个强关联 系统时间 的值,我们可以通过修改系统时间达到修改该值的目的,所以该值是不可靠的值。

比如手机长时间没有开机,开机后系统时间重置为出厂时设置的时间,中间我们发送了一个延迟消息,过了一段时间通过 NTP 同步了最新时间,那么就会导致 延迟消息失效

同时 Message#when 只是用 时间差 来表示先后关系,所以只需要一个相对时间就可以达成目的,它可以是从系统启动开始计时的,也可以是从APP启动时开始计时的,甚至可以是定期重置的(所有消息都减去同一个值,不过这样就复杂了没有必要)。

Q:子线程中可以创建 Handler 对象吗?

不可以在子线程中直接调用 Handler 的无参构造方法,因为 Handler 在创建时必须要绑定一个 Looper 对象,有两种方法绑定

  • 先调用 Looper.prepare() 在当前线程初始化一个 Looper
Looper.prepare();
Handler handler = new Handler();
// ....
// 这一步可别可少了
Looper.loop();
  • 通过构造方法传入一个 Looper
Looper looper = .....;
Handler handler = new Handler(looper);

Q:Handler 是如何与 Looper 关联的

上个问题应该告知了其中一种情况:通过构造方法传参。

还有一种是我们直接调用无参构造方法时会有一个自动绑定过程

// Handler.java:192
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;
}

Q:Looper 是如何与 Thread 关联的

Looper 与 Thread 之间是通过 ThreadLocal 关联的,这个可以看 Looper#prepare() 方法

// Looper.java:93
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));
}

Looper 中有一个 ThreadLocal 类型的 sThreadLocal静态字段,Looper通过它的 getset 方法来赋值和取值。

由于 ThreadLocal是与线程绑定的,所以我们只要把 LooperThreadLocal 绑定了,那 LooperThread 也就关联上了

ThreadLocal的原理在问 Handler 机制的时候也是一个比较常问的点,但是介绍的博客很多,源码也没有多少,这里就不再介绍了,如果有需要的话后期会写新博客。

Q:Handler 有哪些构造方法

如果你上面的问题 子线程中可以创建 Handler 对象吗 没有答上的话,我一般会通过这个问题引导一下。

问这个问题主要是想问你构造方法可以传那些参数,并不是要你完全说出来,但是当你知道可以传哪些参数的时候,也可以推算出来有几个构造方法。

先说可以传那些类型(仅限开放API,被 @hide 标注的不算在内),仅两种类型:LooperHandler$Callback,那么我们就可以退算出有多少个公共构造方法了:无参、单Looper、单Callback、Looper和Handler,共4种。

public Handler() {
    this(null, false);
}
public Handler(Callback callback) {
    this(callback, false);
}
public Handler(Looper looper) {
    this(looper, null, false);
}
public Handler(Looper looper, Callback callback) {
    this(looper, callback, false);
}

还有一个 boolean 的 async, 不过这个不是开放API,即使不知道个人觉得完全没有问题。

Q:looper为什么调用的是Handler的dispatchMessage方法

看一下dispatchMessage方法

// Handler.java:97
public void dispatchMessage(Message msg) {
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        if (mCallback != null) {
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        handleMessage(msg);
    }
}

从上面的代码不难看出有两个原因:

  • msg.callback != null时会执行 handleCallback(msg),这表示这个 msg 对象是通过 handler#postAtTime(Runnable, long) 相关方法发送的,所以 msg.whatmsg.obj 都是零值,不会交给Handler#handleMessage方法。
  • 从上一个问题你应该看到了Handler可以接受一个 Callback 参数,类似于 View 里的 OnTouchListener ,会先把事件交给 Callback#handleMessage(Message) 处理,如果返回 false 时该消息才会交给 Handler#handleMessage(Message)方法。

Q:在子线程中如何获取当前线程的 Looper

Looper.myLooper()

内部原理就是同过上面提到的 sThreadLocal#get() 来获取 Looper

// Looper.java:203
public static @Nullable Looper myLooper() {
    return sThreadLocal.get();
}

Q:如果在任意线程获取主线程的 Looper

Looper.getMainLooper()

这个在我们开发 library 时特别有用,毕竟你不知道别人在调用使用你的库时会在哪个线程初始化,所以我们在创建 Handler 时每次都通过指定主线程的 Looper 的方式保证库的正常运行。

Q:如何判断当前线程是不是主线程

知道了上面两个问题,这个问题就好回答了

方法一:

Looper.myLooper() == Looper.getMainLooper()

方法二:

Looper.getMainLooper().getThread() == Thread.currentThread()

方法三: 方法二的简化版

Looper.getMainLooper().isCurrentThread()

Q:Looper.loop() 会退出吗?

不会自动退出,但是我们可以通过 Looper#quit() 或者 Looper#quitSafely() 让它退出。

两个方法都是调用了 MessageQueue#quit(boolean) 方法,当 MessageQueue#next() 方法发现已经调用过 MessageQueue#quit(boolean) 时会 return null 结束当前调用,否则的话即使 MessageQueue 已经是空的了也会阻塞等待。

Q:MessageQueue#next 在没有消息的时候会阻塞,如何恢复?

当其他线程调用 MessageQueue#enqueueMessage 时会唤醒 MessageQueue,这个方法会被 Handler#sendMessageHandler#post 等一系列发送消息的方法调用。

boolean enqueueMessage(Message msg, long when) {
    // 略
    synchronized (this) {
        // 略
        boolean needWake;
        if (p == null || when == 0 || when < p.when) {
            // New head, wake up the event queue if blocked.
            msg.next = p;
            mMessages = msg;
            needWake = mBlocked;
        } else {
            // 略
        }
        if (needWake) {
            nativeWake(mPtr); // 唤醒
        }
    }
    return true;
}

Q:Looper.loop() 方法是一个死循环为什么不会阻塞APP

我认为更好的回答:
这是一个假象,举个例子

public static void main(String[] args){
    while(true){
        // do work in while
    }
    doSomeThingOutWhile();
}

对于从整个main方法来看,while(true) 确实阻塞了 doSomeThingOutWhile() 这个方法的执行,对于这样看,好像确实是卡住了,因为我们在 doSomeThingOutWhile 方法中想要做的事没法做了,但是如果我们把我们要做的事情通过队列放到 while 里面去做,那么是不是你就不会觉得卡了,你想要做的事情都完成了,虽然有个死循环但并不影响你想要做什么,而Android中 Looper.loop() 就是这样的原理,因为所有让我们会觉得卡住的都被放到 MessageQueue 里,然后通过Looper取出并交给 Handler执行了。

PS:不仅仅是Android,几乎所有和UI操作的都有一个类似Android Handler机制的事件循环处理机制

-----分割线-------

下面是原始回答,会让人觉得卡是因为死循环之后的代码无法执行,如果没有理解到其实我们的代码都是执行在死循环里面的话,还是没有办法理解为什么不会卡。

如果说操作系统是由中断驱动的,那么Android的应用在宏观上可以说是 Handler 机制驱动的,所以主线程中的 Looper 不会一直阻塞的,原因如下(以下是我瞎JB猜的,欢迎补充、指正):

  • 当队列中只有延迟消息的时候,阻塞的时间等于头结点的 when 减去 当前时间,时间到了以后会自动唤醒。
  • 在Android中 一个进程中不会只有一个线程,由于 Handler 的机制,导致我们如果要操作 View 等都要通过 Handler 将事件发送到主线程中去,所以会唤醒阻塞。
  • 传感器的事件,如:触摸事件、键盘输入等。
  • 绘制事件:我们知道要想显示流畅那么屏幕必须保持 60fps的刷新率,那绘制事件在入队列时也会唤醒。
  • 总是有Message 源源不断的被加入到 MessageQueue 中去,事件是一环扣一环的,举个 Fragment 的栗子:
getSupportFragmentManager()
        .beginTransaction()
        .replace(android.R.id.content,new MyFragment())
        .commit();

这个时候并不是立马把 MyFragment显示出来了,而是经过层层的调用来到了 FragmentManager#scheduleCommit() 方法,在这里又有入队列操作,

// FragmentManager.java:2103
private void scheduleCommit() {
    synchronized (this) {
        boolean postponeReady =
                mPostponedTransactions != null && !mPostponedTransactions.isEmpty();
        boolean pendingReady = mPendingActions != null && mPendingActions.size() == 1;
        if (postponeReady || pendingReady) {
            mHost.getHandler().removeCallbacks(mExecCommit);
            mHost.getHandler().post(mExecCommit); // 这里有入队列操作
        }
    }
}

提交后是不是紧接着又是一系列的生命周期的事件分发?所以。。。

你还有什么关于Handler的问题,评论告诉我吧

如果你还有什么在面试中遇到的和 Handler 相关的问题,该博客中没有体现出来的赶紧评论告诉我吧,我会持续补充到这篇博客当中。


我最近刚刚开通了微信公众号(怪盗kidou),欢迎关注

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

推荐阅读更多精彩内容