日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

App定时提醒(AlarmManager实现,适配不同版本)

發布時間:2023/12/20 编程问答 29 豆豆
生活随笔 收集整理的這篇文章主要介紹了 App定时提醒(AlarmManager实现,适配不同版本) 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

本文主要介紹App定時提醒的實現方式及原理。篇幅較長,先提供demo地址。

項目Demo地址

AlarmSample

App定時提醒方案探討

  • 方案一:利用 Handler 實現。Handler可以使用 sendEmptyMessageDelayed 來實現定時發送消息(提醒)的功能,但 sendEmptyMessageDelayed 方法是依賴于 Handler 所在的線程的,如果線程結束,就起不到定時任務的效果,故不適用
  • 方案二:利用 Timer 實現。使用 Timer 可以精確地做到定時操作,但如果手機關屏后長時間不使用, CPU 就會進入休眠模式。這個使用如果使用 Timer 來執行定時任務就會失敗,因為 Timer 無法喚醒 CPU。
  • 方案三:利用 AlarmManager 實現。AlarmManager 依賴的是 Android 系統的服務,具備喚醒機制。

AlarmManager概述

AlarmManager是Android的全局定時器。就是在指定時間做一個事情(封裝在PendingIntent)。通過PendingIntent的getActivity()、getService()或getBroadcast()來執行。聽起來AlarmManager和Timer很類似,但是Timer有可能因為手機休眠而被殺掉服務,但是AlarmManager可以做到喚醒手機。

AlarmManager提供了對系統定時服務的訪問接口,使得開發者可以安排在未來的某個時間運行應用。當到達鬧鈴設定時間,系統就會廣播鬧鈴之前注冊的Intent。如果此時目標應用沒有被啟動,系統還會幫你自動啟動目標應用。**即使設備已經進入睡眠已注冊的鬧鈴也會被保持,只有當設備關閉或是重啟的時候會被清除。**下面基于Android 8.0源碼來一起學習一下。

創建方式

AlarmManager am = (AlarmManager) getSystemService(ALARM_SERVICE);

注:ALARM_SERVICE是Context的一個常量。

鬧鈴類型

AlarmManager中一共提供了四種鬧鐘類型,前兩種對應的System.currentTimeMillis()(系統當前時間)時間,后兩種對應SystemClock.elapsedRealtime()(系統運行時間)時間,以WAKEUP結尾的類型能夠喚醒設備,其他的類型不能喚醒設備,直到設備被喚醒才能出發警報提醒。

public static final int RTC_WAKEUP = 0;// 表示鬧鐘在睡眠狀態下喚醒系統并執行提示功能,絕對時間。 public static final int RTC = 1;// 睡眠狀態下不可用,絕對時間。 public static final int ELAPSED_REALTIME_WAKEUP = 2;// 睡眠狀態下可用,相對時間。 public static final int ELAPSED_REALTIME = 3;// 睡眠狀態下不可用,相對時間。

注1:以上絕對時間就是手機的時間,相對時間是相對于當前開機時間來說(包含了手機睡眠時間)。例如如果是絕對時間,那么你測試可以通過修改系統時間來提前觸發。而相對時間的使用場景是強調多久之后觸發,例如2小時后,這個時候把時間修改到2小時后也是沒用的。
注2:不同鬧鐘類型對應的任務首次時間的獲取方法:若為ELAPSED_REALTIME_WAKEUP,那么當前時間就為 SystemClock.elapsedRealtime();若為RTC_WAKEUP,那么當前時間就為System.currentTimeMillis()
注3:以前還有一個POWER_OFF_WAKEUP,即使在關機后還能提醒,但是Android4.0以后就沒有了。

常用方法

設置時間

在AlarmMananger中提供了setTime和setTimeZone方法分別用來設置系統時間和系統默認時區。其中,設置系統時間需要"android.permission.SET_TIME"權限。

返回值公開方法
voidsetTime(long millis)
voidsetTimeZone(String timeZone)

設置鬧鈴

