Android开发笔记(一百六十)休眠模式下的定时器控制

定时器AlarmManager常常用于需要周期性处理的场合,比如闹钟提醒、任务轮询等等。并且定时器来源于系统服务,即使App已经不在运行了,也能收到定时器发出的广播而被唤醒。似此回光返照的神技,便遭到开发者的滥用,造成用户手机充斥着各种杀不光进程,就算通过手机安全工具一再地清理内存,只要定时设定的时刻到达,刚杀掉的流氓App就会死灰复燃。长此以往,手机的运行速度越来越慢,内存也越来越不够用了,更糟糕的是,电量消耗地越来越快。 Android手机越用越慢的毛病老大不掉,为此每次系统版本升级,Android都力图在稳定性、安全性上有所改善。针对定时器AlarmManager的滥用问题,Android从4.4开始,修改了setRepeating方法的运行规则。原本该方法可指定每隔固定时间就发送定时广播,但在Android4.4之后,操作系统为了节能省电,将会自动调整定时器唤醒的时间。比如原来调用setRepeating方法设定了每隔10秒发送广播,但App在实际运行过程中,很可能过了好几分钟才发送一次广播,这意味着该方法将不再保证每次工作都在开发者设置的时间开始。 正如博文《Android开发笔记(七十五)内存泄漏的处理》描述的那样,当时为了演示定时器发生内存泄漏的场景,并没有直接调用setRepeating方法,而是接力调用set方法。App每次收到定时广播之后,还得重新开始下一次的定时任务,如此方可兼容Android4.4之后的持续定时功能。下面是将setRepeating方法改为使用set方法实现的代码例子:

    private String ALARM_EVENT = "com.example.performance.alarm";
    private static AlarmManager mAlarmManager;
    private static PendingIntent pIntent;
    private static int mDelay = 3000;
    
    // 设置定时任务,注意setRepeating的时间间隔并不可靠,只能调用set方法间接实现定时
    private void setAlarm() {
        Intent intent = new Intent(ALARM_EVENT);
        pIntent = PendingIntent.getBroadcast(this, 0, intent,
                PendingIntent.FLAG_UPDATE_CURRENT);
        mAlarmManager = (AlarmManager) getSystemService(ALARM_SERVICE);
        // 在API 19(即Android4.4)之后,操作系统为了节能省电,会调整alarm唤醒的时间,
        // 所以setRepeating方法不保证每次工作都在指定的时间开始,
        // 此时需要先注销原闹钟,再调用set方法开启新闹钟。
        // mAlarmManager.setRepeating(AlarmManager.RTC_WAKEUP,
        //          System.currentTimeMillis(), mDelay, pIntent);
        mAlarmManager.set(AlarmManager.RTC_WAKEUP,
                System.currentTimeMillis()+mDelay, pIntent);
    }

    // 定义一个定时广播的接收器
    public static class AlarmReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            if (intent != null) {
                if (tv_alarm != null) {
                    mDesc = String.format("%s\n%s 闹钟时间到达", mDesc, DateUtil.getNowTime());
                    tv_alarm.setText(mDesc);
                    // 设置下一次的定时任务
                    repeatAlarm();
                }
            }
        }
    }
    
    // 每次时刻到达,都重新设置下一次的定时任务,从而间接实现了持续唤醒的功能
    private static void repeatAlarm() {
        // 取消原有的定时任务
        mAlarmManager.cancel(pIntent);
        // 开启新的定时任务
        mAlarmManager.set(AlarmManager.RTC_WAKEUP,
                System.currentTimeMillis()+mDelay, pIntent);
    }

上面瞒天过海的办法看似完美规避了Android4.4的运行规则,可惜广大开发者还没来得及沾沾自喜,Android6.0又推出了更加严格的休眠模式。所谓休眠模式,即是当手机屏幕关闭的时候(又称熄屏、暗屏),系统就会自动开启休眠模式,这样原本正在运行的App将进入挂起模式,不能再进行访问网络等常用操作。当然为了保证App不被完全挂死,系统也会定期退出休眠模式,好比青蛙从冬眠之中苏醒过来,在苏醒期间,系统允许挂起的App重新恢复运行,继续先前设定好的任务。可是这个苏醒期是短暂的(通常只有几秒),一旦苏醒期结束,系统又重新进入休眠模式,于是那些App再次挂起,等待下次苏醒期的到来,如此往复。当然,只要手机恢复亮屏,比如用户按下电源键、用户给手机插上电源、手机接到来电等等,系统便自动退出休眠模式,所有挂起的App都会恢复正常运转。 手机在休眠期间,之前通过定时器的set方法设定好的定时任务,即使定时的时刻到达,也要等到苏醒期间才会得到执行。如果一定要在休眠期唤醒闹钟,就得调用setAndAllowWhileIdle代替set方法,或者调用setExactAndAllowWhileIdle代替setExact方法。其中setAndAllowWhileIdle与setExactAndAllowWhileIdle这两个方法是Android从6.0开始新增的定时方法,字面意思是即使正在休眠、也要执行定时任务。然而休眠模式的本意是挂起包括定时任务在内的App事务,现在却提供setAndAllowWhileIdle方法留下了后门,为开发者的鸡鸣狗盗之事大开方便,如此规定岂不是贻笑大方? 这光景,简直是活脱脱的一出Android版本的自相矛盾,话说Android设计师当街叫卖Android的安全盾,号称这面盾很牢固、没有矛可以刺穿;前来踢馆的开发者拿着一把Android的setRepeating矛,说道这把矛可以破了那面盾。设计师眼看不妙,赶忙拿起另一面名叫Android4.4的安全盾,又称你的setRepeating矛不行了;开发者精明得很,随身抄着一把Android的set矛,又道这把矛可以破了那面Android4.4的盾。设计师火冒三丈,心想岂能甘拜下风,于是拿出一面Android6.0的休眠盾,声称有此盾护身不怕set矛;谁料道高一尺、魔高一丈,开发者夺过一把Android出产的setAndAllowWhileIdle矛,依旧能刺开Android6.0休眠盾。结果Android设计师大汗淋漓,却不肯认输,嘴里碎碎念:“此山是我开,此树是我栽,要从此路过,留下买路财。罢了罢了,甭管你的矛有多锋利,反正我规定休眠盾至少能抗住九分钟。”这里的九分钟参见Android官方说明:Neither setAndAllowWhileIdle() nor setExactAndAllowWhileIdle() can fire alarms more than once per 9 minutes, per app,意思是不管是setAndAllowWhileIdle还是setExactAndAllowWhileIdle,在休眠期内每个App每隔9分钟最多只能唤醒一次闹钟。 一方面要照顾用户的手机省电需求,另一方面要考虑开发者的业务实现,开发Android的谷歌公司真是煞费苦心,只可惜鱼与熊掌不可兼得呀。我们作为开发者,要让定时器适配Android6.0的休眠模式倒也不难,只需把下面这行的set方法代码:

        mAlarmManager.set(AlarmManager.RTC_WAKEUP,
                System.currentTimeMillis()+mDelay, pIntent);

改成下面兼容6.0的代码就好了:

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            mAlarmManager.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP,
                    System.currentTimeMillis()+mDelay, pIntent);
        } else {
            mAlarmManager.set(AlarmManager.RTC_WAKEUP,
                    System.currentTimeMillis()+mDelay, pIntent);
        }

其实就是判断当前系统版本,对于Android6.0及以上版本,使用setAndAllowWhileIdle方法替换set方法即可。 点此查看Android开发笔记的完整目录

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券