原文連結https://www.shanya.world/archives/a7b639d4.html
建立定時任務
Android中的定時任務一般有兩種實作方式,一種是使用 Java API 裡提供的 Timer 類,一種是使用 Android 的 Alarm 機制。這兩種方式在多數情況下都能實作類似的效果,但 Timer 有一個明顯的短闆,它并不适用于那些需要長期在背景運作的定時任務。我們都知道,為了能讓電池更加耐用,每種手機都會有自己的休眠政策,Android 手機就會在長時間不操作的情況下自動讓 CPU 進入到睡眠狀态,這就有可能導緻 Timer 中的定時任務無法正常運作。而 Alarm 則具有喚醒 CPU 的功能,它可以在需要執行定時任務的時候大吼一聲:“小UU,不要跟我 bbll ,趕緊給我起來幹活,不然你看我紮不紮你就完了。”
需要注意,這裡喚醒 CPU 和喚醒螢幕完全不是一個概念,千萬不要混淆。
Alarm機制
首先看一下 Alarm 機制的用法,并不複雜,主要就是借助了 AlarmManager 類實作的。
擷取 AlarmManager 的執行個體,代碼如下:
API19之前AlarmManager常用的一些方法
-
//該方法用于設定一次性定時器,到達時間執行完就GG了set(int type,long startTime,PendingIntent pi)
-
//該方法用于設定可重複執行的定時器setRepeating(int type,long startTime,long intervalTime,PendingIntent pi)
-
//該方法用于設定可重複執行的定時器。與setRepeating相比,這個方法更加考慮系統電量,比如系統在低電量情況下可能不會嚴格按照設定的間隔時間執行鬧鐘,因為系統可以調整報警的傳遞時間,使其同時觸發,避免超過必要的喚醒裝置。setInexactRepeating(int type,long startTime,long intervalTime,PendingIntent pi)
參數說明
int type:鬧鐘類型,常用有幾個類型,說明如下:
AlarmManager.ELAPSED_REALTIME | 表示鬧鐘在手機睡眠狀态下不可用,就是睡眠狀态下不具備喚醒CPU的能力(跟普通Timer差不多了),該狀态下鬧鐘使用相對時間,相對于系統啟動開始。 |
AlarmManager.ELAPSED_REALTIME_WAKEUP | 表示鬧鐘在睡眠狀态下會喚醒系統并執行提示功能,該狀态下鬧鐘也使用相對時間 |
AlarmManager.RTC | 表示鬧鐘在睡眠狀态下不可用,該狀态下鬧鐘使用絕對時間,即目前系統時間 |
AlarmManager.RTC_WAKEUP | 表示鬧鐘在睡眠狀态下會喚醒系統并執行提示功能,該狀态下鬧鐘使用絕對時間 |
long startTime: 定時任務的出發時間,以毫秒為機關。
PendingIntent pi: 到時間後的執行意圖。PendingIntent是Intent的封裝類。需要注意的是,如果是通過啟動服務來實作鬧鐘提 示的話,PendingIntent對象的擷取就應該采用Pending.getService(Context c,int i,Intent intent,int j)方法;如果是通過廣播來實作鬧鐘提示的話,PendingIntent對象的擷取就應該采用 PendingIntent.getBroadcast(Context c,int i,Intent intent,int j)方法;如果是采用Activity的方式來實作鬧鐘提示的話,PendingIntent對象的擷取就應該采用 PendingIntent.getActivity(Context c,int i,Intent intent,int j)方法。關于PendingInten不是本文重點,請自行查閱使用方法。
使用舉例
需求:定義一個在CPU休眠情況下也能執行的鬧鐘,到指定時間發送一次廣播,代碼如下:
AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.HOUR_OF_DAY,21);
calendar.set(Calendar.MINUTE,14);
calendar.set(Calendar.SECOND,00);//這裡代表 21.14.00
Intent intent = new Intent("Li_Hua");
intent.putExtra("msg","起床了啊");
PendingIntent pi = PendingIntent.getBroadcast(this,0,intent,0);
// 到了 21點14分00秒 後通過PendingIntent pi對象發送廣播
am.set(AlarmManager.RTC_WAKEUP,calendar.getTimeInMillis(),pi);
AlarmManager的版本适配
以上的講解和代碼在 API < 19 的情況下是可以正常運作的。但是在 API > 19 的手機上運作就會發現 TMD 怎麼就不好使了呢。例如我們設定1分鐘後執行,結果卻是2分鐘後才執行。
But 這不是 BUG!
原因是 Google 對系統耗電性方面進行了優化。系統會自動檢測目前有多少 Alarm 任務存在,然後将觸發時間相近的幾個任務放在一起執行,這就可以大幅度減少 CPU 被喚醒的次數,進而有效延長電池的使用時間。
當然,如果你要求的 Alarm 任務執行時間必須準确無誤,Android 仍然提供了解決方案。使用 AlarmManager 的
setExact()
方法來代替
set()
方法,就基本上可以保證任務能夠準時執行了。
雖然Android的每個系統版本都在手機電量方面努力進行優化,不過一直沒能解決背景服務泛濫、手機電量消耗過快的問題。于是在Android 6.0系統中,谷歌加入了一個全新的Doze模式,進而可以極大幅度地延長電池的使用壽命。
到底什麼是Doze模式。當使用者的裝置是Android 6.0或以上系統時,如果該裝置未插接電源,處于靜止狀态( Android 7.0中删除了這條件), 且螢幕關閉了一段時間之後,就會進人到Doze模式。在Doze模式下,系統會對CPU、網絡、Alarm等活動進行限制,進而延長了電池的使用壽命。
當然,系統并不會一直處于 Doze模式,而是會間歇性地退出Doze模式一小段時間,在這段時間中,應用就可以去完成它們的同步操作、Alarm任務, 等等。
接下來我們具體看- 看在Doze模式下有哪些功能會受到限制吧。
- 網絡通路被禁止
- 系統忽略喚醒CPU或者螢幕操作
- 系統不再執行WIFI掃描
- 系統不再執行同步服務
- Alarm任務将會在下次退出Doze模式的時候執行
注意其中的最後一條, 也就是說,在Doze模式下,我們的Alarm任務将會變得不準時。當然,這在大多數情況下都是合理的,因為隻有當使用者長時間不使用手機的時候才會進入Doze模式,通常在這種情況下對Alarm任務的準時性要求并沒有那麼高。
不過,如果你真的有非常特殊的需求,要求Alarm任務即使在Doze模式下也必須正常執行,Android還是提供了解決方案。調用AlarmManager的
setAndAllowWhileIdle()
或
setExact-AndAllowhileIdle()
方法就能讓定時任務即使在Doze模式下也能正常執行了,這兩個方法之間的差別和
set()
、
setExact()
方法之間的差別是一樣的。
AlarmManager執行個體Demo講解(包含版本适配以及高版本設定重複鬧鐘)
好了經過上面講解,我相信你是似懂非懂的,因為沒看到具體代碼啊,簡單,一個小Demo就全都明白了。
實作功能:在CPU休眠情況下依然可以設定時間啟動一次服務,在服務中執行相應邏輯(Demo中隻是列印Log),适配各個版本。
先看一下最核心的AlarmManagerUtils類(AlarmManager工具類):
package com.shanya.testalarm;
import android.annotation.SuppressLint;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import java.util.Calendar;
public class AlarmManagerUtils {
private static final long TIME_INTERVAL = 10 * 1000;//鬧鐘執行任務的時間間隔
private Context context;
public static AlarmManager am;
public static PendingIntent pendingIntent;
private Calendar calendar;
//
private AlarmManagerUtils(Context aContext) {
this.context = aContext;
}
//singleton
private static AlarmManagerUtils instance = null;
public static AlarmManagerUtils getInstance(Context aContext) {
if (instance == null) {
synchronized (AlarmManagerUtils.class) {
if (instance == null) {
instance = new AlarmManagerUtils(aContext);
}
}
}
return instance;
}
public void createGetUpAlarmManager() {
am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
Intent intent = new Intent(context, MyService.class);
pendingIntent = PendingIntent.getService(context, 0, intent, 0);//每隔10秒啟動一次服務
}
@SuppressLint("NewApi")
public void getUpAlarmManagerStartWork() {
calendar = Calendar.getInstance();
calendar.set(Calendar.HOUR_OF_DAY,23);
calendar.set(Calendar.MINUTE,50);
calendar.set(Calendar.SECOND,00);
//版本适配 System.currentTimeMillis()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {// 6.0及以上
am.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP,
calendar.getTimeInMillis(), pendingIntent);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {// 4.4及以上
am.setExact(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(),
pendingIntent);
} else {
am.setRepeating(AlarmManager.RTC_WAKEUP,
calendar.getTimeInMillis(), TIME_INTERVAL, pendingIntent);
}
}
@SuppressLint("NewApi")
public void getUpAlarmManagerWorkOnOthers() {
//高版本重複設定鬧鐘達到低版本中setRepeating相同效果
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {// 6.0及以上
am.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP,
System.currentTimeMillis() + TIME_INTERVAL, pendingIntent);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {// 4.4及以上
am.setExact(AlarmManager.RTC_WAKEUP, System.currentTimeMillis()
+ TIME_INTERVAL, pendingIntent);
}
}
}
AlarmManagerUtils就是将與AlarmManager有關的操作都封裝起來了,友善解耦。很簡單,主要就是版本适配了,上面已經講解夠仔細了,這裡就是判斷不同版本調用不同API了。
MainActivity代碼:
package com.shanya.testalarm;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
private AlarmManagerUtils alarmManagerUtils;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
alarmManagerUtils = AlarmManagerUtils.getInstance(this);
alarmManagerUtils.createGetUpAlarmManager();
Button button = findViewById(R.id.am);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
alarmManagerUtils.getUpAlarmManagerStartWork();
Toast.makeText(getApplicationContext(),"設定成功",Toast.LENGTH_SHORT).show();
}
});
}
}
MainActivity中就是調用AlarmManagerUtils中已經封裝好的代碼進行初始化以及點選Button的時候調用getUpAlarmManagerStartWork方法完成第一次觸發AlarmManager。
最後看下服務類中具體做了什麼。
MyService類:
package com.shanya.testalarm;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.os.Build;
import android.os.IBinder;
import android.os.SystemClock;
import android.util.Log;
import android.widget.Toast;
import androidx.annotation.RequiresApi;
public class MyService extends Service {
private static final String TAG = "MyService";
public MyService() {
}
@Override
public IBinder onBind(Intent intent) {
// TODO: Return the communication channel to the service.
throw new UnsupportedOperationException("Not yet implemented");
}
@RequiresApi(api = Build.VERSION_CODES.M)
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
new Thread(new Runnable() {
@Override
public void run() {
Log.d(TAG, "run: ");
}
}).start();
AlarmManagerUtils.getInstance(getApplicationContext()).getUpAlarmManagerWorkOnOthers();
return super.onStartCommand(intent, flags, startId);
}
}
總結
好了,本文到此就該結束了,相信經過以上講述你對AlarmManager有了更進一步全面了解,在我們使用的時候請不要濫用,考慮一下使用者電量,盡量優化自己APP。
源碼下載下傳
CSDN 下載下傳
GitHub 下載下傳