Android 将图像加载到位图对象时出现奇怪的内存不足问题

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/477572/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-20 02:12:35  来源:igfitidea点击:

Strange out of memory issue while loading an image to a Bitmap object

androidimagebitmapout-of-memoryandroid-bitmap

提问by Chrispix

I have a list view with a couple of image buttons on each row. When you click the list row, it launches a new activity. I have had to build my own tabs because of an issue with the camera layout. The activity that gets launched for the result is a map. If I click on my button to launch the image preview (load an image off the SD card) the application returns from the activity back to the listviewactivity to the result handler to relaunch my new activity which is nothing more than an image widget.

我有一个列表视图,每行有几个图像按钮。当您单击列表行时,它会启动一个新活动。由于相机布局问题,我不得不构建自己的选项卡。为结果启动的活动是地图。如果我点击我的按钮启动图像预览(从 SD 卡加载图像),应用程序从活动返回到listview活动到结果处理程序以重新启动我的新活动,它只不过是一个图像小部件。

The image preview on the list view is being done with the cursor and ListAdapter. This makes it pretty simple, but I am not sure how I can put a resized image (I.e. Smaller bit size not pixel as the srcfor the image button on the fly. So I just resized the image that came off the phone camera.

列表视图上的图像预览正在使用光标和 完成ListAdapter。这使它变得非常简单,但我不确定如何放置调整大小的图像(即较小的位大小而不是像素作为动态src图像按钮。所以我只是调整了从手机摄像头中取出的图像的大小。

The issue is that I get an out of memory error when it tries to go back and re-launch the 2nd activity.

问题是当它尝试返回并重新启动第二个活动时出现内存不足错误。

  • Is there a way I can build the list adapter easily row by row, where I can resize on the fly (bit wise)?
  • 有没有一种方法可以轻松地逐行构建列表适配器,我可以在其中即时调整大小(有点明智)?

This would be preferable as I also need to make some changes to the properties of the widgets/elements in each row as I am unable to select a row with the touch screen because of the focus issue. (I can use roller ball.)

这将是可取的,因为我还需要对每一行中的小部件/元素的属性进行一些更改,因为由于焦点问题,我无法使用触摸屏选择一行。(我可以使用滚球。

  • I know I can do an out of band resize and save of my image, but that is not really what I want to do, but some sample code for that would be nice.
  • 我知道我可以进行带外调整大小并保存我的图像,但这并不是我真正想要做的,但是一些示例代码会很好。

As soon as I disabled the image on the list view it worked fine again.

一旦我禁用了列表视图上的图像,它又可以正常工作了。

FYI: This is how I was doing it:

仅供参考:这就是我的做法:

String[] from = new String[] { DBHelper.KEY_BUSINESSNAME,DBHelper.KEY_ADDRESS,DBHelper.KEY_CITY,DBHelper.KEY_GPSLONG,DBHelper.KEY_GPSLAT,DBHelper.KEY_IMAGEFILENAME  + ""};
int[] to = new int[] {R.id.businessname,R.id.address,R.id.city,R.id.gpslong,R.id.gpslat,R.id.imagefilename };
notes = new SimpleCursorAdapter(this, R.layout.notes_row, c, from, to);
setListAdapter(notes);

Where R.id.imagefilenameis a ButtonImage.

哪里R.id.imagefilenameButtonImage.

Here is my LogCat:

这是我的 LogCat:

01-25 05:05:49.877: ERROR/dalvikvm-heap(3896): 6291456-byte external allocation too large for this process.
01-25 05:05:49.877: ERROR/(3896): VM wont let us allocate 6291456 bytes
01-25 05:05:49.877: ERROR/AndroidRuntime(3896): Uncaught handler: thread main exiting due to uncaught exception
01-25 05:05:49.917: ERROR/AndroidRuntime(3896): java.lang.OutOfMemoryError: bitmap size exceeds VM budget
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.graphics.BitmapFactory.nativeDecodeStream(Native Method)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.graphics.BitmapFactory.decodeStream(BitmapFactory.java:304)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.graphics.BitmapFactory.decodeFile(BitmapFactory.java:149)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.graphics.BitmapFactory.decodeFile(BitmapFactory.java:174)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.graphics.drawable.Drawable.createFromPath(Drawable.java:729)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.ImageView.resolveUri(ImageView.java:484)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.ImageView.setImageURI(ImageView.java:281)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.SimpleCursorAdapter.setViewImage(SimpleCursorAdapter.java:183)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.SimpleCursorAdapter.bindView(SimpleCursorAdapter.java:129)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.CursorAdapter.getView(CursorAdapter.java:150)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.AbsListView.obtainView(AbsListView.java:1057)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.ListView.makeAndAddView(ListView.java:1616)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.ListView.fillSpecific(ListView.java:1177)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.ListView.layoutChildren(ListView.java:1454)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.AbsListView.onLayout(AbsListView.java:937)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.view.View.layout(View.java:5611)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1119)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.LinearLayout.layoutHorizontal(LinearLayout.java:1108)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.LinearLayout.onLayout(LinearLayout.java:922)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.view.View.layout(View.java:5611)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.FrameLayout.onLayout(FrameLayout.java:294)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.view.View.layout(View.java:5611)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1119)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.LinearLayout.layoutVertical(LinearLayout.java:999)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.LinearLayout.onLayout(LinearLayout.java:920)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.view.View.layout(View.java:5611)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.widget.FrameLayout.onLayout(FrameLayout.java:294)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.view.View.layout(View.java:5611)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.view.ViewRoot.performTraversals(ViewRoot.java:771)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.view.ViewRoot.handleMessage(ViewRoot.java:1103)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.os.Handler.dispatchMessage(Handler.java:88)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.os.Looper.loop(Looper.java:123)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at android.app.ActivityThread.main(ActivityThread.java:3742)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at java.lang.reflect.Method.invokeNative(Native Method)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at java.lang.reflect.Method.invoke(Method.java:515)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:739)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:497)
01-25 05:05:49.917: ERROR/AndroidRuntime(3896):     at dalvik.system.NativeStart.main(Native Method)
01-25 05:10:01.127: ERROR/AndroidRuntime(3943): ERROR: thread attach failed 

