160

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

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

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

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

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

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

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

4

13 回答 13

98

我发现开发 Android 应用程序的最常见错误之一是“java.lang.OutOfMemoryError: Bitmap Size Exceeds VM Budget”错误。我在更改方向后使用大量位图的活动上经常发现此错误:活动被销毁,再次创建,并且布局从 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. 删除每个视图组上的子项
于 2011-07-21T15:57:53.980 回答
74

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

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

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

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

于 2009-12-22T21:55:12.380 回答
11

为避免此问题,您可以在-ing对象(或设置另一个值)Bitmap.recycle()之前使用本机方法。例子:nullBitmap

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

接下来,您可以更改myBitmapw/o 调用System.gc(),例如:

setMyBitmap(null);    
setMyBitmap(anotherBitmap);
于 2012-06-12T11:10:37.957 回答
8

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

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

于 2009-12-22T22:16:10.177 回答
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) 总是假设最后一个框架中存在错误。Dalvik 正在做它应该做的事情。它可能不是您期望的或您想要的,但它是如何工作的。"

于 2012-04-11T00:52:10.570 回答
5

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

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

于 2010-12-20T07:56:19.253 回答
5

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

  1. 尽可能使用应用程序上下文(而不是 activity.this)。
  2. 在活动的 onPause() 方法中停止并释放线程
  3. 在活动的 onDestroy() 方法中释放您的视图/回调
于 2012-05-07T15:12:55.717 回答
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错误不再发生。

于 2011-08-16T08:31:41.723 回答
3

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

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

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

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

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

于 2012-04-23T21:43:34.370 回答
1

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

于 2012-05-07T13:06:56.100 回答
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;
    }
}
于 2012-04-18T06:40:34.653 回答
0

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

ImageView ivBg = (ImageView) findViewById(R.id.main_backgroundImage);
ivBg.setImageDrawable(null);
ivBg.setImageDrawable(getResources().getDrawable(R.drawable.new_picture));
于 2013-05-20T23:22:45.887 回答
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();
    }

}
于 2013-11-10T04:51:06.910 回答