动手写Android内的计划任务定时框架

在我讲解框架之前,我们先来看我一天中的计划需求。

计划任务:

7:30~8:30 起床
8:40~9:00 去公司的路上
9:10~9:30 早会
10:00~11:00 技术群里吹水
11:00~11:10 改了XXXActivity的变量命名(高大上的重构。懂吗?)
11:10~12:00 思考中午吃什么

13:00~14:00 睡午觉
14:30~18:00 群里斗图 吃零食 撩妹子 喝茶 玩手机 逛淘宝(今晚双十一呀)
18:00~18:30 要下班了随便搞了两下代码 顺便git commit -m '今天劳资做得最有意义的事情就是删掉了两行代码 真它娘赞'

19:00~20:00 回家的路上
21:00~22:00 会所X模 大保健
23:00~3:00 刷微博 内X段子 转发在群里 然后吹上一句,我tm才是嗨到最晚的男人
4:00~7:30 该睡觉了

这一天天过得,好呀。 好! 这才叫生活,不叫活着。

我:
“别和我讲什么番茄工作法、四相图,我只知道我的todoList 每天都是这般重复。”

旁白君:

“哥,你们公司还有空岗位不。我也想....。”

我:


目录

  • 需求分析
  • 设计框架
  • 如何使用
  • API
  • 注意
  • 引入
  • 使用场景

需求分析

在上面的时间轴里,我们可以把某段时间点,做某件事情当作是一个任务包。这样如果用代码来表示它就像是这样的。

    
            OneDayTask morning = new OneDayTask();
            morning.setStarTime(dataOne("2017-11-11 07:30:00"));  
            morning.setEndTime(dataOne("2017-11-11 08:30:00"));  
            morning.msg = "起床啦";
            

            OneDayTask work = new OneDayTask();
            work.setStarTime(dataOne("2017-11-11 14:30:00")); 
            work.setEndTime(dataOne("2017-11-11 18:00:00")); 
            work.msg = "群里斗图 吃零食 撩妹子 喝茶 玩手机 逛淘宝";
        
     //为方便展示,省略其它任务

我们用android手机来模拟一下,这些骚操作。那么也就是说:

我们需要在7:30这个时间点上收到一条通知,叫"起床啦"任务。
下午14:30收到一条通知,为下午的工作任务。

这样来想问题的话,想必我们需要一个基于观察者模式的通知,我们想一想如何在指定的时间来触发发送操作呢?

假设1:
开启一个子线程,里面写上一个死循环。不断的获取系统当前时间来判断是否满足任务的开始时间和结束时间。当满足条件时就从队列中取出这个条任务分发出去。
请思考一下,这个有没有毛病?


//伪代码如下
 OneDayTask morning = new OneDayTask();
do{
  if(getNowTime()== morning.getStarTime()){
  //todo 取出任务 分发出去
  }
}while (true);

首先,我想说这个设计时有问题的。

1.cpu在切换代码的执行片段时,可能很快,但是也许有那么一瞬间已经过了那一秒钟,而if语句还未得到执行。当getNowTime方法真正执行时,就已经过期了。

2.线程里做死循环操作,你觉着合适吗?反正我觉得挺不合适的。

然鹅。wing神-大精告诉我说,底层处理还是逃离不了。


当然我不存在说用系统给我发的每秒钟一个的广播去使用,这样不友好。目前的方案是封装AlarmMannager定时任务+广播通知回掉。每解决一个任务塞入下一个任务交给AlarmMannager来处理,当AlarmMannager定时任务结束后会发起广播。广播会再次调用下一组任务注册给AlarmMannager,如此循环。听着有点绕啊。但其实就两个角色,我们可以把它当作类似递归调用。但是好处是我们不需要写什么死循环这种东西。因为AlarmMannager支持定时任务。

没忍住去翻了下系统闹钟的定时实现源码。


接下来我们就要考虑下面的问题。

