2013/01/27

Android の UncaughtExceptionHandler の使い方を間違えると ANR が発生する

Java にはキャッチされなかった例外により Thread が突然終了した場合、あらかじめセットしておいた UncaughtExceptionHandler.uncaughtException() が呼ばれるという機能があります。
本来、キャッチされない例外が発生するのは避けなければなりませんが、どう頑張ってもバグは入り込むもの。
そのため、バグレポートやリソースの保全等に使用されることが多いようです。

この機能、適切に使用すると便利なのですが、使用方法を誤ると ANR (Application Not Responding) が発生する不具合につながります。
以下、その原因と対策について調べてみました。

ANR を発生しないようにする方法

ちょっと順番を変えて、先に対策から書いてみたいと思います。

以下のコードを実行し、ボタンを押すと ANR が発生します。
public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // キャッチされなかった例外発生時の処理を設定する
        Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() {
            @Override
            public void uncaughtException(Thread thread, Throwable ex) {
                Log.d(TAG, "uncaughtException!");
            }
        });

        findViewById(R.id.button01).setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                // 例外発生
                throw new IllegalStateException();
            }
        });
    }

以下のコードでは発生しません。
public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 現在設定されている UncaughtExceptionHandler を退避
        final UncaughtExceptionHandler savedUncaughtExceptionHandler =
                Thread.getDefaultUncaughtExceptionHandler();
        // キャッチされなかった例外発生時の処理を設定する
        Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() {
            @Override
            public void uncaughtException(Thread thread, Throwable ex) {
                Log.d(TAG, "uncaughtException!");

                // 退避しておいた UncaughtExceptionHandler を実行
                savedUncaughtExceptionHandler.uncaughtException(thread, ex);
            }
        });

        findViewById(R.id.button01).setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                // 例外発生
                throw new IllegalStateException();
            }
        });
    }

違いを見比べてもらうと分かるかと思いますが、setDefaultUncaughtExceptionHandler を実行する前に、既に設定されている UncaughtExceptionHandler を退避しておいて、実行してやる必要があるわけです。
このことを忘れると、突然応答しなくなるプログラムが出来上がってしまいます。
実は、これだけでは十分とは言えないのですが、それについては原因の説明の後に書きたいと思います。

なぜ 退避しておかないと ANR?

Java VM は Main thread が終了するとプロセスも終了してくれます。
しかし、Dalvik VM は UI thread が終了してもプロセスを維持する仕様のようです。
そのため、何の処理もせず UI thread が終了してしまった場合、プロセスが生きているのに、タッチなどの外部入力を受け付けるスレッドが存在しないという状況になります。
つまり、UI Thread の終了時には明示的にプロセスを終了させてやる必要があるわけです。

というわけで、Android は立ち上がる時に、DefaultUncaughtExceptionHandler を設定しています。
その実体は com.android.internal.os.RuntimeInit.UncaughtHandler に定義してあり、以下のようになっています。

RuntimeInit.java (android-4.1.2_r1)
    private static class UncaughtHandler implements Thread.UncaughtExceptionHandler {
        public void uncaughtException(Thread t, Throwable e) {
            try {
                // Don't re-enter -- avoid infinite loops if crash-reporting crashes.
                if (mCrashing) return;
                mCrashing = true;

                if (mApplicationObject == null) {
                    Slog.e(TAG, "*** FATAL EXCEPTION IN SYSTEM PROCESS: " + t.getName(), e);
                } else {
                    Slog.e(TAG, "FATAL EXCEPTION: " + t.getName(), e);
                }

                // Bring up crash dialog, wait for it to be dismissed
                ActivityManagerNative.getDefault().handleApplicationCrash(
                        mApplicationObject, new ApplicationErrorReport.CrashInfo(e));
            } catch (Throwable t2) {
                try {
                    Slog.e(TAG, "Error reporting crash", t2);
                } catch (Throwable t3) {
                    // Even Slog.e() fails!  Oh well.
                }
            } finally {
                // Try everything to make sure this process goes away.
                Process.killProcess(Process.myPid());
                System.exit(10);
            }
        }
    }

見ての通り、最後に自プロセスを殺しています。
setDefaultUncaughtExceptionHandler を実行するという事は、この処理を別のもので置き換えてしまうということですので、そのままにするとプロセスが死ななくなってしまうわけです。

どう書くべきか?

上記のコードを見ていると mCrashing というフラグがあることに気づきます。
この fail-safe はとても重要です。
もし、万が一、この関数の中でキャッチされない例外が発生した場合、無限ループに陥る可能性があるからです。

これは自分の書くコードにも取り入れておくべきでしょう。
ログを送信したり保存したりするようなコードであれば、例外が発生することはザラにあります。
もちろん、なるべくそういうことが無いよう、こういった処理は極力シンプルにするのは大原則ですが、例えどんなに気をつけていても、うっかりミスがないとは限りません。

というわけで、以下のように書いておくのが良いのではないかと思います。
ちなみに、Activity の onCreate は様々な理由で何度も呼ばれる可能性があるので、Application の onCreate に書いておく方が良いでしょう。
2013/9/17 追記
以下のコードで、saveLog() 内で Exception が発生した場合、ANR に陥る問題があったのを指摘していただきました。
謹んで訂正させていただきます。(コードが煩雑になるため、訂正後のものだけ載せておきます。ご了承下さい。)
public class ApplicationBase extends Application {
    @Override
    public void onCreate() {
        super.onCreate();

        // 現在設定されている UncaughtExceptionHandler を退避
        final UncaughtExceptionHandler savedUncaughtExceptionHandler =
                Thread.getDefaultUncaughtExceptionHandler();
        // キャッチされなかった例外発生時の処理を設定する
        Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() {
            private volatile boolean mCrashing = false;
            @Override
            public void uncaughtException(Thread thread, Throwable ex) {
                try {
                    if (!mCrashing) {
                        mCrashing = true;

                        // 終了処理
                        saveLog();
                    }
                } finally {
                    savedUncaughtExceptionHandler.uncaughtException(thread, ex);
                }
            }
        });
    }
}

2 件のコメント:

匿名 さんのコメント...

大変役に立ちました。ありがとうございました。
最後の例の形で実際にsaveLog()の部分で例外を出してみたのですが、やはりANRになってしまいました。
RuntimeInit.javaと同様に、uncaughtExceptionの中をtry - finallyで括り、finallyの中にsavedUncaughtExceptionHandler.uncaughtException(thread, ex);と書くとうまくいきました。

Yusuke Miura さんのコメント...

確かに saveLog() で Exception が発生した場合、ANR に陥ってしまうコードを掲載していました。
お恥ずかしい限りです。
コードは修正させていただきました。
ご指摘ありがとうございます。