web-dev-qa-db-fra.com

Minimal service de premier plan Android tué sur un téléphone haut de gamme

J'essaie de créer une application permettant aux utilisateurs de consigner des itinéraires (emplacements/GPS). Pour que les emplacements soient enregistrés même lorsque l'écran est éteint, j'ai créé un foreground service pour l'enregistrement des emplacements. Je stocke les emplacements dans un Room Database qui est injecté dans mon service à l'aide de Dagger2.

Cependant, ce service est détruit par Android, ce qui, bien sûr, n’est pas bon. Je pourrais souscrire à des avertissements de mémoire insuffisante, mais cela ne résout pas le problème sous-jacent de la mort de mon service après environ 30 minutes d'utilisation d'un téléphone haut de gamme sous Android 8.0.

J'ai créé un projet minimal avec uniquement une activité "Hello world" et le service: https://github.com/RandomStuffAndCode/AndroidForegroundService

Le service est démarré dans ma classe Application et l'enregistrement de l'itinéraire via une Binder:

// Application
@Override
public void onCreate() {
    super.onCreate();
    mComponent = DaggerAppComponent.builder()
            .appModule(new AppModule(this))
            .build();

    Intent startBackgroundIntent = new Intent();
    startBackgroundIntent.setClass(this, LocationService.class);
    startService(startBackgroundIntent);
}

// Binding activity
bindService(new Intent(this, LocationService.class), mConnection, Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT);
// mConnection starts the route logging through `Binder` once connected. The binder calls startForeground()

Je n'ai probablement pas besoin du drapeau BIND_AUTO_CREATE, j'ai déjà testé différents drapeaux pour ne pas faire tuer mon service - pas de chance jusqu'à présent.

En utilisant le profileur, il ne semble pas y avoir de fuite de mémoire, l'utilisation de la mémoire est stable à ~ 35 Mo.

 profiler

En utilisant adb Shell dumpsys activity processes > tmp.txt, je peux confirmer que foregroundServices=true et mon service figurent en 8e position dans la liste des LRU:

Proc # 3: prcp F/S/FGS trm: 0 31592:com.example.foregroundserviceexample/u0a93 (fg-service)

Il semble qu'il ne soit pas possible de créer un service de premier plan auquel vous pouvez faire confiance pour ne pas être tué. Alors, que pouvons-nous faire? Bien...

  1. Mettez le service dans un processus séparé, pour tenter de laisser Android tuer l'interface utilisateur/les activités tout en laissant le service seul. Cela aiderait probablement, mais ne semble pas être une garantie
  2. Persistez tout dans le service, par exemple. une base de données de salle. Chaque variable, chaque classe personnalisée, chaque fois qu’une des modifications est apportée, puis démarrez le service avec START_STICKY. Cela semble une sorte de gaspillage et ne conduit pas à un très beau code, mais cela fonctionnerait probablement ... un peu. En fonction du temps nécessaire à Android pour recréer le service après l'avoir tué, une grande partie des emplacements peut être perdue.

Est-ce vraiment l'état actuel de faire des choses en arrière-plan sur Android? N'y a-t-il pas un meilleur moyen?

EDIT: La liste blanche de l'application pour l'optimisation de la batterie (la désactivation) n'empêche pas mon service d'être tué

EDIT: L'utilisation de Context.startForegroundService() pour démarrer le service n'améliore pas la situation

EDIT: Donc, cela ne se produit effectivement que sur some , mais cela se produit systématiquement sur eux. Je suppose que vous devez choisir entre ne pas prendre en charge un grand nombre d'utilisateurs ou écrire un code vraiment moche. Impressionnant.

16
user1202032