I also have a new error when displaying an image:

显示图像时我也有一个新错误:

01-25 22:13:18.594: DEBUG/skia(4204): xxxxxxxxxxx jpeg error 20 Improper call to JPEG library in state %d
01-25 22:13:18.604: INFO/System.out(4204): resolveUri failed on bad bitmap uri: 
01-25 22:13:18.694: ERROR/dalvikvm-heap(4204): 6291456-byte external allocation too large for this process.
01-25 22:13:18.694: ERROR/(4204): VM won't let us allocate 6291456 bytes
01-25 22:13:18.694: DEBUG/skia(4204): xxxxxxxxxxxxxxxxxxxx allocPixelRef failed

采纳答案by AdamK

The Android Trainingclass, "Displaying Bitmaps Efficiently", offers some great information for understanding and dealing with the exception java.lang.OutOfMemoryError: bitmap size exceeds VM budgetwhen loading Bitmaps.

Android的训练课,“显示位图高效”,提供了认识和处理异常一些伟大的信息java.lang.OutOfMemoryError: bitmap size exceeds VM budget加载位图时。



Read Bitmap Dimensions and Type

读取位图尺寸和类型

The BitmapFactoryclass provides several decoding methods (decodeByteArray(), decodeFile(), decodeResource(), etc.) for creating a Bitmapfrom various sources. Choose the most appropriate decode method based on your image data source. These methods attempt to allocate memory for the constructed bitmap and therefore can easily result in an OutOfMemoryexception. Each type of decode method has additional signatures that let you specify decoding options via the BitmapFactory.Optionsclass. Setting the inJustDecodeBoundsproperty to truewhile decoding avoids memory allocation, returning nullfor the bitmap object but setting outWidth, outHeightand outMimeType. This technique allows you to read the dimensions and type of the image data prior to construction (and memory allocation) of the bitmap.

BitmapFactory类提供了几种解码方法(decodeByteArray()decodeFile()decodeResource(),等等),用于创建Bitmap来自各种来源。根据您的图像数据源选择最合适的解码方法。这些方法尝试为构造的位图分配内存,因此很容易导致OutOfMemory异常。每种类型的解码方法都有额外的签名,让您可以通过BitmapFactory.Options类指定解码选项。将inJustDecodeBounds属性设置为truewhile 解码可避免内存分配,返回null位图对象但设置outWidth,outHeightoutMimeType。此技术允许您在位图的构造(和内存分配)之前读取图像数据的尺寸和类型。

BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), R.id.myimage, options);
int imageHeight = options.outHeight;
int imageWidth = options.outWidth;
String imageType = options.outMimeType;

To avoid java.lang.OutOfMemoryexceptions, check the dimensions of a bitmap before decoding it, unless you absolutely trust the source to provide you with predictably sized image data that comfortably fits within the available memory.

为避免出现java.lang.OutOfMemory异常,请在解码之前检查位图的尺寸,除非您绝对相信该源会为您提供大小可预测且适合可用内存的图像数据。



Load a scaled down version into Memory

将缩小版本加载到内存中

Now that the image dimensions are known, they can be used to decide if the full image should be loaded into memory or if a subsampled version should be loaded instead. Here are some factors to consider:

