Android系统功耗优化之Alarm - 从AlarmManager到Linux kernel
1 Overview对于功耗优化时长可以看到alarm唤醒频繁,或者alarm timer持锁时间过长的问题,对于这样的情况Android的各个版本也都有持续性的优化,对于alarm来说,简而言之都是加强管控,尽可能减少唤醒,集中批量处理。2 AlarmManagerAlarmManager提供接口供应用根据自己的需求,来设置alarm以及对应的处理方法frameworks/base/co...
1 Overview
对于系统功耗优化,时常可以看到alarm唤醒频繁,或者alarm timer持锁时间过长的问题,对于这样的情况Android的各个版本也都有持续性的优化,对于alarm来说,简而言之都是加强管控,尽可能减少唤醒,集中批量处理。
2 AlarmManager
AlarmManager提供接口供应用根据自己的需求,来设置alarm以及对应的处理方法
frameworks/base/core/java/android/app/AlarmManager.java
alarm目前有几种类型
- RTC_WAKEUP
/**
* Alarm time in {@link System#currentTimeMillis System.currentTimeMillis()}
* (wall clock time in UTC), which will wake up the device when
* it goes off.
*/
public static final int RTC_WAKEUP = 0;
- RTC
/**
* Alarm time in {@link System#currentTimeMillis System.currentTimeMillis()}
* (wall clock time in UTC). This alarm does not wake the
* device up; if it goes off while the device is asleep, it will not be
* delivered until the next time the device wakes up.
*/
public static final int RTC = 1;
- ELAPSED_REALTIME_WAKEUP
/**
* Alarm time in {@link android.os.SystemClock#elapsedRealtime
* SystemClock.elapsedRealtime()} (time since boot, including sleep),
* which will wake up the device when it goes off.
*/
public static final int ELAPSED_REALTIME_WAKEUP = 2;
- ELAPSED_REALTIME
/**
* Alarm time in {@link android.os.SystemClock#elapsedRealtime
* SystemClock.elapsedRealtime()} (time since boot, including sleep).
* This alarm does not wake the device up; if it goes off while the device
* is asleep, it will not be delivered until the next time the device
* wakes up.
*/
public static final int ELAPSED_REALTIME = 3;
- RTC_POWEROFF_WAKEUP
/**
* Alarm time in {@link System#currentTimeMillis System.currentTimeMillis()}
* (wall clock time in UTC), which will wake up the device when
* it goes off. And it will power on the devices when it shuts down.
* Set as 5 to make it be compatible with android_alarm_type.
* @hide
*/
public static final int RTC_POWEROFF_WAKEUP = 5;
其特点用途如下
-
RTC和ELAPSED_REALTIME 的差异在于前者使用绝对时间,后者使用相对时间来设置
-
WAKEUP类型的alarm在超时时如果系统处于待机休眠状态,则会唤醒系统,非WAKEUP类型的只能等到下一次系统唤醒的时候再被处理
-
RTC_POWEROFF_WAKEUP 基本就是用来给关机闹钟,或者定时开机用
提供的设置接口有
- set(int type, long triggerAtMillis, PendingIntent operation)
设置一次性的 - setRepeating(int type, long triggerAtMillis, long intervalMillis, PendingIntent operation)
设置可重复执行的 - setInexactRepeating(int type, long triggerAtMillis, long intervalMillis, PendingIntent operation)
设置可重复执行的,并且没有精确的时间要求
第三个参数就是Alarm对应的超时处理方法,
除了PendingIntent还有OnAlarmListener类型
public void set(@AlarmType int type, long triggerAtMillis, String tag, OnAlarmListener listener,
Handler targetHandler)
以上的API在小于19的情况下,可以正常工作,因为从Android 4.4开始,set 或者 setRepeating创建的alarm将会变得不准确,原因是做了唤醒对齐, 时间不敏感的放一起触发。
这时候可以用setWindow(), 这个api设置的是一个时间范围,可以确保alarm在这个时间范围内触发
如果需要非常精确的时间,可以使用setExact()
在Android 6.0 API 23 开始,DOZE 又会导致上述方法设置的alarm无法生效
标准 AlarmManager 闹铃(包括 setExact() 和 setWindow())推迟到下一维护时段。
如果您需要设置在低电耗模式下触发的闹铃,请使用 setAndAllowWhileIdle() 或 setExactAndAllowWhileIdle()。
一般情况下,使用 setAlarmClock() 设置的闹铃将继续触发 — 但系统会在这些闹铃触发之前不久退出低电耗模式。
这时候可以使用setAndAllowWhileIdle(int type, long triggerAtMillis, PendingIntent operation)
取消设置的接口
对应到设置的方法,cancel两种类型的超时处理方法
/**
* Remove any alarms with a matching {@link Intent}.
* Any alarm, of any type, whose Intent matches this one (as defined by
* {@link Intent#filterEquals}), will be canceled.
*
* @param operation IntentSender which matches a previously added
* IntentSender. This parameter must not be {@code null}.
*
* @see #set
*/
public void cancel(PendingIntent operation) {
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.remove(operation, null);
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}
}
/**
* Remove any alarm scheduled to be delivered to the given {@link OnAlarmListener}.
*
* @param listener OnAlarmListener instance that is the target of a currently-set alarm.
*/
public void cancel(OnAlarmListener listener) {
if (listener == null) {
throw new NullPointerException("cancel() called with a null OnAlarmListener");
}
ListenerWrapper wrapper = null;
synchronized (AlarmManager.class) {
if (sWrappers != null) {
wrapper = sWrappers.get(listener);
}
}
if (wrapper == null) {
Log.w(TAG, "Unrecognized alarm listener " + listener);
return;
}
wrapper.cancel();
}
3 AlarmManagerService
AlarmManagerService提供接口给AlarmManager,实现上述接口,另外也实现了系统对于Alarm的处理逻辑,包括一些优化
从Android 4.4开始 Alarm默认是非精确的,也可以指定采用精确模式。在非精确模式下,Alarm是批量提醒的,每个Alarm会被根据其触发时间以及最大触发时间,加入到不同的batch中去,同一个batch中的alarm会同时触发,这也是low power的优化。对于
frameworks/base/services/core/java/com/android/server/AlarmManagerService.java
SET
setImpl 是最常用的方法
比如setAndAllowWhileIdle的实现
public void setAndAllowWhileIdle(@AlarmType int type, long triggerAtMillis,
PendingIntent operation) {
setImpl(type, triggerAtMillis, WINDOW_HEURISTIC, 0, FLAG_ALLOW_WHILE_IDLE,
operation, null, null, null, null, null);
}
setImpl() 会把RTC类型的转换成ELAPSED_REALTIME类型,也就是说最终到底层的只有 ELAPSED_REALTIME_WAKEUP和ELAPSED_REALTIME, 也就是id 2和3
final long nominalTrigger = convertToElapsed(triggerAtTime, type);
其调用流程如下
setImpl()
setImplLocked()
setImplLocked()
rescheduleKernelAlarmsLocked()
setlocked()
set()
Service处理逻辑
在添加alarm的时候,比如先添加了一个15s的wakeup alarm(假设其就是下一个wakeup类型的alarm),再添加一个10s的wakeup alarm
在调用进行到 setImplLocked
的时候, 15s的alarm所在的batch已在mAlarmBatches 数组中,这个时候来了10s的,那么10s的batch就会位于mAlarmBatches中15s的前面,之后调用rescheduleKernelAlarmsLocked(), 进而把位于第一个的10s设置下去
如果是先设15s的后面来了一个20s的,实际上20s的在这一次是不会被设置下去的,需要等到其前面的超时之后,alarm thread遍历20s的这个已经排在最前,才会被设置下去
private void setImplLocked(int type, long when, long whenElapsed, long maxWhen, long interval,
PendingIntent operation, boolean isStandalone, boolean doValidate,
WorkSource workSource) {
/**创建一个alarm,其中各参数的含义如下:
* type 闹钟类型 ELAPSED_REALTIME、RTC、RTC_WAKEUP等
* when 触发时间 UTC类型,绝对时间,通过System.currentTimeMillis()得到
* whenElapsed 相对触发时间,自开机算起,含休眠,通过SystemClock.elapsedRealtime()得到
* maxWhen 最大触发时间
* interval 触发间隔,针对循环闹钟有效
* operation 闹钟触发时的行为,PendingIntent类型
*/
Alarm a = new Alarm(type, when, whenElapsed, maxWhen, interval, operation, workSource);
//根据PendingIntent删除之前已有的同一个闹钟
removeLocked(operation);
boolean reschedule;
//尝试将alarm加入到合适的batch中,如果alarm是独立的或者无法找到合适的batch去容纳此alarm,返回-1
int whichBatch = (isStandalone) ? -1 : attemptCoalesceLocked(whenElapsed, maxWhen);
if (whichBatch < 0) {
//没有合适的batch去容纳alarm,则新建一个batch
Batch batch = new Batch(a);
batch.standalone = isStandalone;
//将batch加入mAlarmBatches中,并对mAlarmBatches进行排序:按开始时间升序排列
reschedule = addBatchLocked(mAlarmBatches, batch);
} else {
//如果找到合适了batch去容纳此alarm,则将其加入到batch中
Batch batch = mAlarmBatches.get(whichBatch);
//如果当前alarm的加入引起了batch开始时间和结束时间的改变,则reschedule为true
reschedule = batch.add(a);
if (reschedule) {
//由于batch的起始时间发生了改变,所以需要从列表中删除此batch并重新加入、重新对batch列表进行排序
mAlarmBatches.remove(whichBatch);
addBatchLocked(mAlarmBatches, batch);
}
}
if (DEBUG_VALIDATE) {
if (doValidate && !validateConsistencyLocked()) {
Slog.v(TAG, "Tipping-point operation: type=" + type + " when=" + when
+ " when(hex)=" + Long.toHexString(when)
+ " whenElapsed=" + whenElapsed + " maxWhen=" + maxWhen
+ " interval=" + interval + " op=" + operation
+ " standalone=" + isStandalone);
rebatchAllAlarmsLocked(false);
reschedule = true;
}
}
if (reschedule) {
rescheduleKernelAlarmsLocked();
}
}
void rescheduleKernelAlarmsLocked() {
// Schedule the next upcoming wakeup alarm. If there is a deliverable batch
// prior to that which contains no wakeups, we schedule that as well.
long nextNonWakeup = 0;
if (mAlarmBatches.size() > 0) {
final Batch firstWakeup = findFirstWakeupBatchLocked();
final Batch firstBatch = mAlarmBatches.get(0);
final Batch firstRtcWakeup = findFirstRtcWakeupBatchLocked();
if (firstWakeup != null && mNextWakeup != firstWakeup.start) {
mNextWakeup = firstWakeup.start;
mLastWakeupSet = SystemClock.elapsedRealtime();
setLocked(ELAPSED_REALTIME_WAKEUP, firstWakeup.start);
}
CANCEL
cancel方法都会调用到AlarmManagerService中的 remove
boolean remove(final PendingIntent operation, final IAlarmListener listener)
相比与set,cancel并没有直接调用到底层的所谓cancel的api,kernel也没有提供cancel的接口
假设设置了一个10s的wakeup alarm, 在超时之前cancel,实际上是从超时的执行列表中移除10s alarm对应的处理方法,那这个alarm是怎么处理的呢?
-
首先会从batch中移除这个alarm
-
alarm thread 会调用到
rescheduleKernelAlarmsLocked()
把移除的这个alarm的下一个作为next,重新写到底层
4 JNI 层
frameworks/base/services/core/jni/com_android_server_AlarmManagerService.cpp
在JNI层,可以看到id 2和3对应的分别是 CLOCK_BOOTTIME_ALARM 和 CLOCK_BOOTTIME
static const clockid_t android_alarm_to_clockid[N_ANDROID_TIMERFDS] = {
CLOCK_REALTIME_ALARM,
CLOCK_REALTIME,
CLOCK_BOOTTIME_ALARM,
CLOCK_BOOTTIME,
CLOCK_MONOTONIC,
CLOCK_REALTIME,
};
调用timerfd的接口设置下去
int AlarmImpl::set(int type, struct timespec *ts)
{
if (static_cast<size_t>(type) > ANDROID_ALARM_TYPE_COUNT) {
errno = EINVAL;
return -1;
}
if (!ts->tv_nsec && !ts->tv_sec) {
ts->tv_nsec = 1;
}
/* timerfd interprets 0 = disarm, so replace with a practically
equivalent deadline of 1 ns */
struct itimerspec spec;
memset(&spec, 0, sizeof(spec));
memcpy(&spec.it_value, ts, sizeof(spec.it_value));
return timerfd_settime(fds[type], TFD_TIMER_ABSTIME, &spec, NULL);
}
5 Linux Kernel 层
很核心的概念就是对于alarm timer 或者是hrtimer,用户空间只能设置设置一个下来,新的会把旧的覆盖掉, 比如你通过AlarmManager设置了一个15s以后的wakeup 类型的alrm事件,再设置一个10s以后的,那么timerfd会把先前设置的15s的cancel掉,再设置10s的
timerfd
~/fs/timerfd.c
timerfd_settime 这个系统调用内核的调用路径如下, 根据类型的不同,分为alarm和hrtimer
do_timerfd_settime()
timerfd_setup()
alarm_init() ~/kernel/time/alarmtimer.c
alarm_start()
hrtimer_init() ~/kernel/time/hrtimer.c
hrtimer_start()
static int do_timerfd_settime(int ufd, int flags,
const struct itimerspec *new,
struct itimerspec *old)
{
struct fd f;
struct timerfd_ctx *ctx;
int ret;
if ((flags & ~TFD_SETTIME_FLAGS) ||
!timespec_valid(&new->it_value) ||
!timespec_valid(&new->it_interval))
return -EINVAL;
ret = timerfd_fget(ufd, &f);
if (ret)
return ret;
ctx = f.file->private_data;
if (!capable(CAP_WAKE_ALARM) && isalarm(ctx)) {
fdput(f);
return -EPERM;
}
timerfd_setup_cancel(ctx, flags);
/*
* We need to stop the existing timer before reprogramming
* it to the new values.
*/
for (;;) {
spin_lock_irq(&ctx->wqh.lock);
if (isalarm(ctx)) {
if (alarm_try_to_cancel(&ctx->t.alarm) >= 0)
break;
} else {
if (hrtimer_try_to_cancel(&ctx->t.tmr) >= 0)
break;
}
spin_unlock_irq(&ctx->wqh.lock);
cpu_relax();
}
/*
* If the timer is expired and it's periodic, we need to advance it
* because the caller may want to know the previous expiration time.
* We do not update "ticks" and "expired" since the timer will be
* re-programmed again in the following timerfd_setup() call.
*/
if (ctx->expired && ctx->tintv.tv64) {
if (isalarm(ctx))
alarm_forward_now(&ctx->t.alarm, ctx->tintv); //如果是Alarm timer类型,先cancel前一个alarm类型的
else
hrtimer_forward_now(&ctx->t.tmr, ctx->tintv);
}
old->it_value = ktime_to_timespec(timerfd_get_remaining(ctx));
old->it_interval = ktime_to_timespec(ctx->tintv);
/*
* Re-program the timer to the new value ...
*/
ret = timerfd_setup(ctx, flags, new); //开始设置新的timer
spin_unlock_irq(&ctx->wqh.lock);
fdput(f);
return ret;
}
alarm timer
AlarmTimer 参考https://www.cnblogs.com/arnoldlu/p/7145879.html 这一篇就好
值得注意的是 alarmtimer_suspend方法,在进入睡眠之前,遍历alarm_bases->timerqueue,取最近一次timer的expires;将此expires写入RTC定时器,RTC超时后会唤醒系统。
static int alarmtimer_suspend(struct device *dev)
{
...
rtc = alarmtimer_get_rtcdev();-------------------------------------获取RTC设备
/* If we have no rtcdev, just return */
if (!rtc)
return 0;
/* Find the soonest timer to expire*/
for (i = 0; i < ALARM_NUMTYPE; i++) {------------------------------遍历ALARM_REALTIME和ALARM_BOOTTIME两个alarm_base,取各自最近expires
struct alarm_base *base = &alarm_bases[i];
struct timerqueue_node *next;
ktime_t delta;
spin_lock_irqsave(&base->lock, flags);
next = timerqueue_getnext(&base->timerqueue);
spin_unlock_irqrestore(&base->lock, flags);
if (!next)
continue;
delta = ktime_sub(next->expires, base->gettime());
if (!min.tv64 || (delta.tv64 < min.tv64))---------------------比较两次expires,取最小者
min = delta;
}
if (min.tv64 == 0)
return 0;
if (ktime_to_ns(min) < 2 * NSEC_PER_SEC) {-----------------------如果expires小于2秒,保持系统唤醒2秒,并中断suspend流程。
__pm_wakeup_event(ws, 2 * MSEC_PER_SEC);
return -EBUSY;
}
/* Setup an rtc timer to fire that far in the future */
rtc_timer_cancel(rtc, &rtctimer);-------------------------------取消当前rtctimer
rtc_read_time(rtc, &tm);
now = rtc_tm_to_ktime(tm);
now = ktime_add(now, min);--------------------------------------获取RTC时间,将rtc_timer转换成ktimer_t,将RTC时间加上AlarmTimer超时。
/* Set alarm, if in the past reject suspend briefly to handle */
ret = rtc_timer_start(rtc, &rtctimer, now, ktime_set(0, 0));---设置rtctimer
if (ret < 0)
__pm_wakeup_event(ws, 1 * MSEC_PER_SEC);
return ret;
}
参考文章
https://www.cnblogs.com/leipDao/p/8203684.html
https://www.cnblogs.com/arnoldlu/p/7145879.html
https://blog.csdn.net/wh_19910525/article/details/44303039
更多推荐
所有评论(0)