AlarmManager深入分析
轉載地址http://blog.csdn.net/codefly/article/details/17058425,自己補充了一些代碼分析
1.概述
在Android系統中,鬧鍾和喚醒功能都是由Alarm Manager Service控制並管理的。我們所熟悉的RTC鬧鍾以及定時器都和它有莫大的關系。為了便於稱呼,我常常也把這個service簡稱為ALMS。
另外,ALMS還提供了一個AlarmManager輔助類。在實際的代碼中,應用程序一般都是通過這個輔助類來和ALMS打交道的。就代碼而言,輔助類只不過是把一些邏輯語義傳遞給ALMS服務端而已,具體怎么做則完全要看ALMS的實現代碼了。
ALMS的實現代碼並不算太復雜,主要只是在管理“邏輯鬧鍾”。它把邏輯鬧鍾分成幾個大類,分別記錄在不同的列表中。然后ALMS會在一個專門的線程中循環等待鬧鍾的激發,一旦時機到了,就“回調”邏輯鬧鍾對應的動作。
以上只是一些概要性的介紹,下面我們來看具體的技術細節。
先看下具體ALMS在應用中的使用
- 1. Intent intent = new Intent(this, OneShotAlarm.class);
- 2. PendingIntent sender = PendingIntent.getBroadcast(this, 0, intent, 0);
- 3.
- 4. // 設置警報時間
- 5. Calendar calendar = Calendar.getInstance();
- 6. calendar.setTimeInMillis(System.currentTimeMillis());
- 7. calendar.add(Calendar.SECOND, 30);
- 8.
- 9. // 設置警報時間,除了用Calendar之外,還可以用
- 10. long firstTime = SystemClock.elapsedRealtime();
- 11.
- 12. AlarmManager am = (AlarmManager) getSystemService(ALARM_SERVICE);
- 13. // 只會警報一次
- 14. am.set(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), sender);
- 15. // 會重復警報多次
- 16. am.setRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, firstTime, 15*1000, sender);
- 17.
- 18. // 要取消這個警報,只要通過PendingIntent就可以做到
- 19. am.cancel(sender);
2.AlarmManager
前文我們已經說過,ALMS只是服務端的東西。它必須向外提供具體的接口,才能被外界使用。在Android平台中,ALMS的外部接口為IAlarmManager。其定義位於frameworks\base\core\java\android\app\IAlarmManager.aidl腳本中,定義截選如下:
interface IAlarmManager { void set(int type, long triggerAtTime, in PendingIntent operation); void setRepeating(int type, long triggerAtTime, long interval, in PendingIntent operation); void setInexactRepeating(int type, long triggerAtTime, long interval, in PendingIntent operation); void setTime(long millis); void setTimeZone(String zone); void remove(in PendingIntent operation); }
在一般情況下,service的使用者會通過Service Manager Service接口,先拿到它感興趣的service對應的代理I接口,然后再調用I接口的成員函數向service發出請求。所以按理說,我們也應該先拿到一個IAlarmManager接口,然后再使用它。可是,對Alarm Manager Service來說,情況略有不同,其最常見的調用方式如下:
manager = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
其中,getSystemService()返回的不再是IAlarmManager接口,而是AlarmManager對象。
2.1 AlarmManager的成員函數
AlarmManager的成員函數有:
public void set( int type, long triggerAtTime, PendingIntent operation)
public void setRepeating( int type, long triggerAtTime, long interval,
PendingIntent operation)
public void setInexactRepeating( int type, long triggerAtTime, long interval,
PendingIntent operation)
public void cancel(PendingIntent operation)
public void setTime( long millis)
public void setTimeZone(String timeZone)即1個構造函數,6個功能函數。基本上完全和IAlarmManager的成員函數一一對應。
另外,AlarmManager類中會以不同的公共常量來表示多種不同的邏輯鬧鍾,在Android 4.0的原生代碼中有4種邏輯鬧鍾:
1) RTC_WAKEUP
2) RTC
3) ELAPSED_REALTIME_WAKEUP
4) ELAPSED_REALTIME
應用側通過調用AlarmManager對象的成員函數,可以把語義傳遞到AlarmManagerService,並由它進行實際的處理。
3.AlarmManagerService
ALMS的重頭戲在AlarmManagerService中,這個類繼承於IAlarmManager.Stub,所以是個binder實體。它包含的重要成員如下:
其中,mRtcWakeupAlarms等4個ArrayList<Alarm>數組分別對應着前文所說的4種“邏輯鬧鍾”。為了便於理解,我們可以想象在底層有4個“實體鬧鍾”,注意,是4個,不是4類。上面每一類“邏輯鬧鍾”都會對應一個“實體鬧鍾”,而邏輯鬧鍾則可以有若干個,它們被存儲在ArrayList中,示意圖如下:
當然,這里所說的“實體鬧鍾”只是個概念而已,其具體實現和底層驅動有關,在frameworks層不必過多關心。
Frameworks層應該關心的是那幾個ArrayList<Alarm>。這里的Alarm對應着邏輯鬧鍾。
3.1 邏輯鬧鍾
Alarm是AlarmManagerService的一個內嵌類Alarm,定義截選如下:
- private static class Alarm {
- public int type;
- public int count;
- public long when;
- public long repeatInterval;
- public PendingIntent operation;
- public int uid;
- public int pid;
- . . . . . .
其中記錄了邏輯鬧鍾的一些關鍵信息。
- type域:記錄着邏輯鬧鍾的鬧鍾類型,比如RTC_WAKEUP、ELAPSED_REALTIME_WAKEUP等;
- count域:是個輔助域,它和repeatInterval域一起工作。當repeatInterval大於0時,這個域可被用於計算下一次重復激發alarm的時間,詳細情況見后文;
- when域:記錄鬧鍾的激發時間。這個域和type域相關,詳細情況見后文;
- repeatInterval域:表示重復激發鬧鍾的時間間隔,如果鬧鍾只需激發一次,則此域為0,如果鬧鍾需要重復激發,此域為以毫秒為單位的時間間隔;
- operation域:記錄鬧鍾激發時應該執行的動作,詳細情況見后文;
- uid域:記錄設置鬧鍾的進程的uid;
- pid域:記錄設置鬧鍾的進程的pid。
總體來說還是比較簡單的,我們先補充說明一下其中的count域。這個域是針對重復性鬧鍾的一個輔助域。重復性鬧鍾的實現機理是,如果當前時刻已經超過鬧鍾的激發時刻,那么ALMS會先從邏輯鬧鍾數組中摘取下Alarm節點,並執行鬧鍾對應的邏輯動作,然后進一步比較“當前時刻”和“Alarm理應激發的理想時刻”之間的時間跨度,從而計算出Alarm的“下一次理應激發的理想時刻”,並將這個激發時間記入Alarm節點,接着將該節點重新排入邏輯鬧鍾列表。這一點和普通Alarm不太一樣,普通Alarm節點摘下后就不再還回邏輯鬧鍾列表了。
“當前時刻”和“理應激發時刻”之間的時間跨度會隨實際的運作情況而變動。我們分兩步來說明“下一次理應激發時刻”的計算公式:
1) count = (時間跨度 / repeatInterval ) + 1 ;
2) “下一次理應激發時刻” = “上一次理應激發時刻”+ count * repeatInterval ;
我們畫一張示意圖,其中綠色的可激發時刻表示“上一次理應激發時刻”,我們假定“當前時刻”分別為now_1處或now_2處,可以看到會計算出不同的“下一次理應激發時刻”,這里用桔紅色表示。
可以看到,如果當前時刻為now_1,那么它和“上一次理應激發時刻”之間的“時間跨度”是小於一個repeatInterval的,所以count數為1。而如果當前時刻為now_2,那么“時間跨度”與repeatInterval的商取整后為2,所以count數為3。另外,圖中那兩個虛線箭頭對應的可激發時刻,只是用來做刻度的東西。
3.2 主要行為
接下來我們來看ALMS中的主要行為,這些行為和AlarmManager輔助類提供的成員函數相對應。
3.2.1 設置alarm
外界能接觸的設置alarm的函數是set():publicvoid set(int type,long triggerAtTime, PendingIntent operation)
type:表示要設置的alarm類型。如前文所述,有4個alarm類型。triggerAtTime:表示alarm“理應激發”的時間。
operation:指明了alarm鬧鈴激發時需要執行的動作,比如執行某種廣播通告。
設置alarm的動作會牽扯到一個發起者。簡單地說,發起者會向Alarm Manager Service發出一個設置alarm的請求,而且在請求里注明了到時間后需要執行的動作。由於“待執行的動作”一般都不會馬上執行,所以要表達成PendingIntent的形式。(PendingIntent的詳情可參考其他文章)
另外,triggerAtTime參數的意義也會隨type參數的不同而不同。簡單地說,如果type是和RTC相關的話,那么triggerAtTime的值應該是標准時間,即從1970 年 1 月 1 日午夜開始所經過的毫秒數。而如果type是其他類型的話,那么triggerAtTime的值應該是從本次開機開始算起的毫秒數。
3.2.2 重復性alarm
另一個設置alarm的函數是setRepeating():
- public void setRepeating(int type, long triggerAtTime, long interval,PendingIntent operation)
其參數基本上和set()函數差不多,只是多了一個“時間間隔”參數。事實上,在Alarm Manager Service一側,set()函數內部也是在調用setRepeating()的,只不過會把interval設成了0。
setRepeating()的實現函數如下:
- public void setRepeating(int type, long triggerAtTime, long interval,
- PendingIntent operation)
- {
- if (operation == null) {
- Slog.w(TAG, "set/setRepeating ignored because there is no intent");
- return;
- }
- synchronized (mLock) {
- Alarm alarm = new Alarm();
- alarm.type = type;
- alarm.when = triggerAtTime;
- alarm.repeatInterval = interval;
- alarm.operation = operation;
- // Remove this alarm if already scheduled.
- removeLocked(operation);
- if (localLOGV) Slog.v(TAG, "set: " + alarm);
- int index = addAlarmLocked(alarm);
- if (index == 0) {
- setLocked(alarm);
- }
- }
- }
代碼很簡單,會創建一個邏輯鬧鍾Alarm,而后調用addAlarmLocked()將邏輯鬧鍾添加到內部邏輯鬧鍾數組的某個合適位置。
- private int addAlarmLocked(Alarm alarm) {
- ArrayList<Alarm> alarmList = getAlarmList(alarm.type);
- int index = Collections.binarySearch(alarmList, alarm, mIncreasingTimeOrder);
- if (index < 0) {
- index = 0 - index - 1;
- }
- if (localLOGV) Slog.v(TAG, "Adding alarm " + alarm + " at " + index);
- alarmList.add(index, alarm);
- . . . . . .
- return index;
- }
邏輯鬧鍾列表是依據alarm的激發時間進行排序的,越早激發的alarm,越靠近第0位。所以,addAlarmLocked()在添加新邏輯鬧鍾時,需要先用二分查找法快速找到列表中合適的位置,然后再把Alarm對象插入此處。
如果所插入的位置正好是第0位,就說明此時新插入的這個邏輯鬧鍾將會是本類alarm中最先被激發的alarm,而正如我們前文所述,每一類邏輯鬧鍾會對應同一個“實體鬧鍾”,此處我們在第0位設置了新的激發時間,明確表示我們以前對“實體鬧鍾”設置的激發時間已經不准確了,所以setRepeating()中必須重新調整一下“實體鬧鍾”的激發時間,於是有了下面的句子:
- if (index == 0) {
- setLocked(alarm);
- }
setLocked()內部會調用native函數set():
- private native void set(int fd, int type, long seconds, long nanoseconds);
重新設置“實體鬧鍾”的激發時間。這個函數內部會調用ioctl()和底層打交道。具體代碼可參考frameworks/base/services/jni/com_android_server_AlarmManagerService.cpp文件:
- static void android_server_AlarmManagerService_set(JNIEnv* env, jobject obj, jint fd,
- jint type, jlong seconds, jlong nanoseconds)
- {
- struct timespec ts;
- ts.tv_sec = seconds;
- ts.tv_nsec = nanoseconds;
- int result = ioctl(fd, ANDROID_ALARM_SET(type), &ts);
- if (result < 0)
- {
- ALOGE("Unable to set alarm to %lld.%09lld: %s\n", seconds, nanoseconds, strerror(errno));
- }
- }
我們知道,PendingIntent只是frameworks一層的概念,和底層驅動是沒有關系的。所以向底層設置alarm時只需要type信息以及激發時間信息就可以了。
在AlarmManagerService中真正設置alarm的函數是setImplLocked函數,在這個函數中把alarm添加到mAlarmBatchs中,mAlarmBatchs會把觸發時間相近的Alarm放在同一個bach中,然后每個bach根據時間排序放在mAlarmBatchs中,前面的就是先要觸發的alarm。
- private void setImplLocked(int type, long when, long whenElapsed, long maxWhen, long interval,
- PendingIntent operation, boolean isStandalone, boolean doValidate,
- WorkSource workSource) {
- /**創建一個alarm,其中各參數的含義如下:
- * type 鬧鍾類型 ELAPSED_REALTIME、RTC、RTC_WAKEUP等
- * when 觸發時間 UTC類型,絕對時間,通過System.currentTimeMillis()得到
- * whenElapsed 相對觸發時間,自開機算起,含休眠,通過SystemClock.elapsedRealtime()得到
- * maxWhen 最大觸發時間
- * interval 觸發間隔,針對循環鬧鍾有效
- * operation 鬧鍾觸發時的行為,PendingIntent類型
- */
- Alarm a = new Alarm(type, when, whenElapsed, maxWhen, interval, operation, workSource);
- //根據PendingIntent刪除之前已有的同一個鬧鍾
- removeLocked(operation);
- boolean reschedule;
- //嘗試將alarm加入到合適的batch中,如果alarm是獨立的或者無法找到合適的batch去容納此alarm,返回-1
- int whichBatch = (isStandalone) ? -1 : attemptCoalesceLocked(whenElapsed, maxWhen);
- if (whichBatch < 0) {
- //沒有合適的batch去容納alarm,則新建一個batch
- Batch batch = new Batch(a);
- batch.standalone = isStandalone;
- //將batch加入mAlarmBatches中,並對mAlarmBatches進行排序:按開始時間升序排列
- reschedule = addBatchLocked(mAlarmBatches, batch);
- } else {
- //如果找到合適了batch去容納此alarm,則將其加入到batch中
- Batch batch = mAlarmBatches.get(whichBatch);
- //如果當前alarm的加入引起了batch開始時間和結束時間的改變,則reschedule為true
- reschedule = batch.add(a);
- if (reschedule) {
- //由於batch的起始時間發生了改變,所以需要從列表中刪除此batch並重新加入、重新對batch列表進行排序
- mAlarmBatches.remove(whichBatch);
- addBatchLocked(mAlarmBatches, batch);
- }
- }
- if (DEBUG_VALIDATE) {
- if (doValidate && !validateConsistencyLocked()) {
- Slog.v(TAG, "Tipping-point operation: type=" + type + " when=" + when
- + " when(hex)=" + Long.toHexString(when)
- + " whenElapsed=" + whenElapsed + " maxWhen=" + maxWhen
- + " interval=" + interval + " op=" + operation
- + " standalone=" + isStandalone);
- rebatchAllAlarmsLocked(false);
- reschedule = true;
- }
- }
- if (reschedule) {
- rescheduleKernelAlarmsLocked();
- }
- }
- rescheduleKernelAlarmsLocked函數主要用來選取alarm的觸發時間設置到RTC中去。
- private void rescheduleKernelAlarmsLocked() {
- // Schedule the next upcoming wakeup alarm. If there is a deliverable batch
- // prior to that which contains no wakeups, we schedule that as well.
- if (mAlarmBatches.size() > 0) {
- //查找第一個有wakeup類型alarm的batch
- final Batch firstWakeup = findFirstWakeupBatchLocked();
- //查找第一個batch
- final Batch firstBatch = mAlarmBatches.get(0);
- 判斷條件是為了防止重復設置
- if (firstWakeup != null && mNextWakeup != firstWakeup.start) {
- //將第一個有wakeup類型alarm的batch的時間設置到rtc中
- mNextWakeup = firstWakeup.start;
- setLocked(ELAPSED_REALTIME_WAKEUP, firstWakeup.start);
- }
- if (firstBatch != firstWakeup && mNextNonWakeup != firstBatch.start) {
- mNextNonWakeup = firstBatch.start;
- setLocked(ELAPSED_REALTIME, firstBatch.start);
- }
- }
- }
3.2.3 取消alarm
用戶端是調用AlarmManager對象的cancel()函數來取消alarm的。這個函數內部其實是調用IAlarmManager的remove()函數。所以我們只來看AlarmManagerService的remove()就可以了。
- public void remove(PendingIntent operation)
- {
- if (operation == null) {
- return;
- }
- synchronized (mLock) {
- removeLocked(operation);
- }
- }
注意,在取消alarm時,是以一個PendingIntent對象作為參數的。這個PendingIntent對象正是當初設置alarm時,所傳入的那個operation參數。我們不能隨便創建一個新的PendingIntent對象來調用remove()函數,否則remove()是不會起作用的。PendingIntent的運作細節不在本文論述范圍之內,此處我們只需粗淺地知道,PendingIntent對象在AMS(Activity Manager Service)端會對應一個PendingIntentRecord實體,而ALMS在遍歷邏輯鬧鍾列表時,是根據是否指代相同PendingIntentRecord實體來判斷PendingIntent的相符情況的。如果我們隨便創建一個PendingIntent對象並傳入remove()函數的話,那么在ALMS端勢必找不到相符的PendingIntent對象,所以remove()必然無效。
remove()中調用的removeLocked()如下:
- public void removeLocked(PendingIntent operation)
- {
- removeLocked(mRtcWakeupAlarms, operation);
- removeLocked(mRtcAlarms, operation);
- removeLocked(mElapsedRealtimeWakeupAlarms, operation);
- removeLocked(mElapsedRealtimeAlarms, operation);
- }
簡單地說就是,把4個邏輯鬧鍾數組都遍歷一遍,刪除其中所有和operation相符的Alarm節點。removeLocked()的實現代碼如下:
- private void removeLocked(ArrayList<Alarm> alarmList,
- PendingIntent operation)
- {
- if (alarmList.size() <= 0) {
- return;
- }
- // iterator over the list removing any it where the intent match
- Iterator<Alarm> it = alarmList.iterator();
- while (it.hasNext()) {
- Alarm alarm = it.next();
- if (alarm.operation.equals(operation)) {
- it.remove();
- }
- }
- }
請注意,所謂的取消alarm,只是刪除了對應的邏輯Alarm節點而已,並不會和底層驅動再打什么交道。也就是說,是不存在針對底層“實體鬧鍾”的刪除動作的。所以,底層“實體鬧鍾”在到時之時,還是會被“激發”出來的,只不過此時在frameworks層,會因為找不到符合要求的“邏輯鬧鍾”而不做進一步的激發動作。
3.2.4 設置系統時間和時區
AlarmManager還提供設置系統時間的功能,設置者需要具有android.permission.SET_TIME權限。
- public void setTime(long millis)
- {
- mContext.enforceCallingOrSelfPermission("android.permission.SET_TIME", "setTime");
- SystemClock.setCurrentTimeMillis(millis);
- }
另外,還具有設置時區的功能:
- public void setTimeZone(String tz)
相應地,設置者需要具有android.permission.SET_TIME_ZONE權限。
3.3 運作細節
3.3.1 AlarmThread和Alarm的激發
AlarmManagerService內部是如何感知底層激發alarm的呢?首先,AlarmManagerService有一個表示線程的mWaitThread成員:
- private final AlarmThread mWaitThread = new AlarmThread();
在AlarmManagerService構造之初,就會啟動這個專門的“等待線程”。
- public AlarmManagerService(Context context)
- {
- mContext = context;
- mDescriptor = init();
- . . . . . .
- . . . . . .
- if (mDescriptor != -1)
- {
- mWaitThread.start(); // 啟動線程!
- }
- else
- {
- Slog.w(TAG, "Failed to open alarm driver. Falling back to a handler.");
- }
- }
3.3.1.1 AlarmThread中的run()
AlarmThread本身是AlarmManagerService中一個繼承於Thread的內嵌類:
- private class AlarmThread extends Thread
其最核心的run()函數的主要動作流程圖如下:
我們分別來闡述上圖中的關鍵步驟。
3.3.1.2 waitForAlarm()
首先,從上文的流程圖中可以看到,AlarmThread線程是在一個while(true)循環里不斷調用waitForAlarm()函數來等待底層alarm激發動作的。waitForAlarm()是一個native函數:
- private native int waitForAlarm(int fd);
其對應的C++層函數是android_server_AlarmManagerService_waitForAlarm():
【com_android_server_AlarmManagerService.cpp】
- static jint android_server_AlarmManagerService_waitForAlarm(JNIEnv* env, jobject obj, jint fd)
- {
- int result = 0;
- do
- {
- result = ioctl(fd, ANDROID_ALARM_WAIT);
- } while (result < 0 && errno == EINTR);
- if (result < 0)
- {
- ALOGE("Unable to wait on alarm: %s\n", strerror(errno));
- return 0;
- }
- return result;
- }
3.3.1.3 triggerAlarmsLocked()
一旦等到底層驅動的激發動作,AlarmThread會開始遍歷相應的邏輯鬧鍾列表:
- ArrayList<Alarm> triggerList = new ArrayList<Alarm>();
- . . . . . .
- final long nowRTC = System.currentTimeMillis();
- final long nowELAPSED = SystemClock.elapsedRealtime();
- . . . . . .
- if ((result & RTC_WAKEUP_MASK) != 0)
- triggerAlarmsLocked(mRtcWakeupAlarms, triggerList, nowRTC);
- if ((result & RTC_MASK) != 0)
- triggerAlarmsLocked(mRtcAlarms, triggerList, nowRTC);
- if ((result & ELAPSED_REALTIME_WAKEUP_MASK) != 0)
- triggerAlarmsLocked(mElapsedRealtimeWakeupAlarms, triggerList, nowELAPSED);
- if ((result & ELAPSED_REALTIME_MASK) != 0)
- triggerAlarmsLocked(mElapsedRealtimeAlarms, triggerList, nowELAPSED);
triggerAlarmsLocked函數主要將要發送的alarm降入triggerlist中
- private void triggerAlarmsLocked(ArrayList<Alarm> triggerList, long nowELAPSED, long nowRTC) {
- // batches are temporally sorted, so we need only pull from the
- // start of the list until we either empty it or hit a batch
- // that is not yet deliverable
- while (mAlarmBatches.size() > 0) {
- //獲取第一個batch
- Batch batch = mAlarmBatches.get(0);
- if (batch.start > nowELAPSED) {
- // Everything else is scheduled for the future
- break;
- }
- // We will (re)schedule some alarms now; don't let that interfere
- // with delivery of this current batch
- //將第一個batch去除
- mAlarmBatches.remove(0);
- final int N = batch.size();
- for (int i = 0; i < N; i++) {
- Alarm alarm = batch.get(i);
- alarm.count = 1;
- //遍歷加入triggerList
- triggerList.add(alarm);
- // Recurring alarms may have passed several alarm intervals while the
- // phone was asleep or off, so pass a trigger count when sending them.
- //如果有重復類型的,計算時間重新設置
- if (alarm.repeatInterval > 0) {
- // this adjustment will be zero if we're late by
- // less than one full repeat interval
- alarm.count += (nowELAPSED - alarm.whenElapsed) / alarm.repeatInterval;
- // Also schedule its next recurrence
- final long delta = alarm.count * alarm.repeatInterval;
- final long nextElapsed = alarm.whenElapsed + delta;
- setImplLocked(alarm.type, alarm.when + delta, nextElapsed, alarm.windowLength,
- maxTriggerTime(nowELAPSED, nextElapsed, alarm.repeatInterval),
- alarm.repeatInterval, alarm.operation, batch.standalone, true,
- alarm.workSource);
- }
- }
- }
- }
接下來,只需遍歷一遍triggerList就可以了:
- Iterator<Alarm> it = triggerList.iterator();
- while (it.hasNext())
- {
- Alarm alarm = it.next();
- . . . . . .
- alarm.operation.send(mContext, 0,
- mBackgroundIntent.putExtra(Intent.EXTRA_ALARM_COUNT, alarm.count),
- mResultReceiver, mHandler);
- // we have an active broadcast so stay awake.
- if (mBroadcastRefCount == 0) {
- setWakelockWorkSource(alarm.operation);
- mWakeLock.acquire();
- }
- mInFlight.add(alarm.operation);
- mBroadcastRefCount++;
- mTriggeredUids.add(new Integer(alarm.uid));
- BroadcastStats bs = getStatsLocked(alarm.operation);
- if (bs.nesting == 0) {
- bs.startTime = nowELAPSED;
- } else {
- bs.nesting++;
- }
- if (alarm.type == AlarmManager.ELAPSED_REALTIME_WAKEUP
- || alarm.type == AlarmManager.RTC_WAKEUP) {
- bs.numWakeup++;
- ActivityManagerNative.noteWakeupAlarm(alarm.operation);
- }
- }
PendingIntent的send()函數代碼是:
- public void send(Context context, int code, Intent intent,
- OnFinished onFinished, Handler handler) throws CanceledException
- {
- send(context, code, intent, onFinished, handler, null);
- }
調用了下面的send()函數:
- public void send(Context context, int code, Intent intent,
- OnFinished onFinished, Handler handler, String requiredPermission)
- throws CanceledException
- {
- try
- {
- String resolvedType = intent != null
- ? intent.resolveTypeIfNeeded(context.getContentResolver())
- : null;
- int res = mTarget.send(code, intent, resolvedType,
- onFinished != null
- ? new FinishedDispatcher(this, onFinished, handler)
- : null,
- requiredPermission);
- if (res < 0)
- {
- throw new CanceledException();
- }
- }
- catch (RemoteException e)
- {
- throw new CanceledException(e);
- }
- }
mTarget是個IPendingIntent代理接口,它對應AMS(Activity Manager Service)中的某個PendingIntentRecord實體。需要說明的是,PendingIntent的重要信息都是在AMS的PendingIntentRecord以及PendingIntentRecord.Key對象中管理的。AMS中有一張哈希表專門用於記錄所有可用的PendingIntentRecord對象。
相較起來,在創建PendingIntent對象時傳入的intent數組,其重要性並不太明顯。這種intent數組主要用於一次性啟動多個activity,如果你只是希望啟動一個activity或一個service,那么這個intent的內容有可能在最終執行PendingIntent的send()動作時,被新傳入的intent內容替換掉。
AMS中關於PendingIntentRecord哈希表的示意圖如下:
AMS是整個Android平台中最復雜的一個核心service了,所以我們不在這里做過多的闡述,有興趣的讀者可以參考其他相關文檔。
3.3.1.4 進一步處理“喚醒鬧鍾”
在AlarmThread.run()函數中while循環的最后,會進一步判斷,當前激發的alarm是不是“喚醒鬧鍾”。如果鬧鍾類型為RTC_WAKEUP或ELAPSED_REALTIME_WAKEUP,那它就屬於“喚醒鬧鍾”,此時需要通知一下AMS:
- if (alarm.type == AlarmManager.ELAPSED_REALTIME_WAKEUP
- || alarm.type == AlarmManager.RTC_WAKEUP)
- {
- bs.numWakeup++;
- ActivityManagerNative.noteWakeupAlarm(alarm.operation);
- }
這兩種alarm就是我們常說的0型和2型鬧鍾,它們和我們手機的續航時間息息相關。
AMS里的noteWakeupAlarm()比較簡單,只是在調用BatteryStatsService服務的相關動作,但是卻會導致機器的喚醒:
- public void noteWakeupAlarm(IIntentSender sender)
- {
- if (!(sender instanceof PendingIntentRecord))
- {
- return;
- }
- BatteryStatsImpl stats = mBatteryStatsService.getActiveStatistics();
- synchronized (stats)
- {
- if (mBatteryStatsService.isOnBattery())
- {
- mBatteryStatsService.enforceCallingPermission();
- PendingIntentRecord rec = (PendingIntentRecord)sender;
- int MY_UID = Binder.getCallingUid();
- int uid = rec.uid == MY_UID ? Process.SYSTEM_UID : rec.uid;
- BatteryStatsImpl.Uid.Pkg pkg = stats.getPackageStatsLocked(uid, rec.key.packageName);
- pkg.incWakeupsLocked();
- }
- }
- }
好了,說了這么多,我們還是畫一張AlarmThread示意圖作為總結:
3.3.2 說說AlarmManagerService中的mBroadcastRefCount
下面我們說說AlarmManagerService中的mBroadcastRefCount,之所以要說它,僅僅是因為我在修改AlarmManagerService代碼的時候,吃過它的虧。
我們先回顧一下處理triggerList列表的代碼,如下:
- Iterator<Alarm> it = triggerList.iterator();
- while (it.hasNext())
- {
- Alarm alarm = it.next();
- . . . . . .
- alarm.operation.send(mContext, 0,
- mBackgroundIntent.putExtra(Intent.EXTRA_ALARM_COUNT, alarm.count),
- mResultReceiver, mHandler);
- // we have an active broadcast so stay awake.
- if (mBroadcastRefCount == 0) {
- setWakelockWorkSource(alarm.operation);
- mWakeLock.acquire();
- }
- mInFlight.add(alarm.operation);
- mBroadcastRefCount++;
- . . . . . .
- . . . . . .
- }
可以看到,在AlarmThread.run()中,只要triggerList中含有可激發的alarm,mBroadcastRefCount就會執行加一操作。一開始mBroadcastRefCount的值為0,所以會進入上面那句if語句,進而調用mWakeLock.acquire()。
后來我才知道,這個mBroadcastRefCount變量,是決定何時釋放mWakeLock的計數器。AlarmThread的意思很明確,只要還有處於激發狀態的邏輯鬧鍾,機器就不能完全睡眠。那么釋放這個mWakeLock的地方又在哪里呢?答案就在alarm.operation.send()一句的mResultReceiver參數中。
mResultReceiver是AlarmManagerService的私有成員變量:
- private final ResultReceiver mResultReceiver = newResultReceiver();
類型為ResultReceiver,這個類實現了PendingIntent.OnFinished接口:
- class ResultReceiver implements PendingIntent.OnFinished
當send()動作完成后,框架會間接回調這個對象的onSendFinished()成員函數。
- public void onSendFinished(PendingIntent pi, Intent intent, int resultCode,
- String resultData, Bundle resultExtras)
- {
- . . . . . .
- . . . . . .
- if (mBlockedUids.contains(new Integer(uid)))
- {
- mBlockedUids.remove(new Integer(uid));
- }
- else
- {
- if (mBroadcastRefCount > 0)
- {
- mInFlight.removeFirst();
- mBroadcastRefCount--;
- if (mBroadcastRefCount == 0)
- {
- mWakeLock.release();
- }
- . . . . . .
- }
- . . . . . .
- }
- . . . . . .
- }
我一開始沒有足夠重視這個mBroadcastRefCount,所以把alarm.operation.send()語句包在了一條if語句中,也就是說在某種情況下,程序會跳過alarm.operation.send()一句,直接執行下面的語句。然而此時的mBroadcastRefCount還在堅定不移地加一,這直接導致mBroadcastRefCount再也減不到0了,於是mWakeLock也永遠不會釋放了。令人頭痛的是,這個mWakeLock雖然不讓手機深睡眠下去,卻也不會點亮屏幕,所以這個bug潛藏了好久才被找到。還真是應了我說的那句話:“魔鬼總藏在細節中。”
也許一些使用alarmmanager做定時任務的同學遇到過這樣的問題:設定alarm后,進入設置-->應用程序管理-->強行停止app后,定時任務就失效了。
簡單的講就是:force stop會導致alarm失效。
最典型的例子就是我碰到過的一個bug,使用android手機的時鍾app設置一個鬧鍾,然后進入設置-->應用程序管理里面,將時鍾這個app force stop掉,結果鬧鍾就不響了。
其實這不是bug,這是android系統的新加入的機制。下面我來詳細分析一下來龍去脈。
1. 在設置的應用程序管理里面強行停止app:
這里會最終會調用到 ActivityManagerService的forceStopPackageLocked()
- private void forceStopPackageLocked(final String packageName, int uid) {
- forceStopPackageLocked(packageName, uid, false, false, true, false);
- Intent intent = new Intent(Intent.ACTION_PACKAGE_RESTARTED,
- Uri.fromParts("package", packageName, null));
- if (!mProcessesReady) {
- intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
- }
- intent.putExtra(Intent.EXTRA_UID, uid);
- broadcastIntentLocked(null, null, intent,
- null, null, 0, null, null, null,
- false, false, MY_PID, Process.SYSTEM_UID);
- }
代碼里面發送了一個廣播:ACTION_PACKAGE_RESTARTED,這個廣播大有文章。
最后來看UninstallReceiver,當AlarmManagerService接受到這個廣播后,會把其那些alarm的包名傳過來的給刪除了。
- class UninstallReceiver extends BroadcastReceiver {
- public UninstallReceiver() {
- IntentFilter filter = new IntentFilter();
- filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
- filter.addAction(Intent.ACTION_PACKAGE_RESTARTED);
- filter.addAction(Intent.ACTION_QUERY_PACKAGE_RESTART);
- filter.addDataScheme("package");
- mContext.registerReceiver(this, filter);
- // Register for events related to sdcard installation.
- IntentFilter sdFilter = new IntentFilter();
- sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE);
- sdFilter.addAction(Intent.ACTION_USER_STOPPED);
- mContext.registerReceiver(this, sdFilter);
- }
- @Override
- public void onReceive(Context context, Intent intent) {
- synchronized (mLock) {
- String action = intent.getAction();
- String pkgList[] = null;
- if (Intent.ACTION_QUERY_PACKAGE_RESTART.equals(action)) {
- pkgList = intent.getStringArrayExtra(Intent.EXTRA_PACKAGES);
- for (String packageName : pkgList) {
- if (lookForPackageLocked(packageName)) {
- setResultCode(Activity.RESULT_OK);
- return;
- }
- }
- return;
- } else if (Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE.equals(action)) {
- pkgList = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST);
- } else if (Intent.ACTION_USER_STOPPED.equals(action)) {
- int userHandle = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1);
- if (userHandle >= 0) {
- removeUserLocked(userHandle);
- }
- } else {
- if (Intent.ACTION_PACKAGE_REMOVED.equals(action)
- && intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) {
- // This package is being updated; don't kill its alarms.
- return;
- }
- Uri data = intent.getData();
- if (data != null) {
- String pkg = data.getSchemeSpecificPart();
- if (pkg != null) {
- pkgList = new String[]{pkg};
- }
- }
- }
- if (pkgList != null && (pkgList.length > 0)) {
- for (String pkg : pkgList) {
- //將這個pkg的alarm從AlarmManagerService中去除
- removeLocked(pkg);
- mBroadcastStats.remove(pkg);
- }
- }
- }
- }
- }
為什么google要加入這樣的機制呢?
應該是出於系統安全的考慮,google在4.0系統中在安全方面做了很多努力。
很多病毒程序都不希望自己的進程被用戶強行停止,希望自己的病毒程序可以一直運行,而常見的方式就是通過設置alarm,在病毒進程被殺死后,通過定時發送廣播來拉起病毒進程,來實現病毒進程的重新啟動。
google也正是看到了這個一點,所以加入了forceStopPackage的這一機制,讓用戶能夠有機會干掉病毒進程。
android系統的安全性一直是android系統的短板,google在提升系統安全性方面也在不斷努力,在之后的文章中,我會再進行介紹。