alarm机制概览

常用消息机制

安卓有很多消息传递机制,基于不用使用场景,我们会采用对应的消息传递机制,而无论是那种消息传递机制,都是按照消息发送者和消息接收者这个进行设计的,以下是我们在不同场景下会选择的消息传送机制;

同一app内部的同一组件内的消息通信(单个或多个线程之间) ;

常用方法:Message Handler/Message Loop

原理图:

1562654136209

MessageQueue流程:

  1. Looper 通过addFd 添加文件管道监听和事件回调;
  2. 当文件监听到消息后,handler通过dispatchMessage进行消息派发,Looper->sendMessage;
  3. 收到消息后走到Handler的handleMessgae,进行消息处理

同一app内部的不同组件之间的消息通信(单个进程)

常用方法:EventBus

原理图:

1562053083623

EventBus流程:

还没有接触使用过,暂时不讲,是安卓的一种轻量级的事件通知机制;

其他

  1. 同一app具有多个进程的不同组件之间的消息通信

  2. 不同app之间的组件之间消息通信

  3. Android 系统在特定情况下与App之间的消息通信

常用方法:PendingIntent

原理图:

** 1562056294011

BroadCast流程:

  1. Intentfliterreceiver绑定,可通过静态方式:在AndroidManifest.xml 进行配置,也可以动态方式registerReceiver
  2. Brastcast先打包Intent,然后通过sendBrocast,进行广播;
  3. recevier接受到对应的广播在onReceive中走处理流程;

闹钟app也是依托这些广播的机制进行运作;以下我们会对闹钟设置与闹钟响应的流程进行简单的介绍;

DeskClock APP

代码路径:packages/apps/DeskClock

设置闹钟流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
当用户点击选择相应的闹钟后:
TimePickerDialogFragment.java
public void onClick(DialogInterface dialog, int which) {
listener.onTimeSet(TimePickerDialogFragment.this,
timePicker.getCurrentHour(), timePicker.getCurrentMinute());
}
添加对应闹钟:
src/com/android/deskclock/alarms/AlarmTimeClickHandler.java
asyncAddAlarm(a)
// Create and add instance to db
setupAlarmInstance(newAlarm)
这里会做两件重要的事情:
一:Pending获取和闹钟注册,
private static class AlarmManagerStateChangeScheduler implements StateChangeScheduler {
@Override
public void scheduleInstanceStateChange(Context context, Calendar time,
AlarmInstance instance, int newState) {
final long timeInMillis = time.getTimeInMillis();
LogUtils.i("Scheduling state change %d to instance %d at %s (%d)", newState,
instance.mId, AlarmUtils.getFormattedTime(context, time), timeInMillis);
final Intent stateChangeIntent =
createStateChangeIntent(context, ALARM_MANAGER_TAG, instance, newState);
// Treat alarm state change as high priority, use foreground broadcasts
stateChangeIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
PendingIntent pendingIntent = PendingIntent.getService(context, instance.hashCode(),
stateChangeIntent, PendingIntent.FLAG_UPDATE_CURRENT);

final AlarmManager am = (AlarmManager) context.getSystemService(ALARM_SERVICE);
if (Utils.isMOrLater()) {
// Ensure the alarm fires even if the device is dozing.
am.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, timeInMillis, pendingIntent);//这里就能让闹钟跟pendingIntent进行绑定,闹钟来临时就会广播Intent中的Action
} else {
am.setExact(AlarmManager.RTC_WAKEUP, timeInMillis, pendingIntent);
}
}


当createStateChangeIntent的时候,就会将服务与Intent进行绑定
public static Intent createStateChangeIntent(Context context, String tag,
AlarmInstance instance, Integer state) {
...
Intent intent = AlarmInstance.createIntent(context, AlarmService.class, instance.mId);
intent.setAction(CHANGE_STATE_ACTION);
...
}