1.AlarmMannager在不同的碎片化机型的处理。
2.如果使用AlarmMannager作为核型就必须把队列中的任务按起始时间进行排序。
3.如果使用到了广播,在多组定时任务时,aciton不能重复。否则广播会紊乱。
4.广播最好不要用静态的,要用动态的,因为做成开源轮子,用户如果使用了类似360的插件化框架,将导致静态广播无效的问题。

设计框架

如果不进行封装裸裸的调用定时任务+广播的话,整个代码会非常散乱,毫无设计可言。也无法复用。那么我们索性花点时间给写好一点的。

先来一张UML图。这是整个框架的设计。非常简洁只有两个类和一个接口。其中要处理的任务做了泛型。我把这个框架叫TimeTask。

首先来看Task类。

//  get set 省略
public class Task {
  long  starTime;
  long  endTime;
 }

这里的Task我们可以把它看作是一个任务,他仅仅只有两个字段。一个开始时间,一个结束时间。后续我们自定义的任务都必须继承Task。(这里有点类似Recyclerview.ViewHolder的设计。)

TimeHandler

public interface TimeHandler<T extends Task> {
    void exeTask(T mTask);//马上要执行
    void overdueTask(T mTask);//已过期
    void futureTask(T mTask);//未来会执行
}

TimeHandler是一个接收器,也可以理解为观察者模式里的监听器。它主要接受马上要执行的&已经过期的&未来会执行的任务。

TimeTask

public class TimeTask<T extends Task> {

    private List<TimeHandler> mTimeHandlers = new ArrayList<TimeHandler>();
    private static PendingIntent mPendingIntent;
    private List<T> mTasks= new ArrayList<T>();
    private  List<T> mTempTasks;
    String mActionName;
    private  boolean isSpotsTaskIng = false;
    private  int cursor = 0;
    private Context mContext;
    private TimeTaskReceiver receiver;

    /**
     *
     * @param mContext
     * @param actionName action不要重复
     */
    public TimeTask(Context mContext,@NonNull String actionName) {
       this.mContext=mContext;
       this.mActionName=actionName;
        initBreceiver(mContext);
    }

    private void initBreceiver(Context mContext) {
        receiver = new TimeTaskReceiver();
        IntentFilter filter = new IntentFilter();
        filter.addAction(mActionName);
        mContext.registerReceiver(receiver, filter);
    }


    public void setTasks(List<T> mES) {
        cursorInit();
        if (mTempTasks !=null){
            mTempTasks = mES;
        }else {
            this.mTasks = mES;
        }
    }

    /**
     * 任务计数归零
     */
    private void cursorInit() {
        cursor = 0;
    }

    /**
     * 添加任务监听
     * @param mTH
     * @return
     */
    public TimeTask addHandler(TimeHandler<T> mTH) {
        mTimeHandlers.add(mTH);
        return this;
    }

    /**
     * 开始任务
     */
    public void startLooperTask() {

        if (isSpotsTaskIng&&mTasks.size() == cursor){ //恢复普通任务
            recoveryTask();
            return;
        }

        if (mTasks.size() > cursor){
            T mTask = mTasks.get(cursor);
            long mNowtime = System.currentTimeMillis();
            //在当前区间内立即执行
            if (mTask.getStarTime() < mNowtime && mTask.getEndTime() > mNowtime) {
                for (TimeHandler mTimeHandler : mTimeHandlers) {
                    mTimeHandler.exeTask(mTask);
                }
                Log.d("TimeTask","推送cursor:" + cursor + "时间:" + new Date(mTask.getStarTime()));
            }
            //还未到来的消息 加入到定时任务
            if (mTask.getStarTime() > mNowtime && mTask.getEndTime() > mNowtime) {
                for (TimeHandler mTimeHandler : mTimeHandlers) {
                    mTimeHandler.futureTask(mTask);
                }
                Log.d("TimeTask","预约cursor:" + cursor + "时间:" + new Date(mTask.getStarTime()));
                configureAlarmManager(mTask.getStarTime());
                return;
            }
            //消息已过期
            if (mTask.getStarTime() < mNowtime && mTask.getEndTime() < mNowtime) {
                for (TimeHandler mTimeHandler : mTimeHandlers) {
                    mTimeHandler.overdueTask(mTask);
                }
                Log.d("TimeTask","过期cursor:" + cursor + "时间:" + new Date(mTask.getStarTime()));
            }
            cursor++;
            if (isSpotsTaskIng&&mTasks.size() == cursor){ //恢复普通任务
                configureAlarmManager(mTask.getEndTime());
                return;
            }
            startLooperTask();
        }
    }


