Service being re-Created by AlarmManager

2019-01-12 01:24发布

问题:

I have a fairly standard Service which I wish to trigger using an alarm. Here's the initiation section of the service:

class MyService extends Service {
    private Context context;
    private AlarmManager  alarmManager = null;

    private final String startReason = "com.stuff.myreason";
    private final int REASON_NO_INTENT = 0;
    private final int REASON_ALARM     = 1;
    private final int REASON_X         = 2; // and so on.

    @Override
    void onCreate() {
        super.onCreate();
        context = getApplicationContext();
        alarmManager = (AlarmManager)getSystemService(ALARM_SERVICE);
        // do onCreate stuff
    }

    @Override
    int onStartCommand (Intent intent, int flags, int startId) {
        int reason = REASON_NO_INTENT;
        if (intent != null) {
            reason = intent.getExtra(startReason, REASON_NO_INTENT);
        }

        switch(reason) {
            // handle the different reasons we may have been "started"
        }

        return START_STICKY;
    }
}

When I trigger it using context.startService from an activity, it starts absolutely normally. In particular, if it is already running it doesn't (re)start from scratch but simply enters the existing instantiation via onStartCommand(). This is the expected behaviour. However, when I trigger it using the AlarmManager:

Intent intent = new Intent(context, MyService.class);
intent.putExtra(purposeOfStartCode, REASON_ALARM);

PendingIntent pi = PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);

alarmManager.set(AlarmManager.RTC_WAKEUP, /* A time in the future */, pi);

When the alarm is due it seems to restart the service from scratch: it starts a new instantiation, calls onCreate() and then onStartCommand() rather than just calling onStartCommand() in the already running instantiation.

I have already tried changing the PendingIntent flag to FLAG_ONE_SHOT and replacing context with MyService.this with no improvement.

I am rather bemused by this - can anyone explain this behaviour and suggest ways to get it to behave as expected?

EDIT - The collection of actions that resulted in a solution are in my answer below.

回答1:

After some investigation and work, I've discovered a number of things. Having done all of them, this problem looks like it's disappeared:

  1. If you override onStart and onStartCommand in a service (to allow for older devices) and you put super.onStartCommand in the latter, it will call onStart, meaning you get every intent coming in twice!

  2. As per one of the other answers (and comments on it), the AlarmManager is designed and specified to deliver Broadcast intents, not other types. However, in practice, it isn't picky and seems to honour other forms. I think that this was one of the keys in resolving the issue.

  3. If the service is in the same process as other activites etc, the service sometimes seems to "just get restarted". This may be the actual cause of the issue noted in this question. See Android service onCreate is called multiple times without calling onDestroy.

  4. Things seem to be more stable when solely using intents to communicate with the Service rather than binding and using a Messenger or binding and accessing methods. Whilst both of these are correct, they are quite complex to manage (although you could use this approach: What is the preferred way to call an Android Activity back from a Service thread and Using the Android Application class to persist data). Whilst I fully appreciate that the android docs disagree with me, in my observation moving to broadcast intent only communication seemed key. If you go for the separate process approach you'll have to do this anyway.

  5. It pays to be consistent in how you declare and address your classes. It's a bit messy, but, because it sometimes seems to pay to use full names ("com.company.superapp.CleverService") rather than short ("CleverService" or ".CleverService"). So, it's probably better to always use full names.

  6. The rule of thumb floating around out there about contexts ("use getApplicationContext") isn't really the right way to do it. See When to call activity context OR application context?; in essence use this, unless you really need to use a broader context, and manage your variables well.

  7. It's possible for the garbage collector to clear up something still in use if it was created in an Activity, Service, Thread, AsyncTask, etc that is no longer around. If the application is based around a service, it may be wise to make a copy of classes coming in so that they don't get cleared up later.

  8. A neater way to start a service than is often suggested is to give the service an intentFilter with it's full name as the action. You can then create the intent to start it with just the class name as a string. This means you don't have to worry about context. See Issue in Calling Service.



回答2:

Well, I'm actually surprised that it runs your Service at all! The PendingIntent that you pass to the AlarmManager needs to be a broadcast Intent. So you need to rearchitect your code a bit. The AlarmManager will trigger a BroadcastReceiver and the BroadcastReciever can then call startService().

See the description of AlarmManager.set()



回答3:

I got this to work by using the following code:

AlarmManager almgr = (AlarmManager)MyContext.getSystemService(Context.ALARM_SERVICE);
Intent timerIntent = new Intent(MyUniqueLabel);
timerIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
PendingIntent pendingOffLoadIntent = PendingIntent.getBroadcast(MyContext, 1, timerIntent, 0);

you MUST do these things for it to work.
1.) Call addFlags and the intent and pass it in FLAG_RECEIVER_FORGROUND
2.) Use a non-zero request code in PendingIntent.getBroadcast

If you leave any of those steps out it will not work.