ラベル volley の投稿を表示しています。 すべての投稿を表示
ラベル volley の投稿を表示しています。 すべての投稿を表示

2013年11月19日火曜日

Android Volley の NetworkImageView で Bitmap の最大サイズを指定する

Volley の NetworkImageView は便利なのですが、Bitmap のサイズを最適化してくれません。

NetworkImageView で画像のダウンロードを開始するのが loadImageIfNecessary() です。

https://android.googlesource.com/platform/frameworks/volley/+/master/src/com/android/volley/toolbox/NetworkImageView.java public class NetworkImageView extends ImageView { ... /** Local copy of the ImageLoader. */ private ImageLoader mImageLoader; ... public void setImageUrl(String url, ImageLoader imageLoader) { mUrl = url; mImageLoader = imageLoader; // The URL has potentially changed. See if we need to load it. loadImageIfNecessary(false); } ... /** * Loads the image for the view if it isn't already loaded. * @param isInLayoutPass True if this was invoked from a layout pass, false otherwise. */ private void loadImageIfNecessary(final boolean isInLayoutPass) { ... // The pre-existing content of this view didn't match the current URL. Load the new image // from the network. ImageContainer newContainer = mImageLoader.get(mUrl, new ImageListener() { @Override public void onErrorResponse(VolleyError error) { if (mErrorImageId != 0) { setImageResource(mErrorImageId); } } @Override public void onResponse(final ImageContainer response, boolean isImmediate) { // If this was an immediate response that was delivered inside of a layout // pass do not set the image immediately as it will trigger a requestLayout // inside of a layout. Instead, defer setting the image by posting back to // the main thread. if (isImmediate && isInLayoutPass) { post(new Runnable() { @Override public void run() { onResponse(response, false); } }); return; } if (response.getBitmap() != null) { setImageBitmap(response.getBitmap()); } else if (mDefaultImageId != 0) { setImageResource(mDefaultImageId); } } }); // update the ImageContainer to be the new bitmap container. mImageContainer = newContainer; } ... } ここで ImageLoader の get(url, imageListener) を呼んでいます。
ImageLoader には引数が4つの get(url, imageLoader, maxWidth, maxHeight) もあり、引数が2つの get() を呼んだ場合は、maxWidth, maxHeight には 0 が渡され、生成される Bitmap は実際の画像サイズになります。 public class ImageLoader { ... public ImageContainer get(String requestUrl, final ImageListener listener) { return get(requestUrl, listener, 0, 0); } public ImageContainer get(String requestUrl, ImageListener imageListener, int maxWidth, int maxHeight) { // only fulfill requests that were initiated from the main thread. throwIfNotOnMainThread(); final String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight); // Try to look up the request in the cache of remote images. Bitmap cachedBitmap = mCache.getBitmap(cacheKey); if (cachedBitmap != null) { // Return the cached bitmap. ImageContainer container = new ImageContainer(cachedBitmap, requestUrl, null, null); imageListener.onResponse(container, true); return container; } // The bitmap did not exist in the cache, fetch it! ImageContainer imageContainer = new ImageContainer(null, requestUrl, cacheKey, imageListener); // Update the caller to let them know that they should use the default bitmap. imageListener.onResponse(imageContainer, true); // Check to see if a request is already in-flight. BatchedImageRequest request = mInFlightRequests.get(cacheKey); if (request != null) { // If it is, add this request to the list of listeners. request.addContainer(imageContainer); return imageContainer; } // The request is not already in flight. Send the new request to the network and // track it. Request<?> newRequest = new ImageRequest(requestUrl, new Listener<Bitmap>() { @Override public void onResponse(Bitmap response) { onGetImageSuccess(cacheKey, response); } }, maxWidth, maxHeight, Config.RGB_565, new ErrorListener() { @Override public void onErrorResponse(VolleyError error) { onGetImageError(cacheKey, error); } }); mRequestQueue.add(newRequest); mInFlightRequests.put(cacheKey, new BatchedImageRequest(newRequest, imageContainer)); return imageContainer; } } maxWidth と maxHeight は ImageRequest のコンストラクタに渡されています。 public class ImageRequest extends Request<Bitmap> { ... private final int mMaxWidth; private final int mMaxHeight; ... public ImageRequest(String url, Response.Listener<Bitmap> listener, int maxWidth, int maxHeight, Config decodeConfig, Response.ErrorListener errorListener) { super(Method.GET, url, errorListener); setRetryPolicy( new DefaultRetryPolicy(IMAGE_TIMEOUT_MS, IMAGE_MAX_RETRIES, IMAGE_BACKOFF_MULT)); mListener = listener; mDecodeConfig = decodeConfig; mMaxWidth = maxWidth; mMaxHeight = maxHeight; } ... /** * The real guts of parseNetworkResponse. Broken out for readability. */ private Response<Bitmap> doParse(NetworkResponse response) { byte[] data = response.data; BitmapFactory.Options decodeOptions = new BitmapFactory.Options(); Bitmap bitmap = null; if (mMaxWidth == 0 && mMaxHeight == 0) { decodeOptions.inPreferredConfig = mDecodeConfig; bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions); } else { // If we have to resize this image, first get the natural bounds. decodeOptions.inJustDecodeBounds = true; BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions); int actualWidth = decodeOptions.outWidth; int actualHeight = decodeOptions.outHeight; // Then compute the dimensions we would ideally like to decode to. int desiredWidth = getResizedDimension(mMaxWidth, mMaxHeight, actualWidth, actualHeight); int desiredHeight = getResizedDimension(mMaxHeight, mMaxWidth, actualHeight, actualWidth); // Decode to the nearest power of two scaling factor. decodeOptions.inJustDecodeBounds = false; // TODO(ficus): Do we need this or is it okay since API 8 doesn't support it? // decodeOptions.inPreferQualityOverSpeed = PREFER_QUALITY_OVER_SPEED; decodeOptions.inSampleSize = findBestSampleSize(actualWidth, actualHeight, desiredWidth, desiredHeight); Bitmap tempBitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions); // If necessary, scale down to the maximal acceptable size. if (tempBitmap != null && (tempBitmap.getWidth() > desiredWidth || tempBitmap.getHeight() > desiredHeight)) { bitmap = Bitmap.createScaledBitmap(tempBitmap, desiredWidth, desiredHeight, true); tempBitmap.recycle(); } else { bitmap = tempBitmap; } } if (bitmap == null) { return Response.error(new ParseError(response)); } else { return Response.success(bitmap, HttpHeaderParser.parseCacheHeaders(response)); } } } ImageRequest の doParse() で、mMaxWidth == 0 && mMaxHeight == 0 のときはバイト配列をそのまま Bitmap にしているのがわかりますね。
それ以外のときは BitmapFactory.Options の inJustDecodeBounds や inSampleSize を使って Bitmap をスケールしています。

以下では、View のサイズがわかっている場合はそのサイズを使い、わからないときは画面サイズを指定するようにしてみました。 public class NetworkImageView extends ImageView { ... private void loadImageIfNecessary(final boolean isInLayoutPass) { int width = getWidth(); int height = getHeight(); ... DisplayMetrics metrics = getResources().getDisplayMetrics(); int w = width > 0 ? width : metrics.widthPixels; int h = height > 0 ? height : metrics.heightPixels; // The pre-existing content of this view didn't match the current URL. Load the new image // from the network. ImageContainer newContainer = mImageLoader.get(mUrl, new ImageListener() { @Override public void onErrorResponse(VolleyError error) { if (mErrorImageId != 0) { setImageResource(mErrorImageId); } } @Override public void onResponse(final ImageContainer response, boolean isImmediate) { // If this was an immediate response that was delivered inside of a layout // pass do not set the image immediately as it will trigger a requestLayout // inside of a layout. Instead, defer setting the image by posting back to // the main thread. if (isImmediate && isInLayoutPass) { post(new Runnable() { @Override public void run() { onResponse(response, false); } }); return; } if (response.getBitmap() != null) { setImageBitmap(response.getBitmap()); } else if (mDefaultImageId != 0) { setImageResource(mDefaultImageId); } } }, w, h); // update the ImageContainer to be the new bitmap container. mImageContainer = newContainer; } }



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」は危険!ということですね。



2013年11月11日月曜日

ViewPager で Volley の NetworkImageView を使うときの注意点

ViewPager の子要素は、ページが変わったときにも onLayout() が呼ばれます。

整理すると、ページが切り替わると
・新しく生成されたページ(PagerAdapter の instantiateItem() が呼ばれるところ)では onLayout(true, ...) が呼ばれる
・現在の子要素全てで onLayout(false, ...) が呼ばれる


Volley の NetworkImageView (2013年11月11日)では、 @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); loadImageIfNecessary(true); } のようになっています。

これだとページを切り替えるたびに画像の読み込みが実行されてしまいます。

changed が true のときだけにすれば、新しく生成されたときだけ実行されるようになります。 @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); if (changed) { loadImageIfNecessary(true); } }


