Java.lang.OutOfMemoryError:位图大小超出VM预算 - Android

160
我开发了一款在Android上使用大量图像的应用程序。该应用程序运行一次,在屏幕上填充信息(布局、列表视图、文本视图、图像视图等),用户读取信息。没有动画、特效或任何会占用内存的东西。有时可绘制的对象可能会更改。有些是Android资源,有些是保存在SDCARD文件夹中的文件。然后用户退出(执行onDestroy方法,应用程序由VM保留在内存中),然后在某个时间点再次进入。每次用户进入应用程序时,我都可以看到内存越来越多,直到用户收到java.lang.OutOfMemoryError。所以处理许多图像的最佳/正确方法是什么?我应该将它们放入静态方法中,以便不必一直加载它们吗?我需要以特殊的方式清理布局或在布局中使用的图像吗?

4
如果你有许多可绘制的图形在不断变化,这可能会有所帮助。 这个方法是有效的,因为我自己也使用了这个程序 :) http://androidactivity.wordpress.com/2011/09/24/solution-for-outofmemoryerror-bitmap-size-exceeds-vm-budget/ - user964099
13个回答

98
我在开发 Android 应用时经常遇到的一个常见错误是“java.lang.OutOfMemoryError: Bitmap Size Exceeds VM Budget”(Java.lang.OutOfMemoryError:位图大小超过VM预算)。在更改方向后,使用许多位图的活动中经常发现这个错误:Activity 被销毁,再次创建并从 XML 中“填充”布局,消耗可用于位图的 VM 内存。
由于位图与其 Activity 存在交叉引用,因此前一个 activity 布局上的位图没有被垃圾收集器正确地释放。经过多次实验,我找到了一个相当好的解决方案。
首先,在 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()方法中,调用unbindDrawables()方法并传递对父视图的引用,然后执行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: removeAllViews() 不支持在 AdapterView 中使用。 - user213345
5
只需要更改条件语句:if (view instanceof ViewGroup && !(view instanceof AdapterView))。目的是使之更加通俗易懂,但不改变原意。 - Anke
2
@Adam Varhegyi, 你可以在onDestroy中显式地调用相同的函数以针对画廊视图。像这样解除绑定unbindDrawables(galleryView); 希望这对你有用... - hp.android
1
注意这两个问题:https://dev59.com/zGoy5IYBdhLWcg3wq_sk 和 https://dev59.com/Ym855IYBdhLWcg3wMBPV - max4ever
这个解决方案很好。感谢你的出色解释,伙计。这可能是小事情,但非常重要。再次感谢。 - Reddy Raaz
显示剩余5条评论

73

听起来你的应用存在内存泄漏问题。这个问题并不在于处理多张图片,而是当你的activity被销毁时,你的图片没有得到释放。

没有看到你的代码很难确定原因。然而,这篇文章提供了一些可能有用的提示:

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

特别地,使用静态变量可能会使情况变得更糟,而不是变得更好。你可能需要添加代码,在应用程序重新绘制时删除回调 - 但同样,在这里提供的信息不足以确定是否需要这样做。


8
这是真实的,同时拥有大量图像(而不是内存泄漏)可能会出现内存溢出,原因可能有多个,比如正在运行许多其他应用程序,内存碎片化等。我发现,当处理大量图像时,如果你经常遇到内存溢出的情况,例如总是发生,那么这就是一种泄漏。如果你每天或者某段时间只发生一次,则是因为你太接近极限了。我的建议是在这种情况下尝试删除一些东西,然后再次尝试。此外,图像内存位于堆外存储器中,所以需要关注两者。 - Daniel Benedykt
15
如果你怀疑内存泄漏,通过adb shell dumpsys meminfo命令快速监视堆使用情况是一个不错的办法。如果你在几次垃圾回收(GC_* log row in logcat)之后看到堆使用量增加,那么可以基本确定你有一个内存泄漏。然后通过adb或DDMS创建一个堆转储,或者在不同的时间创建几个堆转储,并通过Eclipse MAT上的支配树工具进行分析。很快就会找到哪个对象的保留堆随着时间不断增长,这就是你的内存泄漏。我在这里写了一篇更详细的文章:http://macgyverdev.blogspot.com/2011/11/android-track-down-memory-leaks.html - Johan Norén

11
为了避免这个问题,您可以在将Bitmap对象设置为null(或设置另一个值)之前使用本地方法Bitmap.recycle()。例如:
public final void setMyBitmap(Bitmap bitmap) {
  if (this.myBitmap != null) {
    this.myBitmap.recycle();
  }
  this.myBitmap = bitmap;
}

