引言

在做Android App开发的过程相信大家都会遇到周期性执行一些任务的需求,比如说每隔一段时间刷新下界面,每隔一段时间刷新下当前的天气情况或者实现类似Windows的若干时间自动播放屏保等等。

一、概述

任务调度是指基于给定时间点,给定时间间隔或者给定执行次数自动执行任务,简而言之可以理解成周期性执行某一项任务。

二、任务调度的实现方式

通常任务调度机制实现的方式主要有以下几种:Timer、ScheduledExecutor、Handler和其他第三方开源库

1、Timer

java.util.Timer是Java语言本身提供的一种最简单实现任务调度的方法,使用起来也很简单,通过对应的api调用即可。Timer 的设计核心是一个 TaskQueue和一个 TaskThread。Timer 将接收到的任务丢到自己的 TaskQueue中,TaskQueue按照 Task 的最初执行时间进行排序。TimerThread 在创建 Timer 时会启动成为一个守护线程,这个守护线程会轮询所有任务,找到一个最近要执行的任务,然后休眠,当到达最近要执行任务的开始时间点,TimerThread 被唤醒并执行该任务。之后 TimerThread 更新最近一个要执行的任务,继续休眠。

  • 继承TimerTask实现具体的作业任务类
  • 重写run方法实现具体的作业操作
  • 构造Timer对象并调用schedule方法传入指定参数即可
public class PeriodicOperations {        public static void main(String[] args) {        testTimerWay();    }    private static void testTimerWay(){        Timer timer = new Timer();         long delay1 = 1 * 1000;         long period1 = 1000;         // 从现在开始 1 秒钟之后,每隔 1 秒钟执行一次 job1         timer.schedule(new TimerTest("job1"), delay1, period1);         long delay2 = 2 * 1000;         long period2 = 2000;         // 从现在开始 2 秒钟之后,每隔 2 秒钟执行一次 job2         timer.schedule(new TimerTest("job2"), delay2, period2);     }    static class TimerTest extends TimerTask{        private String jobName = "";         public TimerTest(String job) {            super();            this.jobName=job;        }        @Override        public void run() {            System.out.println("执行作业:"+jobName);        }    }}

Timer机制实现任务调度的核心类是 Timer 和 TimerTask。其中 Timer 负责设定 TimerTask 的起始与间隔执行时间。使用者只需要创建一个 TimerTask 的继承类,实现自己的 run 方法,然后将其丢给 Timer 去执行即可。Timer 的优点在于简单易用,但由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行的,同一时间只能有一个任务在执行,前一个任务的延迟或异常都将会影响到之后的任务,所以不太适合并发类的任务调度

2、ScheduledExecutor

为了弥补Timer 的上述缺陷,在Java 5的时候推出了基于线程池设计的 ScheduledExecutor。其设计思想是:每一个被调度的任务都会由线程池中一个线程去执行,因此任务是并发执行的,相互之间不会受到干扰。但需要注意的是只有当任务的执行时间到来时,ScheduedExecutor 才会真正启动一个线程,其余时间 ScheduledExecutor 都是在轮询任务的状态

