AlarmManager的踩坑之路

上周做了一个需求,接触了一个让我个人又爱又恨的工具,就是题目中所说的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(),这里用的都是毫秒。

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

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


AlarmManager的踩坑之路_第1张图片

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

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

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

AlarmManager的踩坑之路_第2张图片
AlarmManager的踩坑之路_第3张图片
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分钟的样子。所以负数是已经过去的。

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


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

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


AlarmManager的踩坑之路_第4张图片

通知没有发出来。

填坑

为什么会弹不出来,我最开始把它归因于系统调度,也就是类似于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的踩坑之路)