动手写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
  • 注意
  • 引入
  • 使用场景

需求分析

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

1
2
3
4
5
6
7
8
9
10
11
12
13

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

1
2
3
4
5
6
7
8

//伪代码如下
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类。

1
2
3
4
5
//  get set 省略
public class Task {
long starTime;
long endTime;
}

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

TimeHandler

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

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

TimeTask

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
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对象已经包含了任务的启动时间和结束时间

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

2.定义一个任务接收器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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.定义一个任务分发器,并添加接收器

1
2
3

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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.添加你的任务队列,跑起来.

1
2
3

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

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

Api

TimeTask

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

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

注意:

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

引入

根gradle上添加

1
2
3
4
repositories {
...
maven { url 'https://jitpack.io' }
}

1
2
3
dependencies {
compile 'com.github.BolexLiu:TimeTask:1.1'
}

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

使用场景

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

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

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

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

如何下次找到我?

本文首发 http://www.dajipai.cc -香脆的大鸡排。原创文章转载请先取得联系。

随缘打赏!