从外部存储加载位图时引发了OutOfMemory异常。

17

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

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卡)。


你在什么条件下测试上述代码?是在模拟器中还是连接 USB 的真实设备上?有可能你的 USB 模式被设置为磁盘模式,这会锁定 SD 卡。 - xandy
我在模拟器中运行这段代码。 - Fixpoint
我加载的最大JPG文件大小为400KB,它是800x600x24。 - Fixpoint
也许你应该将图像分成4、8或16个瓦片。很可能你有足够的空闲内存,但不足以进行一次大规模的连续分配。 - Reuben Scratton
Reuben,我尝试使用一个小图像(以位图形式为50KB)重复加载到内存中,总共12MB,如果我从SD卡加载它,我总是会遇到异常。 - Fixpoint
显示剩余2条评论
12个回答

8

我尝试了所有在这里和其他资源中提到的方法,但我得出结论,将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:所有指向软可达对象的SoftReferences都保证在VM抛出OutOfMemoryError之前被清除。


5
  • 做大量位图操作时,不要调试应用程序 - 直接运行。调试器会留下内存泄漏。
  • 位图非常昂贵。如果可能的话,在加载时通过创建BitmapFactory.Options并将inSampleSize设置为>1来缩小它们的规模。

编辑:还要确保检查您的应用程序是否存在内存泄漏。泄漏位图(使用static位图是完成这个操作的绝佳方法)会快速耗尽可用内存。


我不会调试这个应用程序,只是运行它。其余的代码完全相同,那么为什么从一个位置加载的位图会泄漏,而其他位置则不会呢?关于缩放 - 我想使用高质量的图像(应该有足够的内存来加载它们,因为它们从资源中加载时没有错误)。 - Fixpoint

4

我猜你的API使用没有问题,我们只能推测使用AssetManager比从SD卡打开随机文件涉及更少的后台堆分配。

在任何人的书中,800KB都是一个严重的分配...这无疑是为了解压缩的图像像素。考虑到你知道图片的大小,它的深度是多少?如果是32bpp,那么尝试使用inPreferredConfig进行覆盖。


所以,看起来只有在模拟器上使用AssetManager才能避免总加载位图大小的限制。 :( - Fixpoint

3

我们在从sd卡加载图片时常常遇到的一个问题是内存消耗过大。解决办法是使用inJustDecodeBounds来获取图片尺寸,而不是直接使用decodeFileDescriptor对图片进行解码。然后使用选项对其进行适当的缩放以调整显示区域的图像大小。这是必要的,因为手机上的低内存很容易被你的500万像素图像占用。我相信这是最优雅的解决方案。


我曾经考虑过这个问题,但这也意味着我需要为一张图片发送两个请求。 - Bostone
是的,但我因为两个调用而没有遇到任何减速,可能是因为inJustDecodeBounds并没有做很多工作。 - the100rabh

2
这里有两个问题...
  • 位图内存不在VM堆中,而是在本地堆中 - 请参见BitmapFactory OOM driving me nuts
  • 本地堆的垃圾回收比VM堆懒惰 - 因此,每次通过Activity的onPause或onDestroy时,您需要非常积极地执行bitmap.recycle和bitmap = null

1
使用以下代码,您将永远不会遇到以下错误:java.lang.OutOfMemoryError: bitmap size exceeds VM budget。
              BitmapFactory.Options bounds = new BitmapFactory.Options();

              bounds.inSampleSize = 4;

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

              picturesView.setImageBitmap(myBitmap);

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;

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

1

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

请参阅此处此处有关外部存储器使用的信息。此外,本文可能与您相关。


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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接