Qt for Android(十九) —— 用ScheduledFuture替换Timer解决修改系统时间引起定时器挂起的问题

1,000 阅读1分钟

这是我参与8月更文挑战的第20天,活动详情查看:8月更文挑战

背景

  基于上一篇文章,本来做好的NTP对时功能,结果发现一个致命缺陷,将系统时间修改到未来某个时间,然后启动定时器,此时如果再将系统时间改回当前的正确时间,发现定时器挂起了,不运转了。遂查找资料,发现是定时器内部实现的原因。

原因分析

这个问题我们可以简单跟踪下Timer的源码,Timer中有两个重要的对象,一个是TaskQueue,一个是TimerThread。 TaskQueue 是一个队列,里面放的就是我们调用Timer.schedule时传的参数task。TimerThread 是一个线程,继承了Thread ,在它的run方法中调用了一个函数叫:mainLoop()

public void run() {
   try {
       mainLoop();
   } finally {
       // Someone killed this Thread, behave as if Timer cancelled
       synchronized(queue) {
           newTasksMayBeScheduled = false;
           queue.clear();  // Eliminate obsolete references
       }
   }
}

看下mainLoop的代码:

/**
* 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) {
       }
   }
}

  代码中出现了while(true),说明一直在重复地执行,我们的任务被放在了一个queue的队列中,mainLoop方法就是不断地执行队列里面所有的任务。

看下49、50、51三行代码:

//获取当前系统时间
currentTime = System.currentTimeMillis();
//下一次执行任务的时间
executionTime = task.nextExecutionTime;
taskFired = (executionTime<=currentTime)

  如果下一次执行任务的时间小于等于当前时间,证明执行任务的时间已经到了,向下执行任务,如果不是,即taskFired=false,那么就等待,等待的时间为executionTime - currentTime
所以如果我们把系统时间调整为未来的时间,那么executionTime<=currentTime肯定为true了,执行下次任务,程序运行正常。

但是如果我们再次调整系统时间为之前的时间,那么此时executionTime<=currentTime肯定为false了,那么就会执行代码queue.wait(executionTime - currentTime),此时线程就会挂起了,至于等待的时间明显为executionTime - currentTime

解决方案

还好Android提供了ScheduledFuture。 简单用法如下:

import android.util.Log;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

public class TimerUtil {

    private ScheduledExecutorService service;
    private ScheduledFuture<?> future;

    public void startTimer(Runnable runable, int delay,int period) {
        if (service == null) {
            service = Executors.newScheduledThreadPool(1);//线程空间
     future = service.scheduleAtFixedRate(runable, delay, period, TimeUnit.MILLISECONDS);
        }
    }

    public void cancelTimer() {
        Log.d("TimerUtil", "cancelTimer");
        if (future != null) {
            future.cancel(true);
            future = null;
        }
        service = null;
    }

}

上面的代码可以作为一个工具类替代Timer。