1. 程式人生 > >定時任務AlarmManager的使用注意事項

定時任務AlarmManager的使用注意事項

AlarmManager介紹

見名知意鬧鐘管理者,當然不代表AlarmManager只是用來做鬧鐘應用的,作為一個系統級別的提示服務,其實它的作用和Timer有點相似 

1.在指定時長後執行某項操作
2.週期性的執行某項操作
並且AlarmManager物件可以配合Intent使用,定時的開啟一個Activity,傳送一個BroadCast,或者開啟一個Service.那麼用它實現定時任務再好不過了。 

最近在做一個需求:利用輪詢機制獲取通知。一看到這個需求就想到了使用 AlarmManager來實現。 AlarmManager經常被用來執行定時任務,比如設定鬧鈴、傳送心跳包等。也許有人會有疑問:為什麼不能使用相同具有定時效果的 Timer

和 Handler呢?

其實答案非常簡單,相對於 Handler來說,使用 sendEmptyMessageDelayed方法是依賴於 Handler所在的執行緒的,如果執行緒結束,就起不到定時任務的效果;而 AlarmManager依賴的是 Android 系統的服務,具備喚醒機制。比起 Handler也就更合適了。

而至於 Timer可以精確地做到定時操作,但是相比於 AlarmManager而言還是差了一截。同理,如果手機關屏後長時間不使用, CPU 就會進入休眠模式。這個使用如果使用 Timer來執行定時任務就會失敗,因為 Timer無法喚醒 CPU 。

所以,綜上所述,AlarmManager

就成為了最佳選擇。

AlarmManager初體驗

先來一發簡單的Demo體驗一下

AlarmManager am = (AlarmManager) getSystemService(Context.ALARM_SERVICE);

Intent intent = new Intent(this, AlarmService.class);
intent.setAction(AlarmService.ACTION_ALARM);
PendingIntent pendingIntent = PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
if(Build.VERSION.SDK_INT < 19){
    am.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + 5000, pendingIntent);
}else{
    am.setExact(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + 5000, pendingIntent);
}


這個例子就是5秒鐘後傳送一個Action為AlarmService.ACTION_ALARM的Intent到AlarmService。 

AlarmManager的常用API

set(int type, long triggerAtMillis, PendingIntent operation)

該方法用於設定一次性鬧鐘,第一個引數表示鬧鐘型別,第二個引數表示鬧鐘執行時間,第三個引數表示鬧鐘響應動作。

setRepeating(int type, long triggerAtMillis,long intervalMillis, PendingIntent operation)

該方法用於設定重複鬧鐘,第一個引數表示鬧鐘型別,第二個引數表示鬧鐘首次執行時間,第三個引數表示鬧鐘兩次執行的間隔時間,第四個引數表示鬧鐘響應動作。

setInexactRepeating(int type, long triggerAtMillis,long intervalMillis, PendingIntent operation)

該方法也用於設定重複鬧鐘,與第二個方法相似,不過鬧鐘時間不精確。

setExact(int type, long triggerAtMillis, PendingIntent operation)
setWindow(int type, long windowStartMillis, long windowLengthMillis,PendingIntent operation)

方法1和方法2在SDK_INT 19以前是精確的鬧鐘,19以後為了節能省電(減少系統喚醒和電池使用)。使用Alarm.set()和Alarm.setRepeating()已經不能保證精確性,不過還好Google又提供了兩個精確的Alarm方法setWindow()和setExact(),所以19以後需要精確的鬧鐘就需要上面兩個方法,具體原因後面再說

cancel(PendingIntent operation)

取消Intent相同的鬧鐘,這裡是根據Intent中filterEquals(Intent other)方法來判斷是否相同

public boolean filterEquals(Intent other) {
        if (other == null) {
            return false;
        }
        if (!Objects.equals(this.mAction, other.mAction)) return false;
        if (!Objects.equals(this.mData, other.mData)) return false;
        if (!Objects.equals(this.mType, other.mType)) return false;
        if (!Objects.equals(this.mPackage, other.mPackage)) return false;
        if (!Objects.equals(this.mComponent, other.mComponent)) return false;
        if (!Objects.equals(this.mCategories, other.mCategories)) return false;

        return true;
    }