现在已知图像尺寸,它们可用于决定是否应将完整图像加载到内存中,或者是否应加载子采样版本。以下是一些需要考虑的因素:

  • Estimated memory usage of loading the full image in memory.
  • The amount of memory you are willing to commit to loading this image given any other memory requirements of your application.
  • Dimensions of the target ImageView or UI component that the image is to be loaded into.
  • Screen size and density of the current device.
  • 在内存中加载完整图像的估计内存使用量。
  • 考虑到您的应用程序的任何其他内存要求,您愿意承诺加载此图像的内存量。
  • 要加载图像的目标 ImageView 或 UI 组件的尺寸。
  • 当前设备的屏幕大小和密度。

For example, it's not worth loading a 1024x768 pixel image into memory if it will eventually be displayed in a 128x96 pixel thumbnail in an ImageView.

例如,如果一个 1024x768 像素的图像最终会显示在ImageView.

To tell the decoder to subsample the image, loading a smaller version into memory, set inSampleSizeto truein your BitmapFactory.Optionsobject. For example, an image with resolution 2048x1536 that is decoded with an inSampleSizeof 4 produces a bitmap of approximately 512x384. Loading this into memory uses 0.75MB rather than 12MB for the full image (assuming a bitmap configuration of ARGB_8888). Here's a method to calculate a sample size value that is a power of two based on a target width and height:

告诉解码器对图像进行二次采样,将较小的版本加载到内存中,在您的对象中设置inSampleSize为。例如,分辨率为 2048x1536 的图像使用4 进行解码会生成大约 512x384 的位图。将其加载到内存中使用 0.75MB 而不是完整图像的 12MB(假设位图配置为)。这是一种基于目标宽度和高度计算样本大小值的方法,该值是 2 的幂:trueBitmapFactory.OptionsinSampleSizeARGB_8888

public static int calculateInSampleSize(
        BitmapFactory.Options options, int reqWidth, int reqHeight) {
    // Raw height and width of image
    final int height = options.outHeight;
    final int width = options.outWidth;
    int inSampleSize = 1;

    if (height > reqHeight || width > reqWidth) {

        final int halfHeight = height / 2;
        final int halfWidth = width / 2;

        // Calculate the largest inSampleSize value that is a power of 2 and keeps both
        // height and width larger than the requested height and width.
        while ((halfHeight / inSampleSize) > reqHeight
                && (halfWidth / inSampleSize) > reqWidth) {
            inSampleSize *= 2;
        }
    }

    return inSampleSize;
}

Note: A power of two value is calculated because the decoder uses a final value by rounding down to the nearest power of two, as per the inSampleSizedocumentation.

注意:计算二值的幂是因为解码器通过四舍五入到最接近的二的幂来使用最终值,根据 inSampleSize文档。

To use this method, first decode with inJustDecodeBoundsset to true, pass the options through and then decode again using the new inSampleSizevalue and inJustDecodeBoundsset to false:

要使用此方法,首先使用inJustDecodeBoundsset to 进行解码true,传递选项,然后使用新inSampleSize值再次解码并inJustDecodeBounds设置为false

public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
    int reqWidth, int reqHeight) {

    // First decode with inJustDecodeBounds=true to check dimensions
    final BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeResource(res, resId, options);

    // Calculate inSampleSize
    options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

    // Decode bitmap with inSampleSize set
    options.inJustDecodeBounds = false;
    return BitmapFactory.decodeResource(res, resId, options);
}

This method makes it easy to load a bitmap of arbitrarily large size into an ImageViewthat displays a 100x100 pixel thumbnail, as shown in the following example code:

这种方法可以很容易地将任意大尺寸的位图加载到ImageView显示 100x100 像素缩略图的图像中,如以下示例代码所示:

mImageView.setImageBitmap(
    decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));

You can follow a similar process to decode bitmaps from other sources, by substituting the appropriate BitmapFactory.decode*method as needed.

通过BitmapFactory.decode*根据需要替换适当的方法,您可以按照类似的过程对来自其他来源的位图进行解码。

回答by Fedor

To fix the OutOfMemory error, you should do something like this:

要修复 OutOfMemory 错误,您应该执行以下操作:

BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 8;
Bitmap preview_bitmap = BitmapFactory.decodeStream(is, null, options);

This inSampleSizeoption reduces memory consumption.

inSampleSize选项可减少内存消耗。

Here's a complete method. First it reads image size without decoding the content itself. Then it finds the best inSampleSizevalue, it should be a power of 2, and finally the image is decoded.

这里有一个完整的方法。首先它读取图像大小而不解码内容本身。然后找到最好的inSampleSize值,应该是2的幂,最后对图像进行解码。

