START_STICKY在Android KitKat上不起作用。

52
我的一个应用程序有一个后台服务,使用onStartCommand中的START_STICKY返回代码,在系统杀死它时自动重新启动。似乎在Android KitKat上不再起作用。是否有解决方法?我在Kitkat上应该做些不同的事情来保持服务运行吗?
注意:Android-Developers组中有关于从最近的应用列表中滑动应用程序的类似讨论。这两个问题是否相关? https://groups.google.com/forum/#!topic/android-developers/H-DSQ4-tiac 编辑:看到Android问题跟踪器上有未解决的错误。

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

Edit2: 即使使用startForeground在单独的进程中运行服务,并在AndroidManifest.xml文件中使用标志android:stopWithTask="false",仍会发生相同的情况...

Edit3:更多与此相关的Android问题跟踪器上的错误:

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

有没有一些解决方法可以恢复以前的行为?


首先,你的服务是否运行在应用程序相同的进程中?如果是这样,当你的应用程序被kill掉时,服务也会被杀死。因此,请尝试在不同的进程中运行你的服务,以下是相关链接:http://www.vogella.com/articles/AndroidServices/article.html。 - user2652394
尝试在同一进程和不同进程上运行它,结果相同。 - Muzikant
看起来他们复制了iOS 7的行为。如果你滑动关闭一个应用程序,那么它的进程将被终止,并且它将不再允许在后台运行任何东西,包括其服务,直到你手动重新启动该应用程序或重新启动设备。 - Monstieur
据我所知,AOSP #63793的错误描述是正确的:从4.4(.2)开始,START_STICKY服务将不会重新启动,也不会有"Scheduling restart of crashed service"日志条目。4.3没有受到影响,但这是一个AOSP问题,而不是一些修改(例如CM)引入的问题,因为我能够在AOSP模拟器中重现这个问题。我不认为这是一个有意的更改。 - Flow
经过数周的研究,我在这个答案中找到了解决方案:https://dev59.com/vnrZa4cB1Zd3GeqP1FIX#29351792 - Manza
4个回答

29

看起来这是Android 4.4中存在的一个bug, 通过以下方式解决:

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

}

这篇文章中找到了这个答案。


4
感谢您发布来自上面链接的我的解决方案(至少可以指出一下来源...)。正如此链接所述,这不是一个完整的解决方案,仍然有其缺陷。 - Muzikant
1
为什么不发布它?我会删除这篇帖子 :) - Rakeeb Rajbhandari
1
不需要删除您的帖子,因为它不是完整的解决方案。它只处理用户从“最近使用的应用程序”列表中滑动应用程序的情况,但不处理Android杀死进程的情况。此外,在进一步测试后,似乎“+1000”时间缓冲有时不足,进程在那一个秒钟缓冲之后被杀死,所以我将其更改为“+10000”。仍然看起来像是一个丑陋的(而且不总是有效的)解决方法。现在没有更好的办法... - Muzikant
@Muzikant 感谢这个临时解决方案。我的应用程序也遇到了同样的问题,我将使用它直到我们从 Android 收到有关这种奇怪的 KitKat 行为的消息为止。 - jujujuijk
这个 onTaskRemoved 方法不起作用,在 [micromax unite 2] (http://www.micromaxinfo.com/mobiles/smartphones/canvas/Unite-2-A106) 上测试时,有人能告诉我替代方案吗?! - AndroidManifester
何时调用 super.onTaskRemoved(rootIntent); - Phaneendra Charyulu Kanduri

21
这里的问题似乎不会出现在基于AOSP的ROM上。也就是说,我可以很容易地在基于CyanogenMod 11的ROM上重新创建这个问题,但在AOSP ROM(和模拟器)上,START_STICKY的行为恰好符合我的预期。话虽如此,我看到来自Nexus 5用户的报告显示他们似乎遇到了这个问题,所以也许在AOSP中仍然存在这个问题。
在模拟器和AOSP ROM上,当我对进程执行“kill 5838”时,我从logcat中看到以下内容(正如我所预期的)。
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}

