7

Background

Using the Volley library's NetworkImageView is a convenient way to handle showing images from the web.

However, it has some bugs (as i've written here).

the problem

One of the issues that you can have by using it is that it doesn't decode the images from the web in a memory efficient way.

This means that if you use a gridView with multiple NetworkImageView in it, and each shows an image that has an unknown resolution (could be small, could be large), you would end up having an OOM .

as an example, you can set the url of this object to be this one and see for yourself how much memory the app uses after showing the bitmap, compared to how much it used before.

The question

How can i modify the way that NetworkImageView decodes the bitmap ?

One way I could change it is to make it decode the bitmap while downscaling it to the needed size (or at least set the max of it to the screen size), for example using this method of downscaling .

Community
  • 1
  • 1
android developer
  • 114,585
  • 152
  • 739
  • 1,270

2 Answers2

16

Volley has a built in method for fitting an image to a given width and height like you mentioned. You need to stop using the convenience methods of loading images provided by NetworkImageView which don't use it. I suggest using the following methods to decrease the chance for OOM errors:

  1. Stop using NetworkImageView. Use a regular ImageView and implement the listener to apply the image when it is available. This is a prerequisite for step 2. Using a NetworkImageView with the get() method may lead to problems in my experience`.
  2. Create an ImageLoader and use the get() method which receives an ImageRequest. Use the optional constructor that takes in a maxHeight and maxWidth as parameters if you can.
  3. When you use the previously mentioned get() method in the ImageLoader, save the ImageContainer reference that method returns so you'll be able to cancel a request if the view is recycled before the request completes.
  4. Provide a good implementation for an ImageCache in the ImageLoader constructor. That'll lower the redundancy in decoding bitmaps that are already available.
  5. If your architecture allows it, try to use the recycle() method on the bitmaps, but be careful not to recycle the ones you might still need.

EDIT: Added Code Samples

Code snippet for (2) + (4)

// assuming sRequestQueue is your static global request queue 
// and `BitmapCache` is a good implementation for the `ImageCache` interface
sImageLoader = new ImageLoader(sRequestQueue, new BitmapCache());

Code snippet for (3) assuming the ViewHolder pattern and imageContainer is a member of the ViewHolder class. The principal applies to any architecture.

// when applying a new view cancel the previous request first

if (imageContainer != null) {
    imageContainer.cancelRequest();
}

// calculate the max height and max width

imageContainer = sImageLoader.get(imageUrl, 
    new DefaultImageListener(image), maxWidth, maxHeight);

The default image loader (you can do what you here):

private class DefaultImageListener implements ImageListener {
    private ImageView imageView;

    public DefaultImageListener(ImageView view) {
        imageView = view
    }

    @Override
    public void onErrorResponse(VolleyError error) {
        //handle errors
    }

    @Override
    public void onResponse(ImageContainer response, boolean isImmediate) {
        if (response.getBitmap() != null) {
            imageView.setImageBitmap(response.getBitmap());
        }
    }
}
Jens
  • 15
  • 6
Itai Hanski
  • 8,540
  • 5
  • 45
  • 65
  • Could you please show a sample code ? Also, why did google make this class if it can't handle large images in any good/customized way? – android developer Sep 03 '13 at 09:37
  • The `NetworkImageView` is syntactic sugar for loading remote images in a basic way. If you'll listen to the keynote you'll see the Ficus (the lead developer) says that the `NetworkImageView` is basically, and I quote, for "lazy" developers (he means that in a good way). It's a good basic solution, but if you need something specific you'll have to use a different mechanism. I'll edit my answer and put up some samples. – Itai Hanski Sep 03 '13 at 09:45
  • a lazy developer is usually a good developer, which takes the work of others instead of re-inventing the wheel and waste time on new bugs. this is not the case, since this class isn't good enough for images that could fill the memory. it doesn't even have a way to fix this issue in a custom manner. – android developer Sep 03 '13 at 10:12
  • That is not entirely correct, I just posted some custom fixes. Also things are never black and white. You need to take into account that Volley is new, and like all new products it's constantly evolving and expanding. – Itai Hanski Sep 03 '13 at 10:49
  • have you tested your code on super large images, like the one i've posted about? – android developer Sep 09 '13 at 15:07
  • I don't know what "super large" is translated to, but I have tried this with regular sized photos you find on most news sites. You're more than welcome to try it, the image scaling should be fine. In anyway, these are best practices I stand behind. – Itai Hanski Sep 10 '13 at 06:25
  • "super large" is large enough to make your device go OOM in case you really decode the bitmap in the normal way without any downsampling. i think that if the code handles even those kind of images, it can handle smaller images as well... – android developer Sep 10 '13 at 07:16
  • Look at the snippet for (3). There's an example for both the `get()` method and the listener I mentioned on step (1). – Itai Hanski Sep 11 '13 at 06:36
  • Let's go through what I wrote: *"Use a regular ImageView and implement the listener to apply the image when it is available."* On the step (3) code I wrote: `imageContainer = sImageLoader.get(imageUrl, new DefaultImageListener(image), maxWidth, maxHeight);` Assuming `image` is your `ImageView` that has a reference is stored in the `ViewHolder` class. That covers step (1). – Itai Hanski Sep 11 '13 at 07:01
  • again , i still don't understand what it means , so i hoped you'd show me via code. – android developer Sep 11 '13 at 07:26
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/37149/discussion-between-itai-hanski-and-android-developer) – Itai Hanski Sep 11 '13 at 07:46
3

I found a better solution in my search : -)

NetworkImageView knows its width at line no - 104 and height at line no - 105 in the link NetworkImageView.java

Below is the exact code at NetworkImageView.java

    private void loadImageIfNecessary(final boolean isInLayoutPass) {
    int width = getWidth(); // at line no 104
    int height = getHeight(); // at line no 105

you only need to forward this information to the image loader.

On line 141 NetworkImageView.java calls the ImageLoader#get(String requestUrl, final ImageListener listener) method without width and height. Change this call to ImageLoader#get(String requestUrl, ImageListener imageListener, int maxWidth, int maxHeight).

Replace the code from line no 141 to 172 of NetworkImageView.java with below code

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);
                    }
                }
            }, width, height);
Gangadhar Nimballi
  • 1,534
  • 3
  • 18
  • 46
  • can you please show the exact code to put and where to put it (not just line number) ? – android developer Jan 16 '14 at 14:11
  • so this fixes the problem? maybe you should write about it on the issues/requests of the github website? sadly i can't test what you've written since i have stopped using this library. maybe some day i will try it out again. – android developer Jan 17 '14 at 23:07
  • This is what I ended up doing too! Although, I am unsure of the cached image though... If you start off with a Gridview, and with this code change, does this mean that the cached "image" will be smaller? And when you switch to a "full screen" view, upon clicking the GridView image, will it reuse the low sample image? – alpinescrambler Feb 04 '14 at 09:24
  • I am not sure that the cached size is small, but when you are setting image to NetworkImageView, it set the small size of image. – Gangadhar Nimballi Feb 04 '14 at 10:00
  • does this prevent all of your Out of Memory errors? – CQM Jun 11 '14 at 15:09