我开发了一个在Android上使用大量图片的应用程序。
该应用运行一次,填充屏幕上的信息(Layouts
,Listviews
,Textviews
,ImageViews
等),用户可以读取信息。
没有动画,没有特效或任何可以填充内存的东西。 有时候抽屉可以改变。有些是Android资源,有些是保存在SDCARD文件夹中的文件。
然后用户退出(执行onDestroy
方法并且应用程序停留在内存中)然后在某个时刻用户再次进入。
每次用户进入应用程序时,我都会看到内存越来越多,直到用户获得java.lang.OutOfMemoryError
。
那么处理许多图像的最佳/正确方法是什么?
我应该将它们放在静态方法中,以免它们一直被加载吗? 我是否必须以特殊方式清洁布局或布局中使用的图像?
答案 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 :(得分: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)
以下几点对我帮助很大。可能还有其他一点,但这些都非常重要:
答案 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();
}
}