START_STICKY does not work on Android KitKat

2019-01-02 17:12发布

One of my apps has a backgrouod service that uses the START_STICKY return code from onStartCommand to automatically restart when the system kills it. It seems that this is no longer working on Android KitKat. Is there any solution for this ? Should I be doing something different on Kitkat to keep the service running ?

Note: There is a similar discussion on the Android-Devlopers group about swiping the app from the recent apps list behaves. Could this two issues be related ? https://groups.google.com/forum/#!topic/android-developers/H-DSQ4-tiac

Edit: Saw that there are open bugs on Android issue tracker:

https://code.google.com/p/android/issues/detail?id=63793 https://code.google.com/p/android/issues/detail?id=63618

Edit2: The same happens even if service is running using startForeground, in a separate process and with the flag android:stopWithTask="false" in the AndroidManifest.xml file...

Edit3: More related bugs on Android issue tracker:

https://code.google.com/p/android/issues/detail?id=62091 https://code.google.com/p/android/issues/detail?id=53313 https://code.google.com/p/android/issues/detail?id=104308

Is there some sort of workaround to get the previous behavior ?

4条回答
人间绝色
2楼-- · 2019-01-02 17:46

The problem here appears to not to occur on AOSP based ROMs. That is, I can easily recreate this on a CyanogenMod 11 based ROM, but on an AOSP ROM (and on an Emulator), START_STICKY behaves exactly as I'd expect. That said, I am seeing reports from folks on Nexus 5's that appear to be seeing this behavior, so perhaps it is still an issue in AOSP.

On an emulator and on an AOSP ROM, I see the following from a logcat when I do a 'kill 5838' against the process (as I'd expect):

12-22 18:40:14.237 D/Zygote  (   52): Process 5838 terminated by signal (15)
12-22 18:40:14.247 I/ActivityManager(  362): Process com.xxxx (pid 5838) has died.
12-22 18:40:14.247 W/ActivityManager(  362): Scheduling restart of crashed service com.xxxx/com.xxxx.NotifyingService in 5000ms
12-22 18:40:19.327 I/ActivityManager(  362): Start proc com.xxxx for service xxxx.pro/com.xxxx.NotifyingService: pid=5877 uid=10054 gids={50054, 3003, 3002, 1028}

I see the same restart behavior if I end the task by 'swiping' from the recent tasks list. So this is all good - it means that the core AOSP code is behaving as it has in previous levels.

I am looking at the Cyanogenmod service code to try and figure out why things aren't getting scheduled for restart - no luck yet. It appears that it should reschedule it. Cyanogenmod uses a service map which AOSP doesn't - but unclear whether that is an issue or not (doubtful) https://github.com/CyanogenMod/android_frameworks_base/blob/cm-11.0/services/java/com/android/server/am/ActiveServices.java#L2092

A rather hackish workaround you can do is to use a similar mechanism as your onTaskRemoved AlarmService to enable an alarm for X minutes later. Then every few minutes while your app is up and running, you can reset the alarm - so it only goes off if things really have been killed and not restarted. This isn't foolproof - using a Handler gives you uptime vs the alarm service which uses realtime, so it's possible for your alarm to trigger even though it was set at a longer time than your 'reset' handler. But if you set an intent extra you can chose to ignore the onStartCommand if your service was already up and running, turning this into a noop.

I'm not a fan of the following hack at all - but it shouldn't do any real harm. If the user does an explicit Force Close, then the alarm manager will destroy any alarms set so that the service won't restart (which is what the user wants).

First, create a helper method that will set an alarm for 20 minutes which will cause onStartCommand to be triggered for your service. Every 2 minutes have a Handler which will reset the 20 minute alarm. If the handler runs within the realtime 20 minutes, the alarm will never go off. The handler isn't guaranteed to run though if the device is asleep (which is good).

private void ensureServiceStaysRunning() {
    // KitKat appears to have (in some cases) forgotten how to honor START_STICKY
    // and if the service is killed, it doesn't restart.  On an emulator & AOSP device, it restarts...
    // on my CM device, it does not - WTF?  So, we'll make sure it gets back
    // up and running in a minimum of 20 minutes.  We reset our timer on a handler every
    // 2 minutes...but since the handler runs on uptime vs. the alarm which is on realtime,
    // it is entirely possible that the alarm doesn't get reset.  So - we make it a noop,
    // but this will still count against the app as a wakelock when it triggers.  Oh well,
    // it should never cause a device wakeup.  We're also at SDK 19 preferred, so the alarm
    // mgr set algorithm is better on memory consumption which is good.
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
    {
        // A restart intent - this never changes...        
        final int restartAlarmInterval = 20*60*1000;
        final int resetAlarmTimer = 2*60*1000;
        final Intent restartIntent = new Intent(this, NotifyingService.class);
        restartIntent.putExtra("ALARM_RESTART_SERVICE_DIED", true);
        final AlarmManager alarmMgr = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
        Handler restartServiceHandler = new Handler()
        {
            @Override
            public void handleMessage(Message msg) {
                // Create a pending intent
                PendingIntent pintent = PendingIntent.getService(getApplicationContext(), 0, restartIntent, 0);
                alarmMgr.set(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + restartAlarmInterval, pintent);
                sendEmptyMessageDelayed(0, resetAlarmTimer);
            }            
        };
        restartServiceHandler.sendEmptyMessageDelayed(0, 0);  
    }
}

In your onCreate you can call this method. Also - in your onStartCommand, be sure to ignore this if your service is already up and running. EG:

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
    ...
    if ((intent != null) && (intent.getBooleanExtra("ALARM_RESTART_SERVICE_DIED", false)))
    {
        Log.d(TAG, "onStartCommand after ALARM_RESTART_SERVICE_DIED");
        if (IS_RUNNING)
        {
            Log.d(TAG, "Service already running - return immediately...");
            ensureServiceStaysRunning();
            return START_STICKY;
        }
    }
    // Do your other onStartCommand stuff..
    return START_STICKY;
}
查看更多
倾城一夜雪
3楼-- · 2019-01-02 17:49

