これは何?
https://developer.android.com/about/versions/oreo/background.html?hl=ja#services
Android 8.0では、バックグラウンド動作制限の一環として、 **裏でこっそりサービス起動しようとしてもできなくなります!**だって。
まじかよ!!!!!
でも、 リファレンスに書いてあるとおりの実装になっているとは限らないのがAndroidのいいところだ(違w
いちおうソースはちゃんと読んでおこう、ということで調査した自分用メモです。
(長くなったので、前編・後編に分けましたw (が、後半は適当です
実際にアプリを動かすとどんな感じになっているか?
- JobScheduler(とか、それをラップしているGcmNetworkManager)から起動されるジョブサービスはちゃんと裏でも動ける
- でもそこからstartServiceやるとクラッシュ
- Activityから立ち上げたサービスはちゃんと起動できる
- でもActivityを閉じてしばらく経つと、サービスが勝手に終了する
- startForegroundServiceすれば、バックグラウンドからの起動でも死なない。でも
- 5秒以内にstartForegroundを呼ばないとクラッシュ
- startForegroundを呼ばずにstopSelf()してもクラッシュ
- IntentServiceの場合は、どこかでstartForeground呼ばないとクラッシュ
ということで、割と簡単にFATAL EXCEPTIONでクラッシュしてしまう。怖い。
参考:Android Oからのバックグラウンド・サービスの制限事項を実演する。
ソースを読むぞ
startServiceで落ちて、startForegroundServiceで落ちないカラクリ
Context#startService
も Context#startForegroundService
も、結局は ContextImpl#startServiceCommon
を呼び出している。
違いは第2引数の boolean requireForeground
がtrue/falseだけ。
1486 private ComponentName startServiceCommon(Intent service, boolean requireForeground,
1487 UserHandle user) {
1488 try {
1489 validateServiceIntent(service);
1490 service.prepareToLeaveProcess(this);
1491 ComponentName cn = ActivityManager.getService().startService(
1492 mMainThread.getApplicationThread(), service, service.resolveTypeIfNeeded(
1493 getContentResolver()), requireForeground,
1494 getOpPackageName(), user.getIdentifier());
1495 if (cn != null) {
1496 if (cn.getPackageName().equals("!")) {
1497 throw new SecurityException(
1498 "Not allowed to start service " + service
1499 + " without permission " + cn.getClassName());
1500 } else if (cn.getPackageName().equals("!!")) {
1501 throw new SecurityException(
1502 "Unable to start service " + service
1503 + ": " + cn.getClassName());
1504 } else if (cn.getPackageName().equals("?")) {
1505 throw new IllegalStateException(
1506 "Not allowed to start service " + service + ": " + cn.getClassName());
1507 }
1508 }
1509 return cn;
1510 } catch (RemoteException e) {
1511 throw e.rethrowFromSystemServer();
1512 }
1513 }
ActivityManagerServiceにstartServiceを要求してみて、その結果がおかしなもの("!"とか"?"とか)だったら例外を送出するようになっている。
バックグラウンド制限の例外ログをみると
11-29 23:57:37.516 W/AndroidGcmController( 772): java.lang.IllegalStateException: Not allowed to start service Intent { cmp=com.chrome.canary/com.google.ipc.invalidation.ticl.android2.TiclService (has extras) }: app is in background uid UidRecord{3da5198 u0a79 RCVR bg:+3m53s39ms idle procs:2 seq(0,0,0)}
こんなかんじなので、まさに "?"
のルートを通ってるみたい。
この "?"
を設定しているのは、ActivityManagerService側。 app is in background uid
でソース検索すると定義場所が特定できた。
327 ComponentName startServiceLocked(IApplicationThread caller, Intent service, String resolvedType,
328 int callingPid, int callingUid, boolean fgRequired, String callingPackage, final int userId)
329 throws TransactionTooLargeException {
...
365 // If this isn't a direct-to-foreground start, check our ability to kick off an
366 // arbitrary service
367 if (!r.startRequested && !fgRequired) {
368 // Before going further -- if this app is not allowed to start services in the
369 // background, then at this point we aren't going to let it period.
370 final int allowed = mAm.getAppStartModeLocked(r.appInfo.uid, r.packageName,
371 r.appInfo.targetSdkVersion, callingPid, false, false);
372 if (allowed != ActivityManager.APP_START_MODE_NORMAL) {
373 Slog.w(TAG, "Background start not allowed: service "
374 + service + " to " + r.name.flattenToShortString()
375 + " from pid=" + callingPid + " uid=" + callingUid
376 + " pkg=" + callingPackage);
377 if (allowed == ActivityManager.APP_START_MODE_DELAYED) {
378 // In this case we are silently disabling the app, to disrupt as
379 // little as possible existing apps.
380 return null;
381 }
382 // This app knows it is in the new model where this operation is not
383 // allowed, so tell it what has happened.
384 UidRecord uidRec = mAm.mActiveUids.get(r.appInfo.uid);
385 return new ComponentName("?", "app is in background uid " + uidRec);
386 }
387 }
?
が設定されるまで
Context#startForegroundService
↓ requireForeground=true
Contextimpl#startServiceCommon
↓ requireForeground=true
ActivityManagerService#startService
↓ fgRequired=true
ActiveService#startServiceLocked
って感じで呼ばれているようだ。
365 // If this isn't a direct-to-foreground start, check our ability to kick off an
366 // arbitrary service
367 if (!r.startRequested && !fgRequired) {
368 // Before going further -- if this app is not allowed to start services in the
369 // background, then at this point we aren't going to let it period.
370 final int allowed = mAm.getAppStartModeLocked(r.appInfo.uid, r.packageName,
371 r.appInfo.targetSdkVersion, callingPid, false, false);
372 if (allowed != ActivityManager.APP_START_MODE_NORMAL) {
ここの2つのif文がキモ。
1つ目のif文は
- サービスが実行開始(=r.startRequested)されていない状態
- startForegroundServiceではなくstartServiceで起動した状態
ほぼほぼ通りそう。
2つ目のif文では、AppStartModeってのを取得してチェックしている。
ここでNORMAL以外の場合には実際にstartServiceせずに、例外を出すような "?"
のComponentNameを定義している。
AppStartModeってなんだ???
ActivityManagerServiceのソースを雑に読んだところ
- NORMAL→今すぐ動いていい状態
- DELAYED→何らかの理由により、"後回し"にされた状態
- DISABLED→何らかの理由により、"動かないで!"ってされた状態
みたいなイメージ。
AppStartModeがNORMALじゃなくしているのは誰だ?
ActivityManagerServiceで APP_START_MODE_NORMAL
をreturnしている関数をざっくり見てみる。
1つめ、 appRestrictedInBackgroundLocked
。こいつはパット見かなり激しいことをやっていて、 Android Oreo以降であれば問答無用で APP_START_MODE_DELAYED_RIGID を返すようだ。
8372 // Unified app-op and target sdk check
8373 int appRestrictedInBackgroundLocked(int uid, String packageName, int packageTargetSdk) {
8374 // Apps that target O+ are always subject to background check
8375 if (packageTargetSdk >= Build.VERSION_CODES.O) {
8376 if (DEBUG_BACKGROUND_CHECK) {
8377 Slog.i(TAG, "App " + uid + "/" + packageName + " targets O+, restricted");
8378 }
8379 return ActivityManager.APP_START_MODE_DELAYED_RIGID;
8380 }
で、こいつがどこから呼ばれているかというと、
8397 // Service launch is available to apps with run-in-background exemptions but
8398 // some other background operations are not. If we're doing a check
8399 // of service-launch policy, allow those callers to proceed unrestricted.
8400 int appServicesRestrictedInBackgroundLocked(int uid, String packageName, int packageTargetSdk) {
8401 // Persistent app?
8402 if (mPackageManagerInt.isPackagePersistent(packageName)) {
8403 if (DEBUG_BACKGROUND_CHECK) {
8404 Slog.i(TAG, "App " + uid + "/" + packageName
8405 + " is persistent; not restricted in background");
8406 }
8407 return ActivityManager.APP_START_MODE_NORMAL;
8408 }
8409
8410 // Non-persistent but background whitelisted?
8411 if (uidOnBackgroundWhitelist(uid)) {
8412 if (DEBUG_BACKGROUND_CHECK) {
8413 Slog.i(TAG, "App " + uid + "/" + packageName
8414 + " on background whitelist; not restricted in background");
8415 }
8416 return ActivityManager.APP_START_MODE_NORMAL;
8417 }
8418
8419 // Is this app on the battery whitelist?
8420 if (isOnDeviceIdleWhitelistLocked(uid)) {
8421 if (DEBUG_BACKGROUND_CHECK) {
8422 Slog.i(TAG, "App " + uid + "/" + packageName
8423 + " on idle whitelist; not restricted in background");
8424 }
8425 return ActivityManager.APP_START_MODE_NORMAL;
8426 }
8427
8428 // None of the service-policy criteria apply, so we apply the common criteria
8429 return appRestrictedInBackgroundLocked(uid, packageName, packageTargetSdk);
8430 }
ここだ。
- Persistentなアプリ
- AndroidManidestでpersistent=true指定をしたアプリ。システムアプリ向けのフラグなので一般アプリには該当しない
- UIDがBackgroundWhitelistに入ってる
- OreoではBLUETOOTH_UIDだけが固定でホワイトリストに入ってるので大抵のアプリには該当しない
- UIDがDeviceIdleWhitelistに入っている
- ちゃんと読んでないけど、多分大抵の場合には該当しないんじゃないかなー(てきとうw
つまり、 appServicesRestrictedInBackgroundLocked
も、Android Oreo以降であれば ほぼほぼ問答無用で APP_START_MODE_DELAYED_RIGID を返すということになる。
appServicesRestrictedInBackgroundLockedを呼んでいるのは誰だ?
ソース検索してみると・・・ getAppStartModeLocked
おおおおおお!! さっきの2つ目のif文に登場してたやつだ。
8432 int getAppStartModeLocked(int uid, String packageName, int packageTargetSdk,
8433 int callingPid, boolean alwaysRestrict, boolean disabledOnly) {
8434 UidRecord uidRec = mActiveUids.get(uid);
...
8438 if (uidRec == null || alwaysRestrict || uidRec.idle) {
...
8458 final int startMode = (alwaysRestrict)
8459 ? appRestrictedInBackgroundLocked(uid, packageName, packageTargetSdk)
8460 : appServicesRestrictedInBackgroundLocked(uid, packageName,
8461 packageTargetSdk);
...
8482 return startMode;
8483 }
8484 }
8485 return ActivityManager.APP_START_MODE_NORMAL;
8486 }
重要ポイントだけ抽出すると、こんなロジックをしている。
- alwaysRestrictがtrue
- 今回の場合はfalseが指定されて呼ばれているので関係ない
- ActiveUidsに含まれていない
- ActiveUidsには含まれているけどidle
だったら、APP_START_MODE_NORMALではなくAPP_START_MODE_DELAYED_RIGIDが返る。
今回の処理ルートには登場してこないけど、ActivityManagerには以下のようなメソッドがあって、「ActiveUidsに含まれていない or ActiveUidsには含まれているけどidle」であればフォアグラウンド動作していないと見なされているらしい。
7847 @Override
7848 public boolean isAppForeground(int uid) throws RemoteException {
7849 synchronized (this) {
7850 UidRecord uidRec = mActiveUids.get(uid);
7851 if (uidRec == null || uidRec.idle) {
7852 return false;
7853 }
7854 return uidRec.curProcState <= ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND;
7855 }
7856 }
とりあえず突き詰めればきりがないので、
バックグラウンドでstartServiceすると落ちるカラクリはここまで。
startForegroundServiceしたあと5秒以内にstartForegroundしないと落ちるカラクリ
(長くなりそうなので 後編 で説明します...
まとめ
① そもそもActivityManagerServiceは
UidRecord uidRec = mActiveUids.get(uid);
if (uidRec == null || uidRec.idle) {
//バックグラウンドで動いてるやつだ!!
と認識してる。
② ActivityManagerServiceは、startService要求が来たときに、まずstartServiceForegroundかstartSeriviceかをチェックする。
③ startServiceだったら、アプリがフォアグラウンド動作しているかどうかを①の条件にしたがい判定し、バックグラウンド動作しているUIDからのサービス開始要求であれば、APP_START_MODE_DELAYED_RIGID(おまえは今すぐうごくんじゃない!)と判定して、コンポーネント名を ?
に書き換える
④ ContextImpl側で、startServiceの返り値をみて、変なもの( !
とか ?
とか)だったら例外を投げる。
⑤ FATAL EXCEPTION \(^o^)/
ソースを自分で読み進めたい人は
http://androidxref.com/8.0.0_r4/xref/frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java#8400
このあたりから読むのがオススメです。