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"权限。

返回值 公开方法
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)

设置闹铃方法整理总结

1. 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()方法。

2. 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(执行动作)两个成员变量,可以看做是对闹铃事件的一个封装类。

3. 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来代替。

4. 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方法来设置,闹铃才能在低功耗模式下被执行。

5. 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)

用于取消设置过的闹铃,分别对应于PendingIntentAlarmManager.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;

设置多个闹钟

若连续设置多个闹钟,则只有最后一个闹钟会生效,那么这种情况我们怎么处理呢?其实很简单。我们可以给每个闹钟设置唯一的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() {
    @Override
    public 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 something
        doSomething();
    }
};

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() {
    @Override
    public 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 something
        doSomething();
    }
};

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 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<AlarmManager>() {
            @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 == 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);
        }
    }

总结

  1. 设置系统时间需要"android.permission.SET_TIME"权限。
  2. 每当有新的Alarm设置或删除定时服务都会重新计算所属批次,把时间相近的Alarm打包到一个批次里(Batch)一起执行,起到优化电池节省耗电的目的。这就是导致非精确Alarm执行时间存在不确定误差的根本原因。
  3. 如果想要在低耗电模式下触发闹铃需要通过 setAndAllowWhileIdle 和 setExactAndAllowWhileIdle 方法来设置闹铃。
  4. 如果设置的闹铃时间已经过了,闹铃会被立即触发。这个问题可以通过比较闹铃设置时间和当前时间来解决。
  5. 根据实际需求选择是否设置精确闹铃以达到优化电池节省耗电的目的。
  6. 通过设置时区的源码可知,如果想要获取系统时区的相关信息可以通过监听Intent.ACTION_TIMEZONE_CHANGED广播或是直接读取系统属性TIMEZONE_PROPERTY。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,924评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,781评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,813评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,264评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,273评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,383评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,800评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,482评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,673评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,497评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,545评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,240评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,802评论 3 304
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,866评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,101评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,673评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,245评论 2 341