Seems that this is a bug present in Android 4.4, got around it with the following:

@Override
public void onTaskRemoved(Intent rootIntent) {
    Intent restartService = new Intent(getApplicationContext(),
            this.getClass());
    restartService.setPackage(getPackageName());
    PendingIntent restartServicePI = PendingIntent.getService(
            getApplicationContext(), 1, restartService,
            PendingIntent.FLAG_ONE_SHOT);
    AlarmManager alarmService = (AlarmManager)getApplicationContext().getSystemService(Context.ALARM_SERVICE);
    alarmService.set(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() +1000, restartServicePI);

}

Found this answer from this post

查看更多
倾城一夜雪
4楼-- · 2019-01-02 17:59

This is not a 100% working solution but it's the best so far as it almost completely eliminates the problem. So far I integrated this solution along with overriding onTaskRemoved (See this answer) and a keep-alive notification (See this answer). Additional answers are very appreciated !

After further investigation, it seems that the bug already exists in Jelly Bean and looks like there is a solution for that (At least in my case that seems to work. will keep on testing and update the answer if required).

From what I observed this only happens with services that receive broadcasts set by AlarmManager.

To reproduce the bug follow these steps:

  1. Start the app
  2. start the service as a foreground service (use startForeground for that) from within the app
  3. Swipe the app from "Recent Apps" list
  4. Send a broadcast that is handled by the service
  5. The service is killed !

Using adb shell dumpsys >C:\dumpsys.txt you can monitor the state of the service between the different steps. (look for Process LRU list in the dumpsys output) on steps 2 and 3 you will see something like this:

Proc # 2: prcp  F/S/IF trm: 0 11073:<your process name>/u0a102 (fg-service)

Specifically, notice the F/S/IF and the (fg-service) that indicate the service is running as a foreground service (more details on how to analyze the dumpsys at this link: https://stackoverflow.com/a/14293528/624109).

After step 4 you will not see your service in the Process LRU list. Instead, you can look at the device logcat and you will see the following:

I/ActivityManager(449): Killing 11073:<your process name>/u0a102 (adj 0): remove task

What seems to be causing that behavior is the fact that the received broadcast takes the service out of its foreground state and then killed.

To avoid that, you can use this simple solution when creating your PendingIntent for the AlarmManager (Source: https://code.google.com/p/android/issues/detail?id=53313#c7)

AlarmManager am = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
Intent intent = new Intent("YOUR_ACTION_NAME");
intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 1, intent, 0);

Pay attention to the following steps:

  1. Call addFlags on the intent and use FLAG_RECEIVER_FOREGROUND
  2. Use a non-zero request code in PendingIntent.getBroadcast

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

Note that the FLAG_RECEIVER_FOREGROUND was added on API 16 (Jelly Bean) so it makes sense that this is when the bug first appeared...

Most likely that KitKat is just more aggressive when it comes to killing processes and this is why it was emphasized with KitKat, but looks like this was already relevant on Jelly Bean.

Note 2: Notice the details in the question about the service configuration - running in a separate process, as a foreground service, with endWithTask set to false in the manifest.

Note 3: The same thing happens when the app receives the android.appwidget.action.APPWIDGET_CONFIGURE message and shows a configuration activity for a new widget (Replace step 4 above with creating a new widget). I found that only happens when the widget provider (the receiver that handles android.appwidget.action.APPWIDGET_UPDATE) is set to run on a different process than the activity process. After changing that so both the configuration activity and the widget provider are on the same process, this no longer happens.

查看更多
何处买醉
5楼-- · 2019-01-02 18:04

i found this simple trick to solve this problem without using AlarmManager.

  1. create a broadcast receiver that listens broadcast everytime onDestroy() method in service is called:

    public class RestartService extends BroadcastReceiver {
    
    private static final String TAG = "RestartService";
    
    public RestartService() {
    }
    
    @Override
    public void onReceive(Context context, Intent intent) {
    Log.e(TAG, "onReceive");
    context.startService(new Intent(context, YourService.class));
    }
    }
    
  2. add customized broadcast intent to your manifest

    <receiver
        android:name=".RestartService"
        android:enabled="true" >
        <intent-filter>
            <action android:name="restartApps" />
        </intent-filter>
    </receiver>
    
  3. then, send broadcast from onDestroy(), probably like this:

    @Override
    public void onDestroy() {
    Intent intent = new Intent("restartApps");
    sendBroadcast(intent);
    super.onDestroy();
    stopThread();
    }
    
  4. call onDestroy() from onTaskRemoved(Intent intent)

this trick will restart your service everytime user close service from both task manager and force close from settings, i hope this will help you too

查看更多
登录 后发表回答