美文网首页Android开发Android面试相关程序员
你真的懂Handler吗?Handler问答

你真的懂Handler吗?Handler问答

作者: 怪盗kidou | 来源:发表于2018-06-24 23:24 被阅读961次

    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

    如果说操作系统是由中断驱动的,那么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 相关的问题,该博客中没有体现出来的赶紧评论告诉我吧,我会持续补充到这篇博客当中。

    相关文章

      网友评论

      • 上官瑞杰:哇,写的太好了,膜拜膜拜:heart_eyes:
        关于最后一个问题,我理解的是如果Looper不去无限循环,那么main方法执行完程序就退出了XD
      • 3a09afd36e4d:文章不错,AS的主题能否分享一下呢,看着不错。😁
        怪盗kidou:就是默认的黑色主题
      • GeniusWong:我一个iOS的, 都知道有 post 方法, 我还用过 :pensive:
        怪盗kidou:@GeniusWong 哈哈,确实遇到过不知道的:flushed:
      • 下雨就好:我也是搞了三年了,handler我就给别人解释不清楚 这个B玩意到底应该怎么理解,跟context一样
      • 夏天吃冰棍:我还是比较关心内存泄漏的问题。
      • guodongAndroid:知乎上看的为什么不会阻塞都涉及到Linux通道了
        怪盗kidou:@guodongAndroid 就算是没有管道也是可以的
      • AndyJennifer:想问问,Native层的消息机制到底有什么作用,他的具体工作是什么,如果Android层的MessageQueue中next()方法会对Nativce层的消息进行操作,那么我自定义HandlerThread。也会对Natice层的消息进行操作。那不是重复操作了?
        怪盗kidou:@AndyandJennifer 按照我的理解,在HandlerThread中应该是没有native消息需要处理的除了唤醒,就是我们调用 nativePollOnce(timeout) 的时候切到native层等待消息,但是HandlerThread中并没有什么需要特别处理的,这个时候他就只需要处理一种native消息,当我们想MessageQueue中发送消息的时候会调用 nativeWake 方法,这个方法就是向native层发送了一个唤醒的消息,这个时候就会切回java层,光从HandlerThread的层面来说确实是没有必要native相关的,但是就为了这个又重新开发一套几乎一模一样的没有必要,这个是我的想法不一定对
        AndyJennifer:@怪盗kidou 如果不是同一个对象,那么自定义HandlerThread也会处理native的消息,如果Main对应中的natice消息已经处理过了。那感觉就有点多余。非常感谢您给我的回答。再次感谢。
        怪盗kidou:@AndyandJennifer 额,我对nativie没做过什么了解,我也只能尝试分析一下,native也有java层的这一套,当Java层的空闲的时候就会由native层去处理消息,但是你的说的会不会重复操作,这个是不会的,如果我没搞错的JVM中的线程和OS的线程是一一对应的,也就是你创建的HandlerThread和main对应的不是同一个native层的对象,所以两者并不会冲突
      • JamFF:对于loop() 为什么不会阻塞主线程,简单来说,Android是事件驱动,所有的事件分发都建立于loop()之下的,只有代码中的耗时操作,会阻塞住loop()导致ANR,这个问题其实有点本末倒置
        怪盗kidou:就看对这个体系理解程度怎么样,之前确实是在网上看到不少人问为什么不会卡住,我也曾陷入过这个问题
      • MigrationUK:native层 messageQueue不问吗?
        怪盗kidou:@MigrationUK 不会问的吧,一般的应用层开发的应该不会深入到native
      • hacket:IdleHandler有啥作用
        怪盗kidou:@hacket 问题就是这个?还是问的怎么监控Looper或者MessageQueue空闲什么的
        hacket:@怪盗kidou 是的,面腾讯被问过
        怪盗kidou:@hacket 这个是你被问到的吗
      • 有点健忘:这玩意怎么说了,当初也把这几个类都完整的看了一遍,流程也大概都知道了,可平时也不关心这个啊,也就post一下完事了,时间久了,就忘了。
        最害怕面试了,还好运气不错,呆过的几家公司面试都不问太多东西。
        记得很久以前有家面试问我handler是不是一定在主线程执行的,当时感觉脑子是晕的,就斩钉截铁的回答,是啊。。出了门立马反应过来,答错了。哎,不过那家应该也知道我是紧张的,后来也给发offer了。不过我已经去另一家了,就没去成。
      • 糖葫芦_倩倩:一个线程中是否可以有多个Handler?
        怪盗kidou:@糖葫芦_倩倩 是的,主要的点其实就是如何唤醒
        糖葫芦_倩倩:@怪盗kidou 最后那个 “Looper.loop() 方法是一个死循环为什么不会阻塞APP?” 通俗的可以理解是阻塞吧,没消息的时候阻塞,有消息的时候唤醒。
        怪盗kidou:@糖葫芦_倩倩 算是提问吗?当然是可以的,因为主线程本来就已经有handler了,我们平时创建的默认也是在主线程上创建的,一个线程里只允许有一个Looper。

      本文标题:你真的懂Handler吗?Handler问答

      本文链接:https://www.haomeiwen.com/subject/bstdyftx.html