手机休眠引发的重大Bug

问题起因

今天,用户群里有人反馈一个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;
    }


}

参考

Android休眠机制

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容

  • 【Android Handler 消息机制】 前言 在Android开发中,我们都知道不能在主线程中执行耗时的任务...
    Rtia阅读 4,703评论 1 28
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,569评论 25 707
  • 用两张图告诉你,为什么你的 App 会卡顿? - Android - 掘金 Cover 有什么料? 从这篇文章中你...
    hw1212阅读 12,476评论 2 59
  • 谈到拆书,我自认为那是一门深奥的学问。拆书会涉及到拆什么书?用什么方法拆?拆解什么内容?另外还有一个至关重要的环节...
    端银阅读 5,946评论 2 49
  • 7月份挑战赛的收获 1.突破自己去主动的找人聊天,即使被拒绝可,还是厚脸皮的一直在和顾客沟通,虽然最后还是被拒绝了...
    笨nana_fedb阅读 185评论 0 0