  • 定义一个作业线程负责实现具体的操作逻辑
  • 创建一个线程池用于管理作业线程
  • 调用线程池的对应方法(ScheduleWithFixedDelay或ScheduleAtFixedRate )启动周期性机制
import java.text.SimpleDateFormat;import java.util.Date;import java.util.Timer;import java.util.TimerTask;import java.util.concurrent.Executors;import java.util.concurrent.ScheduledExecutorService;import java.util.concurrent.TimeUnit;public class PeriodicOperations {    public static void main(String[] args) {        testScheduledExecutorWay();    }    private static void testScheduledExecutorWay(){        ScheduledExecutorService service = Executors.newScheduledThreadPool(10);//创建线程池的方式有几种,可以根据业务具体选择        long initialDelay1 = 1;        long period1 = 1;        // 从现在开始1秒钟之后,每隔1秒钟执行一次job1        service.scheduleAtFixedRate(                new ScheduledExecutorTest("job1"), initialDelay1,                period1, TimeUnit.SECONDS);        long initialDelay2 = 1;        long delay2 = 1;        // 从现在开始2秒钟之后,每隔2秒钟执行一次job2        service.scheduleWithFixedDelay(                new ScheduledExecutorTest("job2"), initialDelay2,                delay2, TimeUnit.SECONDS);    }    static class ScheduledExecutorTest implements Runnable{        private String jobName = "";        public ScheduledExecutorTest(String jobName) {            super();            this.jobName = jobName;        }        @Override        public void run() {            System.out.println(getNowTime()+"执行作业:" + jobName);        }    }    public static String getNowTime(){        Date now = new Date();        SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");// 可以方便地修改日期格式        return dateFormat.format(now);    }}

ScheduledExecutorService 中两种最常用的调度方法 ScheduleAtFixedRate 和 ScheduleWithFixedDelay。其中ScheduleAtFixedRate 每次执行时间为上一次任务开始起向后推一个时间间隔,即每次执行时间为 :initialDelay, initialDelay+period, initialDelay+2*period, …;而ScheduleWithFixedDelay 每次执行时间为上一次任务结束起向后推一个时间间隔,即每次执行时间为:initialDelay, initialDelay+executeTime+delay, initialDelay+2*executeTime+2*delay。所以两种方式异同在于ScheduleAtFixedRate 是基于固定时间间隔进行任务调度ScheduleWithFixedDelay 取决于每次任务执行的时间长短,是基于不固定时间间隔进行任务调度

3、结合 ScheduledExecutor 和 Calendar 实现复杂任务调度

无论是Timer 和 ScheduledExecutor 都仅能提供基于开始时间与重复间隔的任务调度,不能实现更加复杂的调度需求。比如说设置每星期二的 16:38:10 执行任务等等,单独使用 Timer 或 ScheduledExecutor 都不能直接实现,但我们可以借助 Calendar 间接实现该功能。

import java.util.Calendar;import java.util.Date;import java.util.TimerTask;import java.util.concurrent.Executors;import java.util.concurrent.ScheduledExecutorService;import java.util.concurrent.TimeUnit;public class ScheduledExceutorTest2 extends TimerTask {    private String jobName = "";    public ScheduledExceutorTest2(String jobName) {        super();        this.jobName = jobName;    }    @Override    public void run() {        System.out.println("Date = "+new Date()+", execute " + jobName);    }    /**     * 计算从当前时间currentDate开始,满足条件dayOfWeek, hourOfDay,      * minuteOfHour, secondOfMinite的最近时间     * @return     */    public Calendar getEarliestDate(Calendar currentDate, int dayOfWeek,            int hourOfDay, int minuteOfHour, int secondOfMinite) {        //计算当前时间的WEEK_OF_YEAR,DAY_OF_WEEK, HOUR_OF_DAY, MINUTE,SECOND等各个字段值        int currentWeekOfYear = currentDate.get(Calendar.WEEK_OF_YEAR);        int currentDayOfWeek = currentDate.get(Calendar.DAY_OF_WEEK);        int currentHour = currentDate.get(Calendar.HOUR_OF_DAY);        int currentMinute = currentDate.get(Calendar.MINUTE);        int currentSecond = currentDate.get(Calendar.SECOND);        //如果输入条件中的dayOfWeek小于当前日期的dayOfWeek,则WEEK_OF_YEAR需要推迟一周        boolean weekLater = false;        if (dayOfWeek < currentDayOfWeek) {            weekLater = true;        } else if (dayOfWeek == currentDayOfWeek) {            //当输入条件与当前日期的dayOfWeek相等时,如果输入条件中的            //hourOfDay小于当前日期的            //currentHour,则WEEK_OF_YEAR需要推迟一周               if (hourOfDay < currentHour) {                weekLater = true;            } else if (hourOfDay == currentHour) {                 //当输入条件与当前日期的dayOfWeek, hourOfDay相等时,                 //如果输入条件中的minuteOfHour小于当前日期的                //currentMinute,则WEEK_OF_YEAR需要推迟一周                if (minuteOfHour < currentMinute) {                    weekLater = true;                } else if (minuteOfHour == currentSecond) {                     //当输入条件与当前日期的dayOfWeek, hourOfDay,                      //minuteOfHour相等时,如果输入条件中的                    //secondOfMinite小于当前日期的currentSecond,                    //则WEEK_OF_YEAR需要推迟一周                    if (secondOfMinite < currentSecond) {                        weekLater = true;                    }                }            }        }        if (weekLater) {            //设置当前日期中的WEEK_OF_YEAR为当前周推迟一周            currentDate.set(Calendar.WEEK_OF_YEAR, currentWeekOfYear + 1);        }        // 设置当前日期中的DAY_OF_WEEK,HOUR_OF_DAY,MINUTE,SECOND为输入条件中的值。        currentDate.set(Calendar.DAY_OF_WEEK, dayOfWeek);        currentDate.set(Calendar.HOUR_OF_DAY, hourOfDay);        currentDate.set(Calendar.MINUTE, minuteOfHour);        currentDate.set(Calendar.SECOND, secondOfMinite);        return currentDate;    }    public static void main(String[] args) throws Exception {        ScheduledExceutorTest2 test = new ScheduledExceutorTest2("job1");        //获取当前时间        Calendar currentDate = Calendar.getInstance();        long currentDateLong = currentDate.getTime().getTime();        System.out.println("Current Date = " + currentDate.getTime().toString());        //计算满足条件的最近一次执行时间        Calendar earliestDate = test                .getEarliestDate(currentDate, 3, 16, 38, 10);        long earliestDateLong = earliestDate.getTime().getTime();        System.out.println("Earliest Date = "                + earliestDate.getTime().toString());        //计算从当前时间到最近一次执行时间的时间间隔        long delay = earliestDateLong - currentDateLong;        //计算执行周期为一星期        long period = 7 * 24 * 60 * 60 * 1000;        ScheduledExecutorService service = Executors.newScheduledThreadPool(10);        //从现在开始delay毫秒之后,每隔一星期执行一次job1        service.scheduleAtFixedRate(test, delay, period,                TimeUnit.MILLISECONDS);    }}

其核心在于根据当前时间推算出最近一个星期二 16:38:10 的绝对时间,然后计算与当前时间的时间差,作为调用 ScheduledExceutor 函数的参数。计算最近时间要用到 java.util.calendar 的功能。首先需要解释 Calendar 的一些设计思想。Calendar 有以下几种唯一标识一个日期的组合方式:

  • YEAR + MONTH + DAY_OF_MONTH
  • YEAR + MONTH + WEEK_OF_MONTH +
  • YEAR + MONTH + DAY_OF_WEEK_IN_MONTH + DAY_OF_WEEK
  • YEAR+ DAY_OF_YEAR YEAR + DAY_OF_WEEK + WEEK_OF_YEAR

上述组合分别加上 HOUR_OF_DAY + MINUTE + SECOND 即为一个完整的时间标识。本例采用了最后一种组合方式。输入为 DAY_OF_WEEK, HOUR_OF_DAY, MINUTE, SECOND 以及当前日期 , 输出为一个满足 DAY_OF_WEEK, HOUR_OF_DAY, MINUTE, SECOND 并且距离当前日期最近的未来日期。计算的原则是从输入的 DAY_OF_WEEK 开始比较,如果小于当前日期的 DAY_OF_WEEK,则需要向 WEEK_OF_YEAR 进一, 即将当前日期中的 WEEK_OF_YEAR 加一并覆盖旧值;如果等于当前的 DAY_OF_WEEK, 则继续比较 HOUR_OF_DAY;如果大于当前的 DAY_OF_WEEK,则直接调用 java.util.calenda 的 calendar.set(field, value) 函数将当前日期的 DAY_OF_WEEK, HOUR_OF_DAY, MINUTE, SECOND 赋值为输入值,依次类推,直到比较至 SECOND。也可以根据输入需求选择不同的组合方式来计算最近执行时间。

4、Handler机制

以上三种方式都是由Java语言提供的机制,其实Android 本身也提供了自己的机制,只不过需要我们灵活去应用,尤其是涉及到界面操作的周期性需要灵活控制的任务调度,Handler机制应该算是比较合适一种方式,结合其他机制可以无缝地操作界面。我以实现仿Windows 屏保机制来说明,简单说下需求,当设备在若干分钟无人进行交互的时候,自动播放屏保,这也可以看成一个灵活的任务调度,因为当到达指定时间之前,有人交互,此时又得重新计算起点,而不是固定的在一个时间间隔内一定会触发。

public abstract class BaseActivity extends AppCompatActivity {    public static final int MSG_WELCOME = 11;    private final static long DELAY=10*60*1000;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        ActivityManager.addActivity(this);        setFullScreen();    }    @Override    protected void onResume() {        super.onResume();        startTask();//首先在onResume 中开启任务调度,此时相当于开始倒计时,如果没被打断将会在DELAY 时间间隔之后执行预订的弹出屏保    }    @Override    protected void onStop() {        super.onStop();        stopTask();//停止任务调度    }    @Override    protected void onDestroy() {        super.onDestroy();    }    private void setFullScreen(){        requestWindowFeature(Window.FEATURE_NO_TITLE);        getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,                WindowManager.LayoutParams.FLAG_FULLSCREEN);    }    @Override    public boolean dispatchTouchEvent(MotionEvent ev) {        stopTask();//当有人在交互的时候就中断当前的任务调度,重新开启        startTask();        return super.dispatchTouchEvent(ev);    }    private ObverHandler takePhotoHandler =new ObverHandler(this);    protected void handleMsg(int what){        if(what== MSG_WELCOME) {            ScreenActivity.showScreenActivity(this);//当接收到Message 时,跳到屏保界面            return;        }        return;    }    protected boolean isTopActivity(String activity) {        android.app.ActivityManager am = (android.app.ActivityManager) getSystemService(ACTIVITY_SERVICE);        ComponentName cn = am.getRunningTasks(1).get(0).topActivity;        return cn.getClassName().contains(activity);    }    /**    *开启周期性任务调度    */    protected void startTask(){        if(takePhotoHandler !=null) {            if(isTopActivity("ScreenActivity")){                return;            }else{                takePhotoHandler.postDelayed(new Runnable() {                    @Override                    public void run() {                        takePhotoHandler.sendMsg(MSG_WELCOME);                    }                }, DELAY);            }        }    }    /**    *停止周期性任务调度,假如在还未到所指定的时间任务呗手动停止了,比如说有人交互了,此时需要重新计算任务开始的起点,所以需要在业务逻辑中手动去重新开启任务调度    */    private void stopTask(){        if (takePhotoHandler !=null){            LogUtil.showErroLog("STOP TASK WELCOMEActivity");            takePhotoHandler.removeCallbacksAndMessages(null);//就是把未处理的Meesgae 全部清空        }    }    static class ObverHandler extends Handler {        private final WeakReference mAct;//声明弱引用        //声明构造方法供外部调用构ObverHandler对象        public ObverHandler(BaseActivity act){            mAct=new WeakReference<>(act);        }        public void handleMessage(Message msg){            mAct.get().handleMsg(msg.what);        }        public void sendMsg(int what){            sendEmptyMessage(what);        }    }}

以上屏保案例实质上借助了Handler的postDelayed方法和removeCallbacksAndMessages灵活实现的一种所谓的周期性任务调度,当然也可以借助其他机制来实现开启和停止,一切取决于你的需求。

5、第三方开源库

上述方法实现该任务调度比较麻烦,这就需要一个更加完善的任务调度框架来解决这些复杂的调度问题。幸运的是,开源工具包 Quartz 与 JCronTab 提供了这方面强大的支持(使用攻略参见IBM开发社区。)

小结

对于简单的基于起始时间点与时间间隔的任务调度,使用 Timer 就足够了;如果需要同时调度多个任务,基于线程池的 ScheduledTimer 是更为合适的选择;当任务调度的策略复杂到难以凭借起始时间点与时间间隔来描述时,Quartz 与 JCronTab 则体现出它们的优势。熟悉 Unix/Linux 的开发人员更倾向于 JCronTab,且 JCronTab 更适合与 Web 应用服务器相结合。Quartz 的 Trigger 与 Job 松耦合设计使其更适用于 Job 与 Trigger 的多对多应用场景。

更多相关文章

  1. 修改android 4.4系统下面的休眠时间,只留下永不休眠选项
  2. 关于houdini技术和android x86平台兼容性的问题
  3. android listview adapter中设置点击直接position被重用问题解决
  4. ubuntu下安装MyEclipse10和MySql,为MyEclipse配置android ADT(全
  5. Android和Linux的时间戳
  6. 自定义样式日期时间选择对话框控件(精简版)
  7. 针对Android(安卓)模拟器启动慢的问题,长时间显示 Android(安卓)L
  8. Android滚轮时间选择控件(可扩展自定义)
  9. 实现Android监控任意控件或按键双击事件方法

随机推荐

  1. Android在线修复bug框架AndFix调研
  2. Android实现步进式录像进度条
  3. android studio中的so库调用
  4. Android(安卓)adb常见问题整理
  5. Android运行Socket项目 Error: ShouldNot
  6. Android磁盘管理-之vold源码分析(4)
  7. Android知识点汇总以及常见面试题
  8. android 自定义输入框,禁止输入表情符号,或
  9. android 关于prop属性
  10. Android多渠道打包方案的实践与优化