Android 4.0 Alarm機制淺析
Author: [email protected]
最近在做關於Alarm的一些東西,所以就把Android平臺上的alarm的源代碼給稍微看了看。我個人其實基本不寫文檔的,而且即使寫,也不過區區數字,這個應該是我工作4年來的第二篇文檔(第一篇是寫的和我一直以來工作相關的Messaging)所以內容上和排版上大家就不要見怪。
Android系統中alarm機制最大的體現着就是鬧鐘這個app了。通過這個應用我們可以設置自己的各種定時鬧鐘,當然系統中的各種定時相關功能的實現也基本全部依賴Alarm機制。
鬧鐘的代碼在packages\apps\DeskClock\src\com\android\deskclock目錄下,可以自行查看,這裏主要說的是Alarm機制。
Alarm機制實現的代碼主要在
./frameworks/base/core/java/android/app/AlarmManager.java
./frameworks/base/services/java/com/android/server/AlarmManagerService.java
./frameworks/base/services/jni/com_android_server_AlarmManagerService.cpp
再往下就是驅動和kernel的代碼,個人對驅動和kernel的代碼不瞭解,就不說了。
AlarmManager是framework中提供給用戶來使用的API,其實現在AlarmManagerService,爲一個server,通過binder機制來提供服務,開機便註冊到system_server進程中(所有server實現基本都如此)代碼如下(systemserver.java)
alarm = new AlarmManagerService(context);
ServiceManager.addService(Context.ALARM_SERVICE, alarm);
下面就來介紹一下AlarmManagerService,本來想用ams代替,不過一般情況下ams指的是ActivityManagerService,所以也就罷了。
AlarmManagerService的初始化:
1. mDescriptor = init(); 打開設備驅動,其jni實現爲(com_android_server_AlarmManagerService.cpp)
static jint android_server_AlarmManagerService_init(JNIEnv* env, jobject obj)
{
return open("/dev/alarm", O_RDWR);
}
2. 設置時區
String tz = SystemProperties.get(TIMEZONE_PROPERTY);
if (tz != null) {
setTimeZone(tz);
}
3. mTimeTickSender 這個pendingintent的作用應該是系統中經常用到的,它是用來給發送一個時間改變的broadcast,Intent.ACTION_TIME_TICK,每整數分鐘的開始發送一次,就是每分鐘的開始就發送,應用可以註冊對應的receiver來幹各種事,譬如更新時間顯示等等,具體怎麼觸發的稍後再說。
mDateChangeSender 這個pendingintent的作用是啥?代碼中時這樣寫的Intent.ACTION_DATE_CHANGED,其實和mTimeTickSender 差不多,只是它是每天的開始發送一次,應該就是00:00:00發送吧
這2個pendingintent 和ClockReceiver有莫大的關係,ClockReceiver的構造函數如下
public ClockReceiver() {
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_TIME_TICK);
filter.addAction(Intent.ACTION_DATE_CHANGED);
mContext.registerReceiver(this, filter);
}
同時alarmmanagerservice中還有如下代碼
mClockReceiver.scheduleTimeTickEvent();
mClockReceiver.scheduleDateChangedEvent();
深入scheduleTimeTickEvent 和scheduleDateChangedEvent你就可以知道上面2個pendingintent的作用了
同時ClockReceiver也能收到這2個intent,說明時間變了,立刻set下一次alarm,以便系統不停的發送該消息。
4. mUninstallReceiver 這個還真暫時太不清楚它的作用。貌似和應用的安裝和卸載有比較大的關係。
5. registerReceiver(我就不說這個receiver名字起得太2了),看他的intent filter
mIntentFilter.addAction(UNREGISTER_POWEROFF_CLOCK);
mIntentFilter.addAction(REGISTER_POWEROFF_CLOCK);
我們可以得知,這個是給應用來註冊POWEROFF_CLOCK的,也就是關機鬧鐘啦,這個貌似是4.0新加的功能,不知道是qualcomm實現的還是google新添加的代碼。
它允許用戶註冊關機鬧鐘的權限,在關機情況下,當時間到了以後(可能會提前2分鐘 什麼的),系統會先開機,然後執行你註冊的pendingintent。貌似你需要在 Intent 中設置extra,extra(POWEROFF_CLOCK_INTENT_EXTRA)內容爲package的名字。
如果你的應用沒有通過REGISTER_POWEROFF_CLOCK去註冊這個權限的話,那麼應用是不會在關機時候去開機執行的。這裏的註冊只是類似於權限的註冊,鬧鐘設置還是需要調用set去實現。
問題:如果關機後開機,那先前設置的鬧鐘或者先前設置的alarm(不是指鬧鐘這個應用,是指定時任務)你認爲還有效麼?why?
6. mWaitThread.start() (AlarmThread)
這個應該是AlarmManagerService中最重要的部分了,整個server的執行線程,跑在一個while死循環裏面。具體實現了哪些功能以及怎麼實現的,後面具體講到
至此,初始化完畢了。下次就看看AlarmThread 這個用來支撐Alarm機制實現的線程,來看看它是怎麼運作的。
AlarmThread的具體流程:
1. 首先,int result = waitForAlarm(mDescriptor); 這個我個人理解其作用就是等待一個底層RTC鬧鐘的觸發,這個過程應該是同步阻塞的。
如果result & TIME_CHANGED_MASK,那麼首先remove(mTimeTickSender);然後重新設置mClockReceiver.scheduleTimeTickEvent();就是重新設置scheduleTimeTickEvent這個pendingintent了;然後給系統發送一個ACTION_TIME_CHANGED的broadcast,當然接受者是有Intent.FLAG_RECEIVER_REPLACE_PENDING 和Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT的限制。
2. 針對各種情況的MASK RTC_WAKEUP_MASK| RTC_MASK| ELAPSED_REALTIME_WAKEUP_MASK| ELAPSED_REALTIME_MASK,對此進行
triggerAlarmsLocked(ArrayList<Alarm> alarmList, ArrayList<Alarm> triggerList,
long now)
3. 我們就來分析下triggerAlarmsLocked 這個函數的作用。
AlarmManager中定義了4種類型的alarm :RTC_WAKEUP |RTC ELAPSED_REALTIME_WAKEUP | ELAPSED_REALTIME,所以在service中定義了4個ArrayList<Alarm> 來對應4中類型的alarmmRtcWakeupAlarms|mRtcAlarms;mElapsedRealtimeWakeupAlarms|mElapsedRealtimeAlarms;當應用調用AlarmManager.set接口去設置alarm,隨後就會調用到service中的addAlarmLocked,其根據alarm類型將其add到對應的ArrayList中去。
首先來判斷alarm是否到期了,如果還沒有到期,直接跳出整個while循環(大家注意這裏alarmList是個arraylist,同時其在add的時候對其進行了按照alarm.when從小到大來排序,所以如果alarm.whe>now,那麼後面的alarm.when必定> now);
if (alarm.when > now) {
// don't fire alarms in the future
break;
}
在while裏面,我們逐一的找出alarm.when <=now的alarm,將其add到triggerList(triggerList.add(alarm);)中傳出去,以便後用;同時將其從alarmList中remove掉。這裏面還有一個alarm.count,看說明大家就知道其中的意思了: (this adjustment will be zero if we're late by,less than one full repeat interval),就是說鬧鐘過期了多少個間隔時間段,計算方法:時間差/間隔 + 1
alarm.count += (now - alarm.when) / alarm.repeatInterval 如果是重複的alarm,那麼將其保存在repeats這個arraylist中。
// if it repeats queue it up to be read-added to the list
if (alarm.repeatInterval > 0) {
repeats.add(alarm);
遍歷完了alarmList之後,做2件事:
①:把repeats 這個arraylist中的重複響的鬧鐘計算出新的alarm,將其add到alarm對應的arraylist中去。
②:setLocked,將alarmList中最近的一個鬧鐘(其按照從小到大排列,故第一個就是最小的)set到系統中去。
好了,這個函數的作用分析完了。簡而言之,這個函數作用就是找出對應alarmlist中到期的alarm,將其取出來;同時將重複的alarm計算出新的alarm添加到對應的list中,然後set最近時間的alarm。
4. 執行完triggerAlarmsLocked後,我們得到了需要進行操作的triggerList,逐一取出,隨後就開始了最爲重要的一部分
alarm.operation.send(mContext, 0,
mBackgroundIntent.putExtra(
Intent.EXTRA_ALARM_COUNT, alarm.count),
mResultReceiver, mHandler);
將pendingintent發送出去,這樣,先前註冊alarm到期時所期望做的的操作這個時候就開始執行了。
AlarmThread的執行流程就是這樣,它一直重複的等待着底層alarm到期,然後從列表中取出到期的alarm,逐一對pendingintent進行send操作,直到系統掛了爲止。
AlarmManagerService中重要的操作:
1. set/setRepeating:設置(重複的)alarm
外部如果需要設置一個alarm來進行某些操作,一般流程都是
manager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
manager.set(AlarmManager.RTC_WAKEUP, time, pi);
一般都會用AlarmManager這個代理類來進行對應的操作,其實質會調用到AlarmManagerService中的set/setRepeating函數,當然set其實也是調用setRepeating,所以我們就來看看setRepeating這個函數
setRepeating(int type, long triggerAtTime, long interval, PendingIntent operation),這個函數有4個參數,其中大家要注意的是triggerAtTime和type要對應起來,我們用RTC_WAKEUP和RTC的時候,就要對應系統的絕對時間(System.currentTimeMillis()得到);而用ELAPSED_REALTIME_WAKEUP和ELAPSED_REALTIME的時候,就要對應系統的相對時間(相對開機流逝了多少時間通過SystemClock.elapsedRealtime()得到),interval來表示間隔interval時間間隔重複該alarm,operation就是alarm到期後你要進行的操作。
①:創建一個Alarm來組織傳進來的數據,隨後進行的這一步大家要稍微注意:
// Remove this alarm if already scheduled.
removeLocked(operation);
該函數會從type對應的alarm arraylist中remove掉此operation(pendingintent)對應的alarm,其中匹配規則爲alarm.operation.equals(operation),也就是pendingintent的equal函數。所以這個大家可能要注意,因爲如果你在一個app中不同的地方new pendingintent的時候的參數都一樣的話,那麼new出來的pendingintent通過pendingintent.equal來比較的話是相等的,也就是說你這個時候不能通過一般途徑來設置2個alarm,因爲這個時候會把前一個alarm移除掉,具體可以參見pendingintent.equal()以及pendingintent.mTarget怎麼創建的,其實就是比較mTarget是否相等,如果你要設置2個alarm的話,就要用不同的requestcode或者intent。
②:執行完remove後,開始執行
index = addAlarmLocked(alarm);
此函數的作用首先通過Collections.binarySearch(alarmList, alarm, mIncreasingTimeOrder)將此alarm定位出其在alarmlist(此arraylist爲按照alarm.when從小到大排列)的位置index,然後將該alarm添加到對應的alarm arraylist中去 :alarmList.add(index, alarm);同時返回該index(後面還要用)。
③:接下來,會去判斷調用set的app是否爲系統自帶的鬧鐘或者是在系統中註冊了power_off_clock權限的app(alarmmanagerservice初始化中講到過),如果是,並且alarm.type == AlarmManager.RTC_WAKEUP,那麼newType = RTC_DEVICEUP。
接着看:if (index == 0 || newType == RTC_DEVICEUP) { setLocked(alarm); }
也就是說如果該alarm是最進將要觸發的alarm(index =0)或者有power_off_clock權限的alarm,那麼將立刻執行setLocked操作,也就是設置到系統底層RTC時鐘中去。
setLocked函數中同樣也有關於power_off_clock權限的檢測,代碼如下
String callingPackage = mContext.getPackageManager().getNameForUid(Binder.getCallingUid());
if (SELF_CLOCK.equals(callingPackage) && alarm.type == AlarmManager.RTC_WAKEUP) {
type = RTC_DEVICEUP;
}
SharedPreferences mSharedPref = mContext.getSharedPreferences("power_off_clock", 0);
if (mSharedPref.contains(callingPackage) && alarm.type == AlarmManager.RTC_WAKEUP) {
type = RTC_DEVICEUP;
}
隨後就會通過jni調用底層的set函數,這裏不多說。
2. remove:取消alarm
取消一個alarm的流程那就太simple了,直接removeLocked(operation),這個過程在和set中remove相同。通過pendingintent.equal來確定2個alarm是否相等。
3. setTime:設置系統時間
該功能需要android.permission.SET_TIME,需要注意。
4. setTimeZone:設置系統時區
該功能需要android.permission. SET_TIME_ZONE,需要注意。
疑問:
1.關機alarm問題
在初始化分析中,提出了這樣一個問題:如果關機後開機,那先前設置的鬧鐘或者先前設置的alarm(不是指鬧鐘這個應用,是指定時任務)你認爲還有效麼?why?
有人覺得有效,因爲鬧鐘不就是個很好的例子麼?也有人說無效,因爲自己set一個alarm,重啓到時間後對應的pendingintent並沒有執行。那麼到底是有效還是無效呢?在這裏,我很負責任(個人責任)的告訴大家,是無效的。
Why?
其實我們set alarm最終調用的是jni層的set函數,看看這個函數的參數 set(int fd, int type, long seconds, long nanoseconds);我們很容易發現並沒有將pendingintent保存起來,pendingintent只是保存在arraylist中Alarm結構體中,同時重啓後,先前arraylist的數據並沒有保存到固化空間上,全部都丟失掉了,所以重啓後剛開始的arraylist是空的。那麼你以前設置的alarm顯然就無效啦。
那爲什麼系統的鬧鐘有效呢?帶着這個問題我問了一些同事,但是沒有什麼發現。最後我在鬧鐘的app中發現了這個Receiver:AlarmInitReceiver,
看到這裏,你明白了吧?鬧鐘這個app接收了開機完成事件,然後將其保存在數據庫中的alarm數據重新set到AlarmManagerService(具體看AlarmInitReceiver.java),所以鬧鐘關機重啓還有效。
如果你的程序需要設置定時任務,並且不管系統是否關機重啓等,那麼你就可以仿照鬧鐘註冊開機完成(BOOT_CVOMPLETED)事件,重新set alarm(其實我是偶然看到SMP同事寫的定時信息feature想到的這個問題,鑑定重啓後果然不能定時發送)。
2.pendingintent 比較問題
由於在set和remove alarm的時候都會執行removeLocked(operation)的操作,其中涉及到pendingintent的比較問題,pendingintent.equal函數比較2個pendingintent其實只是比較2個pendingintent的mTarget,所以即使2個pendingintent不相等,但是pendingintent .equal確實相等。
PendingIntent p1 = PendingIntent.getBroadcast(this, 0, new Intent("test1"), 1);
PendingIntent p2 = PendingIntent.getBroadcast(this, 0, new Intent("test1"), 1);
此時 p1 != p2,但是p1.equals(p2) == true。
所以大家需要設置不同的alarm的時候,new PendingIntent時候需要注意用不同的參數,requestCode,intent,flags 有一個不同即可。
注意 :如果Flag是FLAG_CANCEL_CURRENT的話,那麼不管怎麼樣,p1.equals(p2) == false。
注意影響PendingIntent相等的參數就是這個key:
(這裏的意思是這3個Flag在key中不起作用)
如果Flag中有FLAG_CANCEL_CURRENT的話,那麼將會執行:
以及
所以就會造成不相等的情況。
這裏就有一個很嚴重的問題了啊,既然用了FLAG_CANCEL_CURRENT,那麼爲什麼我先前設置了一個10點鐘的鬧鐘,再去取消的時候,爲什麼能夠取消?不是上面剛說了2個pendingintent不相等嗎?這個問題我斷斷續續看了很長時間,今天給別人講鬧鐘的設置取消和AlarmManagerService的時候才總算是弄明白了。
讓我們來看一下取消鬧鐘的代碼
先去得到一個pendingintent,大家可要注意啊,這個sender和我們設置鬧鐘時候得到的sender不相等啊,並且其中的mTarget對象也不相等啊,真是急死人。都不相等了,你還去am.set,am.cancel有什麼作用?我可以負責任的告訴大家,這2個函數在這裏完全是打醬油的作用,完全可以去掉。爲什麼android的源代碼這樣設計,估計也是爲了我們自己看着這樣可以cancel掉吧。
Set我就不講了,我們來看看cancel,最終調用的是AlarmManagerService的remove函數
OMG,就是想從4個列表中remove掉和傳進去的sender相等的pendingintent對應的alarm,關鍵是這個sender不和其中任何一個相等啊,因爲你用了這個該死的FLAG啊親FLAG_CANCEL_CURRENT,所以我說這2行代碼完全可以去掉。這2行代碼本意是好的,想去remove掉對應的alarm,可以效果你懂的,哎,大家不信可以去跟跟斷點,打打log,或者dumpsys alarm看看alarm的信息即可。
既然這2行代碼沒有去remove掉這個alarm,那爲什麼我咱鬧鐘中取消某個鬧鐘後,起作用了呢。這個得從pendingintent的創建說起了。當我們設置了FLAG_CANCEL_CURRENT之後,然後想去get到一個pendingintent,具體的實現代碼在AMS中的getIntentSenderLocked這個函數,上面已經講了一部分,當我們設置了該FLAG後,將會執行 :
請着重注意這行代碼,因爲這個值在pendingintent觸發的時候起到至關重要的作用,我們看看在AlarmManagerService中的當一個alarm時間到了,便會觸發下面的操作:
也就是pendingintent的send操作,最終會調用到PendingIntentRecord的send操作(具體是sendInner,不要問我爲什麼,不懂的可以去看下binder機制),請大家睜大眼睛啊:
各種操作
看清楚沒有啊親,也就是說你先前設置的pendingintent雖然在AlarmManagerService中,但是走到send的時候其實是什麼操作都沒做啊,所以這個就是爲什麼鬧鐘不響了啊。
OK,AlarmManagerService淺析就差不多這些了。
OVER了,如果你對此有什麼疑問或者見解,那麼可以和我一起討論。此文純個人理解,有錯誤在所難免,所以請大家切勿輕信,歡迎指正([email protected])。