美文网首页
AlarmManager的踩坑之路

AlarmManager的踩坑之路

作者: 做梦枯岛醒 | 来源:发表于2019-10-15 20:23 被阅读0次

    上周做了一个需求,接触了一个让我个人又爱又恨的工具,就是题目中所说的AlarmManager,为什么这么说,这个东西如果正常起来是一个很棒的工具,如果不正常的时候就让人头疼,比如这个需求开发完花了一天多,但是测试和解决花了比开发还长的时间,到底是为啥来看下吧。

    需求与实现

    需求是一个实现一个本地推送(在后台存活的情况下),在每天固定的几个时间点发送通知。想着这玩意用轮询肯定是不行了,这样会耗费大量的电量和内存,如果仅靠本地支持的话,AlarmManager是个比较不错的选择,然后我就开始写,项目的代码就不贴了,下面是我后来写了一个类似的demo。

    public class MainActivity extends AppCompatActivity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    AlarmClockManager.setAlarmClock(1,TimeUtils.getCurrentTime(TimeUtils.yyyyMMdd)+" 21:00",MainActivity.this);
                }
            });
        }
    }
    

    这里我封装的比较全面,其中setAlarmClock是一个静态方法,其中接受三个参数。

        /**
         * 设置一个某个时间点的闹钟
         *
         * @param id   闹钟id
         * @param time 闹钟时间
         *             格式 yyyy-MM-dd HH:mm
         */
        public static void setAlarmClock(int id, String time, Context context) {
            Intent intent = new Intent(context, AlarmService.class);
            PendingIntent pendingIntent = PendingIntent.getService(context, id, intent, 0);
            AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
            long times = TimeUtils.getStampByPattarn(time, TimeUtils.yyyyMMddHHmm);
             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, times, pendingIntent);
            } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
                alarmManager.setExact(AlarmManager.RTC_WAKEUP, times, pendingIntent);
            } else {
                alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, times, TimeUtils.dayTimeMillis, pendingIntent);
            }
        }
    

    首先第一个参数是id,这里为了做闹钟的区分在PendingIntent里添加唯一标示。
    第二个参数是时间,这个时间就是真正的闹钟时间,这里我们传入的是进入21点响。第三个参数是context。

    我给PendingIntent添加了指令,如果触发,则唤醒服务去做一些事情。
    最关键的是最后的判断逻辑。

    我们先看使用setRepeating方法设置重复闹钟。参数是四个,其中第一个是闹钟形式,第二个是设定的时间,第三个是重复周期,这里给的是一天,第四个触发功能。

    其中第一个参数有很多取值。
    AlarmManager.ELAPSED_REALTIME_WAKEUP
    会唤醒CPU(唤醒在这里的概念是,如果当前是睡眠状态,也要叫醒CPU去执行事件,反之就不唤醒了),使用相对时间,也就是从手机开机到现在的时间

    AlarmManager.RTC_WAKEUP
    会唤醒CPU,使用绝对时间,也就是时间戳形式。

    AlarmManager.ELAPSED_REALTIME
    时间上与第一个一样,只是不会唤醒CPU

    AlarmManager.RTC**
    时间上与第二个一样,不会唤醒CPU

    第二个参数与第一个参数相关,根据时间的相对绝对来选择合适的方法。
    比如说相对时间使用SystemClock.elapsedRealtime(),绝对时间使用System.currentTimeMillis(),这里用的都是毫秒。

    第三个参数和第四个没什么好说的。

    然后闹钟唤醒时启动服务创建通知即可,后面不是我们讨论的重点。
    看起来很美好的样子。


    在Android10系统上测试了一下,效果还不错。但是会有时间偏差。

    但是实际在开发这个需求的时候却没有这么顺利。其实做上面的兼容也是前辈们早已总结出来的。

    分别是4.4以上Google推荐的setExact方法和6.0及以上setAndAllowWhileIdle方法

    image.png

    其实看起来4.4和6.0所进行的限制都是一样的,都是会统一唤醒CPU执行事件,而不是频繁唤醒,只不过6.0做的更正式一点,有了一个叫Doze模式的东西。

    Google对待这个还算好,都给我们提供了相对简单且有用的方法。

    踩坑

    以为这样就完了么???
    写完之后我分别测了各部分,比如说通知能不能发出,闹钟能不能定上等等。

    这里补充一个查看定的闹钟的方法。

    adb shell dumpsys alarm
    

    这是一条adb命令,可以查看所定的闹钟,同时可以用grep进行过滤,看似很美好,但是……太难理解了……


    我们一点一点来看。
    首先是包名,然后是时间,其中-19m47s308ms 是时间,其中负数代表已经过去了(这也是我刚才才发现的……现在想起来没看这个就一直猜bug是多么的傻*),这条记录我定的闹钟是8点33分,查看的时候已经是8点53分了,也就是大概19-20分钟的样子。所以负数是已经过去的。

    还有另一种情况,就是闹钟没触发的情况。会给出触发时间,和历史响的时间。


    大概可以从中得出一些有价值的问题,可以多利用一下。

    在都测完之后改好时间静静等待通知的发出,然鹅……


    通知没有发出来。

    填坑

    为什么会弹不出来,我最开始把它归因于系统调度,也就是类似于Doze模式的形式,但是产品肯定要确保开发的质量,就这样请教来请教去,想到了一系列方法,比如说开启自启动,开启忽略省电优化,开启小米智能省电无限制等等。

    是有一些效果的,这里强烈推荐申请【忽略省电优化】

    public static void openBattart(Context context) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
                if (pm != null && !pm.isIgnoringBatteryOptimizations(context.getPackageName())) {
                    //1.进入系统电池优化设置界面,把当前APP加入白名单
                    //startActivity(new Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS));
    
                    //2.弹出系统对话框,把当前APP加入白名单(无需进入设置界面)
                    //在manifest添加权限
                    Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
                    intent.setData(Uri.parse("package:" + context.getPackageName()));
                    try {
                        context.startActivity(intent);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    

    这个权限能尽可能的保证弹出的准时和提高弹出的概率。
    但是测试来测试去还是不行。
    一直拖到临近发版前,我也绝望了,一方面因为我还是新手,内心是很想做好需求的,做不好别人可能会怀疑自己的能力(其实本身也会怀疑,哈哈哈),但实际上就本地推送就是完全依靠后台存活的情况下,闹钟准确的情况下等等。

    在绝望的同时我发现一个奇怪的现象,大概是下面这个样子。
    假如现在是上午10点,我定了一个10点1分的闹钟,这个闹钟是正常触发的,但是我定一个早上8点的闹钟,然后我修改时间为7点59,等到8点的时候是不会触发的。

    上面这一段话透露的意思是 如果我改时间的话是不太可能让闹钟响的,但是其实在实际开发过程中这又是一个随机的事件。后来在群中问了一下大家,有一个大哥的回复让我恍然大悟,他说是不是闹钟已经过去了。

    其实确实是这个样子。因为我的定闹钟的逻辑是写在每次打开App的时候,而且闹钟会依赖App活着,加上我每次更改内容都会用数据线重新Run,所以比如说现在 上午十点,我定的上午8点的闹钟其实在打开App的时候就已经执行完了,我再修改系统时间也没有用,因为系统可能对执行完了的闹钟做了相应标记。这个时候实际上上面的adb命令可以查询到这个闹钟的信息,善于分析这些命令可能会发现一些破绽。

    所以至此,闹钟不响的问题我们上面总结了两点。

    • 忽略省电优化,一些机型,如小米开启智能省电无限制效果会更好。
    • 测试的时候先改时间再安装,或者直接使用最近的未来时间进行测试,避免了一些奇葩的锅。

    相关文章

      网友评论

          本文标题:AlarmManager的踩坑之路

          本文链接:https://www.haomeiwen.com/subject/wzkdmctx.html