系统服务之定时服务(AlarmManager)

概述 AlarmManager提供了对系统定时服务的访问接口,使得开发者可以安排在未来的某个时间运行应用。当到达闹铃设定时间,系统就会广播闹铃之前注册的Intent。如果此时目标应用没有被启动,系统还会帮你自动启动目标应用。即使设备已经进入睡眠已注册的闹铃也会被保持,只有当设备关闭或是重启的时候会被清除。下面基于Android 8.0源码来一起学习一下。
闹铃类型 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;

设置时间 在AlarmMananger中提供了setTime和setTimeZone方法分别用来设置系统时间和系统默认时区。其中,设置系统时间需要”android.permission.SET_TIME”权限。
返回值 公开方法
void setTime(long millis)
void setTimeZone(String timeZone)
设置闹铃
返回值 公开方法
void set(int type, long triggerAtMillis, PendingIntent operation)
void set(int type, long triggerAtMillis, String tag, AlarmManager.OnAlarmListener listener, Handler targetHandler)
void setAlarmClock(AlarmManager.AlarmClockInfo info, PendingIntent operation)
void setAndAllowWhileIdle(int type, long triggerAtMillis, PendingIntent operation)
void setExact(int type, long triggerAtMillis, PendingIntent operation)
void setExact(int type, long triggerAtMillis, String tag, AlarmManager.OnAlarmListener listener, Handler targetHandler)
void setExactAndAllowWhileIdle(int type, long triggerAtMillis, PendingIntent operation)
void setInexactRepeating(int type, long triggerAtMillis, long intervalMillis, PendingIntent operation)
void setRepeating(int type, long triggerAtMillis, long intervalMillis, PendingIntent operation)
void setWindow(int type, long windowStartMillis, long windowLengthMillis, PendingIntent operation)
void setWindow(int type, long windowStartMillis, long windowLengthMillis, String tag, AlarmManager.OnAlarmListener listener, Handler targetHandler)
以上是AlarmManager中提供的所有设置闹铃的方法,下面来详细介绍一下。
  • set
返回值 公开方法
void set(int type, long triggerAtMillis, PendingIntent operation)
void set(int type, long triggerAtMillis, String tag, AlarmManager.OnAlarmListener listener, Handler targetHandler)
用于设置一次性闹铃,执行时间在设置时间附近,为非精确闹铃。方法一和方法二的区别:到达设定时间时方法一会广播PendingIntent中设定的Intent,而方法二会直接回调OnAlarmListener 中的onAlarm()方法。
  • setExact
返回值 公开方法
void setExact(int type, long triggerAtMillis, PendingIntent operation)
void setExact(int type, long triggerAtMillis, String tag, AlarmManager.OnAlarmListener listener, Handler targetHandler)
void setAlarmClock(AlarmManager.AlarmClockInfo info, PendingIntent operation)
用于设置一次性闹铃,执行时间更为精准,为精确闹铃。方法一和二的区别参见上面set的区别。setAlarmClock方法等同于通过setExact方法设置的RTC_WAKEUP类型的闹铃,所以把他归在setExact中介绍。其中AlarmClockInfo实现了Android序列化接口Parcelable,里面包含了mTriggerTime(执行时间)和mShowIntent(执行动作)两个成员变量,可以看做是对闹铃事件的一个封装类。
  • setInexactRepeating和setRepeating
返回值 公开方法
void setInexactRepeating(int type, long triggerAtMillis, long intervalMillis, PendingIntent operation)
void setRepeating(int type, long triggerAtMillis, long intervalMillis, PendingIntent operation)
setInexactRepeating和setRepeating两种方法都是用来设置重复闹铃的,setRepeating执行时间更为精准。在Android 4.4之后,Android系统为了省电把时间相近的闹铃打包到一起进行批量处理,这就使得setRepeating方法设置的闹铃不能被精确的执行,必须要使用setExact来代替。
  • setAndAllowWhileIdle和setExactAndAllowWhileIdle
返回值 公开方法
void setAndAllowWhileIdle(int type, long triggerAtMillis, PendingIntent operation)
void setExactAndAllowWhileIdle(int type, long triggerAtMillis, PendingIntent operation)
使用setAndAllowWhileIdle和setExactAndAllowWhileIdle方法设置一次闹铃,可以在低功耗模式下被执行,setExactAndAllowWhileIdle执行时间更为精准。手机灭屏以后会进入低功耗模式(low-power idle modes),这个时候你会发现通过setExact设置的闹铃也不是100%准确了,需要用setExactAndAllowWhileIdle方法来设置,闹铃才能在低功耗模式下被执行。
  • setWindow
