web-dev-qa-db-ja.com

START_STICKYはAndroid KitKatでは機能しません

私のアプリの1つには、onStartCommandからのSTART_STICKYリターンコードを使用してシステムが強制終了したときに自動的に再起動するbackgrouodサービスがあります。 Android KitKat。これに対する解決策はありますか?サービスを実行し続けるためにKitKatで何か別のことを行う必要がありますか?

注:Android-Devlopersグループでも、最近のアプリリストの動作からアプリをスワイプすることについて、同様の議論があります。この2つの問題は関連しているでしょうか? https://groups.google.com/forum/#!topic/Android-developers/H-DSQ4-tiac

編集:Android issue tracker:に未解決のバグがあることを見ました:

https://code.google.com/p/Android/issues/detail?id=6379https://code.google.com/p/Android/issues/detail? id = 63618

Edit2:別のプロセスで、startForegroundを使用し、AndroidManifest.xmlファイルでAndroid:stopWithTask="false"フラグを使用してサービスを実行している場合でも同じことが起こります...

Edit3:Android issue trackerの関連バグ:

https://code.google.com/p/Android/issues/detail?id=62091https://code.google.com/p/Android/issues/detail? id = 5331https://code.google.com/p/Android/issues/detail?id=104308

以前の動作を得るための回避策はありますか?

51
Muzikant

これは100%の実用的なソリューションではありませんが、問題をほぼ完全に排除する限りでは最適です。これまでのところ、onTaskRemovedthis answer を参照)とキープアライブ通知( this answer を参照)をオーバーライドして、このソリューションを統合しました。追加の回答は大歓迎です!

さらなる調査の結果、バグはすでに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://stackoverflow.com/a/14293528/624109 )。

ステップ4の後、Process LRU listにサービスが表示されなくなります。代わりに、デバイスのlogcatを見ると、次が表示されます。

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

その動作を引き起こしていると思われるのは、受信したブロードキャストがサービスをフォアグラウンド状態から取り出してから強制終了するという事実です。

これを回避するには、PendingIntentAlarmManagerを作成するときに、このsimpleソリューションを使用できます(ソース:- 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. インテントで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を処理するレシーバー)がアクティビティプロセスとは異なるプロセスで実行するように設定されている場合にのみ発生することがわかりました。設定アクティビティとウィジェットプロバイダーの両方が同じプロセス上にあるように変更した後、これは発生しなくなりました。

16
Muzikant

これはAndroid 4.4に存在するバグであり、次のように回避できたようです。

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

}

この投稿 からこの回答を見つけました

28

ここでの問題は、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分後にアラームを有効にすることです。その後、アプリの起動中に数分ごとにアラームをリセットできます。そのため、本当に停止されて再起動されなかった場合にのみアラームがオフになります。これは絶対確実ではありません-ハンドラーを使用すると、リアルタイムを使用するアラームサービスに対してアップタイムが提供されるため、「リセット」ハンドラーよりも長い時間に設定されていても、アラームがトリガーされる可能性があります。ただし、インテントエキストラを設定した場合、サービスが既に稼働している場合はonStartCommandを無視して、これを操作に変えることができます。

私は次のハックのファンではありませんが、実際に害を及ぼすことはありません。ユーザーが明示的な強制終了を行うと、アラームマネージャーは設定されたアラームを破棄し、サービスが再起動しないようにします(ユーザーが望んでいることです)。

最初に、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;
}
22
George Tanner

alarmManagerを使用せずにこの問題を解決する簡単なトリックを見つけました。

  1. サービスのonDestroy()メソッドが呼び出されるたびにブロードキャストをリッスンするブロードキャストレシーバーを作成します。

    _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. マニフェストにカスタマイズされたブロードキャストインテントを追加する

    _<receiver
        Android:name=".RestartService"
        Android:enabled="true" >
        <intent-filter>
            <action Android:name="restartApps" />
        </intent-filter>
    </receiver>
    _
  3. 次に、おそらく次のように、onDestroy()からブロードキャストを送信します。

    _@Override
    public void onDestroy() {
    Intent intent = new Intent("restartApps");
    sendBroadcast(intent);
    super.onDestroy();
    stopThread();
    }
    _
  4. onDestroy()からonTaskRemoved(Intent intent)を呼び出す

このトリックは、ユーザーがタスクマネージャからサービスを閉じるたびにサービスを再起動し、設定から強制的に終了します。これもあなたの助けになることを願っています

8
baskara