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

时间:2009-12-22 20:51:32

标签: android memory memory-leaks bitmap out-of-memory

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

该应用运行一次,填充屏幕上的信息(LayoutsListviewsTextviewsImageViews等),用户可以读取信息。

没有动画,没有特效或任何可以填充内存的东西。 有时候抽屉可以改变。有些是Android资源,有些是保存在SDCARD文件夹中的文件。

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

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

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

我应该将它们放在静态方法中,以免它们一直被加载吗? 我是否必须以特殊方式清洁布局或布局中使用的图像?

13 个答案:

答案 0 :(得分:96)

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

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

首先,在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"
     >
     ...

然后,在您的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()方法以递归方式探索视图树:

  1. 删除所有背景drawable上的回调
  2. 删除每个视图组中的子项

答案 1 :(得分:70)

听起来你有内存泄漏。问题是没有处理很多图像,而是当你的活动被破坏时你的图像没有被解除分配。

很难说为什么没有看你的代码。但是,本文提供了一些可能有用的提示:

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

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

答案 2 :(得分:10)

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

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

接下来,您可以更改myBitmap,而无需调用System.gc(),如:

setMyBitmap(null);    
setMyBitmap(anotherBitmap);

答案 3 :(得分:7)

我遇到了这个问题。堆非常小,因此这些图像可能会在内存方面相当快速地失控。一种方法是通过调用its recycle method为垃圾收集器提供一个提示来收集位图上的内存。

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

答案 4 :(得分:7)

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

“快速提示:

1)永远不要自己调用System.gc()。这已在此处作为修复传播,但它不起作用。不要做。如果您在我的解释中注意到,在获取OutOfMemoryError之前,JVM已经运行了垃圾收集,因此没有理由再次执行一次(它会减慢您的程序速度)。在活动结束时执行一项操作就是解决问题。它可能会使位图更快地放在终结器队列中,但没有理由你不能简单地在每个位图上调用recycle。

2)始终在您不再需要的位图上调用recycle()。至少,在您的活动的onDestroy中,通过并回收您正在使用的所有位图。此外,如果您希望更快地从dalvik堆中收集位图实例,则清除对位图的任何引用都没有什么坏处。

3)调用recycle()然后System.gc()仍然可能无法从Dalvik堆中删除位图。不要关心这个。 recycle()完成了它的工作并释放了本机内存,它只需要一些时间来完成我之前概述的步骤,实际上从Dalvik堆中删除了位图。这不是什么大问题,因为大量本机内存已经免费!

4)总是假设框架中有一个错误。达尔维克正在做它应该做的事情。它可能不是您所期望的或您想要的,但它的工作原理。 “

答案 5 :(得分:5)

我遇到了完全相同的问题。经过一些测试后,我发现这个错误出现在大图像缩放中。我减少了图像缩放,问题就消失了。

P.S。起初我试图减小图像大小而不缩小图像。这并没有阻止错误。

答案 6 :(得分:5)

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

  1. 尽可能使用应用程序上下文(而不是activity.this)。
  2. 在onPause()活动方法
  3. 中停止并释放您的主题
  4. 在onDestroy()活动方法
  5. 中发布您的观看/回调

答案 7 :(得分: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错误不再发生。

答案 8 :(得分:3)

我也对outofmemory bug感到沮丧。是的,我也发现在缩放图像时会出现这种错误。起初我尝试为所有密度创建图像大小,但我发现这大大增加了我的应用程序的大小。所以我现在只使用一个图像来显示所有密度并缩放图像。

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

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

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

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

答案 9 :(得分:1)

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

答案 10 :(得分:0)

好吧,我已经尝试过我在互联网上找到的所有内容,但没有一个能够正常工作。调用System.gc()只会降低app的速度。在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;
    }
}

答案 11 :(得分:0)

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

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

答案 12 :(得分: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();
    }

}