phone 未使用时,AlarmManager 重复警报随机丢失

AlarmManager repeating alarm missing randomly when phone is not in use

我正在每隔 30 分钟调用后台 Service 来读取当前位置的 latitude/longitude 并通过 POST API 将其发送到服务器。

我正在使用 AlarmManager class 的 setRepeating() 方法每 30 分钟安排一次闹钟。但有时警报会被错过,服务也不会被呼叫。为了监控每 30 分钟是否调用警报,我在 sdcard 中生成了 Log.txt 文件。对于每次调用的时间警报,当前时间的条目将写入 Log.txt 文件中。但是在比较了 4 到 5 个设备 Log.txt 文件后,我注意到对于某些设备,警报没有调用 UserTrackingReceiver.java(后台服务)的 onCreate() 方法。下面提到的完整代码块。

应用程序启动时registerUserTrackingReceiver()方法被调用如下:

public static void registerUserTrackingReceiver(Context context) {
        try {
            Intent intent = new Intent(context, UserTrackingReceiver.class);

            boolean alarmUp = (PendingIntent.getService(context, 1001, intent, PendingIntent.FLAG_NO_CREATE) == null);

            if (alarmUp) {
                Calendar calendar = Calendar.getInstance();

                if (calendar.get(Calendar.MINUTE) > 0 && calendar.get(Calendar.MINUTE) <= 30) {
                    calendar.set(Calendar.HOUR_OF_DAY, calendar.get(Calendar.HOUR_OF_DAY));
                    calendar.set(Calendar.MINUTE, 30);
                    calendar.set(Calendar.SECOND, 0);
                } else if (calendar.get(Calendar.MINUTE) > 30) {
                    if (calendar.get(Calendar.HOUR_OF_DAY) == 23) {
                        calendar.set(Calendar.HOUR_OF_DAY, 0);
                    } else {
                        calendar.set(Calendar.HOUR_OF_DAY, calendar.get(Calendar.HOUR_OF_DAY) + 1);
                    }
                    calendar.set(Calendar.MINUTE, 0);
                    calendar.set(Calendar.SECOND, 0);
                } else {
                    calendar.set(Calendar.HOUR_OF_DAY, calendar.get(Calendar.HOUR_OF_DAY));
                    calendar.set(Calendar.MINUTE, 0);
                    calendar.set(Calendar.SECOND, 0);
                }

                PendingIntent sender = PendingIntent.getService(context, 1001, intent, 0);
                AlarmManager alarmManager = (AlarmManager) context.getSystemService(context.ALARM_SERVICE);
                alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(),
                        AlarmManager.INTERVAL_HALF_HOUR, sender);
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
}

UserTrackingReceiver.java如下:

public class UserTrackingReceiver extends Service
        implements LocationListener,
        GoogleApiClient.ConnectionCallbacks,
        GoogleApiClient.OnConnectionFailedListener {

    @Override
    public void onCreate() {
        super.onCreate();

        Calendar calendar = Calendar.getInstance();
        Util.appendLog("Tracking Alarm Called on: " + calendar.get(Calendar.HOUR_OF_DAY) + " : " + calendar.get(Calendar.MINUTE) + " : " + calendar.get(Calendar.SECOND));
        stopSelf();
    }
}

Util.java中有appendLog()方法如下:

public static void appendLog(String text) {

        String baseDir = Environment.getExternalStorageDirectory().getAbsolutePath();

        File logFile = new File(baseDir + "/" + Constant.AppNameSuper + "/log.txt");
        if (!logFile.exists()) {
            try {
                logFile.createNewFile();
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
        try {
            //BufferedWriter for performance, true to set append to file flag
            BufferedWriter buf = new BufferedWriter(new FileWriter(logFile, true));
            buf.append(text);
            buf.newLine();
            buf.close();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
}

如果按上述代码每 30 分钟调用一次警报,则应将其写入 SDCARD 中的 Log.txt 文件中。但问题是它不能每 30 分钟写入一次日志文件,这意味着警报丢失了。根据两天的阅读,我注意到当用户连续使用他们的 phone 时,白天不会丢失警报,但当 phone 未被使用时,它会在晚上丢失。

不同设备的输出日志文件如下:

设备A Log.txt

  • Tracking Alarm Called on: 0 : 0 : 31(Start From 12:00 Night)
  • Tracking Alarm Called on: 1 : 10 : 27
  • Tracking Alarm Called on: 3 : 5 : 25
  • Tracking Alarm Called on: 6 : 55 : 31
  • Tracking Alarm Called on: 7 : 0 : 6
  • Tracking Alarm Called on: 7 : 30 : 0
  • Tracking Alarm Called on: 8 : 0 : 6
  • Tracking Alarm Called on: 8 : 30 : 0
  • Tracking Alarm Called on: 9 : 0 : 6
  • Tracking Alarm Called on: 9 : 30 : 0
  • Tracking Alarm Called on: 10 : 0 : 0

设备 B Log.txt

  • Tracking Alarm Called on: 0 : 0 : 27(Start From 12:00 Night)
  • Tracking Alarm Called on: 0 : 30 : 1
  • Tracking Alarm Called on: 1 : 0 : 1
  • Tracking Alarm Called on: 1 : 30 : 2
  • Tracking Alarm Called on: 2 : 0 : 1
  • Tracking Alarm Called on: 2 : 30 : 1
  • Tracking Alarm Called on: 3 : 0 : 1
  • Tracking Alarm Called on: 3 : 30 : 1
  • Tracking Alarm Called on: 4 : 0 : 1
  • Tracking Alarm Called on: 4 : 30 : 29
  • Tracking Alarm Called on: 5 : 0 : 1
  • Tracking Alarm Called on: 5 : 30 : 2
  • Tracking Alarm Called on: 6 : 0 : 30
  • Tracking Alarm Called on: 6 : 30 : 1
  • Tracking Alarm Called on: 7 : 0 : 1
  • Tracking Alarm Called on: 7 : 30 : 1
  • Tracking Alarm Called on: 8 : 0 : 1
  • Tracking Alarm Called on: 8 : 30 : 1
  • Tracking Alarm Called on: 9 : 0 : 32
  • Tracking Alarm Called on: 9 : 30 : 1

设备 C Log.txt

  • Tracking Alarm Called on: 0 : 0 : 7(Start From 12:00 Night)
  • Tracking Alarm Called on: 0 : 30 : 3
  • Tracking Alarm Called on: 1 : 0 : 6
  • Tracking Alarm Called on: 1 : 30 : 1
  • Tracking Alarm Called on: 2 : 0 : 32
  • Tracking Alarm Called on: 2 : 30 : 3
  • Tracking Alarm Called on: 3 : 1 : 50
  • Tracking Alarm Called on: 3 : 30 : 5
  • Tracking Alarm Called on: 4 : 1 : 58
  • Tracking Alarm Called on: 4 : 31 : 14
  • Tracking Alarm Called on: 5 : 0 : 1
  • Tracking Alarm Called on: 5 : 30 : 1
  • Tracking Alarm Called on: 6 : 2 : 1
  • Tracking Alarm Called on: 6 : 30 : 1
  • Tracking Alarm Called on: 7 : 0 : 1
  • Tracking Alarm Called on: 7 : 30 : 1
  • Tracking Alarm Called on: 8 : 0 : 1
  • Tracking Alarm Called on: 8 : 30 : 4
  • Tracking Alarm Called on: 9 : 1 : 44
  • Tracking Alarm Called on: 9 : 30 : 1

设备 D Log.txt

  • Tracking Alarm Called on: 0 : 1 : 25(Start From 12:00 Night)
  • Tracking Alarm Called on: 0 : 30 : 0
  • Tracking Alarm Called on: 1 : 31 : 41
  • Tracking Alarm Called on: 2 : 39 : 52
  • Tracking Alarm Called on: 3 : 0 : 25
  • Tracking Alarm Called on: 3 : 30 : 58
  • Tracking Alarm Called on: 4 : 0 : 25
  • Tracking Alarm Called on: 4 : 30 : 56
  • Tracking Alarm Called on: 5 : 30 : 51
  • Tracking Alarm Called on: 7 : 18 : 55
  • Tracking Alarm Called on: 7 : 30 : 0
  • Tracking Alarm Called on: 8 : 0 : 25
  • Tracking Alarm Called on: 8 : 30 : 43
  • Tracking Alarm Called on: 9 : 0 : 3
  • Tracking Alarm Called on: 9 : 30 : 25
  • Tracking Alarm Called on: 10 : 0 : 25
  • Tracking Alarm Called on: 10 : 30 : 4
  • Tracking Alarm Called on: 11 : 1 : 52
  • Tracking Alarm Called on: 11 : 30 : 27
  • Tracking Alarm Called on: 12 : 1 : 6⁠⁠⁠⁠

您需要使用 BroadcastReceiver 和唤醒锁才能在设备空闲时可靠地实现这一点。另外,请注意,以 API 开头的 19 个警报在默认情况下是不准确的,这将对此产生影响。如果您的目标是 API 21 或更高版本,请考虑使用 JobScheduler。类似这样postAlarm Manager with 2 pending intents only 1 works?

问题可能出在您 PendingIntent 呼叫 Service。在您的 Service 完成(甚至开始)执行之前,设备可以重新进入睡眠状态。

我建议您改用 BroadcastReceiver(因为在 onReceive() 期间保证 WakeLock)。

onReceive() 中获得一个 WakeLock,从那里开始你的 Service,并在适当的时候从 Service 中释放 WakeLock

要简化此过程,您可以使用 WakefulBroadcastReceiver 助手 class:

  1. 调用 PendingIntent.getBroadcast() 而不是 PendingIntent.getService()
  2. 通过调用 WakefulBroadcastReceiver.startWakefulService()onReceive() 开始一个 IntentService
  3. onHandleIntent() 中完成你的工作,完成后调用 WakefulBroadcastReceiver.completeWakefulIntent()

例如,启动唤醒 Service:

BroadcastReceiver
public class ExampleReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        Intent wakefulServiceIntent = new Intent(context,
            ExampleWakefulService.class);

        WakefulBroadcastReceiver.startWakefulService(context,
            wakefulServiceIntent);
    }
}

Service:

public class ExampleWakefulService extends IntentService {

    private static final String NAME = "com.example.ExampleWakefulService";

    public ExampleWakefulService() {
        super(NAME);
    }

    @Override
    protected void onHandleIntent(Intent intent) {

        // doing stuff

        WakefulBroadcastReceiver.completeWakefulIntent(intent);
    }
}

此外,请查看开发人员指南中的 this article 以保持设备唤醒。

API 级别 23+ 你必须处理 Doze.

来自documentation

To help with scheduling alarms, Android 6.0 (API level 23) introduces two new AlarmManager methods: setAndAllowWhileIdle() and setExactAndAllowWhileIdle(). With these methods, you can set alarms that will fire even if the device is in Doze.

很遗憾 setRepeating() 没有其他选择,因此您有两个选择:

  • 设置确切的警报(根据设备的 API 级别使用适当的方法,查看 this answer 示例)并在每次触发时重新安排它们。
  • Whitelist您的应用(不推荐,因为Google的严格修订政策)。

根据 Android 开发人员 document 注意:

Note: as of API 19, all repeating alarms are inexact. If your application needs precise delivery times then it must use one-time exact alarms, rescheduling each time as described above. Legacy applications whose targetSdkVersion is earlier than API 19 will continue to have all of their alarms, including repeating alarms, treated as exact.