二:updateNextAlarm(context) 将用户时钟更新到数据库中,以便保存闹钟信息;这个时间会被保存到settings里面;这样的好处是重新启动后信息不会丢失;确保闹钟能收到闹钟信息;

pendingIntent:等待着的Intent获取pendingIntent有以下方法,getActivity:(用于启动一个Activity的pendingIntent); getBroadcast(方法从系统取得一个用于向BrocastReceiver的Intent广播的PendingIntent); getService(用于启动一个Service的pendingIntent)分别对应着3个行为,跳转到一个activity组件,打开一个广播组件和打开一个服务组件;

如上发起端DeskClock通过PendingIntent.getService会在到在AMS中得到PendingIntentRecord对象,当对应的闹钟事件到来时,AlarmManagerService会通过传递的PendingIntent对象中的send方法发起回调,从而做出执行对应的动作;

通过am的set方法设置后,当闹钟时间来临的时候;就会触发下面的响应流程;

闹钟响应流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

当闹钟来临的时候,就会调到AlarmService的onStartCommand
public int onStartCommand(Intent intent, int flags, int startId) {
...
case AlarmStateManager.CHANGE_STATE_ACTION:
AlarmStateManager.handleIntent(this, intent);
startAlarm(instance);
...
}

private void startAlarm(AlarmInstance instance) {
...
AlarmAlertWakeLock.acquireCpuWakeLock(this);
//当处于后台,此时就会出现我们常见的界面全屏的闹钟提示;
AlarmNotifications.showAlarmNotification(this, mCurrentAlarm);
sendBroadcast(new Intent(ALARM_ALERT_ACTION));
...
}

注意事项:

一:闹钟响应流程中睡眠

从DeskClock应用行为我们也可以看到在应用写闹钟处理函数的时候,需要进行持锁,尤其在耗时较长的操作中;避免还没有进入处理函数,就因为autoSleep进程没有disable从而进入休眠;另外对于在休眠过程中需要唤醒系统的应用来说setExactAndAllowWhileIdle,旧版本setExact在系统处于Doze的时候,同样会被忽略;

二:setForeground问题:

当API从25升至28时,会出现前台服务设置异常问题,修复如下:

一:针对Permission Denial: startForeground ,AndroidManifest.xml需添加属性

1
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

二:针对invalid channel for service notification,需进行channel创建,如下所示:

1
2
3
4
5
6
7
8
9
10
NotificationChannel notificationChannel = new NotificationChannel(
DESKCLOCK_NOTIFICATION_CHANNEL,
"deskclock",
NotificationManager.IMPORTANCE_HIGH);

NotificationCompat.Builder notification = new NotificationCompat.Builder(service,DESKCLOCK_NOTIFICATION_CHANNEL)

NotificationManager notificationManager =
(NotificationManager) service.getSystemService(Service.NOTIFICATION_SERVICE);
notificationManager.createNotificationChannel(notificationChannel);

三:应用在后台的时候PendingIntent 无法startService,所以上述代码流程闹钟应用在后台是不正常的;

  1. android提供了一套JobIntentService的机制,对于响应不是很及时的可以改成这种接口;
  2. 可将PendingIntent改写为BrocastRecevicer然后在OnReceive中startForegroundService将service带到前台;前台服务的创建需要注意两个细节,可参考此文档Android8.0使用通知创建前台服务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
一:Pending改成以下形式
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, instance.hashCode(),
stateChangeIntent, PendingIntent.FLAG_UPDATE_CURRENT);
二:在广播中开启服务startForegroundService
public class AlarmReceiver extends BroadcastReceiver {


@Override
public void onReceive(Context context, Intent intent) {
final int alarmState = intent.getIntExtra(AlarmStateManager.ALARM_STATE_EXTRA, -1);
final long instanceId = AlarmInstance.getId(intent.getData());
Intent in = AlarmInstance.createIntent(context, AlarmService.class, instanceId);
.....
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
{
context.startForegroundService(in);
}
else{
context.startService(in);
}
}
}
三:在服务中startForeground
public int onStartCommand(Intent intent, int flags, int startId) {
...
if(Build.VERSION.SDK_INT>=26){
setForeground();
}
...
}