從方法體可以看出mAction、mData、mType、mPackage、mComponent、mCategories這幾個完全一樣就認定為同一Intent 

鬧鐘型別

這個鬧鐘型別就是前面setxxx()方法第一個引數int type.

AlarmManager.ELAPSED_REALTIME:使用相對時間,可以通過SystemClock.elapsedRealtime() 獲取(從開機到現在的毫秒數,包括手機的睡眠時間),裝置休眠時並不會喚醒裝置。
AlarmManager.ELAPSED_REALTIME_WAKEUP:與ELAPSED_REALTIME基本功能一樣,只是會在裝置休眠時喚醒裝置。
AlarmManager.RTC:使用絕對時間,可以通過 System.currentTimeMillis()獲取,裝置休眠時並不會喚醒裝置。
AlarmManager.RTC_WAKEUP: 與RTC基本功能一樣,只是會在裝置休眠時喚醒裝置。 

舉個栗子

1.點選按鈕,AlarmManager3秒後傳送intent到service彈出toast提示.

activity程式碼

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

    }

    public void alarm(View v){
        AlarmManager am = (AlarmManager) getSystemService(Context.ALARM_SERVICE);

        Intent intent = new Intent(this, AlarmService.class);
        intent.setAction(AlarmService.ACTION_ALARM);
        PendingIntent pendingIntent = PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
        if(Build.VERSION.SDK_INT < 19){
            am.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + 3000, pendingIntent);
        }else{
            am.setExact(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + 3000, pendingIntent);
        }
    }

}

service程式碼

public class AlarmService extends Service {

    public static String ACTION_ALARM = "action_alarm";
    private Handler mHanler = new Handler(Looper.getMainLooper());

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }


    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        mHanler.post(new Runnable() {
            @Override
            public void run() {
                Toast.makeText(AlarmService.this, "鬧鐘來啦", Toast.LENGTH_SHORT).show();
            }
        });
        return super.onStartCommand(intent, flags, startId);
    }
}

具體年月日啟動鬧鐘

核心程式碼如下

Calendar calendar = Calendar.getInstance();
        calendar.set(Calendar.YEAR,2016);
        calendar.set(Calendar.MONTH,Calendar.DECEMBER);
        calendar.set(Calendar.DAY_OF_MONTH,16);
        calendar.set(Calendar.HOUR_OF_DAY,11);
        calendar.set(Calendar.MINUTE,50);
        calendar.set(Calendar.SECOND,0);
        //設定時間為 2016年12月16日11點50分0秒

        AlarmManager am = (AlarmManager) getSystemService(Context.ALARM_SERVICE);

        Intent intent = new Intent(this, AlarmService.class);
        intent.setAction(AlarmService.ACTION_ALARM);
        PendingIntent pendingIntent = PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
        if(Build.VERSION.SDK_INT < 19){
            am.set(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), pendingIntent);
        }else{
            am.setExact(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), pendingIntent);
        }

效果跟上面沒區別 

AlarmManager鬧鐘不準原因

前面介紹的所有的set方法其實都是呼叫內部的一個private的方法setImpl(),只是不同的set方法傳入的值不同而已

private void setImpl(int type, long triggerAtMillis, long windowMillis, long intervalMillis,
            int flags, PendingIntent operation, final OnAlarmListener listener, String listenerTag,
            Handler targetHandler, WorkSource workSource, AlarmClockInfo alarmClock) {
        if (triggerAtMillis < 0) {
            /* NOTYET
            if (mAlwaysExact) {
                // Fatal error for KLP+ apps to use negative trigger times
                throw new IllegalArgumentException("Invalid alarm trigger time "
                        + triggerAtMillis);
            }
            */
            triggerAtMillis = 0;
        }

        ......

        try {
            mService.set(mPackageName, type, triggerAtMillis, windowMillis, intervalMillis, flags,
                    operation, recipientWrapper, listenerTag, workSource, alarmClock);
        } catch (RemoteException ex) {
            throw ex.rethrowFromSystemServer();
        }
    }