Un service démarré par startForeground correspond au deuxième groupe le plus important processus visible :

  1. Un processus visible est un travail dont l'utilisateur est actuellement conscient, le tuer aurait donc un impact négatif notable sur l'utilisateur expérience. Un processus est considéré comme visible dans ce qui suit conditions:

    1. Il exécute une activité visible à l'écran par l'utilisateur mais pas au premier plan (sa méthode onPause () a été appelée). Ce peut se produire, par exemple, si l’activité de premier plan est affichée en tant que dialogue qui permet de voir l’activité précédente derrière elle.

    2. Il a un service qui fonctionne en tant que service de premier plan, via Service.startForeground () (qui demande au système de traiter le service Comme quelque chose que l'utilisateur connaît ou qui est essentiellement visible pour Les.) .

    3. Il héberge un service utilisé par le système pour une fonction particulière connue de l'utilisateur, telle qu'une méthode de saisie de fond d'écran animé service, etc.

    Le nombre de ces processus en cours d'exécution dans le système est moins limité que les processus de premier plan, mais toujours relativement contrôlée. Celles-ci les processus sont considérés comme extrêmement importants et ne seront pas tués à moins que cela ne soit nécessaire pour maintenir tous les processus de premier plan en cours d'exécution.

Cela étant dit, vous ne pouvez jamais être sûr que votre service n'est tué à aucun moment. Par exemple. pression de la mémoire, pile faible, etc. Voir qui-vit-et-qui-meurt }. 


En gros, vous avez répondu vous-même à la question. Le chemin à parcourir est START_STICKY :

Pour les services démarrés, il existe deux autres modes principaux de opération, ils peuvent décider de courir, en fonction de la valeur qu'ils return from onStartCommand():START_STICKY est utilisé pour les services qui sont explicitement démarrés et arrêtés selon les besoins, tandis que START_NOT_STICKY ou START_REDELIVER_INTENT sont utilisés pour des services qui devraient uniquement rester en cours pendant le traitement des commandes qui leur sont envoyées. Voir le documentation liée pour plus de détails sur la sémantique.

En règle générale, vous devez utiliser le moins possible le service d’arrière-plan (au premier plan), c’est-à-dire ne faire que le suivi de la position et conserver tout le reste dans votre activité au premier plan. Seul le suivi devrait nécessiter très peu de configuration et peut être chargé rapidement. Aussi, plus votre service est petit, moins il risque d'être tué. Votre activité sera restaurée par le système dans l'état qui était avant de passer à l'arrière-plan, à condition qu'elle ne soit pas également supprimée. Un "démarrage à froid" de l'activité de premier plan ne devrait en revanche pas poser de problème. 
Je ne considère pas cela comme moche, car cela garantit que le téléphone offre toujours la meilleure expérience à l'utilisateur. C’est la chose la plus importante à faire. Il est regrettable que certains appareils ferment les services après 30 minutes (éventuellement sans interaction de l'utilisateur).

Donc, comme vous l'avez dit, vous devez

Conserver tout dans le service, par exemple une base de données de salle. Chaque variable, chaque classe personnalisée, chaque fois qu’un d’eux change, puis démarrez le service avec START_STICKY. 

Voir création d'un service sans fin

Question implicite: 

Selon le temps nécessaire à Android pour recréer le fichier service après l'avoir tué, une grande partie des lieux peut être perdue.

Cela prend généralement très peu de temps. Surtout parce que vous pouvez utiliser le Api du fournisseur d'emplacement fusionné pour les mises à jour d'emplacement, qui est un service système indépendant et qui a très peu de chances d'être tué. Donc, cela dépend principalement du temps dont vous avez besoin pour recréer le service dans onStartCommand

Notez également qu'à partir d'Android 8.0, vous devez utiliser un fichier forground service en raison de emplacement d'arrière-plan limites _.


Edit: Comme récemment couvert dans les nouvelles: Certains fabricants peuvent vous donner du fil à retordre pour que votre service fonctionne. Le site https://dontkillmyapp.com/ garde une trace des fabricants et des solutions possibles pour votre appareil. Oneplus _ est actuellement (29.01.19) l'un des pires contrevenants.

Lors de la sortie de leurs téléphones 1 + 5 et 1 + 6, OnePlus a introduit l’un des limites de fond les plus sévères sur le marché à ce jour, même en nanisme ceux réalisés par Xiaomi ou Huawei. Les utilisateurs doivent non seulement activer paramètres supplémentaires pour que leurs applications fonctionnent correctement, mais ces paramètres même réinitialiser avec mise à jour du firmware afin que les applications cassent à nouveau et les utilisateurs sont requis pour réactiver ces paramètres régulièrement.

Solution pour les utilisateurs

Désactivez Paramètres système> Applications> Icône engrenage> Accès spécial> Batterie Optimisation.

malheureusement il y a

Aucune solution connue du côté développeur

5
leonardkraemer

Je vous suggère d’utiliser celles-ci: AlarmManager, PowerManager, WakeLock, Thread, WakefulBroadcastReceiver, Handler, Looper

Je suppose que vous utilisez déjà ces "processus séparés" et d’autres réglages également.

Donc, dans votre classe Application:

MyApp.Java:

import Android.app.AlarmManager;
import Android.app.Application;
import Android.app.PendingIntent;
import Android.content.Context;
import Android.content.Intent;
import Android.os.PowerManager;
import Android.util.Log; 

