java.lang.OutOfMemoryError:位图大小超出VM预算-A​​ndroid


159

我开发了一个在Android上使用大量图像的应用程序。

该应用程序运行一次,填满屏幕(上的信息LayoutsListviewsTextviewsImageViews,等)和用户读取的信息。

没有动画,没有特效或任何可以填满内存的东西。有时可绘制对象可以更改。有些是android资源,有些是保存在SDCARD文件夹中的文件。

然后,用户退出(该onDestroy方法被执行,并且应用程序由VM保留在内存中),然后在某个时候用户再次进入。

每次用户进入该应用程序时,我都会看到内存越来越多,直到用户获得java.lang.OutOfMemoryError

那么处理许多图像的最佳/正确方法是什么?

我应该将它们放在静态方法中,这样就不会一直加载它们吗?我是否必须以特殊方式清洁版面或版面中使用的图像?


4
如果要更改许多可绘制对象,这可能会有所帮助。据工作,因为我做的节目我自己:) androidactivity.wordpress.com/2011/09/24/...

Answers:


72

听起来您有内存泄漏。问题不是处理很多图像,而是当您的活动被销毁时图像没有被释放。

如果不查看代码,很难说出这是为什么。但是,本文有一些技巧可能会有所帮助:

http://android-developers.blogspot.de/2009/01/avoiding-memory-leaks.html

特别是,使用静态变量可能会使情况更糟,而不是更好。您可能需要添加代码,以在应用程序重绘时删除回调-但同样,这里没有足够的信息可以肯定地说。


8
这是事实,并且有很多图像(而不是内存泄漏)可能会由于多种原因导致outOfMemory,例如许多其他应用程序正在运行,内存碎片等。系统地,就像总是做同样的事情,那就是泄漏。如果您一天或一次就能得到它,那是因为您太接近极限了。在这种情况下,我的建议是尝试删除一些内容然后重试。此外,图像内存在堆内存之外,因此请注意两者。
丹尼尔·本尼迪克

15
如果您怀疑内存泄漏,可以通过adb shell dumpsys meminfo命令来监视堆使用情况的快速方法。如果您看到堆使用率在几个gc周期内有所增加(logcat中的GC_ *日志行),则可以确定发生泄漏。然后通过adb或DDMS创建一个堆转储(或在不同的时间进行几个转储),并通过Eclipse MAT上的支配树工具对它进行分析。您应该很快找到哪个对象具有随时间增长的保留堆。那是你的内存泄漏。我写了一些更详细的文章在这里:macgyverdev.blogspot.com/2011/11/...
约翰·诺伦

98

我发现开发Android应用程序时最常见的错误之一是“ java.lang.OutOfMemoryError:位图大小超出VM预算”错误。我在更改方向后使用大量位图的活动上经常发现此错误:活动被破坏,再次创建,并且布局从XML中“膨胀”,消耗了可用于位图的VM内存。

垃圾回收器无法正确分配先前活动布局上的位图,因为它们已经交叉引用了其活动。经过多次实验,我发现了一个很好的解决方案。

首先,在XML布局的父视图上设置“ id”属性:

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="fill_parent"
     android:layout_height="fill_parent"
     android:id="@+id/RootView"
     >
     ...

然后,在onDestroy() Activity 的方法上,调用将unbindDrawables()引用传递给父View的方法,然后执行System.gc()

    @Override
    protected void onDestroy() {
    super.onDestroy();

    unbindDrawables(findViewById(R.id.RootView));
    System.gc();
    }

    private void unbindDrawables(View view) {
        if (view.getBackground() != null) {
        view.getBackground().setCallback(null);
        }
        if (view instanceof ViewGroup) {
            for (int i = 0; i < ((ViewGroup) view).getChildCount(); i++) {
            unbindDrawables(((ViewGroup) view).getChildAt(i));
            }
        ((ViewGroup) view).removeAllViews();
        }
    }

