最小 android 前台服务在高端 phone 上终止

Minimal android foreground service killed on high-end phone

我正在尝试创建一个允许用户记录路线的应用程序 (locations/GPS)。为了确保即使在屏幕关闭时也能记录位置,我为位置记录创建了一个 foreground service。我将位置存储在 Room Database 中,使用 Dagger2.

将其注入到我的服务中

然而,这个服务被Android杀死了,这当然不好。我可以订阅低内存警告,但这并不能解决我的服务在现代高端 phone 运行 Android 8.0[=26 上约 30 分钟后被杀死的根本问题=]

我创建了一个只有 "Hello world" activity 和服务的最小项目:https://github.com/RandomStuffAndCode/AndroidForegroundService

服务是在我的Applicationclass中启动的,路由日志是通过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 标志,我一直在测试不同的标志,试图不让我的服务被杀死 - 到目前为止运气不好。

使用探查器我似乎没有任何内存泄漏,内存使用稳定在 ~35mb:

使用 adb shell dumpsys activity processes > tmp.txt 我可以确认 foregroundServices=true 并且我的服务在 LRU 列表中排在第 8 位:

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

似乎无法创建您可以信任不会被杀死的前台服务。所以,我们能做些什么?嗯...

  1. 将服务放在一个单独的进程中,试图让 Android 杀死 UI/Activities 而让服务单独存在。可能会有帮助,但似乎不能保证
  2. 在服务中保留 一切房间数据库。每个变量,每个自定义 class,每次任何更改,然后用 START_STICKY 启动服务。这似乎有点浪费,并且不会产生非常漂亮的代码,但它可能会工作......有点。根据 Android 终止服务后重新创建服务所需的时间,大部分位置可能会丢失。

这真的是 Android 在后台做事的现状吗?有没有更好的办法?

编辑:将应用列入白名单以优化电池(禁用它)不会阻止我的服务被终止

编辑:使用 Context.startForegroundService() 启动服务并没有改善这种情况

编辑:所以这确实只发生在一些 设备上,但它始终发生在它们上。我猜你必须做出选择,要么不支持大量用户,要么编写非常丑陋的代码。太棒了

startForeground 启动的服务属于第二重要的组 visible process:

  1. A visible process is doing work that the user is currently aware of, so killing it would have a noticeable negative impact on the user experience. A process is considered visible in the following conditions:

  2. It is running an Activity that is visible to the user on-screen but not in the foreground (its onPause() method has been called). This may occur, for example, if the foreground Activity is displayed as a dialog that allows the previous Activity to be seen behind it.

  3. It has a Service that is running as a foreground service, through Service.startForeground() (which is asking the system to treat the service as something the user is aware of, or essentially visible to them).

  4. It is hosting a service that the system is using for a particular feature that the user is aware, such as a live wallpaper, input method service, etc.

The number of these processes running in the system is less bounded than foreground processes, but still relatively controlled. These processes are considered extremely important and will not be killed unless doing so is required to keep all foreground processes running.

话虽如此,您永远无法确定您的服务在任何时候都没有被终止。例如。内存压力、电池电量不足等。请参阅 who-lives-and-who-dies


怎么处理,基本都是你自己回答的。要走的路是 START_STICKY:

For started services, there are two additional major modes of operation they can decide to run in, depending on the value they return from onStartCommand(): START_STICKY is used for services that are explicitly started and stopped as needed, while START_NOT_STICKY or START_REDELIVER_INTENT are used for services that should only remain running while processing any commands sent to them. See the linked documentation for more detail on the semantics.

作为一般准则,您应该尽可能少地在后台(或前台)服务中执行操作,即只进行位置跟踪,而将其他所有内容都放在前台 activity。只有跟踪需要很少的配置并且可以快速加载。此外,您的服务越小,被杀死的可能性就越小。你的 activity 将被系统恢复到进入后台之前的状态,只要它没有被杀死。另一方面,前景 activity 的“cold-start”应该不是问题。
我不认为这很丑陋,因为这保证 phone 始终为用户提供最佳体验。这是它必须做的最重要的事情。不幸的是,某些设备在 30 分钟后(可能没有用户交互)关闭服务。

所以,正如您所说,您必须

Persist everything in the service in e.g. a Room database. Every variable, every custom class, every time any of them changes and then start the service with START_STICKY.

creating a never ending service

隐式问题:

Depending on how long it takes for Android to re-create the service after killing it, a large portion of locations may be lost.

这通常只需要很短的时间。特别是因为您可以使用 Fused Location Provider Api 进行位置更新,这是一个独立的系统服务,不太可能被杀死。所以这主要取决于你需要在onStartCommand.

重新创建服务的时间

Also take note that from Android 8.0 onwards you need to use a forground service because of the background location limits.


编辑: 正如最近的新闻报道: 一些制造商可能会让您很难保持服务 运行。该网站 https://dontkillmyapp.com/ keeps track of the manufacturers and possible mitigations for your device. Oneplus 目前 (29.01.19) 是最严重的违规者之一。

When releasing their 1+5 and 1+6 phones, OnePlus introduced one of the most severe background limits on the market to date, dwarfing even those performed by Xiaomi or Huawei. Not only did users need to enable extra settings to make their apps work properly, but those settings even get reset with firmware update so that apps break again and users are required to re-enable those settings on a regular basis.

Solution for users

Turn off System Settings > Apps > Gear Icon > Special Access > Battery Optimization.

遗憾的是

No known solution on the developer end

我建议你使用这些: AlarmManagerPowerManagerWakeLockThreadWakefulBroadcastReceiverHandlerLooper

我假设您已经在使用这些 "separate process" 和其他调整。

所以在你的 Application class:

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

}

忽略"Deprecation"之类的东西,别无选择时使用它们。 我认为代码很清楚,不需要解释。 这只是关于解决方法的建议和解决方案。

我知道已经晚了,但这可能对某些人有所帮助。我也面临着保持前台服务不被不同制造商的 OS 杀死的相同问题。大多数中国制造商的 OS 会终止前台服务,即使它被添加到异常列表(电池、清洁器等)并允许自动启动。

我发现了这个,它解决了我长期保持服务的问题。

您所要做的就是 运行 您在单独进程中的前台服务。就是这样。

您可以通过在 AndroidManifest.xml 中将 android:process 添加到您的服务中来做到这一点。

例如:

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

您可以参考docs了解更多android:process

编辑:SharedPreferences 不能跨多个进程工作。在这种情况下,您必须使用 IPC(进程间通信)方法,或者您可以使用 ContentProviders 来存储和访问要跨进程使用的数据。 引用自 docs