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() {
    @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。
  • 6
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
使用 AlarmManager 实现定时重置流量统计可以分为以下几个步骤: 1. 在 AndroidManifest.xml 文件中添加以下权限: ```xml <uses-permission android:name="android.permission.MODIFY_PHONE_STATE" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> ``` 2. 创建一个 BroadcastReceiver,用于接收定时任务的触发事件,并在其中实现重置流量统计的逻辑。例如: ```java public class TrafficStatsResetReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { // 重置流量统计信息 TrafficStats.clearAllStats(); } } ``` 3. 在 AndroidManifest.xml 文件中注册 BroadcastReceiver,并且添加一个 ACTION_BOOT_COMPLETED 的 intent-filter,以便在设备启动时启动定时任务。例如: ```xml <receiver android:name=".TrafficStatsResetReceiver" android:enabled="true" android:exported="false"> <intent-filter> <action android:name="com.example.trafficstatsreset.ACTION_RESET_TRAFFIC_STATS" /> <action android:name="android.intent.action.BOOT_COMPLETED" /> </intent-filter> </receiver> ``` 4. 在应用程序启动时注册一个定时任务,例如: ```java private void registerTrafficStatsResetTask(Context context) { // 创建一个 PendingIntent,用于触发定时任务 Intent intent = new Intent(context, TrafficStatsResetReceiver.class); intent.setAction("com.example.trafficstatsreset.ACTION_RESET_TRAFFIC_STATS"); PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); // 获取 AlarmManager 实例 AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); // 计算定时任务的触发时间 long triggerTime = System.currentTimeMillis() + 24 * 60 * 60 * 1000; // 每天重置一次流量统计 // 注册定时任务 alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, triggerTime, 24 * 60 * 60 * 1000, pendingIntent); } ``` 在上述代码中,我们创建了一个 PendingIntent,并将其传递给 AlarmManagersetRepeating 方法,以便注册一个每天重置一次流量统计的定时任务。需要注意的是,我们设置的定时任务触发时间是当前时间加上一天的时间间隔,这样可以确保定时任务在第二天凌晨触发。 5. 在应用程序启动时调用 registerTrafficStatsResetTask 方法,注册定时任务即可: ```java @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // 注册定时任务 registerTrafficStatsResetTask(this); } ``` 经过上述步骤,您就可以使用 AlarmManager实现定时重置流量统计的功能了。需要注意的是,使用 AlarmManager 时需要仔细管理定时任务的触发时间和间隔,以确保应用程序的稳定性和安全性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值