Handler 的源码在前面几章分析过,一般也被用于切换线程,刷新UI使用。也被用来做延迟处理事件,一般都是在主线程中使用。
根据业务逻辑,开启了子线程,在其中做耗时操作,同时设置了一个超时时间,如果超过了这个时间,则终止操作,进行下一步逻辑。 这是一个很简单的需求,但由于笔误,造成了一个bug,原因在于Handler的延时操作不是在主线程中执行,而是在子线程中,见代码
Handler mHandler;
void test(){
HandlerThread handlerThread = new HandlerThread("test", -20);
handlerThread.start();
mHandler = new Handler(handlerThread.getLooper(), callback);
mHandler.sendEmptyMessage(0);
}
Handler.Callback callback = new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
if(msg.what == 1){
Log.e("TAG", " handler time out " );
} else if(msg.what == 0){
mHandler.sendEmptyMessageDelayed(1, 5000);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
mHandler.removeMessages(1);
Log.e("TAG", " thread " );
}
return false;
}
};
就是这么个代码,写出了个bug,固定延迟5秒执行超时操作,子线程耗时2秒,实际业务中是不确定的,有可能大于5秒,也有可能小于5秒,所以就导致bug是偶现的:子线程网络请求,超过了5秒,但没执行相应的超时回调。刚开始没注意到这个bug,后来仔细看了看代码,才发现原因所在。
HandlerThread 是个子线程,新建的 Handler 使用了子线程对应的 Looper,说明此Handler是在子线程中执行的;此刻,如果子线程被阻塞了,比如阻塞了7秒,那么Handler也会等7秒然后才检查自身对应的 MessageQueue 中是否有Message需要执行,这也是为什么5秒的时候没有触发延迟消息的原因。如果子线程阻塞了2秒,Handler本身的延迟的是5秒,此刻是没影响的,由于执行了 mHandler.removeMessages(1); 会把消息移除掉,如果没有这行代码,Handler 是会执行延迟发送的消息。
这个好像与平常印象中的Handler不一样啊!印象中只要Handler做了延迟消息发送,只要不取消,它会在指定的时间点执行,为什么在子线程中失效了?其实不是失效了,有这种印象只是说明对Handler的原理还不是很清楚。Handler 中的队列 MessageQueue 存储消息,而 Looper 中则不停的抽取消息,MessageQueue对外提供消息,此时如果有到时间的消息,则提供给外部;如果最近的消息还需要时间等待,则会进入睡眠状态,等待一段时间后被唤醒,执行对外提供消息功能,睡眠的时间则是头部消息还需要等待的时间;子线程中,由于被阻塞了,Looper根本来不及抽取消息,所以等阻塞消失后,才有消息发送出来,外部才接受到消息。