public final class MyApp extends Application{

public static PendingIntent pendingIntent = null;
public static Thread                infiniteRunningThread;
public static PowerManager          pm;
public static PowerManager.WakeLock wl;


@Override
public void onCreate(){
    try{
        Thread.setDefaultUncaughtExceptionHandler(
                (thread, e)->restartApp(this, "MyApp uncaughtException:", e));
    }catch(SecurityException e){
        restartApp(this, "MyApp uncaughtException SecurityException", e);
        e.printStackTrace();
    }
    pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
    if(pm != null){
        wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "TAG");
        wl.acquire(10 * 60 * 1000L /*10 minutes*/);
    }

    infiniteRunningThread = new Thread();

    super.onCreate();
}

public static void restartApp(Context ctx, String callerName, Throwable e){
    Log.w("TAG", "restartApp called from " + callerName);
    wl.release();
    if(pendingIntent == null){
        pendingIntent =
                PendingIntent.getActivity(ctx, 0,
                                          new Intent(ctx, ActivityMain.class), 0);
    }
    AlarmManager mgr = (AlarmManager) ctx.getSystemService(Context.ALARM_SERVICE);
    if(mgr != null){
        mgr.set(AlarmManager.RTC_WAKEUP,
                System.currentTimeMillis() + 10, pendingIntent);
    }
    if(e != null){
        e.printStackTrace();
    }
    System.exit(2);
}
}

Et ensuite à votre service:

ServiceTrackerTest.Java:

import Android.app.Service;
import Android.content.Context;
import Android.content.Intent;
import Android.graphics.BitmapFactory;
import Android.os.Handler;
import Android.os.IBinder;
import Android.os.Looper;
import Android.os.PowerManager;
import Android.support.v4.app.NotificationCompat;
import Android.support.v4.content.WakefulBroadcastReceiver;

public class ServiceTrackerTest extends Service{

private static final int SERVICE_ID = 2018;
private static PowerManager.WakeLock wl;

@Override
public IBinder onBind(Intent intent){
    return null;
}

@Override
public void onCreate(){
    super.onCreate();
    try{
        Thread.setDefaultUncaughtExceptionHandler(
                (thread, e)->MyApp.restartApp(this,
                                              "called from ServiceTracker onCreate "
                                              + "uncaughtException:", e));
    }catch(SecurityException e){
        MyApp.restartApp(this,
                         "called from ServiceTracker onCreate uncaughtException "
                         + "SecurityException", e);
        e.printStackTrace();
    }
    PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
    if(pm != null){
        wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "TAG");
        wl.acquire(10 * 60 * 1000L /*10 minutes*/);
    }


    Handler h = new Handler();
    h.postDelayed(()->{

        MyApp.infiniteRunningThread = new Thread(()->{
            try{
                Thread.setDefaultUncaughtExceptionHandler(
                        (thread, e)->MyApp.restartApp(this,
                                                      "called from ServiceTracker onCreate "
                                                      + "uncaughtException "
                                                      + "infiniteRunningThread:", e));
            }catch(SecurityException e){
                MyApp.restartApp(this,
                                 "called from ServiceTracker onCreate uncaughtException "
                                 + "SecurityException "
                                 + "infiniteRunningThread", e);
                e.printStackTrace();
            }

            Looper.prepare();
            infiniteRunning();
            Looper.loop();
        });
        MyApp.infiniteRunningThread.start();
    }, 5000);
}

@Override
public void onDestroy(){
    wl.release();
    MyApp.restartApp(this, "ServiceTracker onDestroy", null);
}

@SuppressWarnings("deprecation")
@Override
public int onStartCommand(Intent intent, int flags, int startId){
    if(intent != null){
        try{
            WakefulBroadcastReceiver.completeWakefulIntent(intent);
        }catch(Exception e){
            e.printStackTrace();
        }
    }
    startForeground(SERVICE_ID, getNotificationBuilder().build());
    return START_STICKY;
}


private void infiniteRunning(){
    //do your stuff here
    Handler h = new Handler();
    h.postDelayed(this::infiniteRunning, 300000);//5 minutes interval
}

@SuppressWarnings("deprecation")
private NotificationCompat.Builder getNotificationBuilder(){
    return new NotificationCompat.Builder(this)
                   .setContentIntent(MyApp.pendingIntent)
                   .setContentText(getString(R.string.notification_text))
                   .setContentTitle(getString(R.string.app_name))
                   .setLargeIcon(BitmapFactory.decodeResource(getResources(),
                                                              R.drawable.ic_launcher))
                   .setSmallIcon(R.drawable.ic_stat_tracking_service);
}

}

Ignorez "Deprecation" et tout ça, utilisez-les quand vous n'avez pas le choix ... Je pense que le code est clair et qu'il n'a pas besoin d'être expliqué. Il s’agit simplement de suggestions de solutions de contournement. 

0
M D P