【Android】Executorを使用して独自の非同期処理を実装する(3)
前回はAsyncTaskを参考にしつつ必要なものを実装していきました。
今回はAsyncTaskではやっていないExecutorService#shutdownに関する説明と、実際どうやって動かしていくのかを軽く解説して終わりにしたいと思います。
ExecutorServiceのシャットダウン
ExecutorServiceのシャットダウンには二種類あります。
どちらのメソッドも呼び出された段階で新規タスクの受付を行わなくなります。具体的にはExecutor#executeを呼び出そうとするとRejectedExecutionExceptionが発生します。ExecutorServiceの具象クラスでRejectedExecutionHandlerが設定されていればそっちで処理してくれますし、try-catchすらしてなければ未処理の例外に殺されます。
RejectedExecutionExceptionは非検査例外なので、RejectedExecutionHandlerを設定しておらず、どんなスレッドからexecuteが叩かれるかわからないような場合は忘れずに例外処理を組み込んでおきましょう。
shutdown、shutdownNowはキューイング済みのタスク及び実行中のタスクに関わる動作が違ってきます。
shutdownは基本的に新規タスクの受付を取りやめるだけです。実行中のタスクはそのまま動いていますし、キューイングされたタスクがあったらそのうち処理します。
それに対してshutdownNowはキューイング済みのタスクを実行に移すことなくメソッドの戻り値として返却し、実行中のタスクはすべてキャンセルします。
shutdownメソッドを呼び出した後、ある一定時間が過ぎても全タスクが終わらなかったら全員に死んでもらいたい場合はawaitTerminationメソッドを使います。
と言ってもこのメソッドが殺してくれるわけではありません。ドキュメントにも書いてある通り、引数で渡した期間までに終わったらtrue、終わらなかったらfalseが返ってくるだけです。これのメソッドがfalseを返してきたりInterruptedExceptionを投げてきたら改めてshutdownNowを呼び出して確実に殺すようにしましょう、と言うお話です。
public static void shutdown() { if(_executor == null) return; try { _executor.shutdown(); if(!_executor.awaitTermination(1L, TimeUnit.SECONDS)) { _executor.shutdownNow(); } } catch(InterruptedException e) { _executor.shutdownNow(); } finally { _executor = null; _keyHolder.clear(); } }
なんでAsyncTaskはExecutorService#shutdownを呼び出さないの?
なんででしょうね。
前回見た通りAsyncTaskで使っているThreadPoolExecutorはスレッドをデーモン化したりもしていませんし、ソースをgrepしてもshutdownを呼び出している形跡がありません。
AsyncTask内でshutdownをしない一番大きな要因はExecutorServiceを再起動する術が(基本的に)ないからじゃないかなと思います。
当然新しいインスタンスを作り直せばいいんですが、final修飾子がついているので無理です。
public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE, TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory);
また、中でどんな重要な処理を行っているかもわからないし、基本的にAsyncTask内で行われているタスクはcancelメソッドが明示的に呼ばれない限りは途中で止めないようにする、と言う思想があるのでしょう。
AndroidでExecutorService#shutdownを呼ぶ際の注意点
Androidではアプリケーションの終了をアプリ内で感知することができません。と言うか、アプリケーションの終了 / 開始の定義自体がかなり曖昧です。
ApplicationクラスのonTerminateメソッドを使えばいけそうな雰囲気があるんですが、「This method is for use in emulated process environments.」とつれないことが書いてあります。
なのでアプリケーションが終了したときにExecutorServiceを止める、と言うことはできません。精々メインとなるアクティビティのonDestroyで呼び出すのが関の山でしょう。とは言えonDestroyなんて画面を回転させるだけで発生します。
前回わざわざThreadPoolExecutor _executorの初期化のためにinitなんてメソッドを用意したのかと言うのもこのあたりが絡んでいます。
private static final int MAX_THREAD = 128; private static final int MSG_CALLBACK = 0; private static final int MSG_ONERROR = 1; private static ThreadPoolExecutor _executor; /** 処理中のkeyを保存しておくList */ private static final ArrayList<Object> _keyHolder = new ArrayList<Object>(); static { init(); } public static void execute(Object key, ImageCaller caller, ImageCallback callback, boolean isThrowException) { synchronized(_keyHolder) { if(_keyHolder.contains(key)) return; _keyHolder.add(key); if(_executor == null) init(); } _executor.execute(new ImageFuture(key, caller, callback, isThrowException)); } public static void execute(Object key, ImageCaller caller, ImageCallback callback, ErrorHandler onError) { synchronized(_keyHolder) { if(_keyHolder.contains(key)) return; _keyHolder.add(key); if(_executor == null) init(); } _executor.execute(new ImageFuture(key, caller, callback, onError)); } public static void shutdown() { if(_executor == null) return; try { _executor.shutdown(); if(!_executor.awaitTermination(1L, TimeUnit.SECONDS)) { _executor.shutdownNow(); } } catch(InterruptedException e) { _executor.shutdownNow(); } finally { _executor = null; _keyHolder.clear(); } } private static void init() { _executor = new ThreadPoolExecutor(3, MAX_THREAD , 1L, TimeUnit.SECONDS , new LinkedBlockingQueue<Runnable>(20) , new ThreadFactory() { private final AtomicInteger _count = new AtomicInteger(1); @Override public Thread newThread(Runnable r) { Thread t = new Thread(r, "HttpImageManager #" + _count.getAndIncrement()); t.setDaemon(true); return t; } }, new RejectedExecutionHandler(){ @Override public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { if(r instanceof ImageFuture) ((ImageFuture) r).cancel(true); } }); }
メインのアクティビティのonDestroyが呼び出されたらこのshutdownを呼ぶとしましょう。shutdownが呼び出されたら確実に_executorをnullにします。
先述した通り、onDestroyは「アプリケーションが終了した」以外の条件でも発生します。単に画面が回転しただけだとしたら、もう一回executorを起動しなければならない可能性も当然出てくるでしょう。だからThreadPoolExecutor _executorにはfinal修飾子をつけません。
また、アプリケーションを起動する時も、実はバックスタックにあったものを再利用しただけ、なんて可能性が十分にあります。そうなればstatic初期化子の内容は呼び出されません。
そんなわけで、ぶっちゃけてしまうとAndroidのライフサイクルとExecutorの相性は最低最悪です。shutdownを呼び出すのであれば必ず再起動できる術を用意しておきましょう。
って言うかなんでアプリケーションの起動のイベントはフックできるのに終了がフックできないんだよ!その辺のイベントも用意しておけよ!
実例
と言うわけで、一通り解説しきったので完成形を提示しておきます。前回と微妙にコードの中身が違いますが、人は常に進歩するものです。
public final class HttpImageManager { private static final int MAX_THREAD = 128; private static final int MSG_CALLBACK = 0; private static final int MSG_ONERROR = 1; private static ThreadPoolExecutor _executor; /** 処理中のkeyを保存しておくList */ private static final ArrayList<Object> _keyHolder = new ArrayList<Object>(); static { init(); } public static void execute(Object key, ImageCaller caller, ImageCallback callback, boolean isThrowException) { synchronized(_keyHolder) { if(_keyHolder.contains(key)) return; _keyHolder.add(key); if(_executor == null) init(); } _executor.execute(new ImageFuture(key, caller, callback, isThrowException)); } public static void execute(Object key, ImageCaller caller, ImageCallback callback, ErrorHandler onError) { synchronized(_keyHolder) { if(_keyHolder.contains(key)) return; _keyHolder.add(key); if(_executor == null) init(); } _executor.execute(new ImageFuture(key, caller, callback, onError)); } public static void shutdown() { if(_executor == null) return; try { _executor.shutdown(); if(!_executor.awaitTermination(1L, TimeUnit.SECONDS)) { _executor.shutdownNow(); } } catch(InterruptedException e) { _executor.shutdownNow(); } finally { _executor = null; _keyHolder.clear(); } } private static void init() { _executor = new ThreadPoolExecutor(3, MAX_THREAD , 1L, TimeUnit.SECONDS , new LinkedBlockingQueue<Runnable>(20) , new ThreadFactory() { private final AtomicInteger _count = new AtomicInteger(1); @Override public Thread newThread(Runnable r) { Thread t = new Thread(r, "HttpImageManager #" + _count.getAndIncrement()); t.setDaemon(true); return t; } }, new RejectedExecutionHandler(){ @Override public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { if(r instanceof ImageFuture) ((ImageFuture) r).cancel(true); } }); } /** Handler内で使用するコールバック */ public interface ImageCallback { void call(Object key, Bitmap bitmap); } /** Handler内で使用するエラーハンドラ */ public interface ErrorHandler { void call(Exception e); } /** バックグラウンドスレッドで実行する内容(Caller) */ public static class ImageCaller implements Callable<BitmapResult> { private final String _url; private final Point _maxSize; private boolean _isSaveCache; private boolean _isSaveFile; private int _timeout = 30000; public ImageCaller(String url, Point maxSize) { _url = url; _maxSize = maxSize; } public void setSaveCache(boolean isSaveCache) { _isSaveCache = isSaveCache; } public void setSaveFile(boolean isSaveFile) { _isSaveFile = isSaveFile; } public void setTimeout(int timeout) { _timeout = timeout; } @Override public BitmapResult call() throws Exception { android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND); return ImageUtil.getBitmapFromHttp(_url, _maxSize, _timeout, _isSaveCache, _isSaveFile); } } /** ImageCallerの処理結果を受け取り、ImageHandlerに渡すFutureTask */ static class ImageFuture extends FutureTask<BitmapResult> { private ImageHandler _handler; public ImageFuture(Object key, ImageCaller caller, ImageCallback callback, boolean isThrowException) { super(caller); _handler = new ImageHandler(key, callback, isThrowException); } public ImageFuture(Object key, ImageCaller caller, ImageCallback callback, ErrorHandler onError) { super(caller); _handler = new ImageHandler(key, callback, onError); } @Override protected void done() { // 例外が発生してなければMessage#whatにMSG_CALLBACKを、objにbitmapを渡す // (Executor関連も含む)例外が発生した場合はMessage#whatにMSG_ONERRORを、objにExceptionを渡す try { BitmapResult result = get(); if(!result.hasError()) { _handler.obtainMessage(MSG_CALLBACK, result.getBitmap()).sendToTarget(); } else { _handler.obtainMessage(MSG_ONERROR, result.getError()).sendToTarget(); } } catch (InterruptedException e) { _handler.setNotThrowException(); _handler.obtainMessage(MSG_ONERROR, e).sendToTarget(); } catch (ExecutionException e) { _handler.obtainMessage(MSG_ONERROR, e).sendToTarget(); } catch (CancellationException e) { _handler.setNotThrowException(); _handler.obtainMessage(MSG_ONERROR, e).sendToTarget(); } catch (Exception e) { _handler.setNotThrowException(); _handler.obtainMessage(MSG_ONERROR, e).sendToTarget(); } finally { _handler = null; } } } /** UIスレッド内で実行する内容(Handler) */ static class ImageHandler extends Handler { private Object _key; private ImageCallback _callback; private ErrorHandler _onError; private boolean _isThrowException; // コンストラクタはHandler(Looper)を継承する // UIスレッドで確実に実行するため、LooperはLooper#getMainLooper()のみを使用する public ImageHandler(Object key, ImageCallback callback, boolean isThrowException) { super(Looper.getMainLooper()); _key = key; _callback = callback; _isThrowException = isThrowException; } public ImageHandler(Object key, ImageCallback callback, ErrorHandler onError) { super(Looper.getMainLooper()); _key = key; _callback = callback; _onError = onError; } public void setNotThrowException() { _isThrowException = false; } @Override public void handleMessage(Message msg) { synchronized(_keyHolder) { _keyHolder.remove(_key); } switch (msg.what) { case MSG_CALLBACK: _callback.call(_key, (Bitmap) msg.obj); break; case MSG_ONERROR: Exception e = (Exception) msg.obj; if(_onError != null) { _onError.call(e); } else if(_isThrowException) { _callback = null; _onError = null; _key = null; new RuntimeException(e); } else { e.printStackTrace(); } break; } _callback = null; _onError = null; _key = null; } } }
あ、一個説明してなかった。ListViewのArrayAdapter経由でこれが使われるとgetView(+notifyDataSetChanged)の度にキューが飛んできて凄まじい勢いでRejectedExecutionExceptionが飛び交ったのでもう既に読み込み中になっているkeyがあったら何もさせないようにしています。
これを以前作った自家製ImageViewから呼び出してみましょう。
public class UrlImageView extends ImageView { // コンストラクタとかは省略 /** * <p>URLから画像を取得</p> * <p>キャッシュにヒットしなかった場合、実際に画像をセットするのはcallbackの役割となる。</p> * @param url * @param key callbackで対象のViewを特定するためのKey。このメソッドを呼び出すと{@link View#setTag(Object) setTag}にてセットされる。 * @param forceLoad trueの場合はキャッシュチェックを行わない。 * @param isSaveCache trueの場合はキャッシュに保存する。 * @param callback 画像取得後のコールバック。このメソッドに渡したkeyとHttpImageLoaderで読み込んだBitmapが渡されてくる。 */ public synchronized void setUrlImage(final String url, final Object key, final boolean forceLoad , final boolean isSaveCache, final ImageCallback callback) { if(key == null) throw new IllegalArgumentException(String.format("keyがnullです。 URL:%s", url)); setTag(key); // cache check if(!forceLoad) { Bitmap bitmap = ImageCache.get(url); if(bitmap != null && _maxSize.x < (bitmap.getWidth() * 2) && _maxSize.y < (bitmap.getHeight() * 2) ) { setImageBitmap(bitmap); return; } } setImageBitmap(null); ImageCaller caller = new ImageCaller(url, _maxSize); caller.setSaveCache(isSaveCache); HttpImageManager.execute(key, caller, callback, false); } }
うーん。もうあんまり解説することがないぞ。
まとめ
と言うわけで思った以上に長くなってしまいました。
まとめると、AsyncTaskよく出来てるからよっぽどのことがない限りそれ使ったほうがいいよ、ってところですかね。