返回值公開方法
voidset(int type, long triggerAtMillis, PendingIntent operation)
voidset(int type, long triggerAtMillis, String tag, AlarmManager.OnAlarmListener listener, Handler targetHandler)
voidsetAlarmClock(AlarmManager.AlarmClockInfo info, PendingIntent operation)
voidsetAndAllowWhileIdle(int type, long triggerAtMillis, PendingIntent operation)
voidsetExact(int type, long triggerAtMillis, PendingIntent operation)
voidsetExact(int type, long triggerAtMillis, String tag, AlarmManager.OnAlarmListener listener, Handler targetHandler)
voidsetExactAndAllowWhileIdle(int type, long triggerAtMillis, PendingIntent operation)
voidsetInexactRepeating(int type, long triggerAtMillis, long intervalMillis, PendingIntent operation)
voidsetRepeating(int type, long triggerAtMillis, long intervalMillis, PendingIntent operation)
voidsetWindow(int type, long windowStartMillis, long windowLengthMillis, PendingIntent operation)
voidsetWindow(int type, long windowStartMillis, long windowLengthMillis, String tag, AlarmManager.OnAlarmListener listener, Handler targetHandler)

設置鬧鈴方法整理總結

1. set方法

返回值公開方法
voidset(int type, long triggerAtMillis, PendingIntent operation)
voidset(int type, long triggerAtMillis, String tag, AlarmManager.OnAlarmListener listener, Handler targetHandler)

**用于設置一次性鬧鈴,執行時間在設置時間附近,為非精確鬧鈴。**方法一和方法二的區別:到達設定時間時方法一會廣播PendingIntent中設定的Intent,而方法二會直接回調OnAlarmListener 中的onAlarm()方法。

2. setExact方法

返回值公開方法
voidsetExact(int type, long triggerAtMillis, PendingIntent operation)
voidsetExact(int type, long triggerAtMillis, String tag, AlarmManager.OnAlarmListener listener, Handler targetHandler)
voidsetAlarmClock(AlarmManager.AlarmClockInfo info, PendingIntent operation)

**用于設置一次性鬧鈴,執行時間更為精準,為精確鬧鈴。**方法一和二的區別參見上面set的區別。setAlarmClock方法等同于通過setExact方法設置的RTC_WAKEUP類型的鬧鈴,所以把他歸在setExact中介紹。其中AlarmClockInfo實現了Android序列化接口Parcelable,里面包含了mTriggerTime(執行時間)和mShowIntent(執行動作)兩個成員變量,可以看做是對鬧鈴事件的一個封裝類。

3. setInexactRepeating方法和setRepeating方法

返回值公開方法
voidsetInexactRepeating(int type, long triggerAtMillis, long intervalMillis, PendingIntent operation)
voidsetRepeating(int type, long triggerAtMillis, long intervalMillis, PendingIntent operation)

setInexactRepeating和setRepeating兩種方法都是用來設置重復鬧鈴的,setRepeating執行時間更為精準。在Android 4.4之后,Android系統為了省電把時間相近的鬧鈴打包到一起進行批量處理,這就使得setRepeating方法設置的鬧鈴不能被精確的執行,必須要使用setExact來代替。

4. setAndAllowWhileIdle方法和setExactAndAllowWhileIdle方法

返回值公開方法
voidsetAndAllowWhileIdle(int type, long triggerAtMillis, PendingIntent operation)
voidsetExactAndAllowWhileIdle(int type, long triggerAtMillis, PendingIntent operation)

**使用setAndAllowWhileIdle和setExactAndAllowWhileIdle方法設置一次鬧鈴,可以在低功耗模式下被執行,setExactAndAllowWhileIdle執行時間更為精準。**手機滅屏以后會進入低功耗模式(low-power idle modes),這個時候你會發現通過setExact設置的鬧鈴也不是100%準確了,需要用setExactAndAllowWhileIdle方法來設置,鬧鈴才能在低功耗模式下被執行。

5. setWindow方法

返回值公開方法
voidsetWindow(int type, long windowStartMillis, long windowLengthMillis, PendingIntent operation)
voidsetWindow(int type, long windowStartMillis, long windowLengthMillis, String tag, AlarmManager.OnAlarmListener listener, Handler targetHandler)

**用于設置某個時間段內的一次鬧鈴。**比如,我想在下午的2點到4點之間設置一次提醒。兩個方法的區別同set。

取消鬧鈴

返回值公開方法
voidcancel(PendingIntent operation)
voidcancel(AlarmManager.OnAlarmListener listener)

用于取消設置過的鬧鈴,分別對應于PendingIntentAlarmManager.OnAlarmListener方式注冊的鬧鈴。

