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

Posted

技术标签:

【中文标题】java.lang.OutOfMemoryError:位图大小超出 VM 预算 - Android【英文标题】:java.lang.OutOfMemoryError: bitmap size exceeds VM budget - Android 【发布时间】:2010-12-29 06:37:24 【问题描述】:

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

应用程序运行一次,在屏幕上填充信息(LayoutsListviewsTextviewsImageViews 等),然后用户读取信息。

没有动画,没有特殊效果或任何可以填满记忆的东西。 有时drawables可以改变。有些是android资源,有些是保存在SDCARD文件夹中的文件。

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

每次用户进入应用,我可以看到内存越来越大,直到用户得到java.lang.OutOfMemoryError

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

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

【问题讨论】:

如果您有很多可绘制的更改,这可能会有所帮助。自从我自己做了这个程序以来,它一直在工作:) androidactivity.wordpress.com/2011/09/24/… 【参考方案1】:

我发现开发 Android 应用程序的最常见错误之一是“java.lang.OutOfMemoryError: Bitmap Size Exceeds VM Budget”错误。我在更改方向后使用大量位图的 Activity 上经常发现此错误:Activity 被销毁,再次创建,并且布局从 XML 中“膨胀”,消耗了可用于位图的 VM 内存。

前一个活动布局上的位图没有被垃圾收集器正确地释放,因为它们交叉引用了它们的活动。经过多次实验,我找到了一个非常好的解决这个问题的方法。

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

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

然后,在 Activity 的 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() 方法递归地探索视图树并且:

    移除所有背景可绘制对象的回调 删除每个视图组中的子项

【讨论】:

Except... java.lang.UnsupportedOperationException: 在 AdapterView 中不支持 removeAllViews() 感谢这有助于显着减少我的堆大小,因为我有很多可绘制对象...@GJTorikian 尝试在 removeAllViews() 处捕获大多数 ViewGroup 子类都可以正常工作。 或者只是改变条件:if (view instanceof ViewGroup && !(view instanceof AdapterView)) @Adam Varhegyi,您可以在 onDestroy 中为画廊视图显式调用相同的函数。像 unbindDrawables(galleryView);希望这对你有用... 注意这两个陷阱***.com/questions/8405475/… AND ***.com/questions/4486034/…【参考方案2】:

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

如果不查看您的代码,很难说出为什么会这样。不过,本文有一些提示可能会有所帮助:

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

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

【讨论】:

这是真的,而且有很多图像(而不是内存泄漏)可能会由于多种原因导致 outOfMemory,例如许多其他应用程序正在运行、内存碎片等。我了解到,在使用如果您系统地获取 outOfMemory 大量图像,就像总是做同样的事情,那么它就是泄漏。如果你像一天一次那样得到它,那是因为你太接近极限了。在这种情况下,我的建议是尝试删除一些内容并重试。图像内存也在堆内存之外,因此请注意两者。 如果您怀疑内存泄漏,监控堆使用情况的快速方法是通过 adb shell dumpsys meminfo 命令。如果您看到堆使用量在几个 gc 周期内增加(logcat 中的 GC_* 日志行),您可以很确定您有泄漏。然后通过 adb 或 DDMS 创建一个堆转储(或在不同时间创建多个),并通过 Eclipse MAT 上的支配树工具对其进行分析。您应该很快就会发现哪个对象具有随时间增长的保留堆。那是你的内存泄漏。我在这里写了一篇更详细的文章:macgyverdev.blogspot.com/2011/11/…【参考方案3】:

为避免此问题,您可以在 null 之前使用本机方法 Bitmap.recycle() -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);

【讨论】:

如果尝试将这些元素添加到列表视图中,这将不起作用。你对此有什么建议吗? @ddmytrenko 您在分配之前回收了图像。这不会阻止它的像素渲染吗?【参考方案4】:

我遇到了这个确切的问题。堆非常小,因此这些图像在内存方面很快就会失控。一种方法是通过调用its recycle method 向垃圾收集器提示在位图上收集内存。

此外,不保证会调用 onDestroy 方法。您可能希望将此逻辑/清理移动到 onPause 活动中。查看活动生命周期图表/表格on this page 了解更多信息。

【讨论】:

感谢您的信息。我正在管理所有 Drawables 而不是 Bitmaps,所以我不能调用 Recycle。对于drawables还有其他建议吗?谢谢丹尼尔 感谢onPause 的建议。我正在使用 4 个标签,其中全部包含 Bitmap 图像,因此破坏活动可能不会很快发生(如果用户浏览所有标签)。【参考方案5】:

这个解释可能会有所帮助: 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) 始终假设框架中存在错误。 Dalvik 正在做它应该做的事情。它可能不是您期望的或您想要的,但它是如何工作的。 "

【讨论】:

【参考方案6】:

我遇到了完全相同的问题。经过几次测试后,我发现大图像缩放会出现此错误。我减少了图像缩放,问题消失了。

附:起初我试图在不缩小图像的情况下减小图像大小。这并没有阻止错误。

【讨论】:

您能否发布有关如何进行图像缩放的代码,我面临同样的问题,我认为这可能会解决它。谢谢! @Gix - 我相信他的意思是他必须在将图像放入项目资源之前减小图像的大小,从而降低可绘制对象的内存占用。为此,您应该使用您选择的照片编辑器。我喜欢 pixlr.com【参考方案7】:

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

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

【讨论】:

【参考方案8】:

我建议一种方便的方法来解决这个问题。 只需在 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错误就不会再出现了。

【讨论】:

这很有效,因为您避免了重新启动您的应用程序。因此,这与其说是一种解决方案,不如说是一种避免。但有时这就是你所需要的。并且 Activity 中默认的 onConfigurationChanged() 方法会为您翻转方向,因此如果您不需要任何 UI 更改来真正适应不同的方向,您可能会对结果非常满意。不过,请注意其他可以重新启动您的事情。你可能想用这个:android:configChanges="keyboardHidden|orientation|keyboard|locale|mcc|mnc|touchscreen|screenLayout|fontScale"【参考方案9】:

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

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

我采取了第三种方法。我将所有视图设置为空,并将背景设置为黑色。我在我的 onStop() 方法中进行此清理。这是在活动不再可见时立即调用的方法。如果您在 onPause() 方法中执行此操作,用户将看到黑色背景。不理想。至于在 onDestroy() 方法中执行此操作,不能保证它会被调用。

为了防止在用户按下设备上的后退按钮时出现黑屏,我通过调用 startActivity(getIntent()) 和 finish() 方法在 onRestart() 方法中重新加载活动。

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

【讨论】:

【参考方案10】:

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

【讨论】:

【参考方案11】:

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

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

在我的例子中,代码如下所示:

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;
    

【讨论】:

这不会泄露你的上下文吗? (活动数据)【参考方案12】:

我只是用合理的尺寸切换背景图像时遇到了同样的问题。在放入新图片之前将 ImageView 设置为 null,我得到了更好的结果。

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

【讨论】:

【参考方案13】:

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


【讨论】:

以上是关于java.lang.OutOfMemoryError:位图大小超出 VM 预算 - Android的主要内容,如果未能解决你的问题,请参考以下文章