从外部存储加载位图时出现OutOfMemory异常

时间:2010-11-05 00:56:43

标签: android out-of-memory

在我的应用程序中,我从JPEG和PNG文件中加载了几张图像。当我将所有这些文件放入assets目录并以这种方式加载时,一切正常:

InputStream stream = getAssets().open(path);
Bitmap bitmap = BitmapFactory.decodeStream(stream, null, null);
stream.close();
return new BitmapDrawable(bitmap);

但是当我尝试从SD卡加载完全相同的图像时,我得到一个OutOfMemory异常!

InputStream stream = new FileInputStream("/mnt/sdcard/mydata/" + path);
Bitmap bitmap = BitmapFactory.decodeStream(stream, null, null);
stream.close();
return new BitmapDrawable(bitmap);

这是我在日志中得到的:

11-05 00:53:31.003: ERROR/dalvikvm-heap(13183): 827200-byte external allocation too large for this process.
11-05 00:53:31.003: ERROR/GraphicsJNI(13183): VM won't let us allocate 827200 bytes
...
11-05 00:53:31.053: ERROR/AndroidRuntime(13183): Caused by: java.lang.OutOfMemoryError: bitmap size exceeds VM budget
11-05 00:53:31.053: ERROR/AndroidRuntime(13183):     at android.graphics.BitmapFactory.nativeDecodeStream(Native Method)
...

为什么会发生这种情况?

更新:在真实设备上尝试了这两个 - 似乎我无法将超过12MB的位图加载到所谓的“外部存储器”中(这不是SD卡)。

13 个答案:

答案 0 :(得分:8)

我尝试了所有提到的方法here&在其他资源,但我得出的结论是,设置ImageView对null的引用将解决问题:

  public Bitmap getimage(String path ,ImageView iv)
   {
    //iv is passed to set it null to remove it from external memory
    iv=null;
    InputStream stream = new FileInputStream("/mnt/sdcard/mydata/" + path);
    Bitmap bitmap = BitmapFactory.decodeStream(stream, null, null);
    stream.close();
    stream=null;
    return bitmap;
    }

&安培;你完成了!

注意:虽然它可以解决上述问题,但我建议您检查Tom van Zummeren的优化图像加载。

并且还检查SoftReference:在VM将抛出OutOfMemoryError之前,保证指向软可访问对象的所有SoftReferences都被清除。

答案 1 :(得分:5)

  • 使用位图做很多事情时,不要调试应用程序 - 只需运行它。调试器会留下内存泄漏。
  • 位图非常昂贵。如果可能,请通过创建BitmapFactory.Options并将inSampleSize设置为> 1来降低负载。

编辑:此外,请务必检查您的应用是否有内存泄漏。泄漏位图(具有static位图是一种很好的方法)会很快耗尽你的可用内存。

答案 2 :(得分:4)

你的API使用可能没什么问题,我想我们所能做的就是推断使用AssetManager比从SD卡打开一个随机文件更少的幕后堆分配。

800KB在任何人的书中都是一个严肃的分配...这无疑是针对解压缩的图像像素。鉴于您知道图像的大小,它的深度是多少?如果它是32bpp,那么尝试使用inPreferredConfig覆盖它。

答案 3 :(得分:3)

这是一个相当普遍的问题,我们所有人在从SD卡加载图像时都会遇到这个问题。

我发现的解决方案是在使用inJustDecodeBounds加载图片时首先使用decodeFileDescriptor。这实际上不会解码图像,但会给出图像大小。现在我可以适当地缩放它(使用选项),以便调整显示区域的图像大小。它需要,因为手机上的低内存可以很容易地被你的500万像素接管。我认为这是最优雅的解决方案。

答案 4 :(得分:2)

这里有两个问题......

  • 位图内存不在VM堆中,而是在本机堆中 - 请参阅BitmapFactory OOM driving me nuts
  • 本机堆的垃圾收集比VM堆更懒 - 所以每次通过Activity的onPause或onDestroy
  • 时,你需要非常积极地做bitmap.recycle和bitmap = null

答案 5 :(得分:1)

为什么不使用getCacheDir()将图像移动到手机内部存储中的缓存中,或者使用临时目录存储图像,而不是直接从SD卡加载?

请参阅thisthis了解外部内存使用情况。此外,this article可能与您有关。

答案 6 :(得分:1)

使用以下代码,您将永远不会收到以下错误:java.lang.OutOfMemoryError:位图大小超过VM预算

              BitmapFactory.Options bounds = new BitmapFactory.Options();

              bounds.inSampleSize = 4;

              myBitmap = BitmapFactory.decodeFile(imgFile.getAbsolutePath(), bounds);

              picturesView.setImageBitmap(myBitmap);

答案 7 :(得分:1)

The best solution i found and edited according to my need

