【问题标题】:Android AlarmManager setExact() is not exactAndroid AlarmManager setExact() 不准确
【发布时间】:2016-06-29 08:39:44
【问题描述】:

我需要每 10 分钟计划一次计划任务。

由于在 Lollipop 和更高版本中 setRepeating() 不准确,我使用 setExact() 并且(在触发警报时)我在 10 分钟内设置了新的准确警报。

private void setAlarm(long triggerTime, PendingIntent pendingIntent) {
        int ALARM_TYPE = AlarmManager.ELAPSED_REALTIME_WAKEUP;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            alarmManager.setExact(ALARM_TYPE, triggerTime, pendingIntent);
        } else {
            alarmManager.set(ALARM_TYPE, triggerTime, pendingIntent);
        }
    }

triggerTime 计算为SystemClock.elapsedRealtime() + 600_000;

当警报响起时,我首先计划一个新的,然后才运行我计划的任务。

setAlarm();
mySheduledTask;

我的清单中有WAKE_LOCK 权限。

当我在 Android 4 上测试它时 - 它运行良好(偏差可能是 12-15 毫秒)。

但是当我在小米红米 Note 3 Pro (5.1.1) 上运行应用程序时 - 偏差可能高达 15 秒!

例如,我在日志文件中看到:第一次运行是在 1467119934477(RTC 时间),第二次 - 在 1467120541683。差异是 607_206 毫秒,而不是 600_000 ,正如它所计划的那样!

我错过了什么?什么是模拟系统警报行为的方法(这是可以描述我的策略的最接近的用例)?

PS.我为PendingIntent = PendingIntent.getService(context, 0, myIntent, 0);使用IntentService

