使用Volley库中的NetworkImageView时出现OOM错误

7

背景

使用Volley库NetworkImageView是处理从网络上显示图像的方便方式。

然而,它有一些错误(如我在这里所写)。

问题

使用它可能会遇到的问题之一是它不能以内存有效的方式解码来自网络的图像。

这意味着,如果您在其中使用带有多个NetworkImageView的gridView,并且每个视图显示具有未知分辨率的图像(可能很小,也可能很大),则最终会出现OOM。

例如,您可以将此对象的URL设置为此处,并自行查看应用程序在显示位图后使用的内存量与之前使用的内存量相比有多少。

问题

我该如何修改NetworkImageView解码位图的方式?

我可以改变的一种方法是,在缩小图像的同时解码它以达到所需的大小(或者至少将其最大值设置为屏幕大小),例如使用这种缩小的方法。

2个回答

16

Volley内置了一个方法,可以将图像适应给定的宽度和高度,就像你提到的那样。你需要停止使用NetworkImageView提供的方便的加载图像的方法,因为它们没有使用这个方法。我建议使用以下方法来减少OOM错误的几率:

  1. 停止使用NetworkImageView。使用常规的ImageView并实现监听器,在图像可用时应用它。 这是步骤2的前提条件。使用带有get()方法的NetworkImageView可能会导致问题,根据我的经验。
  2. 创建一个ImageLoader并使用接收ImageRequestget()方法。如果可以,请使用可选构造函数,它接受maxHeightmaxWidth作为参数。
  3. 当您在ImageLoader中使用先前提到的get()方法时,保存该方法返回的ImageContainer引用,以便在请求完成之前回收视图时能够取消请求。
  4. ImageLoader构造函数中提供良好的ImageCache实现。这将降低解码已可用的位图的冗余。
  5. 如果您的架构允许,请尝试在位图上使用recycle()方法,但要小心不要回收仍然需要的位图。

编辑:添加代码示例

(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());

假设使用ViewHolder模式,并且imageContainerViewHolder类的成员变量,则以下是用于(3)的代码片段。此原则适用于任何架构。

// 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);

默认的图片加载器(在此您可以进行自定义):

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());
        }
    }
}

请问您能否提供一段示例代码?此外,如果谷歌无法以任何良好/定制的方式处理大型图像,为什么还要创建这个类呢? - android developer
NetworkImageView 是一种基本的语法糖,用于以基本方式加载远程图像。如果您听主题演讲,您会看到 Ficus(首席开发人员)说 NetworkImageView 基本上是“懒惰”的开发人员使用的(他的意思是好的)。这是一个很好的基本解决方案,但如果您需要特定的东西,您将不得不使用不同的机制。我会编辑我的答案并放一些示例。 - Itai Hanski
一个懒惰的开发者通常是一个好开发者,他们会利用他人的工作而不是重复造轮子并浪费时间解决新的问题。 但是在这种情况下并不适用,因为这个类无法处理大量图片导致内存满载。它甚至没有一种自定义解决此问题的方式。 - android developer
这并不完全正确,我只是发布了一些自定义修复。此外,事情永远不是非黑即白的。您需要考虑到Volley是新产品,就像所有新产品一样,它在不断发展和扩展。 - Itai Hanski
“超大”足以使您的设备在没有任何降采样的情况下正常解码位图时OOM。我认为,如果代码可以处理这些类型的图像,它也可以处理较小的图像... - android developer
显示剩余6条评论

3
我在搜索中找到了更好的解决方案:-)
NetworkImageView 在第104行知道它的宽度,在第105行知道它的高度,链接 NetworkImageView.java 以下是 NetworkImageView.java 的确切代码。
    private void loadImageIfNecessary(final boolean isInLayoutPass) {
    int width = getWidth(); // at line no 104
    int height = getHeight(); // at line no 105

你只需要将这些信息转发给图片加载器。
在第141行,NetworkImageView.java调用ImageLoader#get(String requestUrl, final ImageListener listener)方法时没有传入宽度和高度。将此调用更改为ImageLoader#get(String requestUrl, ImageListener imageListener, int maxWidth, int maxHeight)。 请使用下面的代码替换第141到172行的代码NetworkImageView.java
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);

请问您能否展示具体的代码以及放置位置(不仅仅是行号)? - android developer
这样修复了问题吗?也许你应该在GitHub网站的问题/请求中写一下?遗憾的是,我无法测试你所写的内容,因为我已经停止使用这个库了。也许有一天我会再次尝试它。 - android developer
这也是我最终做的!尽管我不确定缓存的图像...如果你从GridView开始,并进行此代码更改,这是否意味着缓存的“图像”会更小?当您切换到“全屏”视图并单击GridView图像时,它是否会重用低样本图像? - alpinescrambler
我不确定缓存的大小是否较小,但当您将图像设置为NetworkImageView时,它会设置图像的较小尺寸。 - Gangadhar Nimballi
这会防止所有的内存不足错误吗? - CQM

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接