Spica*

プログラミングの話。

Android Asynchronous Http Clientで大きな画像をダウンロードする方法

※追記: この実装のせいなのかはまだ定かではないのですが、非同期処理後、AsyncGenericHttpResponseHandler#sendSuccessMessageメソッドを実行した後、稀にhandleMessageが実行されず、コールバックが返ってこないことがありました。調査中です。 ※追記2: 具体的なことは分かっていませんが、ライブラリをver1.4.4 -> ver1.4.5(Development version)にすると治ったっぽかったです。(参考にしたソースがあたらしいからかも)

Android Asynchronous Http Client とは

Instagramも使ってる!ということでちょっと一時話題になった、Androidのための非同期通信ライブラリです。 でもこのライブラリ、ちょっとイケてないところがあるんですね。。

  1. 大きな画像のダウンロードができない
  2. バックグラウンドで各種パース処理ができない

今回はこの2点をなんとかします。

大きなデータをダウンロードするには?

そもそもどうやるとOutOfMemoryErrorにならずに(メモリに展開せずに)大きなデータダウンロードできるのよ、って話ですが、これは受信したデータをInputStreamの状態で直接ストレージに連続で保存し続ければおkです。 HttpClientのリクエストの流れって

  1. リクエストオブジェクトを作る(HttpGetやらHttpPostやらをnewする)
  2. HttpClient#executeメソッドでサーバへのリクエストを開始
  3. HttpEntityインスタンスが取得できるので、HttpEntity#getContentメソッドでInputStreamを取得
  4. そこから文字列やら画像やらバイト列やらへ変換する

みたいな感じになってるので、ここの4を変更して

  1. InputStreamを直接ストレージへ保存
  2. 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にもフルバージョンを置いてます。見づらければこちらをどうぞ…。