我发现如果我从最近任务列表中“滑动”结束任务,会出现相同的重启行为。所以这很好——这意味着核心AOSP代码的行为与以前的级别相同。
我正在查看Cyanogenmod服务代码,试图弄清楚为什么事情没有被安排重新启动——但还没有成功。它似乎应该重新安排它。Cyanogenmod使用一个AOSP没有的服务映射,但不确定是否是问题(可疑的) https://github.com/CyanogenMod/android_frameworks_base/blob/cm-11.0/services/java/com/android/server/am/ActiveServices.java#L2092
一个相当巧妙的解决方法是,使用类似于onTaskRemoved AlarmService的机制来启用一个X分钟后的闹钟。然后,在您的应用程序正在运行时,每隔几分钟重置闹钟,这样只有在事情真正被杀死而没有重新启动时才会响起。这并不是绝对可靠的——使用Handler可以获得正常运行时间,而使用Alarm Service则使用实时时间,因此即使您的“重置”处理程序设置了比较长的时间,也有可能触发您的闹钟。但是,如果您设置了一个意图额外项,您可以选择忽略onStartCommand,如果您的服务已经在运行中,则将其转换为noop。
我根本不喜欢以下hack——但它不会造成任何真正的伤害。如果用户进行显式强制关闭,那么闹钟管理器将销毁任何设置的闹钟,以便服务不会重新启动(这就是用户想要的)。
首先,创建一个帮助程序方法,将设置一个20分钟的闹钟,这将导致onStartCommand被触发为您的服务。每2分钟有一个处理程序,将重置20分钟的闹钟。如果处理程序在实时20分钟内运行,则闹钟永远不会响起。如果设备处于睡眠状态,则不能保证处理程序运行(这很好)。
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);  
    }
}

在您的onCreate方法中,您可以调用此方法。同样,在您的onStartCommand方法中,请确保如果您的服务已经启动并运行,则忽略此方法。例如:
@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;
}

感谢详细的回复。这正是我考虑的最后手段,但我希望能够得到一个更加“标准”的解决方案。如果没有其他的解决方案出现,(遗憾地)这可能会是我们要走的道路... - Muzikant
我能够重现这个行为,即在运行Android 4.4的模拟器上不重新启动粘性服务。而在运行Android 4.2的模拟器上,同一个服务会被重新启动,并显示"W/ActivityManager(2707): Scheduling restart of crashed service org.foo.bar/.FooService in 5000ms"。两个服务都是使用"kill <pidOfService>"命令被SIGKILL杀死的。所以很遗憾,这确实是AOSP中的一个bug。 - Flow
更多信息:我的服务还在运行Android 4.3的模拟器上重新启动,因此这也不是一个Jelly Bean问题。 - Flow
请问有人能告诉我什么是restartAlarmInterval和resetAlarmTimer吗?如果我想每半个小时启动一次服务,那么这两个参数应该设置什么时间呢?(我的确切需求是每隔一个小时检查一次服务是否在运行,通过这个闹钟管理器再次启动服务) - AndroidManifester
在创建待定意图时使用FLAG_CANCEL_CURRENT会取消先前的意图,并仅保留一个活动待定意图。 - abedfar
我仍然收到“ I / ActivityManager:强制停止服务ServiceRecord ...”警告。闹钟管理器与服务一起被终止,因此没有人重新创建该服务。 - user2924714

17

这不是一个百分之百可靠的解决方案,但它是目前为止最好的解决方法,因为它几乎完全消除了问题。到目前为止,我将此解决方案与覆盖 onTaskRemoved(见此答案)和保持活动通知(参见此答案)结合使用。

进一步调查后,发现该错误已经存在于Jelly Bean中,并且看起来有一种解决方案(至少在我的情况下似乎有效,如果需要,将继续测试并更新答案)。

从我观察到的情况来看,这只会发生在接收由 AlarmManager 设置的广播的服务中。

要重现此错误,请按照以下步骤操作:

  1. 启动应用程序
  2. 从应用程序内部以前台服务的方式启动服务(使用 startForeground 进行启动)
  3. 从“最近使用的应用程序”列表中滑动该应用程序
  4. 发送由服务处理的广播
  5. 服务被杀死!

使用 adb shell dumpsys >C:\dumpsys.txt 可以在不同步骤之间监控服务的状态。(在 dumpsys 输出中查找 Process LRU list) 在步骤2和3中,您将看到类似于以下内容的信息:

Proc # 2: prcp  F/S/IF trm: 0 11073:<your process name>/u0a102 (fg-service)