返回值 公开方法
void setWindow(int type, long windowStartMillis, long windowLengthMillis, PendingIntent operation)
void setWindow(int type, long windowStartMillis, long windowLengthMillis, String tag, AlarmManager.OnAlarmListener listener, Handler targetHandler)
用于设置某个时间段内的一次闹铃。比如,我想在下午的2点到4点之间设置一次提醒。两个方法的区别同set。
取消闹铃
返回值 公开方法
void cancel(PendingIntent operation)
void cancel(AlarmManager.OnAlarmListener listener)
用于取消设置过的闹铃,分别对应于PendingIntent和AlarmManager.OnAlarmListener方式注册的闹铃。
获得下一次闹铃事件
返回值 公开方法
AlarmManager.AlarmClockInfo getNextAlarmClock()
用于获得下一次闹铃事件。
常用时间定义 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;

源码分析 设置闹铃 通过深入分析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(); }recipientWrapper = sWrappers.get(listener); // no existing wrapper => build a new one if (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.java static { ... registerService(Context.ALARM_SERVICE, AlarmManager.class, new CachedServiceFetcher() { @Override public 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() { // 设置闹铃的方法 @Override public 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 delivery mAppOps.checkPackage(callingUid, callingPackage); // Repeating alarms must use PendingIntent, not direct listener if (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) { // 重新打包所有的Alarm rebatchAllAlarmsLocked(false); } // 重新规划内核的Alarm rescheduleKernelAlarmsLocked(); // 更新下一个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() { ... // 取消闹铃的方法 @Override public 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) { // 遍历查询并删除匹配的Alarms boolean 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() { ... // 重新规划内核的Alarm rescheduleKernelAlarmsLocked(); // 更新下一个Alarm的时间 updateNextAlarmClockLocked(); ... }

获得下一次闹铃事件 AlarmManager提供了getNextAlarmClock方法来获得下一次闹铃事件,该方法中会把当前的UserId作为查询依据传递到AlarmManagerService中的getNextAlarmClockImpl方法,从而查询出当前用户所对应的下一次闹铃事件。
frameworks/base/core/java/android/app/AlarmManager.java public 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.java private final IBinder mService = new IAlarmManager.Stub() { ... // 获得下次闹铃事件 @Override public 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.java public void setTime(long millis) { try { mService.setTime(millis); } catch (RemoteException ex) { throw ex.rethrowFromSystemServer(); } }frameworks/base/services/core/java/com/android/server/AlarmManagerService.java private final IBinder mService = new IAlarmManager.Stub() { ... // 设置系统时钟的方法 @Override public boolean setTime(long millis) { // 注意,设置系统时间需要"android.permission.SET_TIME"权限 getContext().enforceCallingOrSelfPermission( "android.permission.SET_TIME", "setTime"); if (mNativeData =https://www.it610.com/article/= 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.java public void setTimeZone(String timeZone) { ... try { mService.setTimeZone(timeZone); } catch (RemoteException ex) { throw ex.rethrowFromSystemServer(); } }frameworks/base/services/core/java/com/android/server/AlarmManagerService.java private final IBinder mService = new IAlarmManager.Stub() { ... // 设置系统默认时区的方法 @Override public 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 8.0源码,首先列举了闹铃种类和设置闹铃、取消闹铃、获得下一次闹铃事件、设置系统时间、设置系统时区等相关方法,然后结合系统源码详细分析了各种方法的实现机制。使用过程中有以下几点需要注意:
1、设置系统时间需要”android.permission.SET_TIME”权限。
2、每当有新的Alarm设置或删除定时服务都会重新计算所属批次,把时间相近的Alarm打包到一个批次里(Batch)一起执行,起到优化电池节省耗电的目的。这就是导致非精确Alarm执行时间存在不确定误差的根本原因。
3、如果想要在低耗电模式下触发闹铃需要通过setAndAllowWhileIdle和setExactAndAllowWhileIdle方法来设置闹铃。
4、如果设置的闹铃时间已经过了,闹铃会被立即触发。这个问题可以通过比较闹铃设置时间和当前时间来解决。
5、根据实际需求选择是否设置精确闹铃以达到优化电池节省耗电的目的。
6、通过设置时区的源码可知,如果想要获取系统时区的相关信息可以通过监听Intent.ACTION_TIMEZONE_CHANGED广播或是直接读取系统属性TIMEZONE_PROPERTY。
【系统服务之定时服务(AlarmManager)】具体的使用方法可以参考下面两个链接:
使用示例:Android定时器AlarmManager
不同版本的差异:关于使用AlarmManager的注意事项

    推荐阅读