Handler机制是面试官非常喜欢问的知识点,有关Handler的面试题也是五花八门,举几个例子: Looper的loop()方法中死循环为什么不会造成ANR?死循环会造成cpu资源浪费吗?一个线程中Handler、Looper、MessageQueue的数量?它们的数量源码中怎么实现的?MessageQueue的数据结构是什么样子?为何主线程可以使用 Handler?如果想要在子线程中使用 Handler 要做些什么?这些题目从各个角度考察对handler源码的理解。只有对Handler的源码深入理解,才能够轻松应对。下面列举一些常见的handler面试题(包括上面几个例子)并结合源码解析
Android是事件驱动的,在Loop.loop()中不断接收事件、处理事件,而Activity的生命周期都依靠于主线程的Loop.loop()来调度。整个Android就是在一个Looper的loop循环的,整个Android的一切都是以Handler机制进行的,即只要有代码执行都是通过Handler来执行的,而所谓ANR便是Looper.loop没有得到及时处理。真正会卡死主线程的操作是在回调方法onCreate/onStart等操作时间过长,会导致掉帧,甚至发生ANR,looper.loop本身不会导致应用卡死。(有兴趣可以了解ANR相关源码,了解ANR发生原因)。
死循环会造成cpu资源浪费吗?
安卓在这个问题上使用了Linux 中的pipe/epoll机制进行优化,looper的loop()方法最终会调用MessageQueue的next()方法出队, next方法中会调用nativePollOnce()方法,这是一个nativie方法对应安卓C++层android_os_MessageQueue.cpp的android_os_MessageQueue_nativePollOnce(),最终会调用到Looper.cpp的一些方法完成阻塞操作,此时主线程会释放CPU资源进入休眠状态。Handler的sendMessage()方法最终会调用MessageQueue的enqueueMessage()方法消息入队,而其中会调用nativeWake()方法来唤醒。当没有事件需要处理时,主线程就会阻塞;当子线程往消息队列发送消息,并且往管道文件写数据时,主线程就被唤醒。所以,主线程大多数时候都是处于休眠状态,并不会消耗大量CPU资源。
Message next() {
......
for (;;) {
......
// nativePollOnce native方法 这里进入等待唤醒状态
nativePollOnce(ptr, nextPollTimeoutMillis);
synchronized (this) {
//出队操作
}
}
......
}
boolean enqueueMessage(Message msg, long when) {
......
synchronized (this) {
......
//入队操作
}
if (needWake) {
//这是一个nativie方法,这里唤醒 nativePollOnce
nativeWake(mPtr);
}
}
return true;
}
答:一个线程中只有1个Looper,1个MessageQueue,可以有多个Handler。我们知道要在子线程创建handler需要先调用Looper.prepare()来创建looper,否则会报错。源码里面已经做了说明。
public Handler(Callback callback, boolean async) {
......
mLooper = Looper.myLooper();
if (mLooper == null) {//判断Looper是否被创建
throw new RuntimeException(
"Can't create handler inside thread " + Thread.currentThread()
+ " that has not called Looper.prepare()");
}
mQueue = mLooper.mQueue;
mCallback = callback;
mAsynchronous = async;
}
再来看一下Looper.prepare()做了什么
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));
}
ThreadLoal 是线程局部变量,同一个 ThreadLocal 所包含的对象,在不同的 Thread 中有不同的副本。ThreadLocal在当前线程维护一份Looper的实例。所以一个线程只有一个Looper,而MesssageQueue会随Looper创建时一起创建。
private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}
所以也只有一个MesssageQueue,而handler则没有限制可以有多个。
查看源码可以发现MessageQueue并不是队列的数据结构,其中的元素都是通过next引用连接,是一个单链表结构。
为什么在子线程使用handler报错,在主线程就可以使用 Handler。可以再在Handler的构造方法中找到答案
public Handler(@Nullable Callback callback, boolean async) {
......
mLooper = Looper.myLooper();
if (mLooper == null) {
throw new RuntimeException(
"Can't create handler inside thread " + Thread.currentThread()
+ " that has not called Looper.prepare()");
}
......
}
可以看到是因为没有调用 Looper.prepare()创建looper。那主线程为什么不报错?查看在程序的入口ActivityThread的main 方法 里边调用了Looper.prepareMainLooper()来创建looper和Looper.loop()来开启循环。子线程如何使用handler?自然是需要再子线程调用Looper.prepare和Looper.loop()
handler造成的内存泄漏可以归为内部类造成的内存泄漏,我们通常使用匿名内部类的形式来使用Handler,内部类就会持有外部类引用,当内部类有消息未处理完时,内部类无法回收就会造成外部类也无法回收造成内存泄漏。
可以看第一个问题中贴出的源码,message的入队操作都是在同步代码块中进行的。