在Android開發中,定時執行任務的3種實現方法:
一、采用Handler與線程的sleep(long)方法(不建議使用,Jva的實現方式)
二、采用Handler的postDelayed(Runnable, long)方法(最簡單的android實現)
三、采用Handler與timer及TimerTask結合的方法(比較多的任務時建議使用)
Android消息機制
首先來了解一下Android的消息處理機制
即Handlerd的運行機制,handler的運行需要底層的MessageQueue和Looper的支撐。MessageQueue(消息隊列),它的內部存儲了一些消息,以隊列的形式對外提供插入和刪除的操作(實際為單鏈表存儲)。Looper(消息循環),配合MessageQueue實現實現消息的不斷入隊和出隊工作。
一個關系圖:
通過Handler可以很容易將任務切換到其他線程中執行,以減少主線程的負擔,因此Handler常用來進行UI更新。這里只是簡單的進行一些概述。
當然,現在已經有更好的消息處理辦法了,了解handler和Asynctask可以更好的理解Android內部消息的處理機制。
推薦:EventBus,高度解耦,代碼簡潔明了,有興趣的可以自行參考使用。
1.采用Handle與線程的sleep(long)方法
1) 定義一個Handler類,用於處理接受到的Message。
1
2
3
4
5
6
|
Handler handler =
new
Handler() {
public
void
handleMessage(Message msg) {
// 要做的事情
super
.handleMessage(msg);
}
};
|
2) 新建一個實現Runnable接口的線程類,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public
class
MyThread
implements
Runnable {
@Override
public
void
run() {
// TODO Auto-generated method stub
while
(
true
) {
try
{
Thread.sleep(
10000
);
// 線程暫停10秒,單位毫秒
Message message =
new
Message();
message.what =
1
;
handler.sendMessage(message);
// 發送消息
}
catch
(InterruptedException e) {
e.printStackTrace();
}
} } }
|
3) 在需要啟動線程的地方加入下面語句:
1
|
new
Thread(
new
MyThread()).start();
|
分析:純正的java原生實現,在sleep結束后,並不能保證競爭到cpu資源,這也就導致了時間上必定>=10000的精度問題。
2.采用Handler的postDelayed(Runnable, long)方法
1)定義一個Handler類
1
2
3
4
5
6
7
8
9
10
11
|
Handler handler=
new
Handler();
Runnable runnable=
new
Runnable() {
@Override
public
void
run() {
// TODO Auto-generated method stub
//要做的事情
handler.postDelayed(
this
,
2000
);
}
};
|
2) 啟動與關閉計時器
1
2
|
handler.postDelayed(runnable,
2000
);
//每兩秒執行一次runnable.
handler.removeCallbacks(runnable);
|
分析:嗯,看起蠻不錯,實現上也簡單了,和sleep想必還不會產生阻塞,注意等待和間隔的區別。
3.采用Handler與timer及TimerTask結合的方法
1) 定義定時器、定時器任務及Handler句柄
1
2
3
4
5
6
7
8
9
10
|
private
final
Timer timer =
new
Timer();
private
TimerTask task;
Handler handler =
new
Handler() {
@Override
public
void
handleMessage(Message msg) {
// TODO Auto-generated method stub
// 要做的事情
super
.handleMessage(msg);
}
};
|
2) 初始化計時器任務
1
2
3
4
5
6
7
8
9
|
task =
new
TimerTask() {
@Override
public
void
run() {
// TODO Auto-generated method stub
Message message =
new
Message();
message.what =
1
;
handler.sendMessage(message);
}
};
|
3) 啟動和關閉定時器
1
2
|
timer.schedule(task,
2000
,
3000
);
timer.cancel();
|
此外,Timer也可以配合runOnUiThread實現,如下
1
2
3
4
5
6
7
8
9
10
11
12
|
private
TimerTask mTimerTask =
new
TimerTask() {
@Override
public
void
run() {
runOnUiThread(
new
Runnable() {
@Override
public
void
run() {
//處理延時任務
}
});
}
};
|
分析:timer.schedule(task, 2000, 3000);意思是在2秒后執行第一次,之后每3000秒在執行一次。timer不保證精確度且在無法喚醒cpu,不適合后台任務的定時。
采用AlarmManger實現長期精確的定時任務
AlarmManager的常用方法有三個:
set(int type,long startTime,PendingIntent pi);//一次性setExact(int type, long triggerAtMillis, PendingIntent operation)//一次性的精確版setRepeating(int type,long startTime,long intervalTime,PendingIntent
pi);//精確重復setInexactRepeating(int type,long startTime,long
intervalTime,PendingIntent pi);//非精確,降低功耗
type表示鬧鍾類型,startTime表示鬧鍾第一次執行時間,long intervalTime表示間隔時間,PendingIntent表示鬧鍾響應動作
對以上各個參數的詳細解釋
鬧鍾的類型:
AlarmManager.ELAPSED_REALTIME:休眠后停止,相對開機時間AlarmManager.ELAPSED_REALTIME_WAKEUP:休眠狀態仍可喚醒cpu繼續工作,相對開機時間AlarmManager.RTC:同1,但時間相對於絕對時間AlarmManager.RTC_WAKEUP:同2,但時間相對於絕對時間AlarmManager.POWER_OFF_WAKEUP:關機后依舊可用,相對於絕對時間
startTime:
鬧鍾的第一次執行時間,以毫秒為單位,一般使用當前時間。
SystemClock.elapsedRealtime():系統開機至今所經歷時間的毫秒數System.currentTimeMillis():1970 年 1 月 1 日 0 點至今所經歷時間的毫秒數
intervalTime:執行時間間隔。
PendingIntent :
PendingIntent用於描述Intent及其最終的行為.,這里用於獲取定時任務的執行動作。
利用AlarmManger+Service+BarocastReceiver實現5s一次打印操作
服務類:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
<code>
public
class
HorizonService
extends
Service {
@Override
public
IBinder onBind(Intent intent) {
return
null
;
}
@Override
public
int
onStartCommand(Intent intent,
int
flags,
int
startId) {
new
Thread(
new
Runnable() {
@Override
public
void
run() {
Log.d(
"TAG"
,
"打印時間: "
+
new
Date().
toString());
}
}).start();
AlarmManager manager = (AlarmManager) getSystemService(ALARM_SERVICE);
int
five =
5000
;
// 這是5s
long
triggerAtTime = SystemClock.elapsedRealtime() + five;
Intent i =
new
Intent(
this
, AlarmReceiver.
class
);
PendingIntent pi = PendingIntent.getBroadcast(
this
,
0
, i,
0
);
manager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtTime, pi);
return
super
.onStartCommand(intent, flags, startId);
}
}</code>
|
廣播接受器
1
2
3
4
5
6
7
|
<code>
public
class
AlarmReceiver
extends
BroadcastReceiver {
@Override
public
void
onReceive(Context context, Intent intent) {
Intent i =
new
Intent(context, HorizonService.
class
);
context.startService(i);
}
}</code>
|
啟動定時任務:
1
2
|
<code>Intent intent =
new
Intent(
this
,HorizonService.
class
);
startService(intent);</code>
|
效果Demo:
本例通過廣播接收器和服務的循環調用實現了無限循環的效果,當然,你也可以直接利用setRepeating實現同樣的效果。
注意:不要忘了在manifest文件中注冊服務和廣播接收器。
AlarmManager的取消方法:AlarmManger.cancel();
分析:該方式可喚醒cpu甚至實現精確定時,適用於配合service在后台執行一些長期的定時行為。
前言
項目中總是會因為各種需求添加各種定時任務,所以就打算小結一下Android中如何實現定時任務,下面的解決方案的案例大部分都已在實際項目中實踐,特此列出供需要的朋友參考,如果有什么使用不當或者存在什么問題,歡迎留言指出!直接上干貨!
解決方案
普通線程sleep的方式實現定時任務
創建一個thread,然后讓它在while循環里一直運行着,通過sleep方法來達到定時任務的效果,這是最常見的,可以快速簡單地實現。但是這是java中的實現方式,不建議使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
<code>
public
class
ThreadTask {
public
static
void
main(String[] args) {
final
long
timeInterval =
1000
;
Runnable runnable =
new
Runnable() {
public
void
run() {
while
(
true
) {
System.out.println(
"execute task"
);
try
{
Thread.sleep(timeInterval);
}
catch
(InterruptedException e) {
e.printStackTrace();
}
}
}
};
Thread thread =
new
Thread(runnable);
thread.start();
}
} </code>
|
Timer實現定時任務
和普通線程+sleep(long)+Handler的方式比,優勢在於
可以控制TimerTask的啟動和取消第一次執行任務時可以指定delay的時間。
在實現時,Timer類調度任務,TimerTask則是通過在run()方法里實現具體任務(然后通過Handler與線程協同工作,接收線程的消息來更新主UI線程的內容)。
Timer實例可以調度多任務,它是線程安全的。當Timer的構造器被調用時,它創建了一個線程,這個線程可以用來調度任務。
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
|
<code>
/**
* start Timer
*/
protected
synchronized
void
startPlayerTimer() {
stopPlayerTimer();
if
(playTimer ==
null
) {
playTimer =
new
PlayerTimer();
Timer m_musictask =
new
Timer();
m_musictask.schedule(playTimer,
5000
,
5000
);
}
}
/**
* stop Timer
*/
protected
synchronized
void
stopPlayerTimer() {
try
{
if
(playTimer !=
null
) {
playTimer.cancel();
playTimer =
null
;
}
}
catch
(Exception e) {
e.printStackTrace();
}
}
public
class
PlayerTimer
extends
TimerTask {
public
PlayerTimer() {
}
public
void
run() {
//execute task
}
}</code>
|
然而Timer是存在一些缺陷的
Timer在執行定時任務時只會創建一個線程,所以如果存在多個任務,且任務時間過長,超過了兩個任務的間隔時間,會發生一些缺陷如果Timer調度的某個TimerTask拋出異常,Timer會停止所有任務的運行Timer執行周期任務時依賴系統時間,修改系統時間容易導致任務被掛起(如果當前時間小於執行時間)
注意:
Android中的Timer和java中的Timer還是有區別的,但是大體調用方式差不多Android中需要根據頁面的生命周期和顯隱來控制Timer的啟動和取消
java中Timer:
Android中的Timer:
ScheduledExecutorService實現定時任務
ScheduledExecutorService是從JDK1.5做為並發工具類被引進的,存在於java.util.concurrent,這是最理想的定時任務實現方式。
相比於上面兩個方法,它有以下好處:
相比於Timer的單線程,它是通過線程池的方式來執行任務的,所以可以支持多個任務並發執行 ,而且彌補了上面所說的Timer的缺陷可以很靈活的去設定第一次執行任務delay時間 提供了良好的約定,以便設定執行的時間間隔
簡例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
<code>
public
class
ScheduledExecutorServiceTask
{
public
static
void
main(String[] args)
{
final
TimerTask task =
new
TimerTask()
{
@Override
public
void
run()
{
//execute task
}
};
ScheduledExecutorService pool = Executors.newScheduledThreadPool(
1
);
pool.scheduleAtFixedRate(task,
0
,
1000
, TimeUnit.MILLISECONDS);
}
}</code>
|
實際案例背景:
應用中多個頁面涉及比賽信息展示(包括比賽狀態,比賽結果),因此需要實時更新這些數據
思路分析:
多頁面多接口刷新
就是每個需要刷新的頁面基於自身需求基於特定接口定時刷新每個頁面要維護一個定時器,然后基於頁面的生命周期和顯隱進行定時器的開啟和關閉(保證資源合理釋放)而且這里的刷新涉及到是刷新局部數據還是整體數據,刷新整體數據效率會比較低,顯得非常笨重 多頁面單接口刷新
接口給出一組需要實時進行刷新的比賽數據信息,客戶端基於id進行統一過濾匹配通過單例封裝統一定時刷新回調接口(注意內存泄露的問題,頁面銷毀時關閉ScheduledExecutorService )需要刷新的item統一調用,入口唯一,方便維護管理,擴展性好局部刷新,效率高
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
|
<code>
public
class
PollingStateMachine
implements
INetCallback {
private
static
volatile
PollingStateMachine instance =
null
;
private
ScheduledExecutorService pool;
public
static
final
int
TYPE_MATCH =
1
;
private
Map matchMap =
new
HashMap<>();
private
List<weakreference<view>> list =
new
ArrayList<>();
private
Handler handler;
// private constructor suppresses
private
PollingStateMachine() {
defineHandler();
pool = Executors.newSingleThreadScheduledExecutor();
pool.scheduleAtFixedRate(
new
Runnable() {
@Override
public
void
run() {
doTasks();
}
},
0
,
10
, TimeUnit.SECONDS);
}
private
void
doTasks() {
ThreadPoolUtils.execute(
new
PollRunnable(
this
));
}
public
static
PollingStateMachine getInstance() {
// if already inited, no need to get lock everytime
if
(instance ==
null
) {
synchronized
(PollingStateMachine.
class
) {
if
(instance ==
null
) {
instance =
new
PollingStateMachine();
}
}
}
return
instance;
}
public
<view
extends
=
""
view=
""
>
void
subscibeMatch(VIEW view, OnViewRefreshStatus onViewRefreshStatus) {
subscibe(TYPE_MATCH,view,onViewRefreshStatus);
}
private
<view
extends
=
""
view=
""
>
void
subscibe(
int
type, VIEW view, OnViewRefreshStatus onViewRefreshStatus) {
view.setTag(onViewRefreshStatus);
if
(type == TYPE_MATCH) {
onViewRefreshStatus.update(view, matchMap);
}
for
(WeakReference<view> viewSoftReference : list) {
View textView = viewSoftReference.get();
if
(textView == view) {
return
;
}
}
WeakReference<view> viewSoftReference =
new
WeakReference<view>(view);
list.add(viewSoftReference);
}
public
void
updateView(
final
int
type) {
Iterator<weakreference<view>> iterator = list.iterator();
while
(iterator.hasNext()) {
WeakReference<view> next = iterator.next();
final
View view = next.get();
if
(view ==
null
) {
iterator.remove();
continue
;
}
Object tag = view.getTag();
if
(tag ==
null
|| !(tag
instanceof
OnViewRefreshStatus)) {
continue
;
}
final
OnViewRefreshStatus onViewRefreshStatus = (OnViewRefreshStatus) tag;
handler.post(
new
Runnable() {
@Override
public
void
run() {
if
(type == TYPE_MATCH) {
onViewRefreshStatus.update(view, matchMap);
}
}
});
}
}
public
void
clear() {
pool.shutdown();
instance =
null
;
}
private
Handler defineHandler() {
if
(handler ==
null
&& Looper.myLooper() == Looper.getMainLooper()) {
handler =
new
Handler();
}
return
handler;
}
@Override
public
void
onNetCallback(
int
type, Map msg) {
if
(type == TYPE_MATCH) {
matchMap=msg;
}
updateView(type);
}
}</view></weakreference<view></view></view></view></view></view></weakreference<view></code>
|
需要刷新的item調用
1
2
3
4
5
6
|
<code>PollingStateMachine.getInstance().subscibeMatch(tvScore,
new
OnViewRefreshStatus<scoreitem, textview=
""
>(matchViewItem.getMatchID()) {
@Override
public
void
OnViewRefreshStatus(TextView view, ScoreItem scoreItem) {
//刷新處理
}
});</scoreitem,></code>
|
網絡數據回調接口
1
2
|
<code>
public
interface
INetCallback {
void
onNetCallback(
int
type,Map msg);}</code>
|
刷新回調接口:
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
|
<code>
public
abstract
class
OnViewRefreshStatus<value,
extends
=
""
view=
""
> {
private
static
final
String TAG = OnViewRefreshStatus.
class
.getSimpleName();
private
long
key;
public
OnViewRefreshStatus(
long
key) {
this
.key = key;
}
public
long
getKey() {
return
key;
}
public
void
update(
final
VIEW view, Map<
long
, value=
""
> map) {
final
VALUE value = map.get(key);
if
(value ==
null
) {
return
;
}
OnViewRefreshStatus(view, value);
}
public
abstract
void
OnViewRefreshStatus(VIEW view, VALUE value);
}</
long
,></value,></code>
|
Handler實現定時任務
通過Handler延遲發送消息的形式實現定時任務。
這里通過一個定時發送socket心跳包的案例來介紹如何通過Handler完成定時任務
案例背景
由於移動設備的網絡的復雜性,經常會出現網絡斷開,如果沒有心跳包的檢測, 客戶端只會在需要發送數據的時候才知道自己已經斷線,會延誤,甚至丟失服務器發送過來的數據。 所以需要提供心跳檢測
下面的代碼只是用來說明大體實現流程
WebSocket初始化成功后,就准備發送心跳包每隔30s發送一次心跳創建的時候初始化Handler,銷毀的時候移除Handler消息隊列
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
<code>
private
static
final
long
HEART_BEAT_RATE =
30
*
1000
;
//目前心跳檢測頻率為30s
private
Handler mHandler;
private
Runnable heartBeatRunnable =
new
Runnable() {
@Override
public
void
run() {
// excute task
mHandler.postDelayed(
this
, HEART_BEAT_RATE);
}
};
public
void
onCreate() {
//初始化Handler
}
//初始化成功后,就准備發送心跳包
public
void
onConnected() {
mHandler.removeCallbacks(heartBeatRunnable);
mHandler.postDelayed(heartBeatRunnable, HEART_BEAT_RATE);
}
public
void
onDestroy() {
mHandler.removeCallbacks(heartBeatRunnable)
}</code>
|
注意:這里開啟的runnable會在這個handler所依附線程中運行,如果handler是在UI線程中創建的,那么postDelayed的runnable自然也依附在主線程中。
AlarmManager實現精確定時操作
我們使用Timer或者handler的時候會發現,delay時間並沒有那么准。如果我們需要一個嚴格准時的定時操作,那么就要用到AlarmManager,AlarmManager對象配合Intent使用,可以定時的開啟一個Activity,發送一個BroadCast,或者開啟一個Service.
案例背景
在比賽開始前半個小時本地定時推送關注比賽的提醒信息
AndroidManifest.xml中聲明一個全局廣播接收器
1
2
3
4
5
|
<code> <receiver android:exported=
"false"
android:name=
".receiver.AlarmReceiver"
>
<intent-filter>
</action></intent-filter>
</receiver></code>
|
接收到action為IntentConst.Action.matchRemind的廣播就展示比賽
提醒的Notification
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
<code>
public
class
AlarmReceiver
extends
BroadcastReceiver {
private
static
final
String TAG =
"AlarmReceiver"
;
@Override
public
void
onReceive(Context context, Intent intent) {
if
(intent ==
null
) {
return
;
}
String action = intent.getAction();
if
(TextUtils.equals(action, IntentConst.Action.matchRemind)) {
//通知比賽開始
long
matchID = intent.getLongExtra(Net.Param.ID,
0
);
showNotificationRemindMe(context, matchID);
}
}
}</code>
|
AlarmUtil提供設定鬧鈴和取消鬧鈴的兩個方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
<code>
public
class
AlarmUtil {
private
static
final
String TAG =
"AlarmUtil"
;
public
static
void
controlAlarm(Context context,
long
startTime,
long
matchId, Intent nextIntent) {
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, (
int
) matchId, nextIntent,
PendingIntent.FLAG_ONE_SHOT);
alarmManager.set(AlarmManager.RTC_WAKEUP, startTime, pendingIntent);
}
public
static
void
cancelAlarm(Context context, String action,
long
matchId) {
Intent intent =
new
Intent(action);
PendingIntent sender = PendingIntent.getBroadcast(
context, (
int
) matchId, intent,
0
);
// And cancel the alarm.
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
alarmManager.cancel(sender);
}}</code>
|
關注比賽的時候,會設置intent的action為IntentConst.Action.matchRemind,會根據比賽的開始時間提前半小時設定鬧鈴時間,取消關注比賽同時取消鬧鈴
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
<code>
if
(isSetAlarm) {
long
start_tm = matchInfo.getStart_tm();
long
warmTime = start_tm -
30
*
60
;
Intent intent =
new
Intent(IntentConst.Action.matchRemind);
intent.putExtra(Net.Param.ID, matchInfo.getId());
AlarmUtil.controlAlarm(context, warmTime *
1000
,matchInfo.getId(), intent);
Gson gson =
new
Gson();
String json = gson.toJson(matchInfo);
SportDao.getInstance(context).insertMatchFollow(eventID, matchInfo.getId(), json, matchInfo.getType());
}
else
{
AlarmUtil.cancelAlarm(context, IntentConst.Action.matchRemind,matchInfo.getId());
SportDao.getInstance(context).deleteMatchFollowByID(matchInfo.getId());
}</code>
|