首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Android开发笔记(一百六十)休眠模式下的定时器控制

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

作者头像
aqi00
发布2019-01-18 15:51:37
2.8K0
发布2019-01-18 15:51:37
举报
文章被收录于专栏:老欧说安卓老欧说安卓

定时器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开发笔记的完整目录

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2018年02月14日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档