public static Bitmap getImageBitmap(String path) throws IOException{
        // Allocate files and objects outside of timingoops             
        File file = new File(thumbpath);        
        RandomAccessFile in = new RandomAccessFile(file, "rws");
        final FileChannel channel = in.getChannel();
        final int fileSize = (int)channel.size();
        final byte[] testBytes = new byte[fileSize];
        final ByteBuffer buff = ByteBuffer.allocate(fileSize);
        final byte[] buffArray = buff.array();
        @SuppressWarnings("unused")
        final int buffBase = buff.arrayOffset();

        // Read from channel into buffer, and batch read from buffer to byte array;
        long time1 = System.currentTimeMillis();
        channel.position(0);
        channel.read(buff);
        buff.flip();
        buff.get(testBytes);
        long time1 = System.currentTimeMillis();
        Bitmap bmp = Bitmap_process(buffArray);
        long time2 = System.currentTimeMillis();        
        System.out.println("Time taken to load: " + (time2 - time1) + "ms");

        return bmp;
    }

    public static Bitmap Bitmap_process(byte[] buffArray){
        BitmapFactory.Options options = new BitmapFactory.Options();

        options.inDither=false;                     //Disable Dithering mode
        options.inPurgeable=true;                   //Tell to gc that whether it needs free memory, the Bitmap can be cleared
        options.inInputShareable=true;              //Which kind of reference will be used to recover the Bitmap data after being clear, when it will be used in the future
        options.inTempStorage=new byte[32 * 1024];  //Allocate some temporal memory for decoding

        options.inSampleSize=1;

        Bitmap imageBitmap = BitmapFactory.decodeByteArray(buffArray, 0, buffArray.length, options);
        return imageBitmap;
    }

答案 8 :(得分:1)

感谢所有主题,我找到了一个适用于真实设备的解决方案。 这些技巧都是关于使用

BitmapFactory.Options opts=new BitmapFactory.Options();
opts.inSampleSize=(int)(target_size/bitmap_size); //if original bitmap is bigger

但对我来说这还不够。我的原始图片(取自相机应用程序)是3264x2448。我的正确比例为3,因为我想要一个1024x768的普通VGA图像。

但是将inSampleSize设置为3是不够的:仍然是内存不足异常。 所以最后我选择了迭代方法:我从计算出的正确大小开始,然后增加它,直到我停止出现OOM异常。 对我来说,这是4的样本。

// Decode with inSampleSize
BitmapFactory.Options o2 = new BitmapFactory.Options();
// o2.inSampleSize = scale;
float trueScale = o.outWidth / 1024;
o2.inPurgeable = true;
o2.inDither = false;
Bitmap b = null;
do {
     o2.inSampleSize = (int) trueScale;
     Log.d(TAG, "Scale is " + trueScale);
 try {
    b = BitmapFactory.decodeStream(new FileInputStream(f), null, o2);
    } catch (OutOfMemoryError e) {
        Log.e(TAG,"Error decoding image at sampling "+trueScale+", resampling.."+e);
        System.gc();
    try {
        Thread.sleep(50);
     } catch (InterruptedException e1) { 
         e1.printStackTrace();
     }
}
    trueScale += 1;
} while (b==null && trueScale < 10);
return b;

答案 9 :(得分:0)

尝试另一种方式......

Bitmap bmpOrignal = BitmapFactory.decodeFile("/sdcard/mydata/" + path");

答案 10 :(得分:0)

您不能依靠GC来回收您的位图内存。 在不需要时,您必须清楚地回收位图。

请参阅位图方法:

void recycle() 释放与此位图像素关联的内存,并将位图标记为“死”,这意味着如果调用getPixels()或setPixels(),它将抛出异常,并且不会绘制任何内容。

答案 11 :(得分:0)

我发现开发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()方法将refence传递给父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. 删除所有背景drawable上的回调
  2. 删除每个视图组中的子项

答案 12 :(得分:0)

允许inSampleSize调整最终读取图像的大小。 AssetFileDescriptor的getLength()允许获取文件大小。

您可以根据getLength()改变inSampleSize以防止这样的OutOfMemory:

private final int MAX_SIZE = 500000;

public Bitmap readBitmap(Uri selectedImage)
{
    Bitmap bm = null;
    AssetFileDescriptor fileDescriptor = null;
    try
    {
        fileDescriptor = this.getContentResolver().openAssetFileDescriptor(selectedImage,"r");
        long size = fileDescriptor.getLength();
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inSampleSize = (int) (size / MAX_SIZE);
        bm = BitmapFactory.decodeFileDescriptor(fileDescriptor.getFileDescriptor(), null, options);
    }
    catch (Exception e)
    {
        e.printStackTrace();
    }
    finally
    {
        try {
            if(fileDescriptor != null) fileDescriptor.close();
        } catch (IOException e) {}
    }
    return bm;
}