adb shell am hangコマンドは如何にしてアプリをhangさせているか

  • 21
    いいね
  • 0
    コメント

DroidKaigi2017にて、 @operandoOSさんによる「コマンドなしでぼくはAndroid開発できない話」という発表がありました。
その発表内でadb shell am hangという、アプリをhangさせるための興味深いコマンドが紹介されており、加えて、発表では各コマンドのコードへのリンクも貼られているのも印象的でした。

これまで一度もadbコマンドのコードを読んだことがなかったため、勉強がてら読んでみることにしました。
読書対象コマンドは、発表を聞いていて、どうやってアプリをhangさせているのか気になったadb shell am hangです。

Am.java

発表でamコマンドのコードとしてAm.javaが紹介されていたので、まずはAm.javaから読んでみることにしました。

android/platform_frameworks_baseにてandroid-7.1.1_r28としてタグ打ちされているコードで確認していきます。

Am.java

Am.javaはBaseCommandというクラスを継承しており、adb shell amを実行すると呼び出されるようです。
adb shell amと実行すると出力されるusageも定義されています。
https://github.com/android/platform_frameworks_base/blob/android-7.1.1_r28/cmds/am/src/com/android/commands/am/Am.java#L125-L368

Am.javaを実行しているamコマンドは、以下にありました。
https://github.com/android/platform_frameworks_base/blob/android-7.1.1_r28/cmds/am/am

#!/system/bin/sh
#
# Script to start "am" on the device, which has a very rudimentary
# shell.
#
base=/system
export CLASSPATH=$base/framework/am.jar
exec app_process $base/bin com.android.commands.am.Am "$@"

app_processに先程のAm.javaを引数として指定しているようです。
エミュレータを立ち上げて以下のようにコマンドを実行すると、実際に上述したamが呼ばれている事が確認できます。

adb shell which am
# /system/bin/am
adb shell cat /system/bin/am

app_processは、全てのAndroidアプリの親プロセスとなるzygoteプロセスの実体のようです。
Android アプリケーションが起動するまでの流れ

Am.javaを見てみるとonRunメソッドでamコマンド実行時の引数をチェックしているようです。
https://github.com/android/platform_frameworks_base/blob/android-7.1.1_r28/cmds/am/src/com/android/commands/am/Am.java#L371-L464
あとはこれを追っていけば、実際にhangさせている処理に辿り着けそうです。

ActivityManagerService.java

Am.javaのonRunから、さらに処理を追っていくと、実際にアプリをhangさせていそうな、ActivityManagerService.javaのhangメソッドが見つかります。

https://github.com/android/platform_frameworks_base/blob/android-7.1.1_r28/services/core/java/com/android/server/am/ActivityManagerService.java#L13018-L13055

まずActivityManagerService.javaについてですが、こちらは名前の通り、全アプリのActivityを管理しているクラスで、上述したzygoteプロセスをforkして起動されるsystem_serverプロセス内で実行されるようです。
Android アプリケーションが起動するまでの流れ

エミュレータを起動してadb shell pszygoteプロセスとsystem_serverプロセスを確認してみると、上述した記事で説明されているようなプロセスの親子関係を確認できます。

adb shell ps | grep -e USER -e init -e zygote -e system_server 

スクリーンショット 2017-03-15 9.06.42.png

ActivityManagerServiceが提供する機能のいくつかは、アプリからもActivityManagerクラスを使って呼び出すことができます。
例えば、実行中のプロセス情報を返すActivityManager#getRunningAppProcessesの実装を見てみると以下のようになっています。

public List<RunningAppProcessInfo> getRunningAppProcesses() {
    try {
        return ActivityManagerNative.getDefault().getRunningAppProcesses();
    } catch (RemoteException e) {
        throw e.rethrowFromSystemServer();
    }
} 

https://github.com/android/platform_frameworks_base/blob/android-7.1.1_r28/core/java/android/app/ActivityManager.java#L3025-L3031

ActivityManagerNative.getDefault()を追ってみると、実行元が同一プロセスの場合は、上述したActivityManagerServiceをそのまま返し、別プロセスから呼ばれた場合にはプロセス間通信をする必要があるため、ActivityManagerProxyを返しています。
ActivityManagerは、アプリ開発者がプロセス間通信を意識しなくても良い作りになっているようです。

static public IActivityManager asInterface(IBinder obj) {
    if (obj == null) {
        return null;
    }
    IActivityManager in =
        (IActivityManager)obj.queryLocalInterface(descriptor);
    if (in != null) {
        return in;
    }

    return new ActivityManagerProxy(obj);
}

https://github.com/android/platform_frameworks_base/blob/android-7.1.1_r28/core/java/android/app/ActivityManagerNative.java#L69-L80

hang

