1. 程式人生 > >AlarmManager-系統推薦的定時任務

AlarmManager-系統推薦的定時任務

近期leader提了很多這樣的需求:每隔幾個小時拉取伺服器的配置資訊存在本地、每隔一段時間跟服務端校對一下本地時間、每隔一段時間上傳一下本地日誌等等。其實這些本質都是定時任務,隔一段時間去幹xxx,那麼在安卓中定時任務無非幾種實現方式,Handler(CountDownTimer)、Timer、while迴圈、AlarmManager。(如果有遺漏還望留言告知O(∩_∩)O謝謝)前三種大家基本都用過,也就不多贅言,本篇blog專注介紹AlarmManager。

AlarmManager介紹

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

  1. 在指定時長後執行某項操作
  2. 週期性的執行某項操作

並且AlarmManager物件可以配合Intent使用,定時的開啟一個Activity,傳送一個BroadCast,或者開啟一個Service.那麼用它實現定時任務再好不過了。

使用

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);
    }
}

效果如下




2.具體年月日啟動鬧鐘

核心程式碼如下

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以後他們是精準的.


相關知識

本篇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。

總結

AlarmManager非常適合Android中定時任務.並且因為他具有喚醒CPU的功能,可以保證每次需要執行特定任務時CPU都能正常工作,
或者說當CPU處於休眠時註冊的鬧鐘會被保留(可以喚醒CPU),(老司機們請注意此處有彎道減速慢行)但是國內Rom眾多.有的可能休眠時候無法喚醒..但是我還是推薦用AlarmManager….233333

ps:對對對,差點忘了趕緊加上…是諮詢了錘哥(錘廠CTO)才知道AlarmManager也可以做定時任務的..