先日、Google Developer Day2010に参加してきました。その中のティム ブレイさんの「高性能なAndroidアプリを作るには」というセッションで、ユーザの満足度を高めるためにはUIスレッドが大事という話がありました。
UIスレッドで重たい処理を行ってしまうと、その処理が終わるまではユーザの操作を受け付けなくなってしまいます。そうするとアプリは正規の処理一生懸命しているのですが、ユーザはアプリがハングしてしまったのではないか?と思ったり、反応が遅くて不快に思ったりします。
それを防ぐためにAsynctaskが紹介されていました。今回はそのAsynctaskを使った例を説明したいと思います。
それでは続きで説明してきます。
サンプル:画像処理
重たい処理の例として画像処理をあげたいと思います。AndroidMarketにもトイカメラ風の写真を撮るアプリなど素晴らしいアプリがいくつも公開されていますが今回はシンプルに用意されている画像をモノクロにするというアプリにしてみます。
画面は下のような感じです。
一番上の「Start」ボタンを押すと下に表示されているTechBoosterのイメージキャラクター(?!)をモノクロにする処理が走ります。
重たい処理を実行中にユーザが操作できないということを説明するために、2番目のボタンも用意しています。このボタンは押すとラベルとして表示されている数字がカウントアップしていくものです。
Asynctaskを使わない場合だと、モノクロに変換する処理を行っている間はボタンを押してもラベルが変わりません。
ソースはこちらです。(レイアウトファイル等は省略します)
public class AsyncTaskActivity extends Activity { private ImageView imageView_; private Bitmap image_; private Button countButton_; private Integer count = 0; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); // イメージの準備 image_ = BitmapFactory.decodeResource(getResources(), R.drawable.tb); // 変換前のイメージを表示 imageView_ = (ImageView)findViewById(R.id.ImageView); imageView_.setImageBitmap(image_); Button startButton = (Button)findViewById(R.id.StartButton); startButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View view) { ((Button)view).setEnabled(false); // モノクロにする処理 Bitmap outBitMap = image_.copy(Bitmap.Config.ARGB_8888, true); int width = outBitMap.getWidth(); int height = outBitMap.getHeight(); int totalPixcel = width * height; int i, j; for (j = 0; j < height; j++) { for (i = 0; i < width; i++) { int pixelColor = outBitMap.getPixel(i, j); int y = (int) (0.299 * Color.red(pixelColor) + 0.587 * Color.green(pixelColor) + 0.114 * Color.blue(pixelColor)); outBitMap.setPixel(i, j, Color.rgb(y, y, y)); } } // 変換が終わったので表示する imageView_.setImageBitmap(outBitMap); } }); // 押されるたびにカウントアップするボタン countButton_ = (Button)findViewById(R.id.CountButton); countButton_.setOnClickListener(new OnClickListener() { @Override public void onClick(View view) { count++; ((Button)view).setText(count.toString()); } }); } }
一番上の「Start」ボタンを押すとモノクロにする処理を始めますが、この間は操作ができないため、2番目のボタンを押しても表示が変わりません。
処理が終わった後に2番目のボタンを押した処理が実行されます。
Asynctaskを使う
それではAsynctaskを使ってモノクロに変換する処理を非同期で行ってみましょう。
AsyncTaskを使うにはまずAsyncTaskを継承したクラスを作成します。必ず実装しなければならないメソッドはdoInBackgroundですが、他にも以下のメソッドがあります。
メソッド名 | 内容 |
---|---|
onPreExecute() | 事前準備の処理を記述する |
doInBackground(Params...) | バックグラウンドで行う処理を記述する |
onProgressUpdate(Progress...) | 進捗状況をUIスレッドで表示する処理を記述する |
onPostExecute(Result) | バックグラウンド処理が完了し、UIスレッドに反映する処理を記述する |
MonochromeTaskというAsynctaskのサブクラスを作り、先ほどはActivityで記述していた処理をdoInBackgroundメソッドに記述します。
public class MonochromeTask extends AsyncTask<Bitmap, Integer, Bitmap> { private ImageView imageView_; public MonochromeTask(ImageView imageView) { super(); imageView_ = imageView; } @Override protected Bitmap doInBackground(Bitmap... bitMap) { Bitmap outBitMap = bitMap[0].copy(Bitmap.Config.ARGB_8888, true); int width = outBitMap.getWidth(); int height = outBitMap.getHeight(); int totalPixcel = width * height; int i, j; for (j = 0; j < height; j++) { for (i = 0; i < width; i++) { int pixelColor = outBitMap.getPixel(i, j); int y = (int) (0.299 * Color.red(pixelColor) + 0.587 * Color.green(pixelColor) + 0.114 * Color.blue(pixelColor)); outBitMap.setPixel(i, j, Color.rgb(y, y, y)); } } return outBitMap; } @Override protected void onPostExecute(Bitmap result) { imageView_.setImageBitmap(result); } }
ここで注目してもらいところは 「extendsAsyncTask<Bitmap, void, Bitmap>」です。上の表で引数にParams、Progress、Resultが出てきましたが、ここで引数の型を指定しています。
1番目のParamsはバックグラウンド処理を実行する時にUIスレッド(メインスレッド)から与える引数の型で、2番目のProgressは進捗状況を表示するonProgressUpdateの引数の型です。最後のResultはバックグラウンド処理の後に受け取る型です。
今回はBitmapクラスのイメージを与えてモノクロに変換されたBitmapクラスを受け取ります。進捗の表示は行わないのでProgressにはvoidを指定しています。
Activity側ではこのAsynctaskのサブクラスであるMonochromeTaskを生成してexecuteメソッドに引数(先ほどのParamsに相当)を呼び出すことで、非同期処理を開始させます。
public class AsyncTaskActivity extends Activity { private ImageView imageView_; private Bitmap image_; private Button countButton_; private MonochromeTask task_; private Integer count = 0; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); // イメージの準備 image_ = BitmapFactory.decodeResource(getResources(), R.drawable.tb); // 変換前のイメージを表示 imageView_ = (ImageView)findViewById(R.id.ImageView); imageView_.setImageBitmap(image_); // タスクの生成 task_ = new MonochromeTask(imageView_); Button startButton = (Button)findViewById(R.id.StartButton); startButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View view) { ((Button)view).setEnabled(false); // 非同期処理を開始する task_.execute(image_); } }); countButton_ = (Button)findViewById(R.id.CountButton); countButton_.setOnClickListener(new OnClickListener() { @Override public void onClick(View view) { count++; ((Button)view).setText(count.toString()); } }); } }
「Start」ボタンを押されたらボタンを無効化させてから非同期処理を開始させています。
Asynctaskを使わない場合と違い、UIスレッドで処理は行われていないのでその状態で2番目のボタンを押すとカウントアップされていきます。
進捗を表示する
重たい処理を行う時に見た目が固まるのは良くないことです。その時、進捗表示を行うとユーザは安心します。Windowsなどでファイルをコピーするときに出るプログレスバーが良い例ですね。大量のファイルをコピーする時などプログレスバーが出ていないと本当にコピーしているのか不安になってします。
Asynctaskで進捗表示を行う時はonProgressUpdateを使います。先ほどと異なるは下記の通りです。
- コンストラクタの引数にActivityを追加
- onPreExecuteでプログレスダイアログ(ProgressDialog)の準備
- onProgressUpdateでプログレスダイアログの更新
- doInBackgroundでonProgressUpdateを呼び出す
Activityの方ではMonochromeTaskのコンストラクタの引数の変更に対応します。
public class MonochromeTask extends AsyncTask<Bitmap, Integer, Bitmap> { private ImageView imageView_; private ProgressDialog progressDialog_; private Activity uiActivity_; public MonochromeTask(Activity activity, ImageView imageView) { super(); uiActivity_ = activity; imageView_ = imageView; } @Override protected void onPreExecute() { progressDialog_ = new ProgressDialog(uiActivity_); progressDialog_.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); progressDialog_.setIndeterminate(false); progressDialog_.show(); } @Override protected Bitmap doInBackground(Bitmap... bitMap) { Bitmap outBitMap = bitMap[0].copy(Bitmap.Config.ARGB_8888, true); int width = outBitMap.getWidth(); int height = outBitMap.getHeight(); int totalPixcel = width * height; progressDialog_.setMax(totalPixcel); int i, j; for (j = 0; j < height; j++) { for (i = 0; i < width; i++) { int pixelColor = outBitMap.getPixel(i, j); int y = (int) (0.299 * Color.red(pixelColor) + 0.587 * Color.green(pixelColor) + 0.114 * Color.blue(pixelColor)); outBitMap.setPixel(i, j, Color.rgb(y, y, y)); } onProgressUpdate(i + j); } return outBitMap; } @Override protected void onProgressUpdate(Integer... progress) { progressDialog_.incrementProgressBy(progress[0]); } @Override protected void onPostExecute(Bitmap result) { progressDialog_.dismiss(); imageView_.setImageBitmap(result); } }