Android Asynchronous Http Clientで大きな画像をダウンロードする方法
※追記: この実装のせいなのかはまだ定かではないのですが、非同期処理後、AsyncGenericHttpResponseHandler#sendSuccessMessageメソッドを実行した後、稀にhandleMessageが実行されず、コールバックが返ってこないことがありました。調査中です。 ※追記2: 具体的なことは分かっていませんが、ライブラリをver1.4.4 -> ver1.4.5(Development version)にすると治ったっぽかったです。(参考にしたソースがあたらしいからかも)
Android Asynchronous Http Client とは
Instagramも使ってる!ということでちょっと一時話題になった、Androidのための非同期通信ライブラリです。 でもこのライブラリ、ちょっとイケてないところがあるんですね。。
- 大きな画像のダウンロードができない
- このライブラリは、ダウンロードしたデータを、ライブラリ内でメモリへ展開します。つまり、大きすぎる画像をダウンロードすると、内部で
OutOfMemoryError
例外が発生します。特にメモリの少ない端末はなりやすいです。 - ちなみに、内部で
OutOfMemoryError
をキャッチしてIOException
として流しているので落ちはしませんが、ダウンロード失敗となります
- このライブラリは、ダウンロードしたデータを、ライブラリ内でメモリへ展開します。つまり、大きすぎる画像をダウンロードすると、内部で
- バックグラウンドで各種パース処理ができない
- データの受信に成功すると、問答無用でUIスレッドに返ってくるので、わりと重たい処理である
byte[]
->Bitmap
への変換が別スレッドで出来ない。
- データの受信に成功すると、問答無用でUIスレッドに返ってくるので、わりと重たい処理である
今回はこの2点をなんとかします。
大きなデータをダウンロードするには?
そもそもどうやるとOutOfMemoryError
にならずに(メモリに展開せずに)大きなデータダウンロードできるのよ、って話ですが、これは受信したデータをInputStream
の状態で直接ストレージに連続で保存し続ければおkです。
HttpClientのリクエストの流れって
- リクエストオブジェクトを作る(
HttpGet
やらHttpPost
やらをnewする) HttpClient#execute
メソッドでサーバへのリクエストを開始HttpEntity
インスタンスが取得できるので、HttpEntity#getContent
メソッドでInputStream
を取得- そこから文字列やら画像やらバイト列やらへ変換する
みたいな感じになってるので、ここの4を変更して
InputStream
を直接ストレージへ保存- Android Bitmapをあらかじめ縮小してから読み込む(OutOfMemory対策) - Qiitaといった方法を利用して、表示用に縮小した画像を読み取る
とすれば、今回の件は実現できます。
「2. バックグラウンドで各種パース処理ができない」を解決
このライブラリはそこそこ拡張性があり、基幹クラスもpublicになっているのでカスタマイズが可能です。そこで、まずはバックグラウンドで処理できるような抽象クラスを作ります。
public abstract class AsyncGenericHttpResponseHandler<T> extends AsyncHttpResponseHandler { private static final String LOG_TAG = AsyncGenericHttpResponseHandler.class.getSimpleName(); @SuppressWarnings("unchecked") @Override protected void handleMessage(Message msg) { Object[] response; if (msg.what == AsyncHttpResponseHandler.SUCCESS_MESSAGE) { response = (Object[]) msg.obj; if (response != null && response.length >= 3) { onSuccess((Integer) response[0], (Header[]) response[1], (T) response[2]); } else { Log.e(LOG_TAG, "SUCCESS_MESSAGE didn't got enough params"); } } else if (msg.what == AsyncHttpResponseHandler.FAILURE_MESSAGE) { response = (Object[]) msg.obj; if (response != null && response.length >= 4) { onFailure((Integer) response[0], (Header[]) response[1], (T) response[2], (Throwable) response[3]); } else { Log.e(LOG_TAG, "FAILURE_MESSAGE didn't got enough params"); } } else { super.handleMessage(msg); } } public void onSuccess(Integer integer, Header[] headers, String response) { } public void onSuccess(Integer integer, Header[] headers, T response) { } public void onFailure(Integer integer, Header[] headers, T response, Throwable e) { } /** * ネットワーク通信後、バックグラウンドで実行されるメソッド。 パース後、 {@link #handleMessage(Message)} * が実行される */ @Override public void sendResponseMessage(HttpResponse response) throws IOException { if (!Thread.currentThread().isInterrupted()) { StatusLine status = response.getStatusLine(); T responseBody = parseResponseInBackground(response.getEntity()); // additional cancellation check as getResponseData() can take // non-zero time to process if (!Thread.currentThread().isInterrupted()) { if (status.getStatusCode() >= 300) { sendFailureMessage(status.getStatusCode(), response.getAllHeaders(), responseBody, new HttpResponseException(status.getStatusCode(), status.getReasonPhrase())); } else { sendSuccessMessage(status.getStatusCode(), response.getAllHeaders(), responseBody); } } } } public abstract T parseResponseInBackground(HttpEntity httpEntity) throws IOException; private void sendSuccessMessage(int statusCode, Header[] headers, T responseBytes) { sendMessage(obtainMessage(SUCCESS_MESSAGE, new Object[] { statusCode, headers, responseBytes })); } private void sendFailureMessage(int statusCode, Header[] headers, T responseBody, Throwable throwable) { sendMessage(obtainMessage(FAILURE_MESSAGE, new Object[] { statusCode, headers, responseBody, throwable })); } }
「1. 大きな画像のダウンロードができない」を解決
さっきのクラスを利用して画像を直接ファイルとして保存し、縮小して取得するクラスを作成。
public class AsyncImageHttpResponseHandler extends AsyncGenericHttpResponseHandler<Bitmap> { private static final String LOG_TAG = AsyncImageHttpResponseHandler.class.getSimpleName(); private File mImageFile; public AsyncImageHttpResponseHandler(Context context) { File myCacheDir = context.getDir("imgCache", Context.MODE_PRIVATE); if (!myCacheDir.exists()) { myCacheDir.mkdirs(); } mImageFile = new File(myCacheDir, UUID.randomUUID().toString()); } @Override public Bitmap parseResponseInBackground(HttpEntity httpEntity) throws IOException { InputStream is = httpEntity.getContent(); createFileWithInputStream(is, mImageFile); Bitmap image = getResampledImage(mImageFile); return image; } /** * InputStreamオブジェクトにあるデータをファイルに出力 - Java入門 * http://www.syboos.jp/java/doc/write-inputstream-to-file.html */ static void createFileWithInputStream(InputStream inputStream, File destFile) throws IOException { // ファイルへInputStreamのデータを保存するコード } /** * Android Bitmapをあらかじめ縮小してから読み込む(OutOfMemory対策) - Qiita * http://qiita.com/exilias/items/38075e08ca45d223cf92 */ public static Bitmap getResampledImage(File file) throws IOException { // 画像を縮小して読み取るコード… return bitmap; } }
完成!
あとは実際にリクエストするコードを書けばおkです!
private void request() { sClient.setTimeout(Integer.MAX_VALUE); sClient.get("http://example.com/images/extra_large_photo.jpg", new AsyncImageHttpResponseHandler(this) { @Override public void onSuccess(Integer integer, Header[] headers, Bitmap response) { mImageView.setImageBitmap(response); } @Override public void onFailure(Integer integer, Header[] headers, Bitmap response, Throwable e) { } @Override public void onFinish() { } }); }
まとめ
書いておいてなんですが、結構見づらくなったのでGistsにもフルバージョンを置いてます。見づらければこちらをどうぞ…。