少し話がそれてしまいましたが、いよいよhangメソッドを見てみます。

@Override
public void hang(final IBinder who, boolean allowRestart) {
    if (checkCallingPermission(android.Manifest.permission.SET_ACTIVITY_WATCHER)
            != PackageManager.PERMISSION_GRANTED) {
        throw new SecurityException("Requires permission "
                + android.Manifest.permission.SET_ACTIVITY_WATCHER);
    }

    final IBinder.DeathRecipient death = new DeathRecipient() {
        @Override
        public void binderDied() {
            synchronized (this) {
                notifyAll();
            }
        }
    };

    try {
        who.linkToDeath(death, 0);
    } catch (RemoteException e) {
        Slog.w(TAG, "hang: given caller IBinder is already dead.");
        return;
    }

    synchronized (this) {
        Watchdog.getInstance().setAllowRestart(allowRestart);
        Slog.i(TAG, "Hanging system process at request of pid " + Binder.getCallingPid());
        synchronized (death) {
            while (who.isBinderAlive()) {
                try {
                    death.wait();
                } catch (InterruptedException e) {
                }
            }
        }
        Watchdog.getInstance().setAllowRestart(true);
    }
}

https://github.com/android/platform_frameworks_base/blob/android-7.1.1_r28/services/core/java/com/android/server/am/ActivityManagerService.java#L13018-L13055

メソッドを追っていくと、death.wait()という、いかにもhangさせていそうな処理が見つかります。
waitはObjectクラスのメソッドです。ドキュメントには以下のように説明されています。

別のスレッドがこのオブジェクトのnotify()メソッドまたはnotifyAll()メソッドを呼び出すまで、現在のスレッドを待機させます。
つまり、このメソッドの動作はwait(0)を呼び出した場合と同じです。
現在のスレッドは、このオブジェクトのモニターのオーナーでなければなりません。
スレッドはこのモニターの所有権を解放し、別のスレッドがnotifyメソッドまたはnotifyAllメソッドを呼び出してこのオブジェクトのモニター上で待機するスレッドに通知を出すまで待機します。
そのあと、スレッドはモニターの所有権を再度取得するまで待機してから実行を再開します。

引数が1つのバージョンでは、割り込みやスプリアス・ウェイクアップが発生する可能性があるので、このメソッドは常にループで使用される必要があります。

     synchronized (obj) {
         while (<condition does not hold>)
             obj.wait();
         ... // Perform action appropriate to condition
     }

https://docs.oracle.com/javase/jp/8/docs/api/java/lang/Object.html#wait--

death.wait()を実行すると、ActivityManagerServiceを実行しているスレッドが待機しそうです。
上でも少し触れていますが、ActivityManagerServiceはアプリのプロセスを管理しているサービスです。

Activity Manager 経由で Zygote から fork された アプリ用新規プロセスは、初期処理の段階で Activity Manager とのプロセス間通信を通じて起動対象アプリの情報を取得し、それをもとに所定のアプリをロード~実行する。
また、起動後のアプリケーションプロセスはライフサイクル管理を含め Activity Manager を中心とする文脈の中で一元管理される

Android アプリケーションが起動するまでの流れ

すべてのアプリプロセスの管理者であるActivityManagerServiceが休止すると、各アプリは何もできなくなりそうです。

hang(ANR)が発生すると、端末内の以下ファイルに原因が記録されていくので、実際に確認してみます。

/data/anr/traces.txt

adb shell am hangでhangさせた後にadb shell cat /data/anr/traces.txtと入力して中身を見てみると、それらしい記載が見つかります。

"main" prio=5 tid=1 Blocked                                                                                            
  | group="main" sCount=1 dsCount=0 obj=0x7458e258 self=0xb40b4500                                                     
  | sysTid=1543 nice=-2 cgrp=default sched=0/0 handle=0xb7781c00                                                       
  | state=S schedstat=( 0 0 0 ) utm=50 stm=80 core=1 HZ=100                                                            
  | stack=0xbf3ba000-0xbf3bc000 stackSize=8MB                                                                          
  | held mutexes=                                                                                                      
  at com.android.server.am.ActivityManagerService.broadcastIntent(ActivityManagerService.java:16931)                   
  - waiting to lock <0x0f2e9f09> (a com.android.server.am.ActivityManagerService) held by thread 62                    
  at android.app.ContextImpl.sendBroadcastAsUser(ContextImpl.java:949)                                                 
  at android.app.ContextImpl.sendBroadcastAsUser(ContextImpl.java:938)                                                 
  at com.android.server.DropBoxManagerService$3.handleMessage(DropBoxManagerService.java:162)                          
  at android.os.Handler.dispatchMessage(Handler.java:102)                                                              
  at android.os.Looper.loop(Looper.java:148)                                                                           
  at com.android.server.SystemServer.run(SystemServer.java:283)                                                        
  at com.android.server.SystemServer.main(SystemServer.java:168)                                                       
  at java.lang.reflect.Method.invoke!(Native method)                                                                   
  at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726)                                   
  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)       