unbindDrawables()方法以递归方式浏览视图树,并:

  1. 删除所有背景可绘制对象上的回调
  2. 删除每个视图组上的子级

10
除了... java.lang.UnsupportedOperationException:AdapterView中不支持removeAllViews()

谢谢,因为我有很多可绘制对象,所以这有助于显着减少堆大小。
Eric Chen

5
或只是更改条件:if(view instanceof ViewGroup &&!(view instanceof AdapterView))
Anke

2
@Adam Varhegyi,您可以在onDestroy中为图库视图显式调用相同的函数。像unbindDrawables(galleryView); 希望这对您
有用


11

为避免此问题,可以Bitmap.recycle()null-ing Bitmap对象之前使用本机方法(或设置其他值)。例:

public final void setMyBitmap(Bitmap bitmap) {
  if (this.myBitmap != null) {
    this.myBitmap.recycle();
  }
  this.myBitmap = bitmap;
}

接下来,您可以更改无myBitmap呼叫方式,System.gc()例如:

setMyBitmap(null);    
setMyBitmap(anotherBitmap);

1
如果尝试将这些元素添加到列表视图中,则此方法将无效。您对此有什么建议吗?
拉斐尔·桑切斯2012年

@ddmytrenko您在分配图像之前先对其进行回收。那不会阻止其像素渲染吗?
IgorGanapolsky

8

我遇到了这个确切的问题。堆很小,因此这些映像在内存方面可以很快失控。一种方法是通过调用给垃圾收集器的提示上一个位图收集内存的回收方法

另外,不能保证调用onDestroy方法。您可能需要将此逻辑/清理工作移到onPause活动中。请查看此页面上的活动生命周期图/表获取更多信息。


谢谢(你的)信息。我正在管理所有Drawable而不是Bitmap,因此我无法调用Recycle。还有其他关于绘画的建议吗?感谢Daniel
Daniel Benedykt

谢谢你的onPause建议。我正在使用其中Bitmap包含图像的4个选项卡,因此销毁该活动可能不会太快发生(如果用户浏览所有选项卡)。
Azurespot 2015年

7

该说明可能会有所帮助:http : //code.google.com/p/android/issues/detail?id=8488#c80

“快速提示:

1)切勿自己调用System.gc()。这已作为修复程序传播到这里,并且不起作用。不要做。如果您在我的解释中注意到,则在获取OutOfMemoryError之前,JVM已经运行了垃圾回收,因此没有理由再做一次(这会使程序变慢)。在活动结束时做一个只是掩盖了问题。这可能会使位图更快地进入终结器队列,但是没有理由不能简单地在每个位图上调用回收。

2)始终在不再需要的位图上调用recycle()。至少,在您的活动的onDestroy中,循环使用所有您使用的位图。另外,如果您希望更快地从dalvik堆中收集位图实例,那么清除对位图的任何引用也不会有任何伤害。

3)调用recycle()然后再调用System.gc()仍可能无法从Dalvik堆中删除位图。不要对此感到担忧。recycle()完成了工作并释放了本机内存,这将花费一些时间来完成我之前概述的步骤,以从Dalvik堆中真正删除位图。这没什么大不了的,因为大部分本机内存已经是可用的!

4)最后,请假设框架中存在错误。达尔维克正在做其应做的事情。它可能不是您期望或想要的,但是它是如何工作的。”


5

我有同样的问题。经过几次测试,我发现对于大图像缩放出现此错误。我减小了图像缩放比例,问题消失了。

PS首先,我尝试减小图像尺寸而不缩小图像。那并没有阻止错误。


4
您能否发布有关如何进行图像缩放的代码,我正面临着同样的问题,我认为这可能可以解决它。谢谢!
Gligor

@Gix-我相信他的意思是他必须减小图像的大小,然后才能将其带入项目资源,从而减少可绘制对象的内存占用。为此,您应该使用自己选择的照片编辑器。我喜欢pixlr.com
凯尔·克莱格