AlarmManager

主要代码分布:

core/java/android/app/AlarmManager.java

services/core/java/com/android/server/AlarmManagerService.java

services/core/jni/com_android_server_AlarmManagerService.cpp

安卓是怎么实现对闹钟进行监听和注册,分发的呢?

闹钟的监听

SystemServerstartOtherServices时,会调用SystemServiceManager中的startService,此时AlarmManagerService中的onStart函数就会被调用;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public void onStart() {
//调用native的init
mNativeData = init();
//因为内核不会保存时区,所以每次重启都需要重新设置
setTimeZoneImpl(SystemProperties.get(TIMEZONE_PROPERTY));
//如果获取到的时间小于固件生成的时间,那么利用ro.build.date.utc的时间去更新内核RTC的时间;
//没有的话不是则按照系统当前时间即可;这样能保证起来的系统时间是比较新的
setKernelTime(systemBuildTime);
//注册闹钟服务,用来接收DATE_CHANGED,更新日历时间
mClockReceiver = mInjector.getClockReceiver(this);
//监听量灭屏广播,
new InteractiveStateReceiver();
//监听应用协助重启广播
new UninstallReceiver();
//启动闹钟线程
AlarmThread waitThread = new AlarmThread();
waitThread.start();
//发布相关服务,安卓为了提升通信效率。将service分成binder service和local service
//(用于本进程通信)对应的获取接口为getLocalService(AlarmManagerInternal.class)
publishLocalService(AlarmManagerInternal.class, new LocalService());
//非本进程接口,(AlarmManager) getContext().getSystemService(Context.ALARM_SERVICE)
publishBinderService(Context.ALARM_SERVICE, mService);
}

在start初始化的时候,我们主要注意的是init流程里面建立的对alarm事件的监听,以及在AlarmThread里面对闹钟时间的响应;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

static jlong android_server_AlarmManagerService_init(JNIEnv*, jobject){
...
TimerFds fds;
epollfd = epoll_create(fds.size());
fds[i] = timerfd_create(android_alarm_to_clockid[i], TFD_NONBLOCK);
//wall_clock这里会支持的RTCid,这个rtc可以用来距离启动和休眠时的时间
static const clockid_t android_alarm_to_clockid[N_ANDROID_TIMERFDS] = {
CLOCK_REALTIME_ALARM, //对应安卓闹钟type:RTC_WAKEUP 常用于应用闹钟;
// A settable system-wide real-time clock
CLOCK_REALTIME, //对应安卓闹钟type:RTC

CLOCK_BOOTTIME_ALARM, //ELAPSED_REALTIME_WAKEUP //用于计时器,因为不能随系统时间更改
//单调递增的时钟,包括休眠时候的时间
CLOCK_BOOTTIME, //ELAPSED_REALTIME

//单调递增的时钟,不包括休眠时候的时间,这种没有看到安卓有进行使用
CLOCK_MONOTONIC,

//We also need an extra CLOCK_REALTIME fd which exists specifically to be
//canceled on RTC changes.
CLOCK_REALTIME,
};

简单而言,就是创建了对这几种闹钟类型的监听,但允许设置的时钟类型仅有4种;
然后在waitForAlarm中等待这几类闹钟事件上报,并上报有闹钟时间的类型
int AlarmImpl::waitForAlarm()
{
....
int nevents = epoll_wait(epollfd, events, N_ANDROID_TIMERFDS, -1);
//如果闹钟在底层被取消了,返回闹钟事件需要更新,没有的话则返回闹钟的类型;
ssize_t err = read(fds[alarm_idx], &unused, sizeof(unused));
...
}

闹钟的注册

接回apk设置alarm流程往下梳理注册流程:

1
2
3
4
apk调用:
setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, timeInMillis, pendingIntent);

