52 votes

START_STICKY ne fonctionne pas sur Android KitKat

L'une de mes applications possède un service d'arrière-plan qui utilise l'interface de l'utilisateur. START_STICKY code de retour de onStartCommand pour redémarrer automatiquement lorsque le système le tue. Il semble que cela ne fonctionne plus sous Android KitKat. Existe-t-il une solution à ce problème ? Devrais-je faire quelque chose de différent sur Kitkat pour que le service continue à fonctionner ?

Note : Il y a une discussion similaire sur le groupe Android-Devlopers concernant le comportement du glissement de l'application depuis 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 : J'ai vu qu'il y a des bugs ouverts sur Android issue tracker :

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

Edit2 : La même chose se produit même si le service est en cours d'exécution en utilisant startForeground dans un processus séparé et avec le drapeau android:stopWithTask="false" dans le fichier AndroidManifest.xml...

Edit3 : Plus de bogues liés sur 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

Existe-t-il une solution de contournement pour retrouver le comportement précédent ?

0 votes

Tout d'abord, votre service fonctionne-t-il sur le même processus que votre application ? Si c'est le cas, c'est que lorsque votre application est tuée, le Service l'est aussi. Essayez donc d'exécuter votre Service dans un processus différent, voici le lien pour vous : vogella.com/articles/AndroidServices/article.html

0 votes

J'ai essayé de l'exécuter sur le même processus et sur un processus différent. Même résultat.

0 votes

Il semble qu'ils aient copié le comportement d'iOS 7. Si vous faites glisser une application, son processus est interrompu et elle ne sera plus autorisée à exécuter quoi que ce soit en arrière-plan, y compris ses services, jusqu'à ce que vous relanciez manuellement l'application ou redémarriez l'appareil.

29voto

user2247689 Points 1413

Il semble que ce soit un bug présent dans Android 4.4, je l'ai 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);

}

J'ai trouvé cette réponse dans ce poste

4 votes

Merci de poster MA solution à partir du lien ci-dessus (J'aurais pu au moins en indiquer la source...). groups.google.com/forum/#!topic/Android-developers/H-DSQ4-tiac Comme indiqué sur ce lien, il ne s'agit pas d'une solution complète et elle présente encore des lacunes.

1 votes

Pourquoi ne pas le poster ? Je vais supprimer ce message :)

1 votes

Pas besoin de supprimer votre message. Je ne l'ai pas posté moi-même car ce n'est pas une solution complète. Elle ne gère que le cas où un utilisateur fait glisser l'application depuis la liste des "Applications récentes" mais pas lorsque le processus est tué par Android. De plus, après d'autres tests, il semble que le +1000 n'est parfois pas suffisant et le processus est tué après cette seconde de tampon, j'ai donc changé cette valeur en +10000 . Il n'en reste pas moins qu'il s'agit d'une solution de rechange peu pratique (et qui ne fonctionne pas toujours). Non pas que j'aie quelque chose de mieux pour le moment...

21voto

George Tanner Points 131

Le problème ici semble ne pas se produire sur les ROMs basées sur AOSP. C'est-à-dire que je peux facilement recréer ce problème sur une ROM basée sur CyanogenMod 11, mais sur une ROM AOSP (et sur un émulateur), START_STICKY se comporte exactement comme je m'y attendais. Cela dit, je vois des rapports de personnes sur des Nexus 5 qui semblent avoir ce comportement, donc peut-être que c'est encore un problème dans AOSP.

Sur un émulateur et sur une ROM AOSP, je vois ce qui suit dans un logcat lorsque je fais un 'kill 5838' sur 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 constate le même comportement de redémarrage si je termine la tâche en faisant glisser la souris depuis la liste des tâches récentes. C'est donc une bonne chose - 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 de Cyanogenmod pour essayer de comprendre pourquoi les choses ne sont pas programmées pour le redémarrage - pas encore de chance. Il semble qu'il devrait le reprogrammer. Cyanogenmod utilise une carte de service, ce qui n'est pas le cas d'AOSP, mais je ne sais 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 artisanale consiste à utiliser un mécanisme similaire à celui de votre service d'alarme onTaskRemoved pour activer une alarme pour X minutes plus tard. Ensuite, toutes les quelques minutes, pendant que votre application est en fonctionnement, vous pouvez réinitialiser l'alarme - de sorte qu'elle ne se déclenche que si les choses ont vraiment été tuées et non redémarrées. Ce n'est pas infaillible - l'utilisation d'un Handler vous donne du temps de fonctionnement par rapport au service d'alarme qui utilise le temps réel, il est donc possible que votre alarme se déclenche même si elle a été réglée à un moment plus long que votre handler de 'reset'. Mais si vous définissez une intention supplémentaire, vous pouvez choisir d'ignorer la commande onStartCommand si votre service était déjà en cours d'exécution, ce qui en fait un noop.

Je ne suis pas du tout un fan du hack suivant - mais il ne devrait pas faire de mal. Si l'utilisateur effectue une fermeture forcée explicite, alors 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'aide qui déclenchera une alarme pendant 20 minutes, ce qui entraînera le déclenchement de la commande onStartCommand pour votre service. Toutes les 2 minutes, un Handler réinitialisera 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. L'exécution du gestionnaire n'est pas garantie si l'appareil est endormi (ce qui est une bonne chose).

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 même, dans votre onStartCommand, assurez-vous d'ignorer cette méthode si votre service est déjà opérationnel. 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;
}

0 votes

Merci pour cette réponse détaillée. C'est ce que je pensais en dernier recours, mais j'espérais obtenir une solution plus "standard". Si aucune autre solution ne se présente, (malheureusement) cela pourrait être la voie à suivre...