    /**
     * 停止任务
     */
    public void stopLooper() {
        cancelAlarmManager();
    }

    /**
     * 装在定时任务
     * @param Time
     */
    private void configureAlarmManager(long Time) {
        AlarmManager manager = (AlarmManager) mContext.getSystemService(ALARM_SERVICE);
        PendingIntent pendIntent = getPendingIntent();
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            manager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, Time, pendIntent);
        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            manager.setExact(AlarmManager.RTC_WAKEUP, Time, pendIntent);
        } else {
            manager.set(AlarmManager.RTC_WAKEUP, Time, pendIntent);
        }
    }

    /**
     *  取消定时器
     */
    private void cancelAlarmManager() {
        AlarmManager manager = (AlarmManager) mContext.getSystemService(ALARM_SERVICE);
        manager.cancel(getPendingIntent());
    }

    private PendingIntent getPendingIntent() {
        if (mPendingIntent == null) {
            int requestCode = 0;
            Intent intent = new Intent();
            intent.setAction(mActionName);
            mPendingIntent = PendingIntent.getBroadcast(mContext, requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT);
        }
        return mPendingIntent;
    }

    /**
     * 插播任务
     */
    public void spotsTask(List<T> mSpotsTask) {
        // 2017/10/16 暂停 任务分发
        isSpotsTaskIng = true;
        synchronized (mTasks) {
            if (mTempTasks == null&&mTasks!=null) {//没有发生过插播
                mTempTasks = new ArrayList<T>();
                for (T mTask : mTasks) {
                    mTempTasks.add(mTask);
                }
            }
            mTasks = mSpotsTask;
            //  2017/10/16 恢复 任务分发
            cancelAlarmManager();
            cursorInit();
            startLooperTask();
        }
    }

    /**
     * 恢复普通任务
     */
    private void recoveryTask() {
        synchronized (mTasks) {
            isSpotsTaskIng = false;
            if (mTempTasks != null) {//有发生过插播
                mTasks = mTempTasks;
                mTempTasks = null;
                cancelAlarmManager();
                cursorInit();
                startLooperTask();
            }
        }
    }

    public void onColse(){
        mContext.unregisterReceiver(receiver);
        mContext=null;
    }

    public  class TimeTaskReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            TimeTask.this.startLooperTask(); //预约下一个
        }
    }
}

这段代码略长了点,听我拆开了给大家慢慢道来。
1.首先TimeTask泛型指定了任务必须强制继承Task。在构造方法中。我们调用了initBreceiver注册了一个广播。这里就是我们前面提到的AlarmManager发通知给他的。

2.我们再看addHandler方法接受一个TimeHandler,这里可以多次注册。也就是说内部通过List装了监听器。到时候分发的时候也会多处可收到消息。

3.startLooperTask也就是开启任务执行的方法。内部主要做三件事。恢复插播任务、分发任务、预约任务。

4.上面提到了预约任务,实际预约任务就是利用AlarmManager定时指定时间发送广播通知我们到时间了该做事了。而广播内的onReceive方法回再次回掉startLooperTask方法。这样下来任务会被分发出去。同时会预约一下组任务。

5.需求分析的时候我们提到了AlarmMannager适配实际上就是针对M和KITKAT进行特殊的API处理。

如何使用

1.定义一个Task为你的任务对象,注意基类Task对象已经包含了任务的启动时间和结束时间

    class  MyTask extends Task {
        //// TODO: 这里可以放置你自己的资源,务必继承Task对象
        String name;
    }

