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は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メソッドが見つかります。
まずActivityManagerService.javaについてですが、こちらは名前の通り、全アプリのActivityを管理しているクラスで、上述したzygote
プロセスをforkして起動されるsystem_server
プロセス内で実行されるようです。
Android アプリケーションが起動するまでの流れ
エミュレータを起動してadb shell ps
でzygote
プロセスとsystem_server
プロセスを確認してみると、上述した記事で説明されているようなプロセスの親子関係を確認できます。
adb shell ps | grep -e USER -e init -e zygote -e system_server
ActivityManagerServiceが提供する機能のいくつかは、アプリからもActivityManagerクラスを使って呼び出すことができます。
例えば、実行中のプロセス情報を返すActivityManager#getRunningAppProcessesの実装を見てみると以下のようになっています。
public List<RunningAppProcessInfo> getRunningAppProcesses() {
try {
return ActivityManagerNative.getDefault().getRunningAppProcesses();
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
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);
}
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);
}
}
メソッドを追っていくと、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
}
death.wait()
を実行すると、ActivityManagerServiceを実行しているスレッドが待機しそうです。
上でも少し触れていますが、ActivityManagerServiceはアプリのプロセスを管理しているサービスです。
Activity Manager 経由で Zygote から fork された アプリ用新規プロセスは、初期処理の段階で Activity Manager とのプロセス間通信を通じて起動対象アプリの情報を取得し、それをもとに所定のアプリをロード~実行する。
また、起動後のアプリケーションプロセスはライフサイクル管理を含め Activity Manager を中心とする文脈の中で一元管理される
すべてのアプリプロセスの管理者である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() {
DeathRecipientとは、他プロセスの死亡を検知する仕組みとして提供されているクラスのようです。
Android:DeathRecipientで他プロセスの死亡を検知する
Binder#linkToDeathで死活監視したい対象のBinderに登録できます。
(BinderはAndroidでプロセス間通信するために使用されます)
https://developer.android.com/reference/android/os/Binder.html
hang
メソッド内でdeath
をlinkToDeath
している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();
}
}
};
notifyAll
は待機中のスレッドを再開します。
このオブジェクトのモニターで待機中のすべてのスレッドを再開します。
スレッドは、waitメソッドを呼び出すと、オブジェクトのモニターで待機します。
再開されたスレッドの処理は、現在のスレッドがこのオブジェクトのロックを解除するまでは進むことができません。
再開されたスレッドは、ほかのスレッドと同じように、このオブジェクトと同期するように積極的に競います。
たとえば、このオブジェクトをロックする次のスレッドになろうとする場合でも、再開されたスレッドの扱いはほかのスレッドより優勢でも劣勢でもありません。
このメソッドを呼び出すのは、このオブジェクトのモニターを所有するスレッドだけでなければいけません。スレッドがオブジェクトのモニターのオーナーになる方法については、notifyメソッドを参照してください。
よってadb shell am hang
コマンド実行プロセスがkillされれば、ActivityManagerServiceが再開するため、hang状態も解消されそうです。
実際に試してみます。
adb shell am hang
コマンドを実行すると、以下のプロセスが起動します。
これでPIDが分かったので、以下を実行します。
adb shell kill {{PID}}
実行すると、再び端末が操作を受け付けるようになりました。
コマンドを実行しているプロセスがkillされれば良いので、Ctrl-C
でも問題無さそうです。
ちなみに、hang
メソッド内でも以下のようにログ出力をしているため、そちらからでもコマンド実行プロセスのPIDを確認する事が可能です。
Slog.i(TAG, "Hanging system process at request of pid " + Binder.getCallingPid());
Binder.getCallingPid()
はドキュメントで以下のように説明されているため、トランザクション送信元のプロセス(=adb shell am hang
実行コマンドプロセス)のPIDが出力されそうです。
Return the ID of the process that sent you the current transaction that is being processed.
これでadb shell am hang
を実行後、adb shell reboot
しなくてもhang状態から復帰できる事が分かりました。
さいごに
今回初めてadbコマンドのコードを読んでみましたが、知らないことが多く勉強になりました。
Android Frameworkの理解を深める意味でも、こういったコードを定期的に読んでいけると良いかもと思いました。