這裡只展示了相關程式碼,而具體控是否精確是靠windowMillis這個引數

在看看普通的set()與setRepeating()方法如何傳遞windowMillis引數

public void set(int type, long triggerAtMillis, PendingIntent operation) {
        setImpl(type, triggerAtMillis, legacyExactLength(), 0, 0, operation, null, null,
                null, null, null);
    }

public void setRepeating(int type, long triggerAtMillis,
        long intervalMillis, PendingIntent operation) {
    setImpl(type, triggerAtMillis, legacyExactLength(), intervalMillis, 0, operation,
            null, null, null, null, null);
}

可以發現windowMillis引數為legacyExactLength()方法返回值的,那麼我們接著在看legacyExactLength方法

可以看出mAlwaysExact這個變數控制著該方法的返回值,如果是小於API19的版本會使用 
WINDOW_EXACT引數,這個引數是0(意思就是區間設定為0,那麼就會按照triggerAtMillis這個時間準時觸發,也就是精準觸發)另一個引數WINDOW_HEURISTIC的值是-1,這個值具體的用法就要看AlarmManagerService具體的實現了,反正只要知道這個值是不精準就可以。而setExact()這個值為WINDOW_EXACT,setWindow()的話這個值你可以自己傳所以19以後他們是精準的. 另外也可以查閱 Android 官網中關於 Android 4.4 API 會看到如下幾句話:

原來是 Google 為了追求系統省電,所以“偷偷加工”了一下喚醒的時間間隔。但也正如上面官網中所說的那樣,如果在 Android 4.4 及以上的裝置還要追求精準的鬧鐘定時任務,要使用 setExact() 方法。再次開啟 Android 官網中關於 Android 6.0 變更 ,發現在 Android 6.0 中引入了低電耗模式和應用待機模式。然後接著往下看 對低電耗模式和應用待機模式進行鍼對性優化 ,發現會有下面一段話:

在 Android 4.4 上能用的 setExact()方法在 Android 6.0 上因為低電耗模式又不能正常使用了。但是,Google 又提供了新的方法 setExactAndAllowWhileIdle()來解決在低電耗模式下的鬧鐘觸發。所以,Attention!相關的程式碼寫:

// pendingIntent 為傳送廣播
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
    alarmManager.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime(), pendingIntent);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
    alarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime(), pendingIntent);
} else {
    alarmManager.setRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime(), TIME_INTERVAL, pendingIntent);
}

private BroadcastReceiver alarmReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
        // 重複定時任務
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            alarmManager.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + TIME_INTERVAL, pendingIntent);
        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            alarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + TIME_INTERVAL, pendingIntent);
        }
        // to do something
        doSomething();
    }
};

相關知識

本篇blog只以getService()方式舉了栗子,還可通過getBroadCast()傳送廣播或getActivity()啟動Activity來執行某項固定任務。其中各方法的最後一個引數含有以下常量分別代表不同含義的任務執行效果:

FLAG_CANCEL_CURRENT:如果當前系統中已經存在一個相同的PendingIntent物件,那麼就將先將已有的PendingIntent取消,然後重新生成一個PendingIntent物件。

FLAG_NO_CREATE:如果當前系統中不存在相同的PendingIntent物件,系統將不會建立該PendingIntent物件而是直接返回null。

FLAG_ONE_SHOT:該PendingIntent只作用一次。在該PendingIntent物件通過send()方法觸發過後,PendingIntent將自動呼叫cancel()進行銷燬,那麼如果你再呼叫send()方法的話,系統將會返回一個SendIntentException。

FLAG_UPDATE_CURRENT:如果系統中有一個和你描述的PendingIntent對等的PendingInent,那麼系統將使用該PendingIntent物件,但是會使用新的Intent來更新之前PendingIntent中的Intent物件資料,例如更新Intent中的Extras。