setImpl(type, triggerAtMillis, WINDOW_EXACT, 0, FLAG_ALLOW_WHILE_IDLE, operation, null, null, null, null, null);

我们这里主要关注两个参数:flag和type

flag讲解:

flag主要影响的是idle状态是否需要唤醒

1
2
3
4
1.FLAG_STANDALONE = 1;用于标识该alarm不会被加入到其他alarm集合中去;
2.FLAG_WAKE_FROM_IDLE = 2;当系统处于Idle状态仍然能唤醒系统;
3.FLAG_ALLOW_WHILE_IDLE = 4;在idle下alarm仍会被执行,并且不会退出idle状态;
4.FLAG_ALLOW_WHILE_IDLE_UNRESTRICTED = 8;与3类似,但运行不受任何约束

TYPE讲解:

应用能设置的类型为:RTC_WAKEUP,RTC,ELAPSED_REALTIME_WAKEUP,ELAPSED_REALTIME.这四种;

但是在AlarmManagerService会RTC类型的时间进行转换,convertToElapsed,最终设置的类型转换成ELAPSED_REALTIME类型,所以将闹钟的类型只有ELAPSED_REALTIME_WAKEUP,ELAPSED_REALTIME;

1563011053168

流程总结:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
1.set流程里面会权限进行检查,根据是否为系统应用或者白名单里面的应用,对flag进行调整;
2.setImpl
1.闹钟的时间间隔不得低于5s
2.convertToElapsed(triggerAtTime, type),将绝对时间转换为相对时间,也即是开机时间
3.比对窗口时间,如果窗口时间为0,精确执行,小于0,通过当前时间减去触发时间得到一个窗口时间,如果大于0则把最早触发时间+窗口时间为最晚的时间;
3.setImplLocked
1.removeLocked(operation, directReceiver)
2.构造一个逻辑闹钟alarm,setImplLocked(a, false, doValidate)
4.setImplLocked
1.如果是DeviceIdleController(管理doze模式)设置的闹钟,会被模糊提前;
2.adjustDeliveryTimeBasedOnBucketLocked
androidP对应用进行了分组,
1.活跃:用户正在使用;闹钟延迟0min
2.工作集:经常在运行,但并未处于活跃状态;延迟6min;
3.常用:应用会被定期使用,但不是每天都必须使用;30min;
4.极少使用:应用不经常使用;2h;
5.从未使用;10d
3.insertAndBatchAlarmLocked
1.如果是带有FLAG_STANDALONE标志的,新建一个Batch;
2.如果不是的话会根据两个闹钟设置的时TriggerTime和最大允许的闹钟发生时间(max triigertime);两个 之间取交集(narrow batch)作为闹钟触发时间如果找不到则继续新建;
4.rescheduleKernelAlarmsLocked
从Batch list里面找到近的闹钟,通过timerfd设置到内核;
5.updateNextAlarmClockLocked
更新用户下一次的闹钟时间

如图为batch的形成过程:

1562911201925

闹钟时间分发

对于时间事件的分发,我们主要看AlarmThread

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private class AlarmThread extends Thread
{
while (true)
{
int result = waitForAlarm();
if (WAKEUP_STATS) {
...
//可以记录一天内的闹钟唤醒历史,可以用来统计闹钟优化的程度;
recordWakeupAlarms(mAlarmBatches, nowELAPSED, nowRTC);
...
}
//并获取到点闹钟列表,根据当前是否处于Doze模式或者app standby更新闹钟的响应时间
boolean hasWakeup = triggerAlarmsLocked(triggerList, nowELAPSED);
//将获取到的闹钟进行派发
deliverAlarmsLocked(triggerList, nowELAPSED);
这几个在set的时候也会被调用不再赘述
reorderAlarmsBasedOnStandbyBuckets(triggerPackages);
rescheduleKernelAlarmsLocked();
updateNextAlarmClockLocked();
}
}