2013年7月4日木曜日

Volley を使って XML を処理する

Volley には JsonRequest とか JsonObjectRequest とか用意されているのですが、XML 用(?)のはありません。
残念ながら XML が返ってくる API を利用せねばならない場合もあります。JSON がいいよー。。。

Entity から InputStream を取得して、XmlPullParser を使って parse している処理があったとします。
これの通信部分を Volley を使うようにするには、Request<InputStream> を継承したクラスを作ればいいわけです。 public class InputStreamRequest extends Request<InputStream> { private final Listener mListener; /** * * @param method * @param url * @param listener * @param errorListener */ public InputStreamRequest(int method, String url, Listener<InputStream> listener, ErrorListener errorListener) { super(method, url, errorListener); mListener = listener; } /** * * @param url * @param listener * @param errorListener */ public InputStreamRequest(String url, Listener<InputStream> listener, ErrorListener errorListener) { this(Method.GET, url, listener, errorListener); } @Override protected void deliverResponse(InputStream response) { mListener.onResponse(response); } @Override protected Response<InputStream> parseNetworkResponse(NetworkResponse response) { InputStream is = new ByteArrayInputStream(response.data); return Response.success(is, HttpHeaderParser.parseCacheHeaders(response)); } } public void doRequest(String url) { InputStreamRequest request = new InputStreamRequest(url, new Listener<InputStream>() { @Override public void onResponse(InputStream in) { MyData data = parseXml(in); try { in.close(); } catch (IOException e) { e.printStackTrace(); } if (mListener != null) { mListener.onParseXml(data); } } }, new ErrorListener() { @Override public void onErrorResponse(VolleyError error) { // error } }); mQueue.add(request); }

Volley の使い方は adamrocker のブログがわかりやすいです 「throw Life : Volley(AndroidのHTTP通信ライブラリ)を使おう」