hangの原因(=ActivityManagerServiceを休止させる)は何となく分かりましたが、もう少しだけ深掘りしてみます。

実際にwaitメソッドを実行しているのはdeathインスタンスです。
https://github.com/android/platform_frameworks_base/blob/android-7.1.1_r28/services/core/java/com/android/server/am/ActivityManagerService.java#L13048

hangメソッド内で以下のように生成されています。

final IBinder.DeathRecipient death = new DeathRecipient() {

https://github.com/android/platform_frameworks_base/blob/android-7.1.1_r28/services/core/java/com/android/server/am/ActivityManagerService.java#L13026

DeathRecipientとは、他プロセスの死亡を検知する仕組みとして提供されているクラスのようです。
Android:DeathRecipientで他プロセスの死亡を検知する

Binder#linkToDeathで死活監視したい対象のBinderに登録できます。
(BinderはAndroidでプロセス間通信するために使用されます)
https://developer.android.com/reference/android/os/Binder.html

hangメソッド内でdeathlinkToDeathしているwhoは、Am.javaで生成されているようです。
https://github.com/android/platform_frameworks_base/blob/android-7.1.1_r28/cmds/am/src/com/android/commands/am/Am.java#L1579

Am.javaはadb shell am hangコマンド実行プロセスで動作しているため、hangメソッド内で生成されているDeathRecipientは、adb shell am hangコマンド実行プロセスの死活監視をしていると言えそうです。

DeathRecipientは監視対象プロセスの死亡を検知すると、binderDiedを呼びます。
hangメソッドでは、binderDiedが呼ばれるとnotifyAllを呼んでいます。

final IBinder.DeathRecipient death = new DeathRecipient() {
    @Override
    public void binderDied() {
        synchronized (this) {
            notifyAll();
        }
    }
};

https://github.com/android/platform_frameworks_base/blob/android-7.1.1_r28/services/core/java/com/android/server/am/ActivityManagerService.java#L13026-L13033

notifyAllは待機中のスレッドを再開します。

このオブジェクトのモニターで待機中のすべてのスレッドを再開します。
スレッドは、waitメソッドを呼び出すと、オブジェクトのモニターで待機します。
再開されたスレッドの処理は、現在のスレッドがこのオブジェクトのロックを解除するまでは進むことができません。
再開されたスレッドは、ほかのスレッドと同じように、このオブジェクトと同期するように積極的に競います。
たとえば、このオブジェクトをロックする次のスレッドになろうとする場合でも、再開されたスレッドの扱いはほかのスレッドより優勢でも劣勢でもありません。

このメソッドを呼び出すのは、このオブジェクトのモニターを所有するスレッドだけでなければいけません。スレッドがオブジェクトのモニターのオーナーになる方法については、notifyメソッドを参照してください。

https://docs.oracle.com/javase/jp/8/docs/api/java/lang/Object.html#notifyAll--

よってadb shell am hangコマンド実行プロセスがkillされれば、ActivityManagerServiceが再開するため、hang状態も解消されそうです。

実際に試してみます。
adb shell am hangコマンドを実行すると、以下のプロセスが起動します。
スクリーンショット 2017-03-16 9.13.57.png

これでPIDが分かったので、以下を実行します。

adb shell kill {{PID}}

実行すると、再び端末が操作を受け付けるようになりました。
コマンドを実行しているプロセスがkillされれば良いので、Ctrl-Cでも問題無さそうです。

ちなみに、hangメソッド内でも以下のようにログ出力をしているため、そちらからでもコマンド実行プロセスのPIDを確認する事が可能です。

Slog.i(TAG, "Hanging system process at request of pid " + Binder.getCallingPid());

https://github.com/android/platform_frameworks_base/blob/android-7.1.1_r28/services/core/java/com/android/server/am/ActivityManagerService.java#L13044

Binder.getCallingPid()はドキュメントで以下のように説明されているため、トランザクション送信元のプロセス(=adb shell am hang実行コマンドプロセス)のPIDが出力されそうです。

Return the ID of the process that sent you the current transaction that is being processed.  

https://developer.android.com/reference/android/os/Binder.html#getCallingPid()

これでadb shell am hangを実行後、adb shell rebootしなくてもhang状態から復帰できる事が分かりました。

さいごに

今回初めてadbコマンドのコードを読んでみましたが、知らないことが多く勉強になりました。
Android Frameworkの理解を深める意味でも、こういったコードを定期的に読んでいけると良いかもと思いました。