问题起因
今天,用户群里有人反馈一个Bug,说是安卓手机预定车辆后将app切换到后台,预定倒计时会暂停。app中倒计时是自定义了一个TextView,然后在其内部封装了一个handler,通过handler延迟发送msg实现实时更新倒计时文字,理论上只要activity不被回收,handler在后台也是可以运行的,即便activity被回收,切换到前台重新请求接口,倒计时还是会自动校准,那么为什么用户的手机倒计时会暂停呢?我们起初先在公司的几个测试机上验证这个bug,然而发现一切运行正常,为了考虑各种情况,我还模拟了手机内存不足情况,但发现一切运行良好。然后我怀疑可能是用户描述不正确,直到用户给我们的测试小姐姐截图为证,我才回头认真想是否是代码的问题?
问题分析
首先第一时间想到了是否可能是进程被挂起或手机休眠导致倒计时不走了,但测试了这么多手机都没有问题,然后就google了一下发现 CPU 休眠确实会导致Timer、各种轮询方式失效。然后就想了一下如何使 CPU 休眠来复现这个问题,终于,在试了五六个手机之后发现在 oppo r9s 上开启省电模式会导致倒计时变慢,跟用户的问题是一样的。在 oppo 手机开启省电模式后,将app切到后台然后锁屏,过了30秒钟再打开手机将应用切到前台,发现倒计时只走了7秒,等于说少走了23秒,所以显而易见, oppo 的rom对手机电量优化后锁屏约几秒钟后就会使cpu休眠。问题找到了,那么handler内部延迟消息是如何发送的,为什么会因为CPU休眠而导致发送的晚了?下面我们看代码:
for (;;) {
Message msg = queue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}
// do something
}
looper循环中取消息会调用MessageQueue的next方法,接下来看next方法:
final long now = SystemClock.uptimeMillis();
// do something
if (msg != null) {
if (now < msg.when) {
// Next message is not ready. Set a timeout to wake up when it is ready.
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
// Got a message.
mBlocked = false;
if (prevMsg != null) {
prevMsg.next = msg.next;
} else {
mMessages = msg.next;
}
msg.next = null;
if (DEBUG) Log.v(TAG, "Returning message: " + msg);
msg.markInUse();
return msg;
}
}
对于延迟消息,有一个处理的时间点(when属性)。在取消息时,会判断消息是否到期,就是使用当前时间now和消息when属性进行比较,这里没什么问题,有问题的是获取当前时间(now)的方法:SystemClock.uptimeMillis(),下面看源码如何描述这个方法的:
/**
* Returns milliseconds since boot, not counting time spent in deep sleep.
*
* @return milliseconds of non-sleep uptime since boot.
*/
@CriticalNative
native public static long uptimeMillis();
uptimeMillis是个native方法,我们直接看方法的注释:Returns milliseconds since boot, not counting time spent in deep sleep,即这个函数获取的是自系统启动以来的时间,但不包括系统休眠的时间。比如我发送一个延迟300秒的消息,但cpu在第50秒的时候休眠了,又过了100秒我打开手机将app切换到前台,这个时候cpu被唤醒了,那么休眠的这100秒就不算在uptimeMillis()方法的时间内,等于说实际上隔了400 handler才会收到消息,所以这就是用户反映倒计时暂停的根本原因。
问题修复
手机为什么会休眠呢?当然是为了省电,关于Android的休眠机制,推荐大家看这篇文章,在这里我就不详细介绍了!Android休眠机制
那么针对手机休眠,我们的倒计时如何实现呢?方案有两种,WakeLock和AlarmManager。
Wake Lock是android电源管理中很重要的机制。它是一种锁的机制, 只要有任务拿着这个锁, 系统就无法进入休眠, 可以被用户态进程和内核线程获得。这个锁可以是有超时的或者是没有超时的, 超时的锁会在时间过去以后自动解锁。如果没有锁了或者超时了, 内核就会启动标准linux的那套休眠机制机制来进入休眠。
AlarmManager是系统时钟管理器,说到休眠,就必须讲到AlarmManager。系统在进入休眠后,进程suspend,CPU进入休眠状态。可能有些任务或事情在系统休眠后也要能执行或完成,如闹钟,wx要能在后台正常接收消息等,这就就要借助alarm机制。
系统在休眠后,只有系统时钟RTC(Real Time Clock)在进行工作,RTC是一个独立的硬件时钟,可以在CPU休眠时正常运行,在预设的时间到达时,通过中断唤醒CPU。AlarmManager是Android系统封装的用于管理RTC的模块。
针对我们的情况,我采用了WakeLock,给任务加锁,只要这个锁存在,就不允许系统进行休眠!
app获取wake lock的代码如下:
PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
if (pm != null) {
wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK | PowerManager.ON_AFTER_RELEASE, "CpuWakeLock");
wakeLock.acquire();//或wakeLock.acquire(1000 * 60 * 5);
}
//释放锁
if (wakeLock != null) {
wakeLock.release();
wakeLock = null;
}
代码封装:
/**
* 使用需要在AndroidManifest文件中添加权限如下:
* <uses-permission android:name="android.permission.WAKE_LOCK"/>
*/
public class CpuWakeLock {
private PowerManager.WakeLock wakeLock = null;
private boolean isLock = false;
/**
* 给CPU加锁,使CPU不得休眠
* @param activity activity
* @param timeMilliseconds 锁的失效时间,毫秒值(强制指定失效时间,不然会造成手机耗电增加)
* @return 返回true说明加锁成功
*/
public synchronized boolean lock(Activity activity, long timeMilliseconds) {
if (activity == null)
return false;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
if (activity.isDestroyed()) {
return false;
}
}
PowerManager pm = (PowerManager) activity.getSystemService(Context.POWER_SERVICE);
if (pm != null) {
wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK | PowerManager.ON_AFTER_RELEASE, "CpuWakeLock");
wakeLock.acquire(timeMilliseconds);
isLock = true;
return true;
}
return false;
}
/**
* 给CPU解锁,使CPU可以休眠
*/
public synchronized void unlock() {
if (wakeLock != null) {
wakeLock.release();
wakeLock = null;
isLock = false;
}
}
public boolean isLock() {
return isLock;
}
}