// Decodes image and scales it to reduce memory consumption
private Bitmap decodeFile(File f) {
    try {
        // Decode image size
        BitmapFactory.Options o = new BitmapFactory.Options();
        o.inJustDecodeBounds = true;
        BitmapFactory.decodeStream(new FileInputStream(f), null, o);

        // The new size we want to scale to
        final int REQUIRED_SIZE=70;

        // Find the correct scale value. It should be the power of 2.
        int scale = 1;
        while(o.outWidth / scale / 2 >= REQUIRED_SIZE && 
              o.outHeight / scale / 2 >= REQUIRED_SIZE) {
            scale *= 2;
        }

        // Decode with inSampleSize
        BitmapFactory.Options o2 = new BitmapFactory.Options();
        o2.inSampleSize = scale;
        return BitmapFactory.decodeStream(new FileInputStream(f), null, o2);
    } catch (FileNotFoundException e) {}
    return null;
}

回答by Thomas Vervest

I've made a small improvement to Fedor's code. It basically does the same, but without the (in my opinion) ugly while loop and it always results in a power of two. Kudos to Fedor for making the original solution, I was stuck until I found his, and then I was able to make this one :)

我对 Fedor 的代码做了一个小小的改进。它基本上是一样的,但没有(在我看来)丑陋的 while 循环,它总是导致 2 的幂。感谢 Fedor 制作了原始解决方案,我一直被困住,直到找到他的解决方案,然后我才能做出这个 :)

 private Bitmap decodeFile(File f){
    Bitmap b = null;

        //Decode image size
    BitmapFactory.Options o = new BitmapFactory.Options();
    o.inJustDecodeBounds = true;

    FileInputStream fis = new FileInputStream(f);
    BitmapFactory.decodeStream(fis, null, o);
    fis.close();

    int scale = 1;
    if (o.outHeight > IMAGE_MAX_SIZE || o.outWidth > IMAGE_MAX_SIZE) {
        scale = (int)Math.pow(2, (int) Math.ceil(Math.log(IMAGE_MAX_SIZE / 
           (double) Math.max(o.outHeight, o.outWidth)) / Math.log(0.5)));
    }

    //Decode with inSampleSize
    BitmapFactory.Options o2 = new BitmapFactory.Options();
    o2.inSampleSize = scale;
    fis = new FileInputStream(f);
    b = BitmapFactory.decodeStream(fis, null, o2);
    fis.close();

    return b;
}

回答by Ephraim

I come from iOS experience and I was frustrated to discover an issue with something so basic as loading and showing an image. After all, everyone that is having this issue is trying to display reasonably sized images. Anyway, here are the two changes that fixed my problem (and made my app very responsive).

我来自 iOS 经验,我很沮丧地发现加载和显示图像等基本问题。毕竟,遇到此问题的每个人都在尝试显示合理大小的图像。无论如何,这是解决我的问题的两个更改(并使我的应用程序响应迅速)。

1) Every time you do BitmapFactory.decodeXYZ(), make sure to pass in a BitmapFactory.Optionswith inPurgeableset to true(and preferably with inInputShareablealso set to true).

1) 每次执行时BitmapFactory.decodeXYZ(),请确保传入BitmapFactory.Optionswith inPurgeableset to true(最好inInputShareable也设置为true)。

2) NEVER use Bitmap.createBitmap(width, height, Config.ARGB_8888). I mean NEVER! I've never had that thing not raise memory error after few passes. No amount of recycle(), System.gc(), whatever helped. It always raised exception. The one other way that actually works is to have a dummy image in your drawables (or another Bitmap that you decoded using step 1 above), rescale that to whatever you want, then manipulate the resulting Bitmap (such as passing it on to a Canvas for more fun). So, what you should use instead is: Bitmap.createScaledBitmap(srcBitmap, width, height, false). If for whatever reason you MUST use the brute force create method, then at least pass Config.ARGB_4444.

2) 永远不要使用Bitmap.createBitmap(width, height, Config.ARGB_8888). 我的意思是从不!我从来没有遇到过几次后不会引发内存错误的事情。再多recycle()System.gc(),任何帮助。它总是引发异常。另一种实际可行的方法是在您的可绘制对象(或您使用上述步骤 1 解码的另一个位图)中放置一个虚拟图像,将其重新缩放为您想要的任何内容,然后操作生成的位图(例如将其传递给 Canvas以获得更多乐趣)。所以,你应该使用的是:Bitmap.createScaledBitmap(srcBitmap, width, height, false). 如果出于某种原因您必须使用蛮力创建方法,那么至少通过Config.ARGB_4444.

This is almost guaranteed to save you hours if not days. All that talk about scaling the image, etc. does not really work (unless you consider getting wrong size or degraded image a solution).

这几乎可以保证为您节省数小时甚至数天。所有关于缩放图像等的讨论都不起作用(除非您考虑获得错误的尺寸或降低图像的解决方案)。

回答by Anto Binish Kaspar

It's a known bug, it's not because of large files. Since Android Caches the Drawables, it's going out of memory after using few images. But I've found an alternate way for it, by skipping the android default cache system.

