web-dev-qa-db-fra.com

START_STICKY ne fonctionne pas sur Android KitKat

Une de mes applications dispose d'un service backgrouod qui utilise le START_STICKY renvoie le code de onStartCommand pour redémarrer automatiquement lorsque le système le tue. Il semble que cela ne fonctionne plus sur Android KitKat. Y a-t-il une solution? Dois-je faire quelque chose de différent sur KitKat pour maintenir le service en marche?

Remarque: Il y a une discussion similaire sur le groupe Android-Devlopers sur le fait de glisser l'application de la liste des applications récentes. Ces deux problèmes pourraient-ils être liés? https://groups.google.com/forum/#!topic/Android-developers/H-DSQ4-tiac

Edit: Vu qu'il y a des bogues ouverts sur Android tracker problème:

https://code.google.com/p/Android/issues/detail?id=6379https://code.google.com/p/Android/issues/detail? id = 63618

Edit2: La même chose se produit même si le service s'exécute à l'aide de startForeground, dans un processus distinct et avec l'indicateur Android:stopWithTask="false" dans le fichier AndroidManifest.xml ...

Edit3: Plus de bugs liés sur Android tracker problème:

https://code.google.com/p/Android/issues/detail?id=62091https://code.google.com/p/Android/issues/detail? id = 5331https://code.google.com/p/Android/issues/detail?id=104308

Existe-t-il une sorte de solution pour obtenir le comportement précédent?

51
Muzikant

Ce n'est pas une solution à 100% mais c'est la meilleure dans la mesure où elle élimine presque complètement le problème. Jusqu'à présent, j'ai intégré cette solution en remplaçant onTaskRemoved (Voir cette réponse ) et une notification de maintien en vie (Voir cette réponse ). Des réponses supplémentaires sont très appréciées!

Après une enquête plus approfondie, il semble que le bogue existe déjà dans Jelly bean et semble qu'il existe une solution pour cela (au moins dans mon cas, cela semble fonctionner. Continuera de tester et de mettre à jour la réponse si nécessaire).

D'après ce que j'ai observé, cela ne se produit qu'avec les services qui reçoivent des émissions définies par AlarmManager.

Pour reproduire le bogue, procédez comme suit:

  1. Démarrez l'application
  2. démarrer le service en tant que service de premier plan (utilisez startForeground pour cela) à partir de l'application
  3. Faites glisser l'application depuis la liste "Applications récentes"
  4. Envoyer une diffusion gérée par le service
  5. Le service est tué!

En utilisant adb Shell dumpsys >C:\dumpsys.txt vous pouvez surveiller l'état du service entre les différentes étapes. (chercher Process LRU list dans la sortie de dumpsys) aux étapes 2 et 3, vous verrez quelque chose comme ceci:

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

Plus précisément, notez le F/S/IF et le (fg-service) qui indiquent que le service fonctionne en tant que service de premier plan (plus de détails sur la façon d'analyser les dumpsys sur ce lien: https://stackoverflow.com/a/14293528/624109 ).

Après l'étape 4, vous ne verrez pas votre service dans le Process LRU list. Au lieu de cela, vous pouvez consulter le journal du périphérique et vous verrez ce qui suit:

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

Ce qui semble être à l'origine de ce comportement est le fait que la diffusion reçue retire le service de son état de premier plan puis le tue.

Pour éviter cela, vous pouvez utiliser cette solution simple lors de la création de votre PendingIntent pour le 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);

Faites attention aux étapes suivantes:

  1. Appelez addFlags sur l'intention et utilisez FLAG_RECEIVER_FOREGROUND
  2. Utilisez un code de demande différent de zéro dans PendingIntent.getBroadcast

Si vous omettez l'une de ces étapes, cela ne fonctionnera pas.

Notez que le FLAG_RECEIVER_FOREGROUND a été ajouté sur l'API 16 (Jelly bean), il est donc logique que ce soit à ce moment que le bogue est apparu ...

Très probablement, KitKat est juste plus agressif en ce qui concerne les processus de mise à mort et c'est pourquoi il a été souligné avec KitKat, mais il semble que cela était déjà pertinent pour Jelly Bean.

Remarque 2: notez les détails dans la question sur la configuration du service - en cours d'exécution dans un processus distinct, en tant que service de premier plan, avec endWithTask défini sur false dans le manifeste.

Remarque 3: la même chose se produit lorsque l'application reçoit le Android.appwidget.action.APPWIDGET_CONFIGURE message et affiche une activité de configuration pour un nouveau widget (remplacez l'étape 4 ci-dessus par la création d'un nouveau widget). J'ai constaté que cela ne se produit que lorsque le fournisseur de widgets (le récepteur qui gère Android.appwidget.action.APPWIDGET_UPDATE) est configuré pour s'exécuter sur un processus différent de celui du processus d'activité. Après avoir modifié cela afin que l'activité de configuration et le fournisseur de widgets soient sur le même processus, cela ne se produit plus.

