話題の記事

Androidアプリ開発で例外の発生した場所を特定する

2013.02.18

以前、「iOSアプリ開発で例外の発生した場所を特定する」というブログを書きましたが、
Androidでも同じような機能があるようです。(むしろ、Javaですが。。。)
Thread クラスの setDefaultUncaughtExceptionHandler メソッドというものを使いますが、
ここでオリジナルのクラスをセットすることにより、キャッチしてない例外発生によってアプリが強制終了したときの
「問題が発生したため、xxx(アプリ名)を終了します。」という、みなさんおなじみ?
のダイアログが表示される前に、任意の処理を差し込むことができます。

今回はそれを利用して、例外発生時のスタックトレースを SharedPreferences に保存して、メール送信するサンプルを作ってみます。
開発時などは、アプリをデバッグ起動していればEclipseのLogCatで例外発生時のスタックトレースを簡単に見ることができますが、リリース後のユーザー環境では見ることができないので、原因究明に役立つのではと思います。

それでは作っていきます。
まず、UncaughtExceptionHandler インターフェースを実装した CustomUncaughtExceptionHandler クラスを作成します。

CustomUncaughtExceptionHandler.java
public class CustomUncaughtExceptionHandler implements UncaughtExceptionHandler {

	private Context mContext;
	private UncaughtExceptionHandler mDefaultUncaughtExceptionHandler;

	public CustomUncaughtExceptionHandler(Context context) {
		mContext = context;

		// デフォルト例外ハンドラを保持する。
		mDefaultUncaughtExceptionHandler = Thread
				.getDefaultUncaughtExceptionHandler();
	}

	@Override
	public void uncaughtException(Thread thread, Throwable ex) {
		// スタックトレースを文字列にします。
		StringWriter stringWriter = new StringWriter();
		ex.printStackTrace(new PrintWriter(stringWriter));
		String stackTrace = stringWriter.toString();

		// スタックトレースを SharedPreferences に保存します。
		SharedPreferences preferences = mContext.getSharedPreferences(
				MainActivity.PREF_NAME_SAMPLE, Context.MODE_PRIVATE);
		preferences.edit().putString(MainActivity.EX_STACK_TRACE, stackTrace)
				.commit();

		// デフォルト例外ハンドラを実行し、強制終了します。
		mDefaultUncaughtExceptionHandler.uncaughtException(thread, ex);
	}
}

キャッチされてない例外が発生したときに uncaughtException メソッドが呼ばれます。
ここにアプリが終了する前に行いたい処理を記述します。
このクラスで気を付けるところは、デフォルトでセットされているハンドラクラスのインスタンスを保持して(L10~11)、uncaughtException メソッドの最後(L28)で、保持していたインスタンスの uncaughtException メソッドを呼んであげるところです。そうすることで、デフォルトの動作(アプリのプロセスの終了)が行われるようになります。
本当は、ここでメール送信確認ダイアログを出して、メール送信したかったのですが、うまく実装できませんでした。。。
そもそも、予期していない例外が発生した後なので、ここであまり複雑な処理をしてはいけないと思います。

次にMainActivityを実装します。
まず画面レイアウトを作ります。

activity_main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity" >

    <Button
        android:id="@+id/testBtn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="20dp"
        android:text="テスト" />

</RelativeLayout>

次に画面の処理を実装します

MainActivity.java
public class MainActivity extends FragmentActivity {

	public static final String EX_STACK_TRACE = "exStackTrace";
	public static final String PREF_NAME_SAMPLE = "prefNameSample";

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);

		// UncaughtExceptionHandlerを実装したクラスをセットする。
		CustomUncaughtExceptionHandler customUncaughtExceptionHandler = new CustomUncaughtExceptionHandler(
				getApplicationContext());
		Thread.setDefaultUncaughtExceptionHandler(customUncaughtExceptionHandler);

		// ボタンクリック時に例外が発生するようにします。
		Button testBtn = (Button) findViewById(R.id.testBtn);
		testBtn.setOnClickListener(new OnClickListener() {
			@Override
			public void onClick(View v) {
				// ここでわざと例外を発生させます。
				new ArrayList<String>().get(0);
			}
		});

		// SharedPreferencesに保存してある例外発生時のスタックトレースを取得します。
		SharedPreferences preferences = getApplicationContext()
				.getSharedPreferences(PREF_NAME_SAMPLE, Context.MODE_PRIVATE);
		String exStackTrace = preferences.getString(EX_STACK_TRACE, null);

		if (!TextUtils.isEmpty(exStackTrace)) {
			// スタックトレースが存在する場合は、
			// エラー情報を送信するかしないかのダイアログを表示します。
			new ErrorDialogFragment(exStackTrace).show(
					getSupportFragmentManager(), "error_dialog");
			// スタックトレースを消去します。
			preferences.edit().remove(EX_STACK_TRACE).commit();
		}
	}
}

初期処理のタイミングで UncaughtExceptionHandler を実装したクラス(CustomUncaughtExceptionHandler)を、キャッチされてない例外発生時のハンドラにセットします。
そして、前回アプリ起動時にキャッチされてない例外が発生していた場合はスタックトレースが保存されているので、
スタックトレース取得後、エラー情報送信確認ダイアログを表示します。

最後にエラー情報送信確認ダイアログの処理を実装します。

ErrorDialogFragment.java
public class ErrorDialogFragment extends DialogFragment {

	private String mExStackTrace;

	public ErrorDialogFragment(String exStackTrace) {
		mExStackTrace = exStackTrace;
	}

	@Override
	public Dialog onCreateDialog(Bundle savedInstanceState) {
		AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
		builder.setMessage("前回強制終了したときのエラー情報を送信します。\nよろしいですか?");
		builder.setPositiveButton("OK", new DialogInterface.OnClickListener() {
			@Override
			public void onClick(DialogInterface dialog, int which) {
				Intent intent = new Intent();
				intent.setAction(Intent.ACTION_SENDTO);
				intent.setData(Uri.parse("mailto:" + "aaa@bbb.ccc"));
				intent.putExtra(Intent.EXTRA_SUBJECT, "不具合の報告");
				intent.putExtra(Intent.EXTRA_TEXT, mExStackTrace);
				startActivity(intent);
			}
		});
		builder.setNegativeButton("キャンセル", null);
		return builder.create();
	}
}

これでサンプルは完成です。

このようにして Thread.setDefaultUncaughtExceptionHandler メソッドを使用すると、テストやリリース後の不具合修正などに役立つスタックトレースを簡単に回収することができます。

ではでは。