Alarm消息的传递流程如下所示,如果设置的时候传入的是PendingIntent则会进入send流程,如果设置的时候传入的是Listener那么就会走到onAlarm的流程;

1563105032995

内核闹钟流程

当用户层设置闹钟的时候,内核不会马上设置到RTC寄存器;而是在suspend的时候从队列里面取出最早的时钟,然后设置进去;通过trace我们就能直接看出这种关系来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
timerfd_settime
do_timerfd_settime
timerfd_setup
alarm_start
alarmtimer_enqueue(base, alarm);
timerqueue_add(&base->timerqueue, &alarm->node);
3) | alarm_start() {
3) 2.125 us | alarmtimer_enqueue();
3) + 16.500 us | }
3) | alarm_start() {
3) 3.291 us | alarmtimer_enqueue();
3) + 21.791 us | }
alarmtimer_suspend() {
rtc_timer_start() {
rtc_timer_enqueue() {
__rtc_set_alarm() {
sunxi_rtc_setalarm();
}
}

关机闹钟的实现

当梳理完应用到内核的完整路径后,我们就可以借助这套机制完成我们的所要的需求;

这里主要利用的有两点:

一:DeskClock这类的闹钟应用在设置闹钟的时候会把AlarmClockInfo闹钟信息保存在系统中;此时,我们会将闹钟信息保存,当关机的时候再设置到RTC寄存器中;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
 private void updateNextAlarmInfoForUserLocked(int userId,
AlarmManager.AlarmClockInfo alarmClock) {
if (alarmClock != null) {
if (DEBUG_ALARM_CLOCK) {
Log.v(TAG, "Next AlarmClockInfoForUser(" + userId + "): " +
formatNextAlarm(getContext(), alarmClock, userId));
}
mNextAlarmClockForUser.put(userId, alarmClock);
+ mShutdownReceiver.setTime(alarmClock.getTriggerTime()/1000 - 90);
} else {
if (DEBUG_ALARM_CLOCK) {
Log.v(TAG, "Next AlarmClockInfoForUser(" + userId + "): None");
}
mNextAlarmClockForUser.remove(userId);
+ mShutdownReceiver.setTime(0);
}

mPendingSendNextAlarmClockChangedForUser.put(userId, true);
mHandler.removeMessages(AlarmHandler.SEND_NEXT_ALARM_CLOCK_CHANGED);
mHandler.sendEmptyMessage(AlarmHandler.SEND_NEXT_ALARM_CLOCK_CHANGED);
}

class ShutdownReceiver extends BroadcastReceiver {
private long mTime = 0;
public ShutdownReceiver() {
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_SHUTDOWN);
getContext().registerReceiver(this, filter);
updateNextRtcAlarm(0);
}

@Override
public void onReceive(Context context, Intent intent) {
Slog.i(TAG, "AlarmManagerService receive shutting down set rtc alarm time: " + mTime);
synchronized (mLock) {
updateNextRtcAlarm(mTime);
}
}

public void setTime(long time) {
mTime = time;
}
}

二:timerfd和epoll_wait,在关机充电的时候,需要判断关机时间到来,当闹钟时间到的时候;我们会重启系统;进入android;

关键代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
void *alarm_thread_handler(void *arg)
{
(void)arg;
int ret = 0;
struct epoll_event events[EPOLL_LISTEN_CNT];
struct timeval now_tv = { 0, 0 };
while (true) {
//等待闹钟事件来临,重启系统
int nevents = epoll_wait(epollfd, events, EPOLL_LISTEN_CNT, -1);
if (nevents < 0) {
LOG(WARNING) << __func__ <<" ++++"<< __LINE__ << " event:"<< nevents<< "errno: "<< errno << "\n";
continue;
}
unsigned long long wakeups;

if (read(wakealarm_fd, &wakeups, sizeof(wakeups)) == -1) {
LOGE("wakealarm_event: read wakealarm fd failed\n");
continue;
}
gettimeofday(&now_tv, NULL);
LOG(WARNING) << __func__ <<" " << "rebooting" << "now" << now_tv.tv_sec <<"\n";
request_suspend(false);
reboot(RB_AUTOBOOT);
}

return NULL;
}
static void init_shutdown_alarm(void)
{
long alarm_secs, alarm_in_booting = 0;
struct timeval now_tv = { 0, 0 };
struct timespec ts;
struct epoll_event ev;
//获取闹钟时间
alarm_secs = get_wakealarm_sec();
// have alarm irq in booting ?
alarm_in_booting = is_alarm_in_booting();
gettimeofday(&now_tv, NULL);

LOG(WARNING) << "alarm_in_booting: "<< alarm_in_booting << "alarm_secs " << alarm_secs << "now" << now_tv.tv_sec << "\n";
// alarm interval time == 0 and have no alarm irq in booting
if (alarm_secs <= 0 && (alarm_in_booting != 1))
return;
if (alarm_secs)
ts.tv_sec = alarm_secs;
else
ts.tv_sec = (long)now_tv.tv_sec + 1;

ts.tv_nsec = 0;

struct itimerspec spec;
memset(&spec, 0, sizeof(spec));
memcpy(&spec.it_value, &ts, sizeof(spec.it_value));

//timerfd_init
wakealarm_fd = timerfd_create(CLOCK_REALTIME_ALARM, 0);
if (wakealarm_fd <= 0) {
LOGE("%s, %d, alarm_fd=%d and exit\n", __func__, __LINE__, wakealarm_fd);
return ;
}

if (timerfd_settime(wakealarm_fd, TFD_TIMER_ABSTIME, &spec, NULL) == -1){
LOGE("timerfd_settime failed Error[%d:%s]\n",errno,strerror(errno));
close(wakealarm_fd);
return ;
};
//epoll_init
epollfd = epoll_create(EPOLL_LISTEN_CNT);
if (epollfd > 0) {
ev.events = EPOLLIN | EPOLLWAKEUP;
//add wakealarm_fd to epollfd
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, wakealarm_fd, &ev) == -1) {
LOGE("epoll_ctl failed; errno=%d\n", errno);
return;
}
} else {
LOGE("epoll_create failed; errno=%d\n", errno);
return;
}
pthread_create(&tid_alarm, NULL, alarm_thread_handler, NULL);
return;
}

