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
  1. @Override  
  2. public NetworkResponse performRequest(Request<?> request) throws VolleyError {  
  3.   
  4.     ...  
  5.   
  6.             // Some responses such as 204s do not have content.  We must check.  
  7.             if (httpResponse.getEntity() != null) {  
  8.               responseContents = entityToBytes(httpResponse.getEntity());  
  9.             } else {  
  10.               // Add 0 byte response as a way of honestly representing a  
  11.               // no-content request.  
  12.               responseContents = new byte[0];  
  13.             }  
  14.     ...  
  15. }  
  16.   
  17. ...  
  18.   
  19. /** Reads the contents of HttpEntity into a byte[]. */  
  20. private byte[] entityToBytes(HttpEntity entity) throws IOException, ServerError {  
  21.     PoolingByteArrayOutputStream bytes =  
  22.             new PoolingByteArrayOutputStream(mPool, (int) entity.getContentLength());  
  23.     byte[] buffer = null;  
  24.     try {  
  25.         InputStream in = entity.getContent();  
  26.         if (in == null) {  
  27.             throw new ServerError();  
  28.         }  
  29.         buffer = mPool.getBuf(1024);  
  30.         int count;  
  31.         while ((count = in.read(buffer)) != -1) {  
  32.             bytes.write(buffer, 0, count);  
  33.         }  
  34.         return bytes.toByteArray();  
  35.     } finally {  
  36.         try {  
  37.             // Close the InputStream and release the resources by "consuming the content".  
  38.             entity.consumeContent();  
  39.         } catch (IOException e) {  
  40.             // This can happen if there was an exception above that left the entity in  
  41.             // an invalid state.  
  42.             VolleyLog.v("Error occured when calling consumingContent");  
  43.         }  
  44.         mPool.returnBuf(buffer);  
  45.         bytes.close();  
  46.     }  
  47. }  
entityToBytes() では、PoolingByteArrayOutputStream の write() を呼んでいます。 https://android.googlesource.com/platform/frameworks/volley/+/master/src/com/android/volley/toolbox/PoolingByteArrayOutputStream.java
  1. public class PoolingByteArrayOutputStream extends ByteArrayOutputStream {  
  2.   
  3.     ...  
  4.   
  5.     /** 
  6.      * Ensures there is enough space in the buffer for the given number of additional bytes. 
  7.      */  
  8.     private void expand(int i) {  
  9.         /* Can the buffer handle @i more bytes, if not expand it */  
  10.         if (count + i <= buf.length) {  
  11.             return;  
  12.         }  
  13.         byte[] newbuf = mPool.getBuf((count + i) * 2);  
  14.         System.arraycopy(buf, 0, newbuf, 0, count);  
  15.         mPool.returnBuf(buf);  
  16.         buf = newbuf;  
  17.     }  
  18.   
  19.     @Override  
  20.     public synchronized void write(byte[] buffer, int offset, int len) {  
  21.         expand(len);  
  22.         super.write(buffer, offset, len);  
  23.     }  
  24. }  
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 件のコメント:

コメントを投稿