Минимальное обслуживание переднего плана Android, убитое на высоком телефоне

Я пытаюсь создать приложение, которое позволяет пользователям регистрировать маршруты (местоположения /GPS). Для обеспечения регистрации местоположений, даже когда экран выключен, я создал foreground service для регистрации местоположений. Я храню местоположения в Room Database, который вводится в мой сервис с использованием Dagger2.

Однако этот сервис убит Android, что, конечно, нехорошо. Я мог бы подписаться на предупреждения о нехватке памяти, но это не решает основную проблему, из-за которой мой сервис погибает через ~ 30 минут на современном высококачественном телефоне с Android 8.0

Я создал минимальный проект только с действием "Hello world" и сервисом: https://github.com/RandomStuffAndCode/AndroidForegroundService

Служба запущена в моем классе Application, а запись маршрута запущена через 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()

Мне, вероятно, не нужен флаг BIND_AUTO_CREATE, я тестировал разные флаги, пытаясь не убить мой сервис - пока не повезло.

При использовании профилировщика не возникает утечек памяти, использование памяти стабильно составляет ~ 35 МБ:

profiler

Используя adb shell dumpsys activity processes > tmp.txt, я могу подтвердить, что foregroundServices=true и мой сервис заняли 8-е место в списке LRU:

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

Кажется, что невозможно создать приоритетный сервис, которому можно доверять, чтобы не быть убитым. Так что мы можем сделать? Ну...

  1. Поместите службу в отдельный процесс, пытаясь позволить Android убить пользовательский интерфейс/действия, оставив службу в покое. Возможно, поможет, но не кажется гарантией
  2. Сохраняйте все на службе, например, Комната базы данных. Каждая переменная, каждый пользовательский класс, каждый раз, когда любое из изменений, а затем запустить службу с START_STICKY. Это кажется расточительным и не приводит к очень красивому коду, но это, вероятно, сработает... несколько. В зависимости от того, сколько времени потребуется Android для повторного создания службы после ее уничтожения, большая часть местоположений может быть потеряна.

Это действительно текущее состояние дел в фоновом режиме на Android? Разве нет лучшего способа?

ОБНОВЛЕНИЕ: внесение в белый список приложения для оптимизации заряда аккумулятора (его отключение) не останавливает работу моего сервиса

ОБНОВЛЕНИЕ: Использование Context.startForegroundService() для запуска службы не улучшает ситуацию

ОБНОВЛЕНИЕ: Таким образом, это действительно происходит только на некоторых устройствах, но это происходит последовательно на них. Я думаю, вам придется сделать выбор: либо не поддерживать огромное количество пользователей, либо написать действительно некрасивый код. Потрясающие.

Ответ 1

Служба, запущенная startForeground, входит во вторую наиболее важную группу видимого процесса:

  1. Видимый процесс выполняет работу, о которой в данный момент знает пользователь, так что убийство имело бы заметное негативное влияние на пользователя опыт. Процесс считается видимым в следующем Условия:

    1. Он запускает Activity, которая видна пользователю на экране, но не на переднем плане (был вызван его метод onPause()). Этот может произойти, например, если основной вид деятельности отображается как диалог, позволяющий увидеть предыдущую активность за ней.

    2. У него есть Служба, которая работает как служба переднего плана, через Service.startForeground() (которая просит систему обработать сервис как то, что пользователь знает, или, по сути, видимый для их).

    3. Это хостинг службы, которую система использует для определенной функции, известной пользователю, такой как живые обои, метод ввода сервис и т.д.

    Число этих процессов, запущенных в системе, менее ограничено. чем процессы переднего плана, но все еще относительно контролируемые. Эти процессы считаются чрезвычайно важными и не будут убиты если это не требуется, чтобы все приоритетные процессы работали.

При этом, вы никогда не можете быть уверены, что ваша служба не будет убита в любое время. Например. нехватка памяти, низкий заряд батареи и т.д. См. кто живет, а кто умирает.


За то, как с этим справиться, в основном вы сами ответили на вопрос. Это путь START_STICKY:

Для запущенных сервисов есть два дополнительных основных режима операция, в которой они могут принять решение, в зависимости от значения, которое они возврат из onStartCommand(): START_STICKY используется для служб, которые явно запускаются и останавливаются по мере необходимости, в то время как START_NOT_STICKY или START_REDELIVER_INTENT используются для служб, которые должны только продолжайте работать во время обработки любых команд, отправленных им. Увидеть связанная документация для более подробной информации о семантике.

