关于 Handler 的问题已经是一个老生常谈的问题, 网上有很多优秀的文章讲解 Handler, 之所以还要拿出来讲这个问题, 是因为我发现, 在一些细节上面, 很多人还都似懂非懂, 面试的时候大家都能说出来一些东西, 但是又说不到点子上, 比如今天要说的这个问题: 为什么Looper 中的 loop()方法不能导致主线程卡死??
先普及下 Android 消息机制 的基础知识:
Android 的消息机制涉及了四个类:
- Handler: 消息的发送者和处理着
- Message: 消息的载体
- MessageQueue: 消息队列
- Looper: 消息循环体
其中每一条线程只有一个消息队列MessageQueue, 消息的入队是通过 MessageQueue 中的 enqueueMessage() 方法完成的, 消息的出队是通过Looper 中的loop()方法完成的.
Android 是单线程模型, UI的更新只能在主线程中执行, 在开发过程中, 不能在主线程中执行耗时的操作, 避免造成卡顿, 甚至导致ANR.
这里面, 我故意把执行耗时这四个字突出, 我想大家在面试的时候说个这个问题, 但是造成界面卡顿甚至ANR的原因真的是执行耗时操作本省造成的吗??
现在我们来写个例子, 我们定义一个 button, 在 button 的 onClick 事件中写一个死循环来模拟耗时操作, 代码很简单, 例子如下:
@Override
public void onClick(View v) {
if (v.getId() == R.id.coordination) {
while (true) {
Log.i(TAG, "onClick: 耗时测试");
}
}
}
注意, 这里我们运行程序, 然后点击按钮以后, 接下来不做任何操作
运行程序以后, 你会发现, 我们的程序会已知打印 log, 并不会出现ANR的情况…
按照我们以往的想法, 如果我们在主线程中执行了耗时的操作, 这里还是一个死循环, 那么肯定会造成ANR的情况, 那为什么我们的程序现在还在打印 log, 并没有出现我们所想的ANR呢??
接下来让我们继续, 如果这时候你用手指去触摸屏幕, 比如再次点击按钮或者点击我们的返回键, 你会发现5s 以后就出现了ANR….其实前面的这个例子, 已经很好的说明了我们的问题. 之所以运行死循环不会导致ANR, 而在自循环以后触摸屏幕却出发了ANR, 原因就是因为耗时操作本身并不会导致主线程卡死, 导致主线程卡死的真正原因是耗时操作之后的触屏操作, 没有在规定的时间内被分发。其实这也是我们标题索要讨论的Looper 中的 loop()方法不会导致主线程卡死的原因之一。
看过 Looper 源码的都知道, 在 loop() 方法中也是有死循环的:
for (;;) {
//省略
}
前面我们说过, 死循环并不是导致主线程卡多的真正原因, 真正的原因是死循环后面的事件没有得到分发, 那 loop()方法里面也是一个死循环, 为什么这个死循环后面的事件没有出现问题呢??
熟悉Android 消息机制的都知道, Looper 中的 loop()方法, 他的作用就是从消息队列MessageQueue 中不断地取消息, 然后将事件分发出去:
for (;;) {
/**
* 通过 MessageQueue.next() 方法不断获取消息队列中的消息
*/
Message msg = queue.next(); // might block
if (msg == null) {//如果没有消息就会阻塞在这里
// No message indicates that the message queue is quitting.
return;
}
// This must be in a local variable, in case a UI event sets the logger
Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}
/**
* 取出消息以后调用 handler 的 dispatchMessage() 方法来处理消息
*/
msg.target.dispatchMessage(msg);
if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}
// Make sure that during the course of dispatching the
// identity of the thread wasn't corrupted.
final long newIdent = Binder.clearCallingIdentity();
if (ident != newIdent) {
Log.wtf(TAG, "Thread identity changed from 0x"
+ Long.toHexString(ident) + " to 0x"
+ Long.toHexString(newIdent) + " while dispatching to "
+ msg.target.getClass().getName() + " "
+ msg.callback + " what=" + msg.what);
}
msg.recycleUnchecked();
}
最终调用的是 msg.target.dispatchMessage(msg) 将我们的事件分发出去, 所以不会造成卡顿或者ANR.
对于第一个原因, 我相信大家看那个对应的例子, 一定能看明白怎么回事, 但是对于第二个原因,该如何去验证呢??
想象一下, 我们自己写的那个例子, 造成ANR是因为死循环后面的事件没有在规定的事件内分发出去, 而 loop()中的死循环没有造成ANR, 是因为 loop()中的作用就是用来分发事件的, 那么如果我们让自己写的死循环拥有 loop()方法中同样的功能, 也就是让我们写的死循环也拥有事件分发这个功能, 如果没有造成死循环, 那岂不是就验证了第二点原因??
接下来我将我们的代码改造一下, 我们首先通过一个 Handler 将我们的死循环发送到主线程的消息队列中, 然后将 loop() 方法中的部分代码 copy 过来, 让我们的死循环拥有分发的功能:
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
try {
Looper mainLooper = Looper.getMainLooper();
final Looper me = mainLooper;
final MessageQueue queue;
Field fieldQueue = me.getClass().getDeclaredField("mQueue");
fieldQueue.setAccessible(true);
queue = (MessageQueue) fieldQueue.get(me);
Method methodNext = queue.getClass().getDeclaredMethod("next");
methodNext.setAccessible(true);
Binder.clearCallingIdentity();
for (; ; ) {
Message msg = (Message) methodNext.invoke(queue);
if (msg == null) {
return;
}
msg.getTarget().dispatchMessage(msg);
msg.recycle();
}
} catch (Exception e) {
e.printStackTrace();
}
}
});
运行代码后你会发现, 我们自己写的死循环也不会造成ANR了!! 这也验证了我们的第二个原因
到目前为止, 关于为什么 Looper 中的 loop() 方法不会造成主线程阻塞的原因就分析完了, 主要有两点原因:
1.耗时操作本身并不会导致主线程卡死, 导致主线程卡死的真正原因是耗时操作之后的触屏操作, 没有在规定的时间内被分发。
2.Looper 中的 loop()方法, 他的作用就是从消息队列MessageQueue 中不断地取消息, 然后将事件分发出去。
后记:
关于这个问题, 我上 google 搜了一下, 发现网上有很多博主说原因是因为 linux 内核的 eoll 模型, native 层会通过读写文件的方式来通知我们的主线程, 如果有事件就唤醒主线程, 如果没有就让主线程睡眠。
其实我个人的并不同意这个观点, 这个有点所答非所谓, 如果说没有事件让主线程休眠是不会造成主线程卡死的原因, 那么有事件的时候, 在忙碌的时候不也是在死循环吗??那位什么忙碌的时候没有卡死呢?? 我个人认为 epoll 模型通过读写文件通知主线程的作用, 应该是起到了节约资源的作用, 当没有消息就让主线程休眠, 这样可以节约 cpu 资源, 而并不是不会导致主线程卡死的原因。
免费获取安卓开发架构的资料(包括Fultter、高级UI、性能优化、架构师课程、 NDK、Kotlin、混合式开发(ReactNative+Weex)和一线互联网公司关于android面试的题目汇总可以加:936332305 / 链接:点击链接加入【安卓开发架构】:https://jq.qq.com/?_wv=1027&k=515xp64

网友评论