獲得下一次鬧鈴事件

返回值公開方法
AlarmManager.AlarmClockInfogetNextAlarmClock()

用于獲得下一次鬧鈴事件。

常用時間定義

AlarmManager類已經幫我們定義好了常用的時間常量。

public static final long INTERVAL_FIFTEEN_MINUTES = 15 * 60 * 1000; public static final long INTERVAL_HALF_HOUR = 2*INTERVAL_FIFTEEN_MINUTES; public static final long INTERVAL_HOUR = 2*INTERVAL_HALF_HOUR; public static final long INTERVAL_HALF_DAY = 12*INTERVAL_HOUR; public static final long INTERVAL_DAY = 2*INTERVAL_HALF_DAY;

設置多個鬧鐘

若連續設置多個鬧鐘,則只有最后一個鬧鐘會生效,那么這種情況我們怎么處理呢?其實很簡單。我們可以給每個鬧鐘設置唯一的id,保證id唯一性即可,案例見AlarmSample。
在取消鬧鐘時我們也可以根據這個id關閉不同的鬧鐘。

不同SDK版本注意事項

SDK API < 19

正常使用 set()setRepeating() 即可。

示例代碼:

alarmManager.setRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime(), TIME_INTERVAL, pendingIntent);

SDK API >= 19 && SDK API < 23

Google 為了追求系統省電,所以“偷偷加工”了一下喚醒的時間間隔。如果在 Android 4.4 及以上的設備還要追求精準的鬧鐘定時任務,要使用 setExact() 方法。

