2013年11月19日火曜日

Volley で大きい画像を処理してはいけない

Google I/O 2013 のセッションでも言われてましたね。

ネットワークのレスポンスは com.android.volley.toolbox.BasicNetwork の performRequest() で処理されて、entity は entityToBytes() で一旦バイト配列に格納されます。

https://android.googlesource.com/platform/frameworks/volley/+/master/src/com/android/volley/toolbox/BasicNetwork.java @Override public NetworkResponse performRequest(Request<?> request) throws VolleyError { ... // Some responses such as 204s do not have content. We must check. if (httpResponse.getEntity() != null) { responseContents = entityToBytes(httpResponse.getEntity()); } else { // Add 0 byte response as a way of honestly representing a // no-content request. responseContents = new byte[0]; } ... } ... /** Reads the contents of HttpEntity into a byte[]. */ private byte[] entityToBytes(HttpEntity entity) throws IOException, ServerError { PoolingByteArrayOutputStream bytes = new PoolingByteArrayOutputStream(mPool, (int) entity.getContentLength()); byte[] buffer = null; try { InputStream in = entity.getContent(); if (in == null) { throw new ServerError(); } buffer = mPool.getBuf(1024); int count; while ((count = in.read(buffer)) != -1) { bytes.write(buffer, 0, count); } return bytes.toByteArray(); } finally { try { // Close the InputStream and release the resources by "consuming the content". entity.consumeContent(); } catch (IOException e) { // This can happen if there was an exception above that left the entity in // an invalid state. VolleyLog.v("Error occured when calling consumingContent"); } mPool.returnBuf(buffer); bytes.close(); } } entityToBytes() では、PoolingByteArrayOutputStream の write() を呼んでいます。 https://android.googlesource.com/platform/frameworks/volley/+/master/src/com/android/volley/toolbox/PoolingByteArrayOutputStream.java public class PoolingByteArrayOutputStream extends ByteArrayOutputStream { ... /** * Ensures there is enough space in the buffer for the given number of additional bytes. */ private void expand(int i) { /* Can the buffer handle @i more bytes, if not expand it */ if (count + i <= buf.length) { return; } byte[] newbuf = mPool.getBuf((count + i) * 2); System.arraycopy(buf, 0, newbuf, 0, count); mPool.returnBuf(buf); buf = newbuf; } @Override public synchronized void write(byte[] buffer, int offset, int len) { expand(len); super.write(buffer, offset, len); } } PoolingByteArrayOutputStream の write() では、バッファのサイズが足りない場合 mPool.getBuf() で現在の2倍の配列を確保しようとします。

このように、(Bitmap化する際に縮小する場合でも)いったん元サイズのまま byte 配列に確保されるため、これを並列処理で行ったりすると OutOfMemory Error になることがあります(特に古いデバイスでは)。

Honeycomb (API Level 11) で AsyncTask の実行がシングルスレッドに戻ったのって、こういうメモリエラー回避のためなのかなとか思ったり思わなかったり。ちなみに AsyncTask は API Level 3 で追加されたのですが、追加されたときはシングルスレッドでの実行でした。スレッドプールによる並列処理になったのは Donut (API Level 4) からです。

「2.x のデバイス + Volley + 大きい画像 + AsyncTask」は危険!ということですね。



0 件のコメント:

コメントを投稿