在之前的面试当中,我被问到了这么一个问题“如果一个Handler收到大量的Message的时候会发生什么?”最近闲来无事,做了一个Demo实验了一下,以下是相关的心路历程。
先说结论
如果一个使用主线程Looper的Handler在一段时间内收到大量的message的时候,消息过多,可能会使得消息处理不及时。带来的副作用是可能会使得界面的刷新和touch事件的响应延迟
相应的Demo代码
public class MainActivity extends BaseActivity {
private Handler handler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(@NonNull Message msg) {
if (msg.what == 1) {
String time = (String) msg.obj;
Log.e("Test", time);
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final View circle = findViewById(R.id.circle);
findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.e("Testttttttt", "Click!!!!");
((MiFloatWindowCircle) circle).start();
}
});
}
@Override
protected void onResume() {
super.onResume();
new Thread(new Runnable() {
@Override
public void run() {
int i = 0;
while (true) {
String msg = "0 + " + System.currentTimeMillis();
if (i % 50 == 0) {
} else if (i < 2000) {
handler.sendMessage(handler.obtainMessage(1, msg));
}else {
break;
}
i++;
}
}
}).start();
}
}
心路历程
在寻找这个原因的时候,我首先需要解决的一个问题就是“为什么这么做不会触发ANR?”
首先就需要知道ANR发生的原理
ANR是什么?为什么不会触发ANR
这是需要解决的第一个问题。经过查阅大量的资料后大致可以理解为:
在某个方法开始执行之前发送一个延时任务,如果在延时任务触发之前执行完成,则取消相应的延时任务。那么这个时候就不会触发ANR;如果触发了这个延时任务,那么就会产生ANR。
经过查阅相关资料和代码后发现。
在AMS的UiHandler当中有这么一段代码
final class UiHandler extends Handler {
public UiHandler() {
super(com.android.server.UiThread.get().getLooper(), null, true);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case SHOW_NOT_RESPONDING_UI_MSG: {
mAppErrors.handleShowAnrUi(msg);
ensureBootCompleted();
} break;
可以看到,当UiHandler收到SHOW_NOT_RESPONDING_UI_MSG就会触发展示“程序无响应”的Dialog
到这个时候,似乎可以解释为什么在收到大量Msg的时候不会触发ANR的原因了——(因为Handler的Msg的处理是一个非常典型的生产者——消费者场景)生产者生成了太多的Msg,导致缓存中触发ANR的Msg迟迟得不到处理,从而不会触发ANR。但是当使用下面一种测试代码进行测试的时候,仍然触发了ANR
@Override
protected void onResume() {
super.onResume();
while (true) {
String msg = "0 + " + System.currentTimeMillis();
handler.sendMessage(handler.obtainMessage(1, msg));
}
}
所以这种解释似乎就不能成立。
同时经过查看UiHandler所属的Looper也发现,UiHandler其实并不属于这个Application中的主线程的Looper,而是使用一个com.android.server.UiThread.get().getLooper()的Looper。
如果ANR延时任务所属的Looper和我们发送使用的Looper不是同一个Looper的话,那么这种推测就是不成立的。
同时在查阅资料和实验后,对于Activity的ANR场景有了进一步的理解——只要用户不进行输入操作,其实是不会触发ANR的。Activity的ANR是在inputDisptcher在通知inputChannel inputEvent的同时发送的。所以即使是像上面那样,在onResume中写一个死循环,只要在运行的时候不进行输入的操作。依然不会触发ANR。
换一种思路
既然现象是界面无响应,那么就需要看一下View在postInvalidate()的时候做了什么
/**
* <p>Cause an invalidate to happen on a subsequent cycle through the event
* loop. Waits for the specified amount of time.</p>
*
* <p>This method can be invoked from outside of the UI thread
* only when this View is attached to a window.</p>
*
* @param delayMilliseconds the duration in milliseconds to delay the
* invalidation by
*
* @see #invalidate()
* @see #postInvalidate()
*/
public void postInvalidateDelayed(long delayMilliseconds) {
// We try only with the AttachInfo because there's no point in invalidating
// if we are not attached to our window
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
attachInfo.mViewRootImpl.dispatchInvalidateDelayed(this, delayMilliseconds);
}
}
public void dispatchInvalidateDelayed(View view, long delayMilliseconds) {
Message msg = mHandler.obtainMessage(MSG_INVALIDATE, view);
mHandler.sendMessageDelayed(msg, delayMilliseconds);
}
经过一路追踪以后追踪到,View在调用postInvalidate()以后会发送一条msg到ViewRootHandler中,
而这个ViewRootHandler在初始化的时候没有传递它需要使用哪个Looper来进行消息的处理。
根据Android的基础知识可以得知,View只在主线程进行操作。所以ViewRootHandler的构造方法中的Looper.myLooper()最后得到的是MainThread中的Looper。
到这里似乎可以解释为什么在主线程短时间内大量发送msg的时候可能会导致界面卡顿——是因为相关界面刷新的msg排队相对靠后,无法第一时间进行处理。
但是在使用Demo进行测试的时候除了界面无法刷新这个问题,还有另一个问题——onClick事件也无法进行响应。
有了上面这个思路,这个问题的原因找起来就相对顺利多了。
首先,在View体系下,第一个接收到TouchEvent的一定是RootView的dispatchTouchEvent()。所以我们需要知道,是谁调用的dispatchTouchEvent()即可。
经过查找,发现在ViewRootImpl有上文提到的inputChannel,然后在相关代码中发现了这么一段代码
mInputEventReceiver = new WindowInputEventReceiver(mInputChannel,
Looper.myLooper());
Looper.myLooper()?ViewRootImpl应该也是同属于View体系下的,所以在这里调用这个方法应该返回的是MainThread的Looper,继续跟踪下去发现
/**
* Creates an input event receiver bound to the specified input channel.
*
* @param inputChannel The input channel.
* @param looper The looper to use when invoking callbacks.
*/
public InputEventReceiver(InputChannel inputChannel, Looper looper) {
if (inputChannel == null) {
throw new IllegalArgumentException("inputChannel must not be null");
}
if (looper == null) {
throw new IllegalArgumentException("looper must not be null");
}
mInputChannel = inputChannel;
mMessageQueue = looper.getQueue();
mReceiverPtr = nativeInit(new WeakReference<InputEventReceiver>(this),
inputChannel, mMessageQueue);
mCloseGuard.open("dispose");
}
到这里就进入了native方法了,暂时就不能继续跟踪下去了。
但是它既然把主线程的Looper传进去了,那么就说明点击事件也与主线程的Looper有关。所以就可以解释为什么可能会导致主线程的点击事件可能会出现延迟。同绘制事件的理由一样。