示例代碼:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {alarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime(), pendingIntent); } else {alarmManager.setRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime(), TIME_INTERVAL, pendingIntent); }private BroadcastReceiver alarmReceiver = new BroadcastReceiver() {@Overridepublic void onReceive(Context context, Intent intent) {// 重復定時任務if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {alarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + TIME_INTERVAL, pendingIntent);}// to do somethingdoSomething();} };

SDK API >= 23

Android 6.0 中引入了低電耗模式和應用待機模式,根據官方指導對低電耗模式和應用待機模式進行針對性優化我們需要使用 setExactAndAllowWhileIdle() 來解決在低電耗模式下的鬧鐘觸發。

示例代碼:

// pendingIntent 為發送廣播 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {alarmManager.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime(), pendingIntent); } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {alarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime(), pendingIntent); } else {alarmManager.setRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime(), TIME_INTERVAL, pendingIntent); }private BroadcastReceiver alarmReceiver = new BroadcastReceiver() {@Overridepublic void onReceive(Context context, Intent intent) {// 重復定時任務if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {alarmManager.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + TIME_INTERVAL, pendingIntent);} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {alarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + TIME_INTERVAL, pendingIntent);}// to do somethingdoSomething();} };

SDK API >= 26

Android 8.0 應用不能對大部分的廣播進行靜態注冊,所以推薦使用Service來實現定時提醒功能,案例見AlarmSample

源碼分析

設置鬧鈴

**通過深入分析AlarmManager的源碼,發現上面提到的所有與鬧鈴設置有關的方法(setXXX)最終都會調用setImpl方法,區別在于不同的應用場景設置的參數不同。**setImpl方法的源碼如下:

private void setImpl(@AlarmType 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) {triggerAtMillis = 0;}// 把 OnAlarmListener 封裝起來ListenerWrapper recipientWrapper = null;if (listener != null) {synchronized (AlarmManager.class) {if (sWrappers == null) {sWrappers = new ArrayMap<OnAlarmListener, ListenerWrapper>();}recipientWrapper = sWrappers.get(listener);// no existing wrapper => build a new oneif (recipientWrapper == null) {recipientWrapper = new ListenerWrapper(listener);sWrappers.put(listener, recipientWrapper);}}final Handler handler = (targetHandler != null) ? targetHandler : mMainThreadHandler;recipientWrapper.setHandler(handler);}// 調用Service的set方法try {mService.set(mPackageName, type, triggerAtMillis, windowMillis, intervalMillis, flags,operation, recipientWrapper, listenerTag, workSource, alarmClock);} catch (RemoteException ex) {throw ex.rethrowFromSystemServer();}}

那么這個mService是哪來的呢?通過搜索在SystemServiceRegistry類中找到了一段靜態方法,**SystemServiceRegistry是用來管理所有能由Context.getSystemService方法獲得系統服務的類,通過ServiceManager.getServiceOrThrow根據服務名字來查找到對應的IBinder,進而生成IAlarmManager實例并作為參數傳遞給AlarmManager。**mService就是這個IAlarmManager實例。

android-8.1.0_r2/frameworks/base/core/java/android/app/SystemServiceRegistry.javastatic { ...registerService(Context.ALARM_SERVICE, AlarmManager.class,new CachedServiceFetcher<AlarmManager>() {@Overridepublic AlarmManager createService(ContextImpl ctx) throws ServiceNotFoundException {IBinder b = ServiceManager.getServiceOrThrow(Context.ALARM_SERVICE);IAlarmManager service = IAlarmManager.Stub.asInterface(b);return new AlarmManager(service, ctx);}});...}

接下來看看 IAlarmManager 是由誰實現的呢?熟悉Android源碼的同學自然而然就會想到有可能有一個AlarmManagerService類來提供具體的實現機制。搜一搜還真有這個類,進一步搜索 IAlarmManager 查看哪里實現了這個接口類(當然,熟悉AIDL機制的同學也可以直接搜索 IAlarmManager ,也能找到)。

private final IBinder mService = new IAlarmManager.Stub() {// 設置鬧鈴的方法@Overridepublic void set(String callingPackage,int type, long triggerAtTime, long windowLength, long interval, int flags,PendingIntent operation, IAlarmListener directReceiver, String listenerTag,WorkSource workSource, AlarmManager.AlarmClockInfo alarmClock) {final int callingUid = Binder.getCallingUid();// make sure the caller is not lying about which package should be blamed for// wakelock time spent in alarm deliverymAppOps.checkPackage(callingUid, callingPackage);// Repeating alarms must use PendingIntent, not direct listenerif (interval != 0) {if (directReceiver != null) {throw new IllegalArgumentException("Repeating alarms cannot use AlarmReceivers");}}if (workSource != null) {getContext().enforcePermission(android.Manifest.permission.UPDATE_DEVICE_STATS,Binder.getCallingPid(), callingUid, "AlarmManager.set");}// No incoming callers can request either WAKE_FROM_IDLE or// ALLOW_WHILE_IDLE_UNRESTRICTED -- we will apply those later as appropriate.flags &= ~(AlarmManager.FLAG_WAKE_FROM_IDLE| AlarmManager.FLAG_ALLOW_WHILE_IDLE_UNRESTRICTED);// Only the system can use FLAG_IDLE_UNTIL -- this is used to tell the alarm// manager when to come out of idle mode, which is only for DeviceIdleController.if (callingUid != Process.SYSTEM_UID) {flags &= ~AlarmManager.FLAG_IDLE_UNTIL;}// If this is an exact time alarm, then it can't be batched with other alarms.if (windowLength == AlarmManager.WINDOW_EXACT) {flags |= AlarmManager.FLAG_STANDALONE;}// If this alarm is for an alarm clock, then it must be standalone and we will// use it to wake early from idle if needed.if (alarmClock != null) {flags |= AlarmManager.FLAG_WAKE_FROM_IDLE | AlarmManager.FLAG_STANDALONE;// If the caller is a core system component or on the user's whitelist, and not calling// to do work on behalf of someone else, then always set ALLOW_WHILE_IDLE_UNRESTRICTED.// This means we will allow these alarms to go off as normal even while idle, with no// timing restrictions.} else if (workSource == null && (callingUid < Process.FIRST_APPLICATION_UID|| callingUid == mSystemUiUid|| Arrays.binarySearch(mDeviceIdleUserWhitelist,UserHandle.getAppId(callingUid)) >= 0)) {flags |= AlarmManager.FLAG_ALLOW_WHILE_IDLE_UNRESTRICTED;flags &= ~AlarmManager.FLAG_ALLOW_WHILE_IDLE;}// 最終會調用AlarmManagerService的setImpl方法setImpl(type, triggerAtTime, windowLength, interval, operation, directReceiver,listenerTag, flags, workSource, alarmClock, callingUid, callingPackage);}...};

從上面這段源碼可以看出,set方法最終會調用AlarmManagerService的setImpl方法。在setImpl中,根據傳遞的參數經過一系列的計算,傳遞給setImplLocked方法進行下一步處理。

void setImpl(int type, long triggerAtTime, long windowLength, long interval,PendingIntent operation, IAlarmListener directReceiver, String listenerTag,int flags, WorkSource workSource, AlarmManager.AlarmClockInfo alarmClock,int callingUid, String callingPackage) {...synchronized (mLock) {...setImplLocked(type, triggerAtTime, triggerElapsed, windowLength, maxElapsed,interval, operation, directReceiver, listenerTag, flags, true, workSource,alarmClock, callingUid, callingPackage);}}

setImplLocked方法會把傳遞過來的參數封裝成一個Alarm對象,調用setImplLocked的另一個重載方法。在setImplLocked中,會去計算Alarm所屬的批次(Batch),然后根據批次進行重新打包,打包后對內核Alarm進行重新規劃,更新下一個Alarm時間。

private void setImplLocked(int type, long when, long whenElapsed, long windowLength,long maxWhen, long interval, PendingIntent operation, IAlarmListener directReceiver,String listenerTag, int flags, boolean doValidate, WorkSource workSource,AlarmManager.AlarmClockInfo alarmClock, int callingUid, String callingPackage) {// 參數封裝成一個Alarm對象Alarm a = new Alarm(type, when, whenElapsed, windowLength, maxWhen, interval,operation, directReceiver, listenerTag, workSource, flags, alarmClock,callingUid, callingPackage);try {if (ActivityManager.getService().isAppStartModeDisabled(callingUid, callingPackage)) {Slog.w(TAG, "Not setting alarm from " + callingUid + ":" + a+ " -- package not allowed to start");return;}} catch (RemoteException e) {}removeLocked(operation, directReceiver);setImplLocked(a, false, doValidate);}private void setImplLocked(Alarm a, boolean rebatching, boolean doValidate) {// 計算alarm所屬的批次int whichBatch = ((a.flags&AlarmManager.FLAG_STANDALONE) != 0)? -1 : attemptCoalesceLocked(a.whenElapsed, a.maxWhenElapsed);if (whichBatch < 0) {Batch batch = new Batch(a);addBatchLocked(mAlarmBatches, batch);} else {Batch batch = mAlarmBatches.get(whichBatch);if (batch.add(a)) {// The start time of this batch advanced, so batch ordering may// have just been broken. Move it to where it now belongs.mAlarmBatches.remove(whichBatch);addBatchLocked(mAlarmBatches, batch);}}...if (!rebatching) {...if (needRebatch) {// 重新打包所有的AlarmrebatchAllAlarmsLocked(false);}// 重新規劃內核的AlarmrescheduleKernelAlarmsLocked();// 更新下一個Alarm的時間updateNextAlarmClockLocked();}}

在rescheduleKernelAlarmsLocked方法中會調用setLocked方法,setLocked方法內部會去調用native方法set,最終把Alarm設置到內核中去。

private void setLocked(int type, long when) {if (mNativeData != 0) {// The kernel never triggers alarms with negative wakeup times// so we ensure they are positive.long alarmSeconds, alarmNanoseconds;if (when < 0) {alarmSeconds = 0;alarmNanoseconds = 0;} else {alarmSeconds = when / 1000;alarmNanoseconds = (when % 1000) * 1000 * 1000;}// native方法set(mNativeData, type, alarmSeconds, alarmNanoseconds);} else {Message msg = Message.obtain();msg.what = ALARM_EVENT;mHandler.removeMessages(ALARM_EVENT);mHandler.sendMessageAtTime(msg, when);}}

取消鬧鈴

**在AlarmManager提供了兩個cancel方法來取消鬧鈴,調用時候需要傳遞一個PendingIntent或是OnAlarmListener實例作為參數,從此也可以看出鬧鈴服務內部是以PendingIntent或是OnAlarmListener作為區分不同鬧鈴的唯一標識的。**cancel(PendingIntent operation) 和 cancel(OnAlarmListener listener) 的實現原理是差不多的,最終都會調用mService.remove方法來移除鬧鈴,這里以 cancel(PendingIntent operation) 方法為例進行詳細分析。

public void cancel(PendingIntent operation) {// 如果 PendingIntent 為空,在N和之后的版本會拋出空指針異常if (operation == null) {final String msg = "cancel() called with a null PendingIntent";if (mTargetSdkVersion >= Build.VERSION_CODES.N) {throw new NullPointerException(msg);} else {Log.e(TAG, msg);return;}}try {// mService是一個IBinder,由來及對應方法的實現同上面設置鬧鈴中的解析mService.remove(operation, null);} catch (RemoteException ex) {throw ex.rethrowFromSystemServer();}}

mService.remove方法中首先會去判斷PendingIntent 和 IAlarmListener 是否都為空,有一個不為空則調用removeLocked繼續進行處理。

private final IBinder mService = new IAlarmManager.Stub() {...// 取消鬧鈴的方法@Overridepublic void remove(PendingIntent operation, IAlarmListener listener) {// PendingIntent 和 IAlarmListener 必須有一個不為空if (operation == null && listener == null) {Slog.w(TAG, "remove() with no intent or listener");return;}synchronized (mLock) {// 調用AlarmManagerService中的removeLocked方法removeLocked(operation, listener);}}...};

在removeLocked方法中,會根據傳遞過來的參數在mAlarmBatches和mPendingWhileIdleAlarms兩個列表中查詢當前要刪除的Alarm,如果匹配到則刪除。刪除后會對所有鬧鈴重新打包,如果刪除的是非低功耗模式下啟動的鬧鈴則需要刷新非低功耗下啟動的鬧鈴設置,最后更新下一次鬧鈴時間。

private void removeLocked(PendingIntent operation, IAlarmListener directReceiver) {// 遍歷查詢并刪除匹配的Alarmsboolean didRemove = false;for (int i = mAlarmBatches.size() - 1; i >= 0; i--) {Batch b = mAlarmBatches.get(i);didRemove |= b.remove(operation, directReceiver);if (b.size() == 0) {mAlarmBatches.remove(i);}}for (int i = mPendingWhileIdleAlarms.size() - 1; i >= 0; i--) {if (mPendingWhileIdleAlarms.get(i).matches(operation, directReceiver)) {// Don't set didRemove, since this doesn't impact the scheduled alarms.mPendingWhileIdleAlarms.remove(i);}}if (didRemove) {if (DEBUG_BATCH) {Slog.v(TAG, "remove(operation) changed bounds; rebatching");}boolean restorePending = false;if (mPendingIdleUntil != null && mPendingIdleUntil.matches(operation, directReceiver)) {mPendingIdleUntil = null;restorePending = true;}if (mNextWakeFromIdle != null && mNextWakeFromIdle.matches(operation, directReceiver)) {mNextWakeFromIdle = null;}// 重新打包所有的鬧鈴rebatchAllAlarmsLocked(true);if (restorePending) {// 重新存儲非低功耗下啟動的鬧鈴restorePendingWhileIdleAlarmsLocked();}// 更新下一次鬧鈴時間updateNextAlarmClockLocked();}}

最終在restorePendingWhileIdleAlarmsLocked方法中會調用rescheduleKernelAlarmsLocked和updateNextAlarmClockLocked 重新規劃內核的Alarm并更新下一個Alarm的時間。

void restorePendingWhileIdleAlarmsLocked() {...// 重新規劃內核的AlarmrescheduleKernelAlarmsLocked();// 更新下一個Alarm的時間updateNextAlarmClockLocked();...}

獲得下一次鬧鈴事件

AlarmManager提供了getNextAlarmClock方法來獲得下一次鬧鈴事件,該方法中會把當前的UserId作為查詢依據傳遞到AlarmManagerService中的getNextAlarmClockImpl方法,從而查詢出當前用戶所對應的下一次鬧鈴事件。

frameworks/base/core/java/android/app/AlarmManager.javapublic AlarmClockInfo getNextAlarmClock() {return getNextAlarmClock(UserHandle.myUserId());}public AlarmClockInfo getNextAlarmClock(int userId) {try {return mService.getNextAlarmClock(userId);} catch (RemoteException ex) {throw ex.rethrowFromSystemServer();}}frameworks/base/services/core/java/com/android/server/AlarmManagerService.javaprivate final IBinder mService = new IAlarmManager.Stub() {...// 獲得下次鬧鈴事件@Overridepublic AlarmManager.AlarmClockInfo getNextAlarmClock(int userId) {userId = ActivityManager.handleIncomingUser(Binder.getCallingPid(),Binder.getCallingUid(), userId, false /* allowAll */, false /* requireFull */,"getNextAlarmClock", null);return getNextAlarmClockImpl(userId);}...};AlarmManager.AlarmClockInfo getNextAlarmClockImpl(int userId) {synchronized (mLock) {return mNextAlarmClockForUser.get(userId);}}

設置系統時間

設置系統時間的功能實現流程比較簡單,在AlarmManager提供的setTime方法中直接調用mService.setTime方法,進而通過AlarmManagerService中聲明的native方法setKernelTime把時間設置到底層內核中去。

frameworks/base/core/java/android/app/AlarmManager.javapublic void setTime(long millis) {try {mService.setTime(millis);} catch (RemoteException ex) {throw ex.rethrowFromSystemServer();}}frameworks/base/services/core/java/com/android/server/AlarmManagerService.javaprivate final IBinder mService = new IAlarmManager.Stub() {...// 設置系統時鐘的方法@Overridepublic boolean setTime(long millis) {// 注意,設置系統時間需要"android.permission.SET_TIME"權限getContext().enforceCallingOrSelfPermission("android.permission.SET_TIME","setTime");if (mNativeData == 0) {Slog.w(TAG, "Not setting time since no alarm driver is available.");return false;}synchronized (mLock) {// native 方法,直接設置到底層kernel中return setKernelTime(mNativeData, millis) == 0;}}...};

設置系統時區

在AlarmManager提供的setTimeZone方法中直接調用mService的setTimeZone方法,進而調用AlarmManagerService的setTimeZoneImpl方法,并由此方法完成整個系統時區設置的相關邏輯(包括系統屬性值修改、設置內核時區和廣播系統時區變化)。

frameworks/base/core/java/android/app/AlarmManager.javapublic void setTimeZone(String timeZone) {...try {mService.setTimeZone(timeZone);} catch (RemoteException ex) {throw ex.rethrowFromSystemServer();}}frameworks/base/services/core/java/com/android/server/AlarmManagerService.javaprivate final IBinder mService = new IAlarmManager.Stub() {...// 設置系統默認時區的方法@Overridepublic void setTimeZone(String tz) {getContext().enforceCallingOrSelfPermission("android.permission.SET_TIME_ZONE","setTimeZone");final long oldId = Binder.clearCallingIdentity();try {setTimeZoneImpl(tz);} finally {Binder.restoreCallingIdentity(oldId);}}...};void setTimeZoneImpl(String tz) {...boolean timeZoneWasChanged = false;synchronized (this) {String current = SystemProperties.get(TIMEZONE_PROPERTY);if (current == null || !current.equals(zone.getID())) {timeZoneWasChanged = true;// 設置SystemProperties中時區對應的字段值SystemProperties.set(TIMEZONE_PROPERTY, zone.getID());}int gmtOffset = zone.getOffset(System.currentTimeMillis());// native 方法,直接設置到底層kernel中setKernelTimezone(mNativeData, -(gmtOffset / 60000));}TimeZone.setDefault(null);// 廣播系統時區變化if (timeZoneWasChanged) {Intent intent = new Intent(Intent.ACTION_TIMEZONE_CHANGED);intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING| Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND| Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS);intent.putExtra("time-zone", zone.getID());getContext().sendBroadcastAsUser(intent, UserHandle.ALL);}}

總結

  • 設置系統時間需要"android.permission.SET_TIME"權限。
  • 每當有新的Alarm設置或刪除定時服務都會重新計算所屬批次,把時間相近的Alarm打包到一個批次里(Batch)一起執行,起到優化電池節省耗電的目的。這就是導致非精確Alarm執行時間存在不確定誤差的根本原因。
  • 如果想要在低耗電模式下觸發鬧鈴需要通過 setAndAllowWhileIdle 和 setExactAndAllowWhileIdle 方法來設置鬧鈴。
  • 如果設置的鬧鈴時間已經過了,鬧鈴會被立即觸發。這個問題可以通過比較鬧鈴設置時間和當前時間來解決。
  • 根據實際需求選擇是否設置精確鬧鈴以達到優化電池節省耗電的目的。
  • 通過設置時區的源碼可知,如果想要獲取系統時區的相關信息可以通過監聽Intent.ACTION_TIMEZONE_CHANGED廣播或是直接讀取系統屬性TIMEZONE_PROPERTY。
  • 總結

    以上是生活随笔為你收集整理的App定时提醒(AlarmManager实现,适配不同版本)的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。