然后你可以这样更改myBitmap,而无需调用System.gc()

setMyBitmap(null);    
setMyBitmap(anotherBitmap);

1
这样做无法将这些元素添加到列表视图中。你有什么建议吗? - Rafael Sanches
@ddmytrenko 你在分配图像之前进行了回收利用。这样做不会防止其像素渲染吗? - IgorGanapolsky

8
我遇到过这个确切的问题。堆空间很小,所以这些图像在内存方面很容易失控。一种方法是通过调用其回收方法向垃圾收集器提示对位图进行内存回收。
此外,onDestroy方法不能保证被调用。您可能需要将此逻辑/清理移动到onPause活动中。有关更多信息,请查看此页面上的Activity生命周期图/表(链接)

谢谢提供的信息。我正在管理所有的Drawable而不是Bitmap,所以我不能调用Recycle方法。 还有其他关于Drawables的建议吗?谢谢丹尼尔 - Daniel Benedykt
感谢您对“onPause”建议的支持。我在4个选项卡中都使用了“Bitmap”图像,因此如果用户浏览所有选项卡,则活动可能不会很快被销毁。 - Azurespot

7
这里有一些解释可能会有帮助: http://code.google.com/p/android/issues/detail?id=8488#c80 “快速提示: 1)千万不要自己调用System.gc()。这个方法被传播为修复内存问题的方法,但它并没有起到作用。绝对不要这样做。如果您注意到,在发生OutOfMemoryError之前,JVM已经运行了垃圾收集器,所以没有理由再运行一次垃圾收集器(这会减慢您的程序速度)。在您的Activity结束时进行垃圾回收只是掩盖了问题,您可以简单地调用每个bitmap的recycle方法来解决问题。 2)始终调用不再需要的位图的recycle()方法。至少,在Activity的onDestroy()中遍历并回收您正在使用的所有位图。此外,如果您希望从dalvik堆中更快地收集位图实例,则清除对位图的任何引用也无妨。 3)调用recycle()然后System.gc()仍然可能无法从Dalvik堆中删除位图。请不要担心这个问题。recycle()已经完成了其工作并释放了本地内存,只需要一些时间按照我之前概述的步骤将位图从Dalvik堆中实际删除。这不是什么大问题,因为大块本地内存已经被释放了! 4)始终假设框架中存在最后一个错误。Dalvik正在按照预期工作。它可能不是您所期望的或想要的,但这就是它的工作方式。”

5

以下几点对我帮助很大。还可能有其他要点,但这些非常关键:

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

5

我曾经遇到过完全相同的问题。经过一番测试,我发现这个错误是由于大图缩放引起的。我减小了图像的缩放比例,问题就消失了。

另外,我先尝试了不缩放图像而只是减小图像尺寸的方法,但并没有解决这个错误。


4
你能否发布一下你如何进行图像缩放的代码?我遇到了同样的问题,我认为这可能会解决它。谢谢! - Gligor
@Gix - 我相信他的意思是在将图像导入项目资源之前,他必须减小它们的大小,从而降低可绘制对象的内存占用。为此,您应该使用您选择的照片编辑器。我喜欢pixlr.com。 - Kyle Clegg

4
我建议一种方便的方法来解决这个问题。只需在Manifest.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
我也对内存溢出问题感到沮丧。我发现在缩放图像时,经常出现这个错误。起初,我试图为所有密度创建图像大小,但发现这会大大增加我的应用程序的大小。所以现在我只是使用一种图像来适配所有密度并缩放我的图像。
当用户从一个活动转到另一个活动时,我的应用程序会抛出内存溢出错误。将我的可绘制对象设置为null并调用System.gc()没有起作用,使用getBitMap().recycle()回收我的位图对象也不行。无论是第一种方法还是废弃位图并使用第二种方法,Android都会继续抛出内存溢出错误或在使用已回收的位图时抛出画布错误消息。
我采用了第三种做法, 在onStop()方法中将所有视图设置为null,并将背景设置为黑色。这个方法是在活动不可见时立即调用的。如果您在onPause()方法中执行此操作,用户将看到黑色背景。不理想。关于在onDestroy()方法中执行此操作,不能保证它会被调用。
为了防止用户在设备上按下返回按钮时出现黑屏,我在onRestart()方法中通过调用startActivity(getIntent())和finish()方法重新加载活动。
注意:更改背景为黑色并非必要。

1

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


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