这是一个已知的错误,不是因为大文件。由于 Android 缓存 Drawables,它在使用少量图像后内存不足。但是我找到了另一种方法,跳过android默认缓存系统。

Solution: Move the images to "assets" folder and use the following function to get BitmapDrawable:

解决方案:将图像移动到“assets”文件夹并使用以下函数获取 BitmapDrawable:

public static Drawable getAssetImage(Context context, String filename) throws IOException {
    AssetManager assets = context.getResources().getAssets();
    InputStream buffer = new BufferedInputStream((assets.open("drawable/" + filename + ".png")));
    Bitmap bitmap = BitmapFactory.decodeStream(buffer);
    return new BitmapDrawable(context.getResources(), bitmap);
}

回答by Fraggle

I had this same issue and solved it by avoiding the BitmapFactory.decodeStream or decodeFile functions and instead used BitmapFactory.decodeFileDescriptor

我遇到了同样的问题,并通过避免使用 BitmapFactory.decodeStream 或 decodeFile 函数来解决它,而是使用 BitmapFactory.decodeFileDescriptor

decodeFileDescriptorlooks like it calls different native methods than the decodeStream/decodeFile.

decodeFileDescriptor看起来它调用的本地方法与 decodeStream/decodeFile 不同。

Anyways, what worked was this (note that I added some options as some had above, but that's not what made the difference. What is critical is the call to BitmapFactory.decodeFileDescriptorinstead of decodeStreamor decodeFile):

无论如何,这是有效的(请注意,我添加了一些上面的选项,但这并不是区别所在。关键是调用BitmapFactory.decodeFileDescriptor而不是decodeStreamdecodeFile):

private void showImage(String path)   {

    Log.i("showImage","loading:"+path);
    BitmapFactory.Options bfOptions=new BitmapFactory.Options();
    bfOptions.inDither=false;                     //Disable Dithering mode
    bfOptions.inPurgeable=true;                   //Tell to gc that whether it needs free memory, the Bitmap can be cleared
    bfOptions.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
    bfOptions.inTempStorage=new byte[32 * 1024]; 

    File file=new File(path);
    FileInputStream fs=null;
    try {
        fs = new FileInputStream(file);
    } catch (FileNotFoundException e) {
        //TODO do something intelligent
        e.printStackTrace();
    }

    try {
        if(fs!=null) bm=BitmapFactory.decodeFileDescriptor(fs.getFD(), null, bfOptions);
    } catch (IOException e) {
        //TODO do something intelligent
        e.printStackTrace();
    } finally{ 
        if(fs!=null) {
            try {
                fs.close();
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }
    //bm=BitmapFactory.decodeFile(path, bfOptions); This one causes error: java.lang.OutOfMemoryError: bitmap size exceeds VM budget

    im.setImageBitmap(bm);
    //bm.recycle();
    bm=null;                        
}

I think there is a problem with the native function used in decodeStream/decodeFile. I have confirmed that a different native method is called when using decodeFileDescriptor. Also what I've read is "that Images (Bitmaps) are not allocated in a standard Java way but via native calls; the allocations are done outside of the virtual heap, but are counted against it!"

我认为 decodeStream/decodeFile 中使用的本机函数存在问题。我已经确认在使用 decodeFileDescriptor 时调用了不同的本机方法。另外我读到的是“图像(位图)不是以标准 Java 方式分配的,而是通过本机调用分配的;分配是在虚拟堆之外完成的,但会被 计算在内!

回答by coocood

I think best way to avoid the OutOfMemoryErroris to face it and understand it.

我认为避免这种OutOfMemoryError情况的最好方法是面对它并理解它。

I made an appto intentionally cause OutOfMemoryError, and monitor memory usage.

我制作了一个应用程序来故意引起OutOfMemoryError和监视内存使用情况。

After I've done a lot of experiments with this App, I've got the following conclusions:

经过我对这款App的大量实验,得出以下结论:

I'm gonna talk about SDK versions before Honey Comb first.

我先说一下 Honey Comb 之前的 SDK 版本。

  1. Bitmap is stored in native heap, but it will get garbage collected automatically, calling recycle() is needless.

  2. If {VM heap size} + {allocated native heap memory} >= {VM heap size limit for the device}, and you are trying to create bitmap, OOM will be thrown.

    NOTICE: VM HEAP SIZE is counted rather than VM ALLOCATED MEMORY.

  3. VM Heap size will never shrink after grown, even if the allocated VM memory is shrinked.

  4. So you have to keep the peak VM memory as low as possible to keep VM Heap Size from growing too big to save available memory for Bitmaps.

  5. Manually call System.gc() is meaningless, the system will call it first before trying to grow the heap size.

  6. Native Heap Size will never shrink too, but it's not counted for OOM, so no need to worry about it.

  1. Bitmap 存储在本地堆中,但它会自动进行垃圾回收,无需调用 recycle()。

  2. 如果 {VM heap size} + {allocated native heap memory} >= {VM heap size limit for the device},并且您正在尝试创建位图,则会抛出 OOM。

    注意:计算 VM HEAP SIZE 而不是 VM ALLOCATED MEMORY。

  3. 虚拟机堆大小在增长后永远不会缩小,即使分配的虚拟机内存缩小。

  4. 因此,您必须将峰值 VM 内存保持在尽可能低的水平,以防止 VM 堆大小变得太大而无法为位图节省可用内存。

  5. 手动调用 System.gc() 是没有意义的,系统会在尝试增加堆大小之前先调用它。

  6. Native Heap Size 也永远不会缩小,但它不计入 OOM,因此无需担心。

Then, let's talk about SDK Starts from Honey Comb.

然后,让我们谈谈 SDK 从 Honey Comb 开始。

  1. Bitmap is stored in VM heap, Native memory is not counted for OOM.

  2. The condition for OOM is much simpler: {VM heap size} >= {VM heap size limit for the device}.

  3. So you have more available memory to create bitmap with the same heap size limit, OOM is less likely to be thrown.

  1. 位图存储在 VM 堆中,本机内存不计入 OOM。

  2. OOM 的条件要简单得多:{VM heap size} >= {VM heap size limit for the device}。

  3. 所以你有更多的可用内存来创建具有相同堆大小限制的位图,OOM 不太可能被抛出。

Here is some of my observations about Garbage Collection and Memory Leak.

以下是我对垃圾收集和内存泄漏的一些观察。

You can see it yourself in the App. If an Activity executed an AsyncTask that was still running after the Activity was destroyed, the Activity will not get garbage collected until the AsyncTask finish.

您可以在 App 中自行查看。如果 Activity 执行了一个在 Activity 被销毁后仍在运行的 AsyncTask,则在 AsyncTask 完成之前,Activity 不会被垃圾回收。

This is because AsyncTask is an instance of an anonymous inner class, it holds a reference of the Activity.

这是因为 AsyncTask 是一个匿名内部类的实例,它持有 Activity 的引用。

Calling AsyncTask.cancel(true) will not stop the execution if the task is blocked in an IO operation in background thread.

如果任务在后台线程的 IO 操作中被阻塞,则调用 AsyncTask.cancel(true) 不会停止执行。

Callbacks are anonymous inner classes too, so if a static instance in your project holds them and do not release them, memory would be leaked.

回调也是匿名内部类,因此如果项目中的静态实例持有它们而不释放它们,则会泄漏内存。

If you scheduled a repeating or delayed task, for example a Timer, and you do not call cancel() and purge() in onPause(), memory would be leaked.

如果您安排了一个重复或延迟的任务,例如 Timer,并且您没有在 onPause() 中调用 cancel() 和 purge(),则会泄漏内存。

回答by Wroclai

I have seen a lot of questions about OOM exceptions and caching lately. The developer guide has a really good articleon this, but some tends to fail on implementing it in a suitable way.

最近看到很多关于OOM异常和缓存的问题。开发人员指南对此有一篇非常好的文章,但有些人往往无法以合适的方式实现它。

Because of this I wrote an example application that demonstrates caching in an Android environment. This implementation has not yet gotten an OOM.

因此,我编写了一个示例应用程序来演示 Android 环境中的缓存。这个实现还没有出现 OOM。

Look at the end of this answer for a link to the source code.

查看此答案的末尾以获取源代码的链接。

Requirements:

要求:

  • Android API 2.1 or higher (I simply could not manage to get the available memory for an application in API 1.6 - that is the only piece of code that doesn't work in API 1.6)
  • Android support package
  • Android API 2.1 或更高版本(我根本无法为 API 1.6 中的应用程序获取可用内存 - 这是唯一一段在 API 1.6 中不起作用的代码)
  • 安卓支持包

Screenshot

截屏

Features:

特征:

  • Retains the cache if there is an orientation change, using a singleton
  • Use one eighthof the assigned application memory to the cache (modify if you want)
  • Large bitmaps gets scaled(you can define the maximum pixels that you want to allow)
  • Controls that there is an internet connection availablebefore downloading the bitmaps
  • Makes sure that you are only instantiating one taskper row
  • If you are flingingthe ListViewaway, it simply won't download the bitmaps between
  • 如果方向发生变化,则使用单例保留缓存
  • 使用八分之一指定的应用程序内存来缓存(如果你想修改)
  • 大位图被缩放(你可以定义你想要允许的最大像素)
  • 在下载位图之前控制是否有可用的互联网连接
  • 确保每行只实例化一个任务
  • 如果你丢ListView了,它根本不会下载的位图

This does not include:

这不包括:

  • Disk caching. This should be easy to implement anyway - just point to a different task that grabs the bitmaps from the disk
  • 磁盘缓存。无论如何,这应该很容易实现 - 只需指向从磁盘获取位图的不同任务

Sample code:

示例代码:

The images that are being downloaded are images (75x75) from Flickr. However, put whatever image urls you want to be processed, and the application will scale it down if it exceeds the maximum. In this application the urls are simply in a Stringarray.

正在下载的图像是来自 Flickr 的图像 (75x75)。但是,将您要处理的任何图像 url 放入,如果超过最大值,应用程序将缩小它。在这个应用程序中,url 只是在一个String数组中。

The LruCachehas a good way to deal with bitmaps. However, in this application I put an instance of an LruCacheinside another cache class that I created in order to get the application more feasible.

LruCache有一个很好的方式来处理位图。但是,在这个应用程序中,我将一个 的实例LruCache放在我创建的另一个缓存类中,以使应用程序更可行。

Cache.java's critical stuff (the loadBitmap()method is the most important):

Cache.java 的关键内容(loadBitmap()方法最重要):

public Cache(int size, int maxWidth, int maxHeight) {
    // Into the constructor you add the maximum pixels
    // that you want to allow in order to not scale images.
    mMaxWidth = maxWidth;
    mMaxHeight = maxHeight;

    mBitmapCache = new LruCache<String, Bitmap>(size) {
        protected int sizeOf(String key, Bitmap b) {
            // Assuming that one pixel contains four bytes.
            return b.getHeight() * b.getWidth() * 4;
        }
    };

    mCurrentTasks = new ArrayList<String>();    
}

/**
 * Gets a bitmap from cache. 
 * If it is not in cache, this method will:
 * 
 * 1: check if the bitmap url is currently being processed in the
 * BitmapLoaderTask and cancel if it is already in a task (a control to see
 * if it's inside the currentTasks list).
 * 
 * 2: check if an internet connection is available and continue if so.
 * 
 * 3: download the bitmap, scale the bitmap if necessary and put it into
 * the memory cache.
 * 
 * 4: Remove the bitmap url from the currentTasks list.
 * 
 * 5: Notify the ListAdapter.
 * 
 * @param mainActivity - Reference to activity object, in order to
 * call notifyDataSetChanged() on the ListAdapter.
 * @param imageKey - The bitmap url (will be the key).
 * @param imageView - The ImageView that should get an
 * available bitmap or a placeholder image.
 * @param isScrolling - If set to true, we skip executing more tasks since
 * the user probably has flinged away the view.
 */
public void loadBitmap(MainActivity mainActivity, 
        String imageKey, ImageView imageView,
        boolean isScrolling) {
    final Bitmap bitmap = getBitmapFromCache(imageKey); 

    if (bitmap != null) {
        imageView.setImageBitmap(bitmap);
    } else {
        imageView.setImageResource(R.drawable.ic_launcher);
        if (!isScrolling && !mCurrentTasks.contains(imageKey) && 
                mainActivity.internetIsAvailable()) {
            BitmapLoaderTask task = new BitmapLoaderTask(imageKey,
                    mainActivity.getAdapter());
            task.execute();
        }
    } 
}

You shouldn't need to edit anything in the Cache.java file unless you want to implement disk caching.

除非您想实现磁盘缓存,否则您不需要编辑 Cache.java 文件中的任何内容。

MainActivity.java's critical stuff:

MainActivity.java 的关键内容:

public void onScrollStateChanged(AbsListView view, int scrollState) {
    if (view.getId() == android.R.id.list) {
        // Set scrolling to true only if the user has flinged the       
        // ListView away, hence we skip downloading a series
        // of unnecessary bitmaps that the user probably
        // just want to skip anyways. If we scroll slowly it
        // will still download bitmaps - that means
        // that the application won't wait for the user
        // to lift its finger off the screen in order to
        // download.
        if (scrollState == SCROLL_STATE_FLING) {
            mIsScrolling = true;
        } else {
            mIsScrolling = false;
            mListAdapter.notifyDataSetChanged();
        }
    } 
}

// Inside ListAdapter...
@Override
public View getView(final int position, View convertView, ViewGroup parent) {           
    View row = convertView;
    final ViewHolder holder;

    if (row == null) {
        LayoutInflater inflater = getLayoutInflater();
        row = inflater.inflate(R.layout.main_listview_row, parent, false);  
        holder = new ViewHolder(row);
        row.setTag(holder);
    } else {
        holder = (ViewHolder) row.getTag();
    }   

    final Row rowObject = getItem(position);

    // Look at the loadBitmap() method description...
    holder.mTextView.setText(rowObject.mText);      
    mCache.loadBitmap(MainActivity.this,
            rowObject.mBitmapUrl, holder.mImageView,
            mIsScrolling);  

    return row;
}

getView()gets called very often. It's normally not a good idea to download images there if we haven't implemented a check that ensure us that we won't start an infinite amount of threads per row. Cache.java checks whether the rowObject.mBitmapUrlalready is in a task and if it is, it won't start another. Therefore, we are most likely not exceeding the work queue restriction from the AsyncTaskpool.

getView()经常被调用。如果我们没有实施检查以确保我们不会每行启动无限数量的线程,那么在那里下载图像通常不是一个好主意。Cache.java 检查是否rowObject.mBitmapUrl已经在一个任务中,如果是,它不会启动另一个。因此,我们很可能不会超出AsyncTask池中的工作队列限制。

Download:

下载:

You can download the source code from https://www.dropbox.com/s/pvr9zyl811tfeem/ListViewImageCache.zip.

您可以从https://www.dropbox.com/s/pvr9zyl811tfeem/ListViewImageCache.zip下载源代码。



Last words:

最后的话:

I have tested this for a few weeks now, I haven't gotten a single OOM exception yet. I have tested this on the emulator, on my Nexus One and on my Nexus S. I have tested image urls that contain images that were in HD quality. The only bottleneck is that it takes more time to download.

我已经测试了几个星期了,我还没有收到一个 OOM 异常。我已经在模拟器、我的 Nexus One 和我的 Nexus S 上对此进行了测试。我已经测试了包含高清质量图像的图像 URL。唯一的瓶颈是下载需要更多时间。

There is only one possible scenario where I can imagine that the OOM will appear, and that is if we download many, really big images, and before they get scaled and put into cache, will simultaneously take up more memory and cause an OOM. But that isn't even an ideal situation anyway and it most likely won't be possible to solve in a more feasible way.

我可以想象只有一种可能的场景会出现 OOM,那就是如果我们下载很多非常大的图像,并且在它们被缩放并放入缓存之前,会同时占用更多内存并导致 OOM。但这无论如何都不是理想的情况,而且很可能无法以更可行的方式解决。

Report errors in the comments! :-)

在评论中报告错误!:-)

回答by Chrispix

I did the following to take the image and resize it on the fly. Hope this helps

我做了以下操作来拍摄图像并即时调整其大小。希望这可以帮助

Bitmap bm;
bm = Bitmap.createScaledBitmap(BitmapFactory.decodeFile(filepath), 100, 100, true);
mPicture = new ImageView(context);
mPicture.setImageBitmap(bm);    

回答by RayHaque

It seems that this is a very long running problem, with a lot of differing explanations. I took the advice of the two most common presented answers here, but neither one of these solved my problems of the VM claiming it couldn't afford the bytes to perform the decodingpart of the process. After some digging I learned that the real problem here is the decoding process taking away from the NATIVEheap.

似乎这是一个运行时间很长的问题,有很多不同的解释。我在这里接受了两个最常见的答案的建议,但这些都没有解决我的 VM 问题,声称它无法负担执行过程解码部分的字节。经过一番挖掘,我了解到这里真正的问题是从NATIVE堆中带走的解码过程。

See here: BitmapFactory OOM driving me nuts

在这里看到:BitmapFactory OOM 把我逼疯了

That lead me to another discussion thread where I found a couple more solutions to this problem. One is to callSystem.gc();manually after your image is displayed. But that actually makes your app use MORE memory, in an effort to reduce the native heap. The better solution as of the release of 2.0 (Donut) is to use the BitmapFactory option "inPurgeable". So I simply added o2.inPurgeable=true;just after o2.inSampleSize=scale;.

这导致我进入另一个讨论线程,在那里我找到了更多解决此问题的方法。一种是System.gc();在您的图像显示后手动调用。但这实际上会使您的应用程序使用更多内存,以减少本机堆。自 2.0 (Donut) 发布以来,更好的解决方案是使用 BitmapFactory 选项“inPurgeable”。所以我o2.inPurgeable=true;只是在o2.inSampleSize=scale;.

More on that topic here: Is the limit of memory heap only 6M?

有关该主题的更多信息:内存堆的限制是否仅为 6M?

Now, having said all of this, I am a complete dunce with Java and Android too. So if you think this is a terrible way to solve this problem, you are probably right. ;-) But this has worked wonders for me, and I have found it impossible to run the VM out of heap cache now. The only drawback I can find is that you are trashing your cached drawn image. Which means if you go RIGHT back to that image, you are redrawing it each and every time. In the case of how my application works, that is not really a problem. Your mileage may vary.

现在,说了这么多,我对 Java 和 Android 也是个十足的笨蛋。因此,如果您认为这是解决此问题的糟糕方法,那么您可能是对的。;-) 但这对我来说很神奇,我发现现在不可能在堆缓存之外运行 VM。我能找到的唯一缺点是您正在破坏缓存的绘制图像。这意味着如果你直接回到那个图像,你每次都在重新绘制它。就我的应用程序的工作方式而言,这并不是真正的问题。你的旅费可能会改变。