2.定义一个任务接收器

   TimeHandler<MyTask> timeHandler = new TimeHandler<MyTask>() {
        @Override
        public void exeTask(MyTask mTask) {
               //准时执行
              // 一般来说,在exeTask方法中处理你的逻辑就好可以,过期和未来的都不需要关注 
        }

        @Override
        public void overdueTask(MyTask mTask) {
                 ///已过期的任务
        }

        @Override
        public void futureTask(MyTask mTask) {
              //未来将要执行的任务
        }
    };

3.定义一个任务分发器,并添加接收器

 
        TimeTask<MyTask> myTaskTimeTask = new TimeTask<>(MainActivity.this,ACTION); // 创建一个任务处理器
        myTaskTimeTask.addHandler(timeHandler); //添加时间回掉

4.配置你的任务时间间隔,(启动时间,结束时间)

    private List<MyTask> creatTasks() {
        return  new ArrayList<MyTask>() {{
            MyTask BobTask = new MyTask();
                        //******测试demo请务必修改时间******
                      BobTask.setStarTime(dataOne("2017-11-08 21:57:00"));   //当前时间
                      BobTask.setEndTime(dataOne("2017-11-08 21:57:05"));  //5秒后结束
                      BobTask.name="Bob";
                      add(BobTask);

                      MyTask benTask = new MyTask();
                      benTask.setStarTime(dataOne("2017-11-08 21:57:10")); //10秒开始
                      benTask.setEndTime(dataOne("2017-11-08 21:57:15")); //15秒后结束
                      benTask.name="Ben";
                      add(benTask);
        }};
    }

5.添加你的任务队列,跑起来.

        
        myTaskTimeTask.setTasks(creatTasks());//创建时间任务资源 把资源放进去处理
        myTaskTimeTask.startLooperTask();//  启动

这样下来,当调用 myTaskTimeTask.startLooperTask()后,会先分发给timeHandler名称为Bob的任务。
随后10秒分发Ben名称的任务。 任务处理器会根据我们配置的启动时间和结束时间进行分发工作。

Api

TimeTask

  • TimeTask(Context mContext,String actionName);//初始化
  • setTasks(List<T> mES);//设置任务列表
  • addHandler(TimeHandler<T> mTH);//添加任务监听器
  • startLooperTask();//启动任务
  • stopLooper();//停止任务
  • spotsTask(List<T> mSpotsTask);//插播任务
  • onColse();//关闭 防止内存泄漏

代码中已有详细注释,代码不是很复杂看原理读最好了。

注意:

  • 1.务必确保你的任务队列中的任务时已经按照时间排序的。
  • 2.务必使用泛型继承Task任务。
  • 3.如果你需要用到多组TimeTask,要保证actionName不要重复,就是自己给取一个名字。

引入

根gradle上添加

    repositories {
            ...
            maven { url 'https://jitpack.io' }
        }
dependencies {
    compile 'com.github.BolexLiu:TimeTask:1.1'
}

github: https://github.com/BolexLiu/TimeTask

使用场景

简单来说满足以下应用场景:

  • 1.当你需要为任务定时启动和结束
  • 2.你有多组任务,时间线上可能存在重叠的情况

目前线上正式环境的使用情况:

  • 1.电视机顶盒媒体分发
  • 2.android大屏幕广告机任务轮播

如何下次找到我?

本文首发香脆的大鸡排 原创文章转载请先取得联系。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,569评论 25 707
  • 国家电网公司企业标准(Q/GDW)- 面向对象的用电信息数据交换协议 - 报批稿:20170802 前言: 排版 ...
    庭说阅读 10,507评论 6 13
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,100评论 18 139
  • 之前有妈妈在群里面问,平时小孩有没有必要做推拿,我认为这个问题很有代表性,有必要给大家谈一谈,小孩平时到底需不需要...
    星淳来恩阅读 218评论 0 1
  • 岑目远三江,白衣轻飏。蒹葭一片水苍茫。只此逍遥横渡客,且赋诗章。 酹酒向幽篁,欲写清狂。去年今夜醉潇湘。误入云天仙...
    曾慕青衫阅读 338评论 4 7