具体而言,请注意F/S/IF(fg-service),它们表明服务正在作为前台服务运行(有关如何分析dumpsys的更多详细信息,请参见此链接:https://dev59.com/i2w15IYBdhLWcg3wYawx#14293528)。

在第4步之后,您将看不到服务在进程LRU列表中。相反,您可以查看设备的日志记录,您会看到以下内容:

I/ActivityManager(449): Killing 11073:<your process name>/u0a102 (adj 0): remove task

导致该行为的原因似乎是接收到广播后将服务从前台状态退出,然后再将其杀死。

为了避免这种情况,您可以在创建AlarmManagerPendingIntent时使用这个简单的解决方案(来源: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);

请注意以下步骤:

  1. 在intent上调用addFlags,并使用FLAG_RECEIVER_FOREGROUND
  2. 使用非零请求代码获取PendingIntent.getBroadcast

如果省略其中任何一步,则不起作用。

请注意,FLAG_RECEIVER_FOREGROUND是API 16(Jelly Bean)中添加的,因此这就是为什么该错误首次出现的原因...

很可能KitKat在杀死进程方面更加积极,这就是为什么它在KitKat上得到强调的原因,但看起来在Jelly Bean上已经相关了。

备注2:请注意问题中有关服务配置的细节-在清单中将其设置为独立进程,作为前台服务运行,并将endWithTask设置为false。

备注3:当应用程序接收android.appwidget.action.APPWIDGET_CONFIGURE消息并显示一个新小部件的配置活动时,同样的事情会发生(将步骤4替换为创建一个新的小部件)。我发现只有在小部件提供程序(处理android.appwidget.action.APPWIDGET_UPDATE的接收器)设置为在不同的进程上运行时,才会发生这种情况。更改为配置活动和小部件提供程序位于同一进程中后,就不再发生这种情况。


1
在我的情况下,这并没有帮助我,但我没有使用前台服务。(http://stackoverflow.com/questions/21073136/kitkat-service-is-null-after-exiting-main-activity) - Angelo Tricarico
什么意思?我不明白!即使我不使用闹钟管理器的接收器,从最近使用的应用程序中滑动应用程序也会关闭我的服务,有时甚至系统会在没有滑动的情况下关闭我的服务。@Muzikant - AVEbrahimi
1
我已经成功复现了该错误,并且使用的是一个没有接收到AlarmManager广播的服务。 - Flow
@Muzikant,使用Intent.FLAG_RECEIVER_FOREGROUND来接收广播是否可行?我在接收操作系统发送的android.intent.action.PACKAGE_ADDED时遇到了问题! - Ali
@NullPointer,你不能这样做。Intent.FLAG_RECEIVER_FOREGROUND 标志是在发送方设置的,而不是在接收方设置的。 - Muzikant
显示剩余2条评论

9
我发现了一个简单的技巧,可以解决这个问题,而不需要使用AlarmManager。
  1. create a broadcast receiver that listens broadcast everytime onDestroy() method in service is called:

    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. add customized broadcast intent to your manifest

    <receiver
        android:name=".RestartService"
        android:enabled="true" >
        <intent-filter>
            <action android:name="restartApps" />
        </intent-filter>
    </receiver>
    
  3. then, send broadcast from onDestroy(), probably like this:

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

这个技巧可以在用户从任务管理器或设置中强制关闭服务时,每次重启您的服务。我希望这对您有所帮助。


这看起来与所有AlarmManager技巧相比几乎太容易了。我会快速尝试一下这个。 - Hless
1
由于在最近使用的应用程序列表中滑动应用程序时根本没有调用onDestroy(),因此此解决方案在4.4.2上无法正常工作。 - abedfar
1
@abedfar 然后将 onDestroy() 中的所有内容移动到一个新方法中,并从 onTaskRemoved() 调用它。 - baskara
我有两个设备,一个是Moto G4 Plus(API 23),另一个是三星Note 3(API 22)。现在这个程序在Moto上可以运行,但在三星上却不能。你有什么想法吗? - Nikhil Borad
@NikhilBorad 我认为这是一个ROM问题。我觉得不同的ROM在处理服务级别的事情时有所不同。但是,仍然需要进行更多的调查。 - baskara
三星智能管理器会杀掉服务以提高性能,这就是原因。但如果你找到任何解决方案,请让我知道。 - Nikhil Borad

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