【问题讨论】:

    标签: android alarmmanager


    【解决方案1】:

    操作系统会根据您指定的时间来选择警报的工作方式。因此,当手机进入“半睡眠”模式时,它不需要在您希望的时间使用资源。基本上,它会等待操作系统为其打开的“窗口”,然后才会运行您要运行的警报,这就是您遇到时间间隔的原因。

    这是在 Marshmallow OS 上引入的,并将继续在 Nougat OS 上继续,作为 Google 尝试改进设备电池的一部分。

    事情是这样的,你有两个选择:

    1. 接受时间延迟(但可以考虑使用JobScheduler,这是更推荐的方式,可以节省电池电量)。
    2. 使用setExactAndAllowWhileIdle,这可能会导致电池出现问题(请谨慎使用,过多的警报对电池不利)。 此方法不会重复,因此您必须声明要在 pendingIntent 打开的服务上运行的下一个作业。

    如果您选择选项 2,请从这里开始:

    AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
    int ALARM_TYPE = AlarmManager.RTC_WAKEUP;
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
        am.setExactAndAllowWhileIdle(ALARM_TYPE, calendar.getTimeInMillis(), pendingIntent);
    else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
        am.setExact(ALARM_TYPE, calendar.getTimeInMillis(), pendingIntent);
    else
        am.set(ALARM_TYPE, calendar.getTimeInMillis(), pendingIntent);
    

    【讨论】:

    • 好点,谢谢。但问题在于棒棒糖,而不是棉花糖。另外,系统警报是如何工作的(在计划的时候效果很好)?
    • @Dus 第三种情况并不准确,因为具有 targetSdkVersion 19 或更高版本的应用程序会将 set 在具有 18 及更早版本的设备上视为不精确。那么,如何在具有 targetSdkVersion 25 的前 Kitkat 设备上获得准确的警报?
    • 第二个if Lollipop 应该用 Kitkat 代替。
    • 哦,我需要向用户显示一条消息:“抱歉,您没有听到警报并且没有按时起床,因为准确的警报触发会导致电池问题”。跨度>
    • 为闹钟应用推荐 JobScheduler?为什么?
    【解决方案2】:

    您可以从 support.v4 中调用该方法:

    AlarmManagerCompat.setExact(...);
    

    内部实现包含按 sdk 版本检查。

    【讨论】:

      【解决方案3】:

      可能的解决方法可能是这样的: 您将警报安排在预期时间前约 1 分钟,而不是使用 Handler.postDelayed 来覆盖剩余时间。

      您可以在此处找到此类实现的示例。 该活动刚刚设置了第一个警报:

      public class MainActivity extends AppCompatActivity {
      
          private static int WAIT_TIME = 60*1000; //1 minute
          public static int DELAY_TIME = 10*60*1000; // delay between iterations: 10min
          public static String UPDATE_TIME_KEY = "update_time_key";
      
      
          @Override
          protected void onCreate(Bundle savedInstanceState) {
              super.onCreate(savedInstanceState);
              setContentView(R.layout.activity_main);
              setAlarm(this,(new Date().getTime())+DELAY_TIME);
          }
      
          public static void setAlarm(Context context, long delay) {
      
              long fireDelay = delay-WAIT_TIME;
              SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
              sharedPreferences.edit().putLong(UPDATE_TIME_KEY,delay).apply();
      
              Intent startIntent = new Intent(context, UpdateReceiver.class);
              PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 1, startIntent,PendingIntent.FLAG_UPDATE_CURRENT );
              AlarmManager alarmManager = (AlarmManager) context.getApplicationContext().getSystemService(Context.ALARM_SERVICE);
              int ALARM_TYPE = AlarmManager.RTC;
              if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
                  alarmManager.setExact(ALARM_TYPE, fireDelay, pendingIntent);
              } else {
                  alarmManager.set(ALARM_TYPE, fireDelay, pendingIntent);
              }
          }
      
      }
      

      接收者继续循环:

      public class UpdateReceiver extends BroadcastReceiver {
      
          @Override
          public void onReceive(final Context context, Intent intent) {
              Log.e("RECEIVED","RECEIVED");
              SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
              long fireTime = sharedPreferences.getLong(MainActivity.UPDATE_TIME_KEY, (new Date()).getTime());
      
              long fireDelay  =(fireTime-(new Date().getTime())>0)?fireTime-(new Date().getTime()):0;
      
              (new Handler()).postDelayed(new Runnable() {
                  @Override
                  public void run() {
                      Log.e("RECEIVED","PERFORMED");
                      MainActivity.setAlarm(context,(new Date()).getTime()+MainActivity.DELAY_TIME);
                  }
              },fireDelay);
      
          }
      
      }
      

      希望对你有帮助。

      【讨论】:

      • 有趣的解决方案,点赞。但它非常hacky,只会将其用作最后的手段。谢谢!
      • 这是一种有趣的方法,但如果您的初始警报没有触发怎么办?
      【解决方案4】:

      回答关于系统报警的问题...

      Android 的普通闹钟/桌面时钟应用结合使用 setAlarmClocksetExactAndAllowWhileIdle

      以下代码用于更新通知:

      final PendingIntent operation = PendingIntent.getBroadcast(context, 0, 
          AlarmStateManager.createIndicatorIntent(context), flags);
      final AlarmClockInfo info = new AlarmClockInfo(alarmTime, viewIntent);
      
      alarmManager.setAlarmClock(info, operation);
      


      同时使用以下代码来安排实际的警报:

      if (Utils.isMOrLater()) {
          // Ensure the alarm fires even if the device is dozing.
          alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, timeInMillis, pendingIntent);
      } else {
          alarmManager.setExact(AlarmManager.RTC_WAKEUP, timeInMillis, pendingIntent)
      }
      


      setExactAndAllowWhileIdle 中设置的 Pending Intent 触发警报,而 setAlarmClock 的 Intent 则被简单地忽略。


      Android Googlesource

      【讨论】:

      • 欢迎来到 Android 的世界,即使是 Google 开发人员也必须破解他们的出路。
      • 我没有看到第一部分。它到底在哪里?
      • 但是你怎么看第二个是在另一个之后被调用的呢?它们在不同的功能中,我没有看到它们被一个接一个地调用。
      • @androiddeveloper 代码不容易理解,但这一切都以registerInstance() 开始,它设置警报状态(最终调用第二段代码)并更新通知(第一段代码)。
      【解决方案5】:

      来自AlarmManager的android文档

      从 API 19 (KITKAT) 开始,警报传递是不准确的:操作系统将切换警报以最大程度地减少唤醒和电池使用。有新的 API 来支持需要严格交付保证的应用程序;参见 setWindow(int, long, long, PendingIntent) 和 setExact(int, long, PendingIntent)。 targetSdkVersion 早于 API 19 的应用程序将继续看到以前的行为,即所有警报都在请求时准确传递。

      同时使用 setExact() :

      警报将尽可能接近发送到请求的触发时间。

      所以它仍然不能保证 setExact 将是 Exact。

      【讨论】:

      • 我确实阅读了文档。好的,那么设备上的警报如何工作?效果很好,没有任何偏差。
      【解决方案6】:

      您可以尝试使用AlarmManager.setAlarmClock,也许它可以帮助您。

      另外你需要检查你使用的是哪种类型的BroadcastReceiver,使用WakefulBroadcastReceiver会更好

      顺便说一句,您需要更改用于支持 Android M 的警报管理器的逻辑,您可以这样:

      if(Build.VERSION.SDK_INT < 23){
          if(Build.VERSION.SDK_INT >= 19) {
              setExact(...);
          } else {
              set(...);
          }
      } else {
          setExactAndAllowWhileIdle(...);
      }
      

      【讨论】:

      • 它写到 setAlarmClock 就像 setExact,所以我不认为,切换会有多大帮助。关于 WakefulBroadcastReceiver - 很有趣,会尝试,目前我正在使用服务。
      • WakefulBroadcastReceiver 现已弃用。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-02-02
      • 1970-01-01
      • 2012-09-14
      • 1970-01-01
      • 2012-09-17
      • 2016-10-19
      相关资源
      最近更新 更多