Android 4.0 Alarm機制淺析

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的代碼不瞭解,就不說了。

AlarmManagerframework中提供給用戶來使用的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的作用應該是系統中經常用到的,它是用來給發送一個時間改變的broadcastIntent.ACTION_TIME_TICK,每整數分鐘的開始發送一次,就是每分鐘的開始就發送,應用可以註冊對應的receiver來幹各種事,譬如更新時間顯示等等,具體怎麼觸發的稍後再說。

mDateChangeSender  這個pendingintent的作用是啥?代碼中時這樣寫的Intent.ACTION_DATE_CHANGED,其實和mTimeTickSender   差不多,只是它是每天的開始發送一次,應該就是00:00:00發送吧

2pendingintent 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你就可以知道上面2pendingintent的作用了

同時ClockReceiver也能收到這2intent,說明時間變了,立刻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 中設置extraextraPOWEROFF_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_CHANGEDbroadcast,當然接受者是有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中定義了4ArrayList<Alarm> 來對應4中類型的alarmmRtcWakeupAlarms|mRtcAlarmsmElapsedRealtimeWakeupAlarms|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 <=nowalarm,將其addtriggerListtriggerList.add(alarm);中傳出去,以便後用;同時將其從alarmListremove掉。這裏面還有一個alarm.count,看說明大家就知道其中的意思了:                this adjustment will be zero if we're late byless 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,將其addalarm對應的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個參數,其中大家要注意的是triggerAtTimetype要對應起來,我們用RTC_WAKEUPRTC的時候,就要對應系統的絕對時間(System.currentTimeMillis()得到);而用ELAPSED_REALTIME_WAKEUPELAPSED_REALTIME的時候,就要對應系統的相對時間(相對開機流逝了多少時間通過SystemClock.elapsedRealtime()得到),interval來表示間隔interval時間間隔重複該alarmoperation就是alarm到期後你要進行的操作。

①:創建一個Alarm來組織傳進來的數據,隨後進行的這一步大家要稍微注意:

           // Remove this alarm if already scheduled.

            removeLocked(operation);

該函數會從type對應的alarm arraylistremove掉此operationpendingintent)對應的alarm,其中匹配規則爲alarm.operation.equals(operation),也就是pendingintentequal函數。所以這個大家可能要注意,因爲如果你在一個app中不同的地方new pendingintent的時候的參數都一樣的話,那麼new出來的pendingintent通過pendingintent.equal來比較的話是相等的,也就是說你這個時候不能通過一般途徑來設置2alarm,因爲這個時候會把前一個alarm移除掉,具體可以參見pendingintent.equal()以及pendingintent.mTarget怎麼創建的,其實就是比較mTarget是否相等,如果你要設置2alarm的話,就要用不同的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(後面還要用)。

③:接下來,會去判斷調用setapp是否爲系統自帶的鬧鐘或者是在系統中註冊了power_off_clock權限的appalarmmanagerservice初始化中講到過),如果是,並且alarm.type == AlarmManager.RTC_WAKEUP,那麼newType = RTC_DEVICEUP

接着看:if (index == 0 || newType == RTC_DEVICEUP) {     setLocked(alarm);   }

也就是說如果該alarm是最進將要觸發的alarmindex =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),這個過程在和setremove相同。通過pendingintent.equal來確定2alarm是否相等。

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只是保存在arraylistAlarm結構體中,同時重啓後,先前arraylist的數據並沒有保存到固化空間上,全部都丟失掉了,所以重啓後剛開始的arraylist是空的。那麼你以前設置的alarm顯然就無效啦。

那爲什麼系統的鬧鐘有效呢?帶着這個問題我問了一些同事,但是沒有什麼發現。最後我在鬧鐘的app中發現了這個ReceiverAlarmInitReceiver

看到這裏,你明白了吧?鬧鐘這個app接收了開機完成事件,然後將其保存在數據庫中的alarm數據重新setAlarmManagerService(具體看AlarmInitReceiver.java),所以鬧鐘關機重啓還有效。

如果你的程序需要設置定時任務,並且不管系統是否關機重啓等,那麼你就可以仿照鬧鐘註冊開機完成(BOOT_CVOMPLETED)事件,重新set alarm(其實我是偶然看到SMP同事寫的定時信息feature想到的這個問題,鑑定重啓後果然不能定時發送)。

2.pendingintent 比較問題

由於在setremove alarm的時候都會執行removeLocked(operation)的操作,其中涉及到pendingintent的比較問題,pendingintent.equal函數比較2pendingintent其實只是比較2pendingintentmTarget,所以即使2pendingintent不相等,但是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時候需要注意用不同的參數,requestCodeintentflags 有一個不同即可。

注意 :如果FlagFLAG_CANCEL_CURRENT的話,那麼不管怎麼樣,p1.equals(p2) == false

注意影響PendingIntent相等的參數就是這個key

(這裏的意思是這3Flagkey中不起作用)

如果Flag中有FLAG_CANCEL_CURRENT的話,那麼將會執行:

以及

所以就會造成不相等的情況。

這裏就有一個很嚴重的問題了啊,既然用了FLAG_CANCEL_CURRENT,那麼爲什麼我先前設置了一個10點鐘的鬧鐘,再去取消的時候,爲什麼能夠取消?不是上面剛說了2pendingintent不相等嗎?這個問題我斷斷續續看了很長時間,今天給別人講鬧鐘的設置取消和AlarmManagerService的時候才總算是弄明白了。

讓我們來看一下取消鬧鐘的代碼

先去得到一個pendingintent,大家可要注意啊,這個sender和我們設置鬧鐘時候得到的sender不相等啊,並且其中的mTarget對象也不相等啊,真是急死人。都不相等了,你還去am.set,am.cancel有什麼作用?我可以負責任的告訴大家,這2個函數在這裏完全是打醬油的作用,完全可以去掉。爲什麼android的源代碼這樣設計,估計也是爲了我們自己看着這樣可以cancel掉吧。

Set我就不講了,我們來看看cancel,最終調用的是AlarmManagerServiceremove函數

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時間到了,便會觸發下面的操作:

也就是pendingintentsend操作,最終會調用到PendingIntentRecordsend操作(具體是sendInner,不要問我爲什麼,不懂的可以去看下binder機制),請大家睜大眼睛啊:

各種操作

                   

看清楚沒有啊親,也就是說你先前設置的pendingintent雖然在AlarmManagerService中,但是走到send的時候其實是什麼操作都沒做啊,所以這個就是爲什麼鬧鐘不響了啊。

OKAlarmManagerService淺析就差不多這些了。

OVER了,如果你對此有什麼疑問或者見解,那麼可以和我一起討論。此文純個人理解,有錯誤在所難免,所以請大家切勿輕信,歡迎指正([email protected])。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章