其他

系统时间设置与RTC时间

1
2
3
4
5
6
7
8
9
10
11
12
在启动或者联网的时候,安卓会帮我们将网络时间设置到RTC中,前面的过程我们先跳过直接讲最后的
在framework/base/services/core/jni/com_android_server_AlarmManagerService.cpp中
1.通过setKernelTimeZone将时区设置到内核 通过getprop persist.sys.timezone 可以获取设置的时区,如无则默认没有0时区
2.通过setKernelTime更新时间到RTC中
设置的流程如下:
setKernelTime
-->setTime
setTimeofday设置墙上时间
gmtimer_r 将当前时间转换成格林威治时间;
ioctl(fd, RTC_SET_TIME, &rtc)将转化后的格林威治时间写入RTC中
所以当我们看cat /proc/driver/rtc时间的时候,就是当前时间与时区计算到的格林时间,
在看这个rtc节点的时候会出现与系统时间差上时区;

代办

谷歌闹钟管理优化梳理;

参考资料

Android组件系列–Intent详解

Intent的基本使用

Android消息机制,从java层到Native层剖析

Android事件总线(二)EventBus3.0源码解析

Android BroadcastReceiver使用详解

后台执行限制

Android广播机制

Google出品的序列化神奇Protocol Buffer使用攻略

理解AlarmManager机制

说说PendingIntent的内部机制