В качестве общего руководства вы должны делать как можно меньше в фоновом режиме (рудный план), то есть только отслеживать местоположение и сохранять все остальное в своей деятельности на переднем плане. Только отслеживание должно потребовать очень мало настроек и может быть загружено быстро. Кроме того, чем меньше ваш сервис, тем меньше вероятность, что его убьют. Ваша активность будет восстановлена системой в том состоянии, в котором она находилась до того, как перешла в фоновый режим, если только она не будет уничтожена. С другой стороны, "холодный старт" переднего плана не должен быть проблемой.
Я не считаю это уродливым, потому что это гарантирует, что телефон всегда обеспечивает лучший опыт для пользователя. Это самое важное, что нужно сделать. К сожалению, некоторые устройства закрывают службы через 30 минут (возможно, без взаимодействия с пользователем).

Итак, как вы заявили, вы должны

Сохраняйте все на службе, например, Комната базы данных. каждый переменная, каждый пользовательский класс, каждый раз, когда любой из них изменяется, а затем начать службу с START_STICKY.

Смотрите создание бесконечного сервиса

Скрытый вопрос:

В зависимости от того, сколько времени потребуется Android для воссоздания после его уничтожения большая часть локаций может быть потеряна.

Обычно это занимает очень короткое время. Тем более, что вы можете использовать Fui Location Provider Api для обновлений местоположения, который является независимой системной службой и вряд ли будет убит. Так что это в основном зависит от времени, которое вам нужно для воссоздания сервиса в onStartCommand.

Также обратите внимание, что начиная с Android 8.0 вам нужно использовать forground service из-за фонового местоположения Пределы.


Редактировать: Как недавно освещалось в новостях: Некоторые производители могут затруднить работу службы. Сайт https://dontkillmyapp.com/ отслеживает производителей и возможные меры по снижению риска для вашего устройства. Oneplus в настоящее время (29.01.19) является одним из худших преступников.

Выпуская свои телефоны 1 + 5 и 1 + 6, OnePlus представил один из самые серьезные фоновые ограничения на рынке на сегодняшний день, даже затмение в исполнении Xiaomi или Huawei. Не только пользователи должны включить дополнительные настройки, чтобы их приложения работали правильно, но эти настройки даже получить сброс с обновлением прошивки, так что приложения снова ломаются и пользователи необходимо регулярно включать эти настройки.

Solution for users

Выключите Системные настройки> Приложения> Значок шестеренки> Специальный доступ> Аккумулятор Оптимизация.

к сожалению, есть

Нет известного решения на стороне разработчика

Ответ 2

Я знаю это поздно, но это может помочь кому-то. Я также столкнулся с той же проблемой поддержания службы переднего плана без потери ОС от разных производителей. Большинство ОС китайского производителя убивает службу переднего плана, даже если она добавлена в списки исключений ("Батарея", "Очиститель" и т.д.) И разрешена автозапуск.

Я нашел эту ссылку, которая разрешила мне эту долгую проблему поддержания службы.

Все, что вам нужно сделать, это запустить службу переднего плана в отдельном процессе. Вот и все.

Вы можете сделать это, добавив android:process к вашему сервису в вашем AndroidManifest.xml.

Например:

<service android:name=".YourService"
        android:process=":yourProcessName" />

Вы можете обратиться к документам, чтобы узнать больше о android:process

ОБНОВЛЕНИЕ: SharedPreferences не будет работать в нескольких процессах. В этом случае вы должны использовать методы IPC (межпроцессное взаимодействие) или использовать ContentProviders для хранения и доступа к вашим данным, которые будут использоваться разными процессами. По ссылке docs.

Ответ 3

Я предлагаю вам использовать следующие: AlarmManager, PowerManager, WakeLock, Thread, WakefulBroadcastReceiver, Handler, Looper

Я предполагаю, что вы уже используете эти "отдельные процессы" и другие настройки.

Итак, в вашем классе 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);
}
}

И затем в вашем служении:

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

}

Игнорируйте "устаревание" и прочее, используйте их, когда у вас нет выбора. Я думаю, что код ясен и его не нужно объяснять. Это примерно обходные предложения и решения.