Android ANRを回避する簡単な実装方法

これたまに聞かれるんですが、Thread関連の話をしだすと長いので あまり難しい事を考えずにANRを回避する実装方法についての説明。

ANRとは=・・・は応答していません。このアプリを終了しますか? というメッセージ。

Application Not Respondingでアプリケーションが応答しない状態になったとOS側で認識して「こいつ固まっちゃいましたけど、どーします?」って聞いてくるアレです。

Application Not Responding

Application Not Responding

ANRを起こすコード

簡単な例を以下に示します。

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

    Button button = (Button) findViewById(R.id.main_button);
    button.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            sleepTenSecs();
        }
    });
}

/* ボタンが押されたときの処理 */
private void sleepTenSecs() {
    /* ① GUIに対して手を出している部分 */
    TextView text_view = (TextView)findViewById(R.id.main_text);
    text_view.setText("寝ています...");

    /* ② 実際の処理部分 */
    try {
        Thread.sleep(10000);
    } catch (InterruptedException e) {
    }

    /* ③ GUIに対して手を出している部分 */
    text_view.setText("起きました!");
}

ActivityにはボタンとTextViewが一個ずつ配置されていて、ボタンを押すとTextViewのメッセージを変更した後、10秒寝て、起きるとまたTextViewのメッセージを変更するだけの処理です。

このコードでは以下の問題があります。

  • ボタンを押しても直後のTextViewの変更「寝ています」が反映しない。
  • ボタンを押すと10秒間ユーザはアプリケーションに対して何をやっても反応しない。操作をしようとすると、しばらくしてANRが発生してOSがアプリを止めようとしてくる。

これを回避しようと思った場合、以下のようにAsyncTaskで処理を置き換える事が可能です。本当はMulti Threadが云々と、UI Threadが云々いろいろあるのですが、機械的に置き換える形でやってみたいと思います。

書き換え方は以下の3ステップ

  1. GUIに手を出している部分(これはすばやく終わる必要があります)は、AsyncTask内に入れない(本当はonPreExecuteに入れてやるのでもいいんですけど)
  2. GUIに手を出していない処理に時間のかかる部分を上記のようなAsyncTask内のdoInBackgroundの中に書く。この関数の中でGUIに直接手を出してはいけません。
  3. 2の処理が終了後に実行したい処理(主にGUIに関する更新など)を別の関数に移動。ここではsleepTenSecsDone()にしてます。
  4. onPostExecute内で3の関数を呼び出す様にする。
private void sleepTenSecs() {
    /* ① GUIに対して手を出している部分 */
    TextView text_view = (TextView) findViewById(R.id.main_text);
    text_view.setText("寝ています...");

    AsyncTask<Object, Integer, Integer> task = new AsyncTask<Object, Integer, Integer>() {
        @Override
        protected Integer doInBackground(Object... params) {
            /* ② 実際の処理部分 */
            /* ここでButtonとかTextとかのユーザインタフェースの表示を変えたりしてはいけません */
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
            }

            return null;
        }

        @Override
        protected void onPostExecute(Integer result) {
            sleepTenSecsDone();
        }

    };
    task.execute(this);
}

private void sleepTenSecsDone() {
    /* ③ GUIに対して手を出している部分 */
    TextView text_view = (TextView) findViewById(R.id.main_text);
    text_view.setText("起きました!");
}

ここまででANRを回避そのものはできますが、以下に示すような問題があるためこれだけだと対応としては不十分です。

まず第一にsleepTenSecs()の呼び出しはすぐに終わって戻ってきてしまいますが、背後では時間のかかる処理が進行中です。この時にもう一度ユーザがボタンを押すとsleepTenSecsがまた呼ばれてしまいます。このため多重に処理がかからない様にする必要があります。

いくつか方法が考えられますが、まず単純に「処理の開始時①の処理中にボタンをbutton.setEnabled(false);をボタンに対して実行してボタンを押せない様にする」というのがあります。ただし、この場合、②の処理が他の全然関係無い処理と並行に実行されても問題無い事を保障する必要があります。また、③の処理の中や適当なところでボタンをenableにする処理も必要になるでしょう。

もう一つの方法はModal Dialogを表示してActivityに対するユーザ操作をさせない方法です。良くProgressDialogを表示してActivityには触れないようにするアレです。こちらの方であればユーザがアプリケーションを操作する事ができなくなるため、並行で複数の処理が走る事を気にする必要がないので一般的にはこちらの方が多いように思います。

ProgressDialog

AsyncTaskのインスタンスを作る直前で以下の様にdialogを作成するコードを挟みます。

    final ProgressDialog dialog = new ProgressDialog(this);
    dialog.setTitle("寝ています");
    dialog.setMessage("起きるまで待っててください");
    dialog.setCancelable(false);
    dialog.show();

これだけだとProgressDialogを閉じる処理が無いので、AsyncTaskが終わっても操作ができないままになってしまいます。処理完了後にdialog.dismiss();を入れておきます。無名inner classからmethod内のlocalな変数を叩くために こっそりfinalを入れてあります(^_^;)。

        @Override
        protected void onPostExecute(Integer result) {
            dialog.dismiss();
            sleepTenSecsDone();
        }

これでActivityに対する操作を無効化できます。

というわけで、最初のコードをANRが出ない様に書き換えたコードを以下に示します。

private void sleepTenSecs() {
    /* ① GUIに対して手を出している部分 */
    TextView text_view = (TextView) findViewById(R.id.main_text);
    text_view.setText("寝ています...");

    final ProgressDialog dialog = new ProgressDialog(this);
    dialog.setTitle("寝ています");
    dialog.setMessage("起きるまで待っててください");
    dialog.setCancelable(false);
    dialog.show();

    AsyncTask<Object, Integer, Integer> task = new AsyncTask<Object, Integer, Integer>() {
        @Override
        protected Integer doInBackground(Object... params) {
            /* ② 実際の処理部分 */
            /* ここでButtonとかTextとかのユーザインタフェースの表示を変えたりしてはいけません */
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
            }

            return null;
        }

        @Override
        protected void onPostExecute(Integer result) {
            dialog.dismiss();
            sleepTenSecsDone();
        }

    };
    task.execute(this);
}

private void sleepTenSecsDone() {
    /* ③ GUIに対して手を出している部分 */
    TextView text_view = (TextView) findViewById(R.id.main_text);
    text_view.setText("起きました!");
}

とりあえず、これでANRを回避する事は出来ますが、いくつか改良すべき点もあります。

  • 処理をキャンセルする事が出来ない。
  • 処理の進捗状況が分からない
  • 処理実行中に画面を回転させるとエラーになる
  • などなど

ここら辺をもっとちゃんと処理したい場合には、Threadに関する知識を勉強してUI ThreadとWorker Threadの違い、AsyncTask、Handler回りを勉強してみてください。

ちなみにAndroidの本が無くても、Java関連の本がある場合、Swing関連が乗っているような本であればSwingWorker周りを調べると恐らく多少は参考になると思います。Androidと同じ様にSwingもシングルスレッドモデルになのでSwingWorkerを使ってUI ThreadからWorker Threadを実行する形をとる必要があるからです。

ここら辺詳細に扱ってそうな書籍を探してみたのですが、私の手元にはありませんでした(^_^;)
Amazonで評判が良さそうでThreadを扱っていそうなのは以下あたりなのかな?(Gingerbread 2.3ベースですが) 今度書店行ったら探してみます。

One thought on “Android ANRを回避する簡単な実装方法

  1. Androyer.jp

    分かりやすいです。勉強になりました。貴重な記事を掲載して頂き、ありがとうございます。

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.