標籤雲

搜尋此網誌

2015/12/07

Best Practices for Background Jobs - Scheduling Repeating Alarms

鬧鐘 (基於 AlarmManager class) 提供一個以時間為基礎進行操作的方式
有一些特點:
1. 在設定時間或週期啟動 Intent
2. 搭配 broadcast receiver 來啟動 service 及進行其他操作
3. 由於它在 app 之外運行,即使 app 沒有運行或休眠中我們仍可以用來觸發事件
4. 因為可以週期喚醒,所以 app 不用常駐而可以節省系統資源(比 Handler + Timer + Thread 省資源)

repeating alarm 是一個靈活度有限的簡單機制
如果需要網路操作的話這可能不會是一個最佳選擇
設計不當的話甚至會榨乾電池及造成系統負載

常見的狀況是在 app 外觸發操作去跟 server 進行資料同步
但如果你對 server 有足夠的掌控權的話應該使用 Google Cloud Messaging (GCM) 搭配 sync adapter 會比較好
sync adapter 比 AlarmManager 更有靈活性

官方建議了一些關於 alarm 的 Best practices:
* 對任何 network requests 加入隨機性觸發
- 當 alarm 觸發時執行任何 local (不須網路的)工作
- 同時安排一個在隨機時間觸發的 alarm 去執行 network requests
* alarm 越少、越不頻繁,越好
* 若無必要不要喚醒 device
* 使用 setInexactRepeating() 取代 setRepeating()
android 可以把不同 app 的 repeating alarm 同步及同時觸發,這會減少系統喚醒 device 的次數並改善電池消耗
(在 Android 4.4 (API Level 19)之後,所有的 repeating alarms 都是 inexact)
* 盡量避免根據時鐘的時間設定 alarm
定義精確時間的 alarm 比較無法展延,可能的話使用 ELAPSED_REALTIME 這個 alarm type

一個 repeating alarm 有以下幾個屬性:
* alarm type
- ELAPSED_REALTIME (建議選項,適合間隔時間的鬧鐘,以 time since system boot 作為參照,所以不受時區影響)
- ELAPSED_REALTIME_WAKEUP (WAKEUP 版本會在 screen off 狀況下喚醒 CPU,以確保能準時觸發)
- RTC (real time clock) 適合固定時間的鬧鐘或與地區相關的運用,使用 UTC (wall clock)為參照
- RTC_WAKEUP (RTC 的 WAKEUP 版本)

* trigger time - 如果時間已經過了,alarm 會立即觸發
* interval(間隔)
* PendingIntent - alarm 觸發時會啟動。你的第二個 alarm 也會使用同一個 PendingIntent(把原本的蓋過)

接下來看一下官方提供的範例

ELAPSED_REALTIME_WAKEUP:
30 分鐘後喚醒設備並啟動鬧鐘,每 30 分鐘重複一次
// 每半小時喚醒還是蠻耗電的,官方強烈希望你把間隔拉長一點
alarmMgr.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
        AlarmManager.INTERVAL_HALF_HOUR,
        AlarmManager.INTERVAL_HALF_HOUR, alarmIntent);
一分鐘後啟動的單次鬧鐘
alarmMgr.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
        SystemClock.elapsedRealtime() + 60 * 1000, alarmIntent);

RTC_WAKEUP:
鬧鐘訂於大約下午兩點,並重複於每日同一時間
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(System.currentTimeMillis());
calendar.set(Calendar.HOUR_OF_DAY, 14);

// setInexactRepeating() 必須傳入 AlarmManager interval 常數,這裡用的是 AlarmManager.INTERVAL_DAY
// 注意時間會是大約值,無法非常精確
alarmMgr.setInexactRepeating(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(),
        AlarmManager.INTERVAL_DAY, alarmIntent);
喚醒設備於當日 8:30 am,每 20 分鐘重複
private AlarmManager alarmMgr;
private PendingIntent alarmIntent;
...
alarmMgr = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
Intent intent = new Intent(context, AlarmReceiver.class);
alarmIntent = PendingIntent.getBroadcast(context, 0, intent, 0);

Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(System.currentTimeMillis());
calendar.set(Calendar.HOUR_OF_DAY, 8);
calendar.set(Calendar.MINUTE, 30);

// 使用 setRepeating 可以精確指定鬧鐘響的時間,重複時間就不用 AlarmManager 常數,而是直接給間隔的毫秒值
// 再提醒一次:在 Android 4.4 (API Level 19)之後,所有的 repeating alarms 都是 inexact
alarmMgr.setRepeating(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), 1000 * 60 * 20, alarmIntent);

Cancel an Alarm
在 PendingIntent 裡我們可以取出 alarmMgr 再用 alarmMgr.cancel 取消鬧鐘
//如果鬧鐘已經設定了就取消
if (alarmMgr!= null) {
    alarmMgr.cancel(alarmIntent);
}

Start an Alarm When the Device Boots
當關機時,所有鬧鐘都會被取消,所以我們可以在開機時重設鬧鐘
首先當然我們要宣告接收 BOOT_COMPLETED 事件,還有該事件的 receiver
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
...
<receiver android:name=".SampleBootReceiver"
        android:enabled="false">
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED"></action>
    </intent-filter>
</receiver/>
然後實作這個 BroadcastReceiver 去接收 BOOT_COMPLETED
public class SampleBootReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        if (intent.getAction().equals("android.intent.action.BOOT_COMPLETED")) {
            // Set the alarm here.
        }
    }
}
由於上面在 manifest 中把 receiver 的 android:enabled 設為 false 以避免不必要的通知
所以我們在需要接收通知時在程式中將它 enable
ComponentName receiver = new ComponentName(context, SampleBootReceiver.class);
PackageManager pm = context.getPackageManager();

pm.setComponentEnabledSetting(receiver,
        PackageManager.COMPONENT_ENABLED_STATE_ENABLED, //或 PackageManager.COMPONENT_ENABLED_STATE_DISABLED
        PackageManager.DONT_KILL_APP);
一旦設為 PackageManager.COMPONENT_ENABLED_STATE_ENABLED 後,因為會覆蓋原本 manifest 的設定,即使重新開機它也會保持在 enable
所以當使用者取消鬧鐘(或我們不再需要接收這事件的情況下)可以再用一樣方法將 enabled 設回 PackageManager.COMPONENT_ENABLED_STATE_DISABLED

相關資料:
Managing Device Awake State
Scheduling Repeating Alarms

沒有留言: