高端手机上被杀死的最小化Android前台服务

51

我正在尝试创建一个应用程序,让用户记录路线(位置/GPS)。为了确保即使屏幕关闭也能记录位置,我创建了一个前台服务用于位置记录。我使用Dagger2将位置存储在Room数据库中注入到我的服务中。

但是,这个服务被安卓杀掉了,当然,这不好。我可以订阅低内存警告,但这并没有解决我的服务在运行安卓8.0的现代高端手机约30分钟后被杀掉的根本问题。

我创建了一个最小的项目,只有一个“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 标志,我一直在尝试使用不同的标志来避免我的服务被杀死,但到目前为止还没有成功。

使用分析器似乎没有内存泄漏,内存使用稳定在约35MB:

profiler

使用 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. 将服务中的 所有东西 持久化,例如在 Room 数据库中。每个变量、每个自定义类、任何时候发生的任何更改,然后使用 START_STICKY 启动服务。这似乎有点浪费,而且不会产生非常好看的代码,但它可能会起作用... 取决于 Android 在杀死服务后需要多长时间才能重新创建服务,可能会丢失大部分位置。

这真的是在 Android 上后台处理事务的当前状态吗?难道没有更好的方法吗?

编辑:将应用程序列入电池优化白名单(禁用)不能防止我的服务被杀死

编辑:使用 Context.startForegroundService() 启动服务并不能改善情况

编辑:因此,这确实只发生在一些设备上,但在这些设备上始终如一地发生。我想你必须选择不支持大量用户或编写非常丑陋的代码。真棒。


"我的服务在大约30分钟后被终止了" -- 你是如何确定这个问题的? - CommonsWare
2
在此之后,前台服务的通知将被移除,并停止将位置插入数据库。根据 @ianhanniballake 的说法,如果您正在使用前台服务,则似乎可以频繁且可靠地获取位置数据,因为它应不受待机模式影响。 - user1202032
1
你尝试过使用新的Context.startForegroundService()来启动服务吗?服务本身仍然必须调用startForeground(),但也许初始启动服务的方式会有所不同。 - StackOverthrow
@CommonsWare 它已经升级到8.0。这可能是制造商的问题,但理想情况下,我希望支持所有运行最新版本Android的设备。尽管我觉得它不应该对前台服务产生影响,但我将测试电池优化白名单。我会回来报告结果。 - user1202032
相关,但听起来你已经在接受的答案中做了一切:https://dev59.com/i2w15IYBdhLWcg3wYawx - StackOverthrow
显示剩余10条评论
3个回答

40
startForeground 启动的服务属于第二重要的组 可见进程

  1. 可见进程正在进行用户当前知晓的工作,因此杀掉它将对用户体验产生明显的负面影响。以下情况被认为是可见进程:

  2. 它正在运行一个Activity,在屏幕上对用户可见但不在前台(其 onPause() 方法已被调用)。例如,如果前台 Activity 显示为允许查看其后面的前一个 Activity 的对话框,则可能会发生这种情况。

  3. 它有一个正在作为前台服务运行的 Service,通过 Service.startForeground()(这要求系统将服务视为用户知晓的东西或者本质上可见的东西)。

  4. 它托管了一个系统正在使用的服务来实现用户知晓的特定功能,例如实时壁纸、输入法服务等。

系统中运行这些进程的数量比前台进程少受限,但仍相对受控制。 这些进程被认为非常重要,只有在需要保持所有前台进程运行时,才会被杀死。

也就是说,您永远无法确保在任何时候都不会杀死您的服务。例如,内存压力、低电量等情况。请参见 who-lives-and-who-dies


关于如何处理它,基本上你已经回答了问题。应该采用的方式是START_STICKY

对于启动的服务,有两种额外的主要模式根据从onStartCommand()返回的值,它们可以决定要运行的操作:START_STICKY用于需要随时启动和停止的服务,而START_NOT_STICKYSTART_REDELIVER_INTENT用于只应在处理发送给它们的任何命令时保持运行的服务。有关语义的更多详细信息,请参见相关文档。

通常情况下,在后台(或前台)服务中应尽可能少地进行操作,即仅在前台活动中执行位置跟踪并保留其他所有内容。仅跟踪应该需要很少的配置,并且可以快速加载。此外,您的服务越小,被杀死的可能性就越小。只要没有被杀死,您的活动将由系统恢复到进入后台之前的状态。另一方面,前台活动的“冷启动”不应成为问题。
我认为这并不丑陋,因为这确保手机始终为用户提供最佳体验。这是它必须做的最重要的事情。遗憾的是,某些设备在30分钟后关闭服务(可能没有用户交互)。

因此,如你所述,你必须:

将服务中的所有内容都保存在Room数据库中。每个变量,每个自定义类,任何一个更改都要持久化,然后使用START_STICKY启动服务。

请参见创建永不停止的服务

暗示的问题:

取决于Android在杀死服务后重新创建它的时间,可能会丢失大量位置。

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

此外,请注意,从Android 8.0开始,由于后台位置限制,你需要使用前台服务。


编辑:

最近新闻报道:

一些制造商可能会让你很难保持服务运行。网站https://dontkillmyapp.com/跟踪制造商和设备的可能缓解措施。Oneplus目前(29.01.19)是最严重的制造商之一。

在发布1+5和1+6手机时,OnePlus引入了市场上迄今为止最严重的后台限制之一,甚至超过了小米或华为的限制。用户不仅需要启用额外的设置才能使其应用程序正常工作,而且这些设置甚至会在固件更新时重置,导致应用程序再次中断,用户需要定期重新启用这些设置。

用户的解决方案

关闭系统设置>应用程序>齿轮图标>特殊访问>电池优化。

很遗憾开发者端没有已知的解决方法。


6
更新:START_STICKY 完全不可靠。有时服务会被重启,但通常情况下不会...它被终止并且永远不会重新启动。您提供的“永不停止的服务”链接无法始终正常工作,因为 onDestroy() 不会被一致地调用(按设计要求)。 - user1202032
1
@TKK 我等了一个多小时。 - user1202032
1
我猜,但考虑到没有可行的解决方案存在,这个方法已经很接近了。有很多好的信息。 - user1202032
@Steve M 你是指最后一位吗?是的,有些制造商甚至会停止前台服务,而不考虑任何电池豁免列表或设置。 - leonardkraemer
@leonardkraemer 不,我的意思是第一个引用块与前台服务无关,也不是对OP问题的好回答。 - Steve M
显示剩余6条评论

13

我知道现在已经有点晚了,但这可能会帮助到某些人。我也面临同样的问题,如何让前台服务保持运行状态而不被来自不同制造商的操作系统杀死。大多数中国制造商的操作系统都会将前台服务杀死,即使它被添加到例外列表(电池、清洁等)并允许自动启动。

我找到了这个链接,解决了我长时间以来保持服务运行的问题。

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

你可以通过在AndroidManifest.xml文件中添加android:process来实现这一点。

例如:

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

您可以参考文档了解有关android:process的更多信息。

编辑:在多个进程间,SharedPreferences无法正常工作。在这种情况下,您必须采用IPC(进程间通信)方法,或者您可以使用ContentProviders存储和访问跨进程使用的数据。参考自文档


1
谢谢,我的前台服务在我的Redmi Note 6 Pro上不再被终止。 - k8C
@KhoaChuAnh,它会像魔术一样运行得很好,直到您在两个进程中使用相同的“SharedPreferences”。 - Joshua
我在我的服务中使用SharedPreferences和android:process属性,没有任何问题。正如我在互联网上读到的那样,您仍然可以使用BroadcastReceiver、ResultReceiver或Messenger在活动和运行在另一个进程中的服务之间进行通信。 - k8C
1
是的,MessengerResultReceiver是一些IPC方法。但这些似乎不能在后台工作。BroadcastReceiver可以在后台工作,但从Oreo开始,它有一些限制,比如即使是显式广播,在后台(长时间处于后台)每小时只能接收到少量事件(我在某个地方读到的)。 - Joshua
1
运行在一个独立的进程中会有什么后果? - IgorGanapolsky
在所有的线程中,这是最有价值的。我想补充一点,这可以让服务不被"清除后台应用程序"操作所清除。 - Sandor Mezil

-1
我建议您使用以下内容: AlarmManagerPowerManagerWakeLockThreadWakefulBroadcastReceiverHandlerLooper

我假设你已经在使用那些"独立进程"和其他调整了。

所以在您的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);
}

}

忽略“弃用”等内容,当你别无选择时再使用它们。 我认为代码很清晰,不需要解释。 这只是关于解决问题的建议和解决方案。


自从Lolipop以来,闹钟管理器就不能再像这样被滥用了,请参见https://dev59.com/W10a5IYBdhLWcg3wzbjV。 - leonardkraemer
@leoderprofi 是的,START_STICKY只是一个玩笑,并且它不会在间隔后重新启动,我从自己的应用程序中复制了它,该应用程序已经使用了2年,它在infiniteRunning()方法内在每个间隔内保存最后的最佳位置,只有当发生错误时才会重新启动。不要担心棒棒糖,我将minSdkVersion设置为14,targetSdkVersioncompileSdkVersion设置为27。即使在Android N上也可以正常工作。 - M D P
这肯定是可靠的,因为人们根据我应用程序报告到服务器的位置历史而得到报酬。所以,是的,它比较可靠。 - M D P
@M D P,这不是 minSdkVersion 的工作方式,它不会改变某个 Android 版本的行为,只会影响应用程序运行的设备。您能否详细说明一下当应用程序由于内存压力而关闭时,重启应用程序的机制以及用于防止关闭的机制是什么? - leonardkraemer
看起来1分钟的限制只适用于重复闹钟。考虑到这一点,我认为“AlarmManager”方法可以奏效。 - leonardkraemer
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接