在工作中偶遇到我们的Launcher在监听TIME_TICK广播超时,导致Launcher ANR问题,遂研究一下Android的广播超时机制。
从前面ActivityManagerService分发广播一文可以看出AMS分发广播的关键函数processNextBroadcast,该函数首先是分发并行队列中的广播,然后依次分发串行队列中的广播。并行队列中的广播的分发是循环取出每一个广播,并直接分发,由于调用客户端的方法是one-way的,并且不需要等待客户端的反馈,所以并行队列中的广播分发很快;但是串行队列中的广播必须要等待前面的广播接收器执行完成并调用了PendingResult的sendFinish函数才会分发下一个,如果客户端的BroadcastReceiver运行在主线程,但是主线程又很繁忙,就有可能导致超时。串行队列中的广播不存在超时概念,只有串行队列中的广播才会超时(1.存在静态注册的广播接收器的普通广播或有序广播 2.存在动态注册的广播接收器的有序广播)
另外前台广播和后台广播超时时间是不一样的,前者是BROADCAST_FG_TIMEOUT(10s),后者是BROADCAST_BG_TIMEOUT(60s),这里是指单个广播接收器可以处理的最大的时间。
在BroadcastQueue的processNextBroadcast中开始处理分发串行队列的里的广播的接收器时,有如下代码段:
if (! mPendingBroadcastTimeoutMessage) {
long timeoutTime = r.receiverTime + mTimeoutPeriod;
setBroadcastTimeoutLocked(timeoutTime);
}
//setBroadcastTimeoutLocked
final void setBroadcastTimeoutLocked(long timeoutTime) {
if (! mPendingBroadcastTimeoutMessage) {
Message msg = mHandler.obtainMessage(BROADCAST_TIMEOUT_MSG, this);
mHandler.sendMessageAtTime(msg, timeoutTime);
mPendingBroadcastTimeoutMessage = true;
}
}
这里的mPendingBroadcastTimeoutMessage表示Handler中是否存在超时的消息,如果没有则设置在timeoutTime时间点发送一个消息,这里timeoutTime是广播分发给当前接收器的时间点加上mTimeoutPeriod,这里的mTimeoutPeriod是前面所说的,前台队列或后台队列的超时时间,前者10s,后者60s,如后台队列,表示在60s之后发送一条超时消息,如果当前的接收器处理的时间超过60s,就会抛出广播超时异常。
当设置的时间点到了之后,会调用broadcastTimeoutLocked函数
final void broadcastTimeoutLocked(boolean fromMsg) {
if (fromMsg) {
mPendingBroadcastTimeoutMessage = false;
}
if (mOrderedBroadcasts.size() == 0) {
return;
}
long now = SystemClock.uptimeMillis();
BroadcastRecord r = mOrderedBroadcasts.get(0);
if (fromMsg) {
if (mService.mDidDexOpt) {
mService.mDidDexOpt = false;
long timeoutTime = SystemClock.uptimeMillis() + mTimeoutPeriod;
setBroadcastTimeoutLocked(timeoutTime);
return;
}
if (!mService.mProcessesReady) {
return;
}
long timeoutTime = r.receiverTime + mTimeoutPeriod;
if (timeoutTime > now) {
setBroadcastTimeoutLocked(timeoutTime);
return;
}
}
BroadcastRecord br = mOrderedBroadcasts.get(0);
if (br.state == BroadcastRecord.WAITING_SERVICES) {
br.curComponent = null;
br.state = BroadcastRecord.IDLE;
processNextBroadcast(false);
return;
}
//下面的处理是超时之后的逻辑,如打印traces.txt文件等
}
从设计角度看,当开始广播给一个接收器时,即延迟特定时间发送超时时间,然后在接收器处理完成之后,通知BroadcastQueue,remove掉这条消息,这样就不会执行到broadcastTimeoutLocked函数了,但是搜索所有的代码只有在广播发送完成或者被终止之后才会remove 超时的消息,这不禁让人很疑惑,那到底是如何在接收器处理完成后,使其不至于弹出应用无响应的弹框的呢?
仔细查看broadcastTimeoutLocked函数,发现如下端倪
long timeoutTime = r.receiverTime + mTimeoutPeriod;
if (timeoutTime > now) {
setBroadcastTimeoutLocked(timeoutTime);
return;
}
尽管Handler队列中的BROADCAST_TIMEOUT_MSG会执行,但是在这里又判断了一次当前时间和超时时间,如果超时时间点大于当前时间就直接返回,相当于remove了BROADCAST_TIMEOUT_MSG这条消息。r.receiverTime是广播分发给一个BroadcastReceiver的开始时间。如果当前的Receiver处理时间超过了超时时间,那r.receiverTime + mTimeoutPeriod就会大于now,这样会继续往下执行,不会终止。如果当前Receiver处理时间小于超时时间,在处理完成后会开始分发给下一个Receiver,这样就会重置r.receiverTime的时间,就会重新发一条超时消息,时间点以下一个Receiver的开始分发时间加上超时时间,然后返回。这样就解决了前面说的代码中并未出现remove 超时消息的问题,不得不说Android的有些设计真的需要仔细研究。不管Android为何如此设计,还不清楚,因为我觉得应用处理完成通知AMS remove掉这个消息,是否更容易理解呢?
另外前面的broadcastTimeoutLocked中还有一段代码:
BroadcastRecord br = mOrderedBroadcasts.get(0);
if (br.state == BroadcastRecord.WAITING_SERVICES) {
br.curComponent = null;
br.state = BroadcastRecord.IDLE;
processNextBroadcast(false);
return;
}
这个是查看当前广播的状态,如果是WAITING_SERVICES则说明广播接收器实际已经处理完成,只是在等待后台服务启动,导致超时,这样情况直接分发给下一个广播接收器即可。 ActivityManagerService分发广播这篇文件有说过WAITING_SERVICE的处理,前台广播队列和后台广播队列除了超时时间不同之外,就是前台广播不需要等待服务的启动,而后台广播需要等待。
从上面的分析得出很重要的一点:
只有串行队列中的接收器存在超时问题,如果是普通广播,以及动态注册的接收器是不存在超时的。
知道了超时的机制,可以总结一下前台广播和后台广播,
我们知道如果希望广播能够更快的被接收,可以将广播设置成前台广播,那为什么前台广播比后台广播更快呢?
1.系统默认的广播是后台广播,因此前台广播队列相对空闲,可以更快的响应
2.前台广播超时时间是10S,后台广播超时时间是60s,从设计思想来看,设计者希望前台广播可以更快的处理,接收器不要在接收这类广播时做太多耗时操作
3.如果BroadcastQueue正在处理一个串行队列中的BroadRecord,那此时发送普通广播,对应动态注册的接收器
还是可以收到的,有序广播或者静态注册的接收器需要排队,这也是并行处理和串行处理的区别。所以个人感觉如果广播是普通广播,接收器又是动态注册的,那前台广播和后台广播应该差别不大
再回到文章开头发现我们的Launcher存在处理TIME_TICK超时原因:
1.TIME_TICK是有序广播,所以存在超时的可能
2.TIME_TICK是前台广播,超时时间只有10s
3.我们的Launcher是在主线程中处理该广播,当时主线程负载很重,未及时处理,导致Launcher无响应问题