在开发过程中经常用到 Timer,用于隔一段时间重复执行相应的任务,用起来也很简单,相信聪明的你肯定会用了,没错简单的如下:
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
//do something
}
}, 0, 100);
用起来也是相当简单,想要取消该任务时也很简单:
timer.cancel();
突然有一天测试报了个 bug:切换手机系统时间之后 重复执行的动作不执行了。一看到这个bug感觉到奇怪,系统切换时间,我这个应用并没有执行啥动作啊,不应该影响到我的应用才对,况且没有主动执行 cancel 动作。
下面开始一点一点的 Debug, 是我的代码有问题还是其他原因。
首先怀疑是不是在切换时间时,Timer 被系统(或者其他)调用 cancel 了,导致 run 代码不执行,先做个试验,写个类 MyTimer 来继承 Timer, 然后重新 cancel 方法,在cance 方法里面添加 log,如下:
class MyTimer extends Timer{
@Override
public void cancel() {
super.cancel();
Log.d("TAG", "Timer cancel " + Log.getStackTraceString(new Throwable()));
}
}
Timer time = new MyTimer();
.....
当问题复现之后, 添加的 log 并没有被打印,看来这个猜想是不成立的。
没有调用 cancel,感觉很奇怪,既然这个猜想是错误的,那就只能乖乖的去看源码。
看源码之后,可以看出 Timer 的实现原理是:将 TimerTask 放进一个队列里面, 然后开启一个线程 在 while 循环中根据时间间隔去去队列里面的任务来执行
private final TaskQueue queue = new TaskQueue()
private final TimerThread thread = new TimerThread(queue);
public Timer() {
this("Timer-" + serialNumber());
}
public Timer(String name) {
thread.setName(name);
thread.start();
}
private void sched(TimerTask task, long time, long period) {
if (time < 0)
throw new IllegalArgumentException("Illegal execution time.");
// Constrain value of period sufficiently to prevent numeric
// overflow while still being effectively infinitely large.
if (Math.abs(period) > (Long.MAX_VALUE >> 1))
period >>= 1;
synchronized(queue) {
if (!thread.newTasksMayBeScheduled)
throw new IllegalStateException("Timer already cancelled.");
synchronized(task.lock) {
if (task.state != TimerTask.VIRGIN)
throw new IllegalStateException(
"Task already scheduled or cancelled");
task.nextExecutionTime = time;
task.period = period;
task.state = TimerTask.SCHEDULED;
}
queue.add(task);
if (queue.getMin() == task)
queue.notify();
}
}
从源码可以指定,会默认开启一个 线程名为Timer- 前缀的线程, 调用 timer.schedule,最后都会执行到 方法 private void sched(TimerTask task, long time, long period),
到这里在猜想是不是切换时间时,线程被杀死,导致没有执行了。怎么验证线程是否被杀死呢?
这里不得不安利 adb 命令一波了,会简单的使用 adb 命令使你 debug 能力提升很多。
adb命令大全
这里先介绍两个实用命令:
adb shell ps | grep -iE "keyword” , 左边是列出当前的所有进程信息, 右边是过来关键字,
adb shell ps -T -p <pid> 列出对应 pid 进程的所有线程信息
pc~$ adb shell ps | grep -iE "toolbag"
system 6517 469 5347764 132712 0 0 S ....toolbag
-pc:~$ adb shell ps -T -p 6517 | grep -iE "tim"
system 6517 13046 469 5347764 132136 0 0 S Timer-0
system 6517 13047 469 5347764 132136 0 0 R Timer-1
system 6517 13053 469 5347764 132136 0 0 S Timer-2
这里通过第一个命令查询到 pid 6517, 代入第二个命令的 pid参数中,可以查看到该应用使用了三个 Timer 实例。
这里通过 bug 前后对比, adb shell ps -T -p <pid> 出来的情况没有发生变化, 说明切换时间之后线程没有被杀死。
通过实验跟 adb 命令 前后排查都没有定位到问题,那只能乖乖的去看源码了,主要到 Timer 里面有个变量
TimerThread thread = new TimerThread(queue);
这个应该是Timer 重写了 Thread 类,去看源码
public void run() {
try {
mainLoop();
} finally {
// Someone killed this Thread, behave as if Timer cancelled
synchronized(queue) {
newTasksMayBeScheduled = false;
queue.clear(); // Eliminate obsolete references
}
}
}
/**
* The main timer loop. (See class comment.)
*/
private void mainLoop() {
while (true) {
try {
TimerTask task;
boolean taskFired;
synchronized(queue) {
// Wait for queue to become non-empty
while (queue.isEmpty() && newTasksMayBeScheduled)
queue.wait();
if (queue.isEmpty())
break; // Queue is empty and will forever remain; die
// Queue nonempty; look at first evt and do the right thing
long currentTime, executionTime;
task = queue.getMin();
synchronized(task.lock) {
if (task.state == TimerTask.CANCELLED) {
queue.removeMin();
continue; // No action required, poll queue again
}
currentTime = System.currentTimeMillis();
executionTime = task.nextExecutionTime;
if (taskFired = (executionTime<=currentTime)) {
if (task.period == 0) { // Non-repeating, remove
queue.removeMin();
task.state = TimerTask.EXECUTED;
} else { // Repeating task, reschedule
queue.rescheduleMin(
task.period<0 ? currentTime - task.period
: executionTime + task.period);
}
}
}
if (!taskFired) // Task hasn't yet fired; wait
queue.wait(executionTime - currentTime);
}
if (taskFired) // Task fired; run it, holding no locks
task.run();
} catch(InterruptedException e) {
}
}
}
TimerThread 的run 方法执行了 mainLoop,里面执行 while 循环,
注意到下面片段
currentTime = System.currentTimeMillis();
executionTime = task.nextExecutionTime;
if (taskFired = (executionTime<=currentTime)) {
if (task.period == 0) { // Non-repeating, remove
queue.removeMin();
task.state = TimerTask.EXECUTED;
} else { // Repeating task, reschedule
queue.rescheduleMin(
task.period<0 ? currentTime - task.period
: executionTime + task.period);
}
}
if (!taskFired) // Task hasn't yet fired; wait
queue.wait(executionTime - currentTime);
注意到 currentTime = System.currentTimeMillis(), 调整时间后(时间向后调整)导致 taskFired为false了,timer 线程卡在了 queue.wait(executionTime - currentTime),如果时间向后调整一个小时那么需要等待一个小时,看起来是 timer 不执行任务了,
问题是发现了,估计这个是 java 源码的一个 bug 啊,那该怎么处理呢,我们不能通过修改源码来 解这个bug吧。
注意到系统改变时间时会发送时间改变的广播,我们只需要监听该广播,然后重启 Timer 就可以了。
总结:多看源码,学会adb 命令也是又有的。