5

遵循以下几点确实对我有很大帮助。也许还有其他要点,但是这些非常关键:

  1. 尽可能使用应用程序上下文(而不是activity.this)。
  2. 在onPause()活动方法中停止和释放线程
  3. 在活动的onDestroy()方法中释放您的视图/回调

4

我建议一种解决此问题的简便方法。只需在Mainfest.xml中为错误活动分配属性“ android:configChanges”值,如下所示。像这样:

<activity android:name=".main.MainActivity"
              android:label="mainActivity"
              android:configChanges="orientation|keyboardHidden|navigation">
</activity>

我给出的第一个解决方案确实将OOM错误的发生频率降低到一个较低的水平。但是,它不能完全解决问题。然后,我将给出第二个解决方案:

正如OOM所详述的,我已经使用了太多的运行时内存。因此,我在我的项目的〜/ res / drawable中减小图片大小。例如,分辨率为128X128的超标图片可以调整为64x64,这也适合我的应用。当我对一堆图片进行处理后,OOM错误不再发生。


1
之所以有效,是因为您避免了重新启动应用程序。因此,与其说是回避,不如说是解决方案。但是有时候这就是您所需要的。并且Activity中默认的onConfigurationChanged()方法将为您翻转方向,因此,如果不需要任何UI更改即可真正适应不同的方向,则可能会对结果感到完全满意。不过,请注意其他可以重启您的事情。您可能要使用:android:configChanges =“ keyboardHidden | orientation | keyboard | locale | mcc | mnc | touchscreen | screenLayout | fontScale”
Carl

3

我也对内存不足的错误感到沮丧。是的,我也发现缩放图像时会弹出很多错误。最初,我尝试为所有密度创建图像大小,但是发现这大大增加了我的应用程序的大小。因此,我现在仅使用一张图像来实现所有密度并缩放图像。

每当用户从一种活动转到另一种活动时,我的应用程序都会抛出内存不足错误。将我的drawable设置为null并调用System.gc()无效,也没有使用getBitMap()。recycle()回收bitmapDrawables。Android将继续使用第一种方法抛出内存不足错误,并且每当尝试使用第二种方法使用回收位图时,都会抛出画布错误消息。

我采取了第三种方法。我将所有视图设置为null,背景设置为黑色。我在onStop()方法中进行此清理。一旦不再可见活动,就会调用此方法。如果在onPause()方法中执行此操作,则用户将看到黑色背景。不理想。至于在onDestroy()方法中执行此操作,则无法保证会被调用。

为了防止用户按下设备上的“后退”按钮时出现黑屏,我先调用startActivity(getIntent())然后使用finish()方法将活动重新加载到onRestart()方法中。

注意:实际上没有必要将背景更改为黑色。


1

如果从磁盘或网络位置(或实际上是内存以外的任何其他源)读取源数据,则不应在主UI线程上执行“ 有效加载大位图”课程中讨论的BitmapFactory.decode *方法。该数据加载所需的时间是不可预测的,并且取决于多种因素(从磁盘或网络读取的速度,图像大小,CPU的功率等)。如果这些任务之一阻止了UI线程,则系统会将您的应用程序标记为无响应,并且用户可以选择关闭它(有关更多信息,请参阅设计响应性)。


0

好吧,我已经尝试了在互联网上找到的所有内容,但没有一个起作用。调用System.gc()只会降低应用程序的速度。在onDestroy中回收位图对我也不起作用。

现在唯一起作用的是拥有所有位图的静态列表,以便位图在重新启动后仍然存在。而且,每次重新启动活动后,只需使用保存的位图,而不是每次都创建新的位图即可。

就我而言,代码如下所示:

private static BitmapDrawable currentBGDrawable;

