你真的懂Handler吗?Handler问答

96
怪盗kidou
1.4 2018.06.24 23:24* 字数 2845

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 相关的问题,该博客中没有体现出来的赶紧评论告诉我吧,我会持续补充到这篇博客当中。


我最近刚刚开通了微信公众号,欢迎关注

我的微信公众号
Android/Java开发