0 votes

J'ai pu reproduire ce comportement, c'est-à-dire ne pas redémarrer un service collant, avec un émulateur fonctionnant sous Android 4.4. Alors que le même service a été redémarré avec W/ActivityManager(2707): Scheduling restart of crashed service org.foo.bar/.FooService in 5000ms sur un émulateur fonctionnant sous Android 4.2. Les deux services ont été tués avec SIGKILL en utilisant kill <pidOfService> . Donc, malheureusement est un bug dans l'AOSP.

0 votes

Plus d'informations : Mon service a également été redémarré sur un émulateur fonctionnant sous Android 4.3, il ne s'agit donc pas d'un problème lié à Jelly Bean.

17voto

Muzikant Points 2073

Cette solution ne fonctionne pas à 100 % mais c'est la meilleure jusqu'à présent car elle élimine presque complètement le problème. Jusqu'à présent, j'ai intégré cette solution ainsi que le remplacement de la fonction onTaskRemoved (Voir cette réponse ) et une notification de type "keep-alive" (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 il semble qu'il existe une solution pour cela (au moins dans mon cas qui semble fonctionner. Je vais continuer à tester et 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 diffusions définies par AlarmManager .

Pour reproduire le bogue, suivez les étapes suivantes :

  1. Lancer l'application
  2. démarrer le service en tant que service de premier plan (utiliser startForeground pour cela) à partir de l'application
  3. Faites glisser l'application de la liste "Apps récentes".
  4. Envoyer une diffusion qui est traitée par le service
  5. Le service est tué !

Utilisation de adb shell dumpsys >C:\dumpsys.txt vous pouvez surveiller l'état du service entre les différentes étapes. (recherchez 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, remarquez les F/S/IF y el (fg-service) qui indiquent que le service est exécuté en tant que service d'avant-plan (plus de détails sur la façon d'analyser les dumpsys à ce lien : https://stackoverflow.com/a/14293528/624109 ).

Après l'étape 4, vous ne verrez plus votre service dans la section Process LRU list . Au lieu de cela, vous pouvez regarder le périphérique logcat 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 fait sortir le service de son état de premier plan, puis le tue.

Pour éviter cela, vous pouvez utiliser ceci 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. Utiliser un code de demande non nul dans PendingIntent.getBroadcast

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

Notez que le FLAG_RECEIVER_FOREGROUND a été ajouté à l'API 16 (Jelly Bean). Il est donc logique que ce soit à ce moment-là que le bogue soit apparu...

Il est fort probable que KitKat soit simplement plus agressif lorsqu'il s'agit de tuer des processus et c'est pourquoi il a été mis en avant avec KitKat, mais il semble que cela était déjà pertinent sur Jelly Bean.

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

Note 3 : La même chose se produit lorsque l'application reçoit le message android.appwidget.action.APPWIDGET_CONFIGURE et montre une activité de configuration pour un nouveau widget (Remplacer 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 traite les messages android.appwidget.action.APPWIDGET_UPDATE ) est configuré pour s'exécuter sur un processus différent de celui de l'activité. Après avoir modifié ce paramètre pour que l'activité de configuration et le fournisseur de widgets soient tous deux sur le même processus, ce problème ne se produit plus.

1 votes

Dans mon cas, cela n'a pas aidé, mais je n'utilise pas de service de premier plan. ( stackoverflow.com/questions/21073136/ )

0 votes

Qu'est-ce que tu veux dire ? Je ne comprends pas ! Même si je n'utilise pas les récepteurs pour le gestionnaire d'alarme, le fait de glisser l'application depuis les applications récentes ferme mon service, même parfois le système ferme mon service, sans glisser. @Muzikant

1 votes

J'ai pu reproduire le bug avec un service qui n'est pas recevoir une diffusion de AlarmManager.

9voto

baskara Points 986

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 onDestroy() dans le 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, envoyer la diffusion de onDestroy() probablement comme ça :

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

Cette astuce va redémarrer votre service à chaque fois que l'utilisateur ferme le service à partir du gestionnaire de tâches et de la fermeture forcée à partir des paramètres, j'espère que cela vous aidera aussi.

-5voto

Ilya_Gazman Points 3685

De Android Service.START_STICKY

Constante pour revenir de onStartCommand(Intent, int, int) si le processus de ce service est tué alors qu'il est en cours de démarrage (après être revenu de l'opération onStartCommand(Intent, int, int)), alors laissez-le dans l'état de démarrage mais ne conservez pas l'intention livrée. Plus tard, le système essaiera de recréer le service. Comme il est dans l'état démarré, il va garantir l'appel de onStartCommand(Intent, int, int) après la création de la nouvelle instance de service. nouvelle instance de service ; s'il n'y a pas de commandes de démarrage en attente de service, elle sera appelée avec un objet intentionnel nul. nul, vous devez donc veiller à vérifier ce point.

Ce mode a du sens pour les choses qui seront explicitement démarrées et explicitement démarrées et arrêtées pour s'exécuter pendant des périodes arbitraires, telles qu'un service qui lit de la musique en arrière-plan.

Il ne dit pas que votre service sera restauré après avoir été tué, il parle de la situation où vous avez demandé à démarrer votre service, et qu'il a été détruit avant que l'opération ne soit terminée. onStartCommand a été appelé.

Si vous souhaitez éviter que votre service ne soit tué, vous devez l'exécuter dans Premier plan mode. Il est également recommandé de séparer votre service du cycle de vie de l'application en l'exécutant dans son propre mode de fonctionnement. processus .

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X