if (new File(uriString).exists()) {
    if (!uriString.equals(currentBGUri)) {
        freeBackground();
        bg = BitmapFactory.decodeFile(uriString);

        currentBGUri = uriString;
        bgDrawable = new BitmapDrawable(bg);
        currentBGDrawable = bgDrawable;
    } else {
        bgDrawable = currentBGDrawable;
    }
}

这不会泄漏您的上下文吗?(活动数据)
拉斐尔·桑切斯

0

我只是以合理的大小切换背景图像而遇到同样的问题。在放入新图片之前,将ImageView设置为null可以获得更好的结果。

ImageView ivBg = (ImageView) findViewById(R.id.main_backgroundImage);
ivBg.setImageDrawable(null);
ivBg.setImageDrawable(getResources().getDrawable(R.drawable.new_picture));

0

FWIW,这是我编写的轻量级位图缓存,使用了几个月。这不是千篇一律,所以在使用之前请阅读代码。

/**
 * Lightweight cache for Bitmap objects. 
 * 
 * There is no thread-safety built into this class. 
 * 
 * Note: you may wish to create bitmaps using the application-context, rather than the activity-context. 
 * I believe the activity-context has a reference to the Activity object. 
 * So for as long as the bitmap exists, it will have an indirect link to the activity, 
 * and prevent the garbaage collector from disposing the activity object, leading to memory leaks. 
 */
public class BitmapCache { 

    private Hashtable<String,ArrayList<Bitmap>> hashtable = new Hashtable<String, ArrayList<Bitmap>>();  

    private StringBuilder sb = new StringBuilder(); 

    public BitmapCache() { 
    } 

    /**
     * A Bitmap with the given width and height will be returned. 
     * It is removed from the cache. 
     * 
     * An attempt is made to return the correct config, but for unusual configs (as at 30may13) this might not happen.  
     * 
     * Note that thread-safety is the caller's responsibility. 
     */
    public Bitmap get(int width, int height, Bitmap.Config config) { 
        String key = getKey(width, height, config); 
        ArrayList<Bitmap> list = getList(key); 
        int listSize = list.size();
        if (listSize>0) { 
            return list.remove(listSize-1); 
        } else { 
            try { 
                return Bitmap.createBitmap(width, height, config);
            } catch (RuntimeException e) { 
                // TODO: Test appendHockeyApp() works. 
                App.appendHockeyApp("BitmapCache has "+hashtable.size()+":"+listSize+" request "+width+"x"+height); 
                throw e ; 
            }
        }
    }

    /**
     * Puts a Bitmap object into the cache. 
     * 
     * Note that thread-safety is the caller's responsibility. 
     */
    public void put(Bitmap bitmap) { 
        if (bitmap==null) return ; 
        String key = getKey(bitmap); 
        ArrayList<Bitmap> list = getList(key); 
        list.add(bitmap); 
    }

    private ArrayList<Bitmap> getList(String key) {
        ArrayList<Bitmap> list = hashtable.get(key);
        if (list==null) { 
            list = new ArrayList<Bitmap>(); 
            hashtable.put(key, list); 
        }
        return list;
    } 

    private String getKey(Bitmap bitmap) {
        int width = bitmap.getWidth();
        int height = bitmap.getHeight();
        Config config = bitmap.getConfig();
        return getKey(width, height, config);
    }

    private String getKey(int width, int height, Config config) {
        sb.setLength(0); 
        sb.append(width); 
        sb.append("x"); 
        sb.append(height); 
        sb.append(" "); 
        switch (config) {
        case ALPHA_8:
            sb.append("ALPHA_8"); 
            break;
        case ARGB_4444:
            sb.append("ARGB_4444"); 
            break;
        case ARGB_8888:
            sb.append("ARGB_8888"); 
            break;
        case RGB_565:
            sb.append("RGB_565"); 
            break;
        default:
            sb.append("unknown"); 
            break; 
        }
        return sb.toString();
    }

}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.