位图大小超过 VM 预算-Android

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

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

没有动画,没有特效或任何可以填充内存的东西。 有时可绘制的内容会发生变化,有些是 android 资源,有些是保存在 SDCARD 文件夹中的文件。

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

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

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

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

115864 次浏览

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

如果不查看您的代码,就很难说明为什么会出现这种情况。然而,这篇文章提供了一些可能有帮助的建议:

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

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

我也遇到过同样的问题。堆非常小,所以这些映像可以相当快地失去对内存的控制。一种方法是通过调用 它的回收方法向垃圾收集器提示在位图上收集内存。

另外,onDestroy 方法不能保证被调用。您可能希望将此逻辑/清理移动到 onPause 活动中。查看活动生命周期图/表 在这一页以获得更多信息。

我也有同样的问题。经过一些测试,我发现这个错误是出现在大图像缩放。我减少了图像缩放,问题就消失了。

附言。起初,我试图缩小图像的尺寸,但没有缩小图像。但这并没有阻止错误的发生。

我发现开发 Android 应用程序最常见的错误之一就是“ java.lang”。位图大小超过 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"
>
...

然后,在 Activity 的 onDestroy()方法上,调用传递对父 View 的引用的 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. 移除每个视图组上的子级

我建议用一种简便的方法来解决这个问题。 只需在 Mainfest.xml 中为您的错误活动分配属性“ android: configChanges”值即可。 像这样:

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

我给出的第一个解决方案确实将 OOM 错误的频率降低到了一个较低的水平。但是,这并没有完全解决问题。然后我会给出第二个解决方案:

正如 OOM 所详述的,我已经使用了太多的运行时内存。因此,我减少图片大小在 ~/res/绘制我的项目。比如一张分辨率为128X128的超限照片,可以调整到64X64,这也适合我的申请。在我对一堆图片这样做之后,OOM 错误就不会再次发生了。

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

“快速提示:

1)永远不要自己调用 System.gc ()。这是作为一个修复程序在这里传播的,但它不起作用。不要这样做。如果您在我的解释中注意到,在获得 OutOfMemory 错误之前,JVM 已经运行了一个垃圾收集,因此没有理由再次执行垃圾收集(这会降低程序运行速度)。在活动结束时做一个只是为了掩盖问题。这可能会导致位图放到终结器队列上的速度加快,但是没有理由不直接在每个位图上调用循环。

2)总是在你不再需要的位图上调用再循环()。至少,在销毁你的活动时,检查并回收你使用的所有位图。另外,如果您希望更快地从 dalvik 堆收集位图实例,那么清除对位图的任何引用都没有坏处。

3)调用 trace ()然后 System.gc ()仍然可能无法从 Dalvik 堆中删除位图。不要担心这个。再循环()完成了它的工作并释放了本机内存,它只是需要一些时间来完成我前面概述的步骤,以实际从 Dalvik 堆中删除位图。这没什么大不了的,因为大块的本机内存已经是空闲的了!

4)最后总是假设框架中有一个 bug。达尔维克正在做它应该做的事。它可能不是你所期望或想要的,但它是如何运作的。”

我在网上找到的所有方法都试过了,没有一个管用。调用 System.gc ()只会降低应用程序的速度。在“毁灭”中重复使用位图对我来说也不管用。

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

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

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

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

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

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

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

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

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

以下几点确实帮了我很大的忙。也许还有其他几点,但这些都是非常关键的:

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

为了避免这个问题,您可以在 null-ing Bitmap对象之前使用本机方法 Bitmap.recycle()(或者设置另一个值):

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

接下来,你可以改变 myBitmap/O 呼叫 System.gc()像:

setMyBitmap(null);
setMyBitmap(anotherBitmap);

我有同样的问题,只是切换合理的大小背景图像。在添加新图片之前,我将 ImageView 设置为 null,得到了更好的结果。

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

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


}