Timer 填坑指南 ,发现 Google 的 Bug啦!

在开发过程中经常用到 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 命令也是又有的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值