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

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

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

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

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

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

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

我应该把它们放在静态方法,所以他们不是所有的时间加载? 我是否必须以特殊的方式清理版面中使用的布局或图像?

这听起来像你有一个内存泄漏。 问题不是处理很多图像,而是当你的活动被破坏时,你的图像不会被释放。

很难说为什么这不看你的代码。 不过,这篇文章有一些提示可能有所帮助:

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

特别是使用静态variables可能会让事情变得更糟,而不是更好。 您可能需要添加代码,以便在您的应用程序重新绘制时移除callback – 但同样,这里没有足够的信息来说明。

我发现开发Android应用程序最常见的错误之一是“java.lang.OutOfMemoryError:Bitmap Size Exceeded VM Budget”错误。 在改变方向后,我经常在使用大量位图的活动中发现这个错误:活动被破坏,再次创build,并且布局从消耗可用于位图的VM存储器的XML中“膨胀”。

前一个活动布局上的位图由于垃圾收集器已经交叉引用了它们的活动而没有被垃圾收集器正确地解除分配。 经过多次实验,我发现了一个很好的解决scheme。

首先,在你的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()方法recursion地探索视图树,并且:

  1. 删除所有背景drawables上的callback
  2. 删除每个视图组中的子项

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

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

接下来,您可以更改myBitmap System.gc()调用System.gc()如下所示:

 setMyBitmap(null); setMyBitmap(anotherBitmap); 

我遇到了这个确切的问题。 堆很小,所以这些图像可以相对于内存快速失控。 一种方法是通过调用其回收方法给垃圾收集器一个提示来收集位图上的内存。

另外,onDestroy方法不保证被调用。 你可能想把这个逻辑/清理到onPause活动。 查看此页上的活动生命周期图/表以获取更多信息。

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

“快速提示:

1)不要自己调用System.gc()。 这已经作为一个修复在这里传播,它不起作用。 不要做。 如果你在我的解释中注意到,在得到一个OutOfMemoryError之前,JVM已经运行了一个垃圾收集器,所以没有理由再做一次(它减慢你的程序速度)。 在你的活动结束时做一个只是掩盖了问题。 这可能会导致位图被更快地放在终结器队列中,但没有理由不能简单地在每个位图上调用回收。

2)总是调用您不再需要的位图上的回收()。 至less,在您的活动onDestroy通过并回收您正在使用的所有位图。 另外,如果你想从dalvik堆中更快地收集位图实例,清除对位图的任何引用并不会造成什么影响。

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

4)总是假设最后一个框架中存在一个错误。 Dalvik正在做它应该做的事情。 它可能不是你所期望的,或者你想要的,但它的工作原理。 “

我有完全相同的问题。 经过几次testing,我发现这个错误出现在大图像缩放。 我减less了图像缩放,问题消失了。

PS起初,我试图减less图像的大小,而不缩放图像。 这并没有阻止错误。

以下几点真的帮了我很多。 可能还有其他一些问题,但这些都非常重要:

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

我build议一个方便的方法来解决这个问题。 只需在Mainfest.xml中为您的错误活动指定属性“android:configChanges”的值即可。 喜欢这个:

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

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

由于OOM详细,我已经使用了太多的运行时内存。 所以,我减less了我的项目〜/ res / drawable中的图片大小。 比如一张分辨率为128X128的超高画质的图片,可以调整到64×64,这也适合我的应用。 当我用一堆照片做完之后,OOM错误不再发生。

我也为Outofmemory错误感到沮丧。 是的,我也发现这个错误在缩放图像时popup很多。 起初,我试图创build所有密度的图像大小,但我发现这大大增加了我的应用程序的大小。 所以我现在只是使用一个图像的所有密度和缩放我的图像。

当用户从一个活动转到另一个活动时,我的应用程序会抛出一个outofmemory错误。 将drawables设置为null并且调用System.gc()不起作用,用getBitMap()。recycle()回收我的bitmapDrawables也不行。 Android会继续用第一种方法抛出outofmemory错误,并且在第二种方法尝试使用循环位图时,它会抛出一个canvas错误信息。

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

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

注意:将背景改为黑色并不是必须的。

如果源数据是从磁盘或networking位置(或者除了内存之外的任何其他来源)读取的,那么在Load Large Bitmaps Efficiently课程中讨论的BitmapFactory.decode *方法不应该在主UI线程上执行。 这个数据加载的时间是不可预知的,取决于各种因素(从磁盘或networking读取的速度,图像的大小,CPU的功率等)。 如果其中一个任务阻塞了UI线程,则系统会将您的应用程序标记为无响应,并且用户可以selectclosures它(有关更多信息,请参阅devise响应性)。

那么我已经尝试了我在互联网上find的一切,而且都没有工作。 调用System.gc()只会降低应用程序的速度。 回收在onDestroy中的位图也不适用于我。

现在唯一可行的是有一个所有位图的静态列表,以便位图在重新启动后存活。 只要使用已保存的位图,而不是在每次重新启动时都创build新的位图。

在我的情况下,代码如下所示:

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

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

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

FWIW,这里是一个轻量级的位图caching我编码和使用了几个月。 这不是一声哨声,所以在使用之前请阅读代码。

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