2018年8月1日以前谢绝全文转载(已授权除外)
本文作者:@怪盗kidou
本文链接:https://www.jianshu.com/p/f70ee1765a61
周末在家有点儿无聊,不知道该干些啥,想了想开通博客这么长时间以来好像并没有些什么关于 Android 的东西,所以这次来写写Android 相关的博客 —— Handler。
为什么写 Handler
确实 Handler
是 Android
开发过程中非常非常常见的东西,讲Handler的博客也不胜枚举为什么我还要写关于Handler的内容?
起因是这样的,公司为了扩张业务准备做一个新的产品线所以给移动端这边分配了4个招聘名额(iOS和Android各两名),头一个星期我因为在忙着做需求并没有参与公司的面试,都是公司的另外两个同事在参与面试,后一个星期我也参与到其中,但是我发现一个很严重的问题:在我面试的几个人虽然工作经验都集中3~6年但都没有把 Handler
讲清楚。
与其他的博客有什么不同
市面上有太多讲 Handler 的博客了,那我的博客要如何做到让人耳目一新并且切实能让大家受益呢?
我想了一下,Handler的基本原理很简单,但细节还是蛮多的,这次发现问题也是通过面试得出的,所以我决定通过模拟面试的方式告诉你关于 Handler 的那些事儿。
约定
本文的各个问题只是我自己想出来的,并不是出自真实的面试中(除了部分我面试别人时的提问),其他的均为我为了给大家介绍 Handler 机制时想出的问题。
本文后面会出现的部分源码,为避免小伙伴们在 Android Studio 中看到代码与我博客中的不一致,这里先统一一下环境:
- sdk版本:API 27
android{
compileSdkVersion 27
// ......
}
- 源码版本:27_r03
Q:说一下 Handler机制中涉及到那些类,各自的功能是什么
A:Handler
、Looper
、MessageQueue
、Message
,主要是这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 循环结束时 prev
和p
指向的样子应该是下图的
由于这个特性,所以当两个 Message#when
一致时插入序按先后顺序,比如两个的 when 都是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
通过它的 get
和 set
方法来赋值和取值。
由于 ThreadLocal
是与线程绑定的,所以我们只要把 Looper
与 ThreadLocal
绑定了,那 Looper
和 Thread
也就关联上了
ThreadLocal
的原理在问 Handler
机制的时候也是一个比较常问的点,但是介绍的博客很多,源码也没有多少,这里就不再介绍了,如果有需要的话后期会写新博客。
Q:Handler 有哪些构造方法
如果你上面的问题 子线程中可以创建 Handler 对象吗 没有答上的话,我一般会通过这个问题引导一下。
问这个问题主要是想问你构造方法可以传那些参数,并不是要你完全说出来,但是当你知道可以传哪些参数的时候,也可以推算出来有几个构造方法。
先说可以传那些类型(仅限开放API,被 @hide 标注的不算在内),仅两种类型:Looper
、Handler$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.what
和msg.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#sendMessage
、Handler#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
相关的问题,该博客中没有体现出来的赶紧评论告诉我吧,我会持续补充到这篇博客当中。
我最近刚刚开通了微信公众号,欢迎关注