16
Muzikant

Semble qu'il s'agit d'un bogue présent dans Android 4.4, l'a contourné avec ce qui suit:

@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);

}

Trouvé cette réponse de ce message

28
Rakeeb Rajbhandari

Le problème semble ne pas se produire ici sur les ROM basées sur AOSP. Autrement dit, je peux facilement recréer cela sur une ROM basée sur CyanogenMod 11, mais sur un AOSP ROM (et sur un émulateur), START_STICKY se comporte exactement comme je m'y attendais. Cela dit, je suis voir les rapports des gens sur Nexus 5 qui semblent voir ce comportement, donc c'est peut-être toujours un problème dans AOSP.

Sur un émulateur et sur une ROM AOSP, je vois ce qui suit d'un logcat quand je fais un 'kill 5838' contre le processus (comme je m'y attendais):

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}

Je vois le même comportement de redémarrage si je termine la tâche en "glissant" dans la liste des tâches récentes. Tout cela est donc bon - cela signifie que le code AOSP de base se comporte comme il l'a fait dans les niveaux précédents.

Je regarde le code de service Cyanogenmod pour essayer de comprendre pourquoi les choses ne sont pas programmées pour un redémarrage - pas de chance pour le moment. Il semble qu'il devrait le reprogrammer. Cyanogenmod utilise une carte de service que l'AOSP ne fait pas - mais ne sait pas si c'est un problème ou non (douteux) https://github.com/CyanogenMod/Android_frameworks_base/blob/cm-11.0/services/Java/com /Android/server/am/ActiveServices.Java#L2092

Une solution de contournement plutôt hackée que vous pouvez faire consiste à utiliser un mécanisme similaire à celui de votre service d'alarme onTaskRemoved pour activer une alarme pendant X minutes plus tard. Ensuite, toutes les quelques minutes pendant que votre application est opérationnelle, vous pouvez réinitialiser l'alarme - elle ne se déclenche donc que si les choses ont vraiment été tuées et n'ont pas été redémarrées. Ce n'est pas infaillible - l'utilisation d'un gestionnaire vous donne un temps de disponibilité par rapport au service d'alarme qui utilise en temps réel, il est donc possible que votre alarme se déclenche même si elle a été définie sur une période plus longue que votre gestionnaire de réinitialisation. Mais si vous définissez une intention supplémentaire, vous pouvez choisir d'ignorer la commande onStartCommand si votre service était déjà opérationnel et en faire un noop.

Je ne suis pas du tout fan du hack suivant - mais cela ne devrait pas faire vraiment de mal. Si l'utilisateur effectue une fermeture forcée explicite, le gestionnaire d'alarmes détruira toutes les alarmes définies afin que le service ne redémarre pas (ce que l'utilisateur souhaite).

Tout d'abord, créez une méthode d'assistance qui déclenchera une alarme pendant 20 minutes, ce qui provoquera le déclenchement de onStartCommand pour votre service. Toutes les 2 minutes, un gestionnaire réinitialise l'alarme de 20 minutes. Si le gestionnaire s'exécute dans les 20 minutes en temps réel, l'alarme ne se déclenchera jamais. Le gestionnaire n'est cependant pas garanti de fonctionner si le périphérique est en veille (ce qui est bien).

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);  
    }
}

Dans votre onCreate, vous pouvez appeler cette méthode. De plus, dans votre onStartCommand, assurez-vous de l'ignorer si votre service est déjà opérationnel. PAR EXEMPLE:

@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;
}
22
George Tanner

j'ai trouvé cette astuce simple pour résoudre ce problème sans utiliser AlarmManager.

  1. créer un récepteur de diffusion qui écoute la diffusion à chaque fois que la méthode onDestroy() en service est appelée:

    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. ajouter une intention de diffusion personnalisée à votre manifeste

    <receiver
        Android:name=".RestartService"
        Android:enabled="true" >
        <intent-filter>
            <action Android:name="restartApps" />
        </intent-filter>
    </receiver>
    
  3. puis, envoyez une diffusion depuis onDestroy(), probablement comme ceci:

    @Override
    public void onDestroy() {
    Intent intent = new Intent("restartApps");
    sendBroadcast(intent);
    super.onDestroy();
    stopThread();
    }
    
  4. appeler onDestroy() à partir de onTaskRemoved(Intent intent)

cette astuce redémarrera votre service à chaque fois que le service de fermeture d'utilisateur du gestionnaire de tâches et la fermeture forcée des paramètres, j'espère que cela vous aidera aussi

8
baskara