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


1287

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

列表视图上的图像预览是使用光标和进行的ListAdapter。这非常简单,但是我不确定如何放置调整大小后的图像(即,较小的位大小而不是像素,而不是像像素src按钮那样动态显示。因此,我只是调整了从电话摄像头放出的图像的大小。

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

  • 有没有一种方法可以轻松地逐行构建列表适配器,从而可以即时调整大小(逐)?

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

  • 我知道我可以进行带外调整大小并保存图像,但这并不是我真正想做的,但是一些示例代码会很不错。

一旦禁用列表视图中的图像,它就会再次正常工作。

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

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

哪里R.id.imagefilenameButtonImage

这是我的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 

显示图像时也出现了新错误:

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

8
我通过避免使用Bitmap.decodeStream或encodeFile并使用BitmapFactory.decodeFileDescriptor方法解决了这一问题。
Fraggle等

1
几周前,我也面临类似的问题,我通过将图像缩小到最佳点来解决了这个问题。我在我的博客写完整的方法codingjunkiesforum.wordpress.com/2014/06/12/...并上传完整的示例项目,OOM易码VS OOM证明代码athttps://github.com/shailendra123/BitmapHandlingDemo
赛伦德拉·辛格Rajawat

5
关于这个问题的公认答案正在meta
2015年

4
当您不阅读Android开发人员指南时就会发生这种情况
Pedro Varela

2
发生这种情况是由于Android体系结构不佳。它应该像ios一样调整图像本身的大小,而UWP会这样做。我不必自己做这些事情。Android开发人员已经习惯了这种想法,并认为它可以按应有的方式工作。
拒绝访问

Answers:


650

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


读取位图尺寸和类型

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

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;

为避免java.lang.OutOfMemory异常,请在对位图进行解码之前检查其尺寸,除非您完全相信该源可为您提供大小适中且可容纳在可用内存中的可预测大小的图像数据。


将缩小版本加载到内存

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

  • 将完整映像加载到内存中的估计内存使用量。
  • 给定应用程序的任何其他内存要求,您愿意承诺加载此映像的内存量。
  • 要将图像加载到其中的目标ImageView或UI组件的尺寸。
  • 当前设备的屏幕尺寸和密度。

例如,如果将1024x768像素的图像最终显示在的128x96像素缩略图中,则不值得将其加载到内存中ImageView

为了告诉解码器对图像进行二次采样,将较小的版本加载到内存中,inSampleSizetrue在您的BitmapFactory.Options对象中将其设置为。例如,分辨率为2048x1536的图像(以inSampleSize4 解码)产生的位图大约为512x384。将其加载到内存中需要使用0.75MB而不是完整图像的12MB(假设的位图配置ARGB_8888)。这是一种根据目标宽度和高度计算样本大小值(是2的幂)的方法:

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

注意:根据inSampleSize文档,解码器使用四舍五入到最接近的2的幂来计算最终值,因此计算出2的幂 。

要使用此方法,请先将inJustDecodeBounds设置为true,然后将选项传递给解码,然后再使用新inSampleSizeinJustDecodeBounds设置为再次解码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);
}

使用此方法可以轻松地将任意大尺寸的位图加载到ImageView显示100x100像素缩略图的,如下例代码所示:

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

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


21
正在meta
rene 2015年

9
该答案(通过链接获得的信息除外)并未为答案提供很多解决方案。链接的重要部分应合并到问题中。
FallenAngel

7
该答案与该问题相同,其他答案均为Community Wiki,因此社区可以通过编辑来解决此问题,而无需主持人干预。
马丁·彼得斯

当前指向内容和Kotlin支持的链接可以在以下网站找到:developer.android.com/topic/performance/graphics/load-bitmap
Panos Gr

890

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

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

inSampleSize选项可减少内存消耗。

这是一个完整的方法。首先,它读取图像大小而不解码内容本身。然后找到最佳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;
}

31
请注意,10可能不是inSampleSize最佳值虽然,文档建议2.利用权力
米尔科N.

70
我面临与Chrispix相同的问题,但是我认为这里的解决方案不能真正解决问题,而是可以绕开它。更改样本大小会减少使用的内存量(以图像质量为代价,这对于图像预览来说可能是可以的),但是如果解码了足够大的图像流(如果有多个图像流,则无法避免此异常)解码。如果找到更好的解决方案(可能没有解决方案),我将在此处发布答案。
Flynn81 2010年

4
您只需要一个合适的尺寸以匹配屏幕的像素密度,即可进行放大,因此您可以以更高的密度拍摄图像样本。
secretthcopter

4
REQUIRED_SIZE是您要缩放到的新尺寸。
Fedor

8
这个解决方案对我有帮助,但是图像质量很差。我正在使用viewfilpper来显示图像有什么建议吗?
user1106888 2013年

372

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

40
是的,虽然没那么漂亮,但您是对的。我只是想让所有人都清楚。感谢您的代码。
费多(Fedor)2010年

10
@Thomas Vervest-该代码存在一个大问题。^不会将2提高到幂,它会将结果与2异或。您需要Math.pow(2.0,...)。否则,这看起来不错。
DougW

6
哦,这是一个非常好的!不好,我会立即改正,谢谢您的答复!
Thomas Vervest 2010年

8
您正在创建两个新的FileInputStreams,每次调用时一个BitmapFactory.decodeStream()。您是否不必保存对它们的引用,以便它们可以在一个finally块中关闭?
matsev

1
@Babibu文档没有说明流已为您关闭,因此我认为仍应关闭该流。可以在这里找到有趣且相关的讨论。注意Adrian Smith的评论,它直接关系到我们的辩论。
Thomas Vervest

232

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

1)你做的每一次BitmapFactory.decodeXYZ(),请务必在一传BitmapFactory.OptionsinPurgeable集于true(并优选inInputShareable也设置为true)。

2)切勿使用Bitmap.createBitmap(width, height, Config.ARGB_8888)。我的意思是永远!经过数次传递之后,我再也没有出现过这样的事情不会引起内存错误的情况。再多recycle()System.gc(),任何帮助。它总是引发异常。实际可行的另一种方法是在可绘制对象(或使用上述步骤1解码的另一个位图)中具有虚拟图像,将其缩放到所需的大小,然后处理生成的位图(例如将其传递到Canvas)以获取更多乐趣)。因此,您应该改为使用:Bitmap.createScaledBitmap(srcBitmap, width, height, false)。如果出于任何原因必须使用蛮力创建方法,则至少要通过Config.ARGB_4444

几乎可以保证,即使没有几天,您也可以节省几个小时。关于缩放图像等的所有讨论都无法真正起作用(除非您考虑使用错误的尺寸或降级的图像作为解决方案)。


22
BitmapFactory.Options options = new BitmapFactory.Options(); options.inPurgeable = true;Bitmap.createScaledBitmap(srcBitmap, width, height, false);解决了我在android 4.0.0上出现内存不足异常的问题。谢了哥们!
Jan-TerjeSørensen'2

5
在Bitmap.createScaledBitmap()调用中,您可能应该将true用作flag参数。否则,放大时图像的质量将不平滑。检查这个线程stackoverflow.com/questions/2895065/...
rOrlig

11
那真的是很棒的建议。希望我能再给您+1,让Google来解决这个令人惊讶的皱纹粉红色bug。我的意思是……如果这不是一个错误,那么文档中确实需要有一些闪烁的霓虹灯,上面写着“这是您处理照片的方式”,因为我已经为此苦苦挣扎了两年了,而现在才找到这篇文章。很棒的发现。
叶夫根尼·辛金

缩小图像绝对有帮助,但这是重要的一步,最终为我解决了这个问题。仅缩放图像的问题是如果图像很多,或者源图像很大,那么您仍然会遇到相同的问题。+1给你以法莲。
戴夫

10
棒棒糖的,BitmapFactory.Options.inPurgeableBitmapFactory.Options.inInputShareable已被弃用developer.android.com/reference/android/graphics/...
丹尼斯Kniazhev

93

这是一个已知的错误,不是因为文件很大。由于Android会缓存Drawable,因此使用少量图像后,内存就会耗尽。但是我通过跳过android默认的缓存系统找到了另一种方法。

解决方案:将图像移到“资产”文件夹,然后使用以下函数获取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);
}

79

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

decodeFileDescriptor 看起来它调用的是与解码流/解码文件不同的本机方法。

无论如何,这是行得通的(请注意,我添加了一些上述选项,但这并没有什么不同。关键是对BitmapFactory.decodeFileDescriptor的调用,而不是defineStreamencodeFile的调用):

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

我认为在解码流/解码文件中使用的本机函数存在问题。我已经确认,使用解码文件描述符时会调用其他本机方法。我还读到的是“图像(位图)不是以标准Java方式分配,而是通过本机调用分配;分配是在虚拟堆之外完成的,但要对它进行 计数!


1
内存中的结果相同,实际上,使用哪种方法并不重要,取决于您要读取的内存中保留的字节数。
PiyushMishra,2011年

72

我认为避免的最佳方法OutOfMemoryError是面对它并理解它。

我制作了一个有意引起的应用程序OutOfMemoryError,并监视内存使用情况。

在使用此应用程序进行了大量实验后,我得出以下结论:

我要先介绍Honey Comb之前的SDK版本。

  1. 位图存储在本机堆中,但会自动收集垃圾,因此无需调用recycle()。

  2. 如果{VM堆大小} + {已分配的本机堆内存}> = {设备的VM堆大小限制},而您尝试创建位图,则会抛出OOM。

    注意:计算的是VM HEAP SIZE,而不是VM分配的内存。

  3. 即使增加了分配的VM内存,VM Heap大小也将永远不会缩小。

  4. 因此,必须将峰值VM内存保持尽可能低,以防止VM Heap Size变得太大而无法为Bitmap保存可用内存。

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

  6. 本机堆大小也永远不会缩小,但不算在OOM中,因此无需担心。

然后,让我们讨论从Honey Comb开始的SDK。

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

  2. OOM的条件要简单得多:{VM堆大小}> = {设备的VM堆大小限制}。

  3. 因此,您拥有更多可用内存来创建具有相同堆大小限制的位图,因此不太可能引发OOM。

这是我对垃圾回收和内存泄漏的一些观察。

您可以在应用程序中自己查看。如果活动在销毁活动后执行了仍在运行的AsyncTask,则直到AsyncTask完成后,活动才会被垃圾回收。

这是因为AsyncTask是匿名内部类的实例,它包含Activity的引用。

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

回调也是匿名内部类,因此,如果项目中的静态实例将其保留且不释放它们,则内存将被泄漏。

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


AsyncTask不一定必须是“匿名内部类的实例”,并且Callbackks也是如此。您可以在扩展AsyncTask的自己文件中创建一个新的公共类,甚至可以private static class在同一类中创建一个新的公共类。他们将不会对活动进行任何引用(除非您当然给他们之一)
Simon Forsberg

65

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

因此,我编写了一个示例应用程序,演示了在Android环境中的缓存。此实现尚未获得OOM。

在此答案的末尾找到源代码的链接。

要求:

  • Android API 2.1或更高版本(我根本无法在API 1.6中获取应用程序的可用内存-这是唯一在API 1.6中不起作用的代码)
  • Android支持包

屏幕截图

特征:

  • 如果方向改变则保留缓存,使用单例
  • 使用八分之一指定的应用程序内存来缓存(如果你想修改)
  • 大型位图会缩放(您可以定义要允许的最大像素)
  • 在下载位图之前控制是否有Internet连接可用
  • 确保每行仅实例化一个任务
  • 如果你丢ListView了,它根本不会下载的位图

这不包括:

  • 磁盘缓存。无论如何,这应该很容易实现-只是指向另一个任务,该任务从磁盘上获取位图

样例代码:

正在下载的图像是来自Flickr的图像(75x75)。但是,请放置您要处理的任何图像url,如果超出最大数量,应用程序将按比例缩小它。在此应用程序中,URL仅位于String数组中。

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

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

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

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()经常被打电话。如果我们没有执行检查以确保我们不会在每行中启动无限数量的线程,那么在那儿下载映像通常不是一个好主意。Cache.java检查rowObject.mBitmapUrl一个任务中是否已经存在,如果已经存在,它将不会启动另一个任务。因此,我们很可能不会超出AsyncTask池中的工作队列限制。

下载:

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


最后的话:

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

我可以想象只有一种可能的情况,即OOM会出现,也就是说,如果我们下载许多非常大的图像,并且在将它们缩放和放入缓存之前,它们将同时占用更多内存并导致OOM。但这毕竟不是一个理想的情况,很可能不可能以更可行的方式解决。

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


43

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

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

26
此方法缩放位图。但是它不能解决OutOfMemory问题,因为无论如何,完整的位图仍在解码中。
费多(Fedor)2010年

5
我将看看是否可以查看旧代码,但是我认为它确实解决了内存不足的问题。将再次检查我的旧代码。
Chrispix

2
至少在此示例中,您似乎没有保留对完整位图的引用,因此节省了内存。
NoBugs 2013年

对我来说,它确实解决了内存问题,但降低了图像质量。
帕米拉·西拉

35

似乎这是一个长期运行的问题,有许多不同的解释。我在这里提出了两个最常见的答案,但是这些都没有解决我关于VM的问题,声称VM无法承受执行该过程的解码部分的字节。经过一番挖掘后,我了解到真正的问题是解码过程不占用NATIVE堆。

看到这里:BitmapFactory OOM让我发疯

这导致我进入另一个讨论线程,在那里我找到了解决该问题的更多解决方案。一种是System.gc();在显示图像后手动呼叫。但这实际上使您的应用程序使用更多的内存,以减少本机堆。从2.0版(甜甜圈)开始,更好的解决方案是使用BitmapFactory选项“ inPurgeable”。所以我只是o2.inPurgeable=true;在之后添加o2.inSampleSize=scale;

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

现在,说了这么多,我对Java和Android也是一个完全的傻瓜。因此,如果您认为这是解决此问题的可怕方法,那么您可能是对的。;-)但这对我来说是个奇迹,我发现现在无法在堆缓存之外运行VM。我可以发现的唯一缺点是,您正在丢弃缓存的绘制图像。这意味着,如果您向右返回该图像,则每次都会重新绘制它。就我的应用程序如何工作而言,这并不是真正的问题。你的旅费可能会改变。


inPurgeable固定的OOM对我来说。
Artem Russakovskii 2011年

35

不幸的是,如果上述方法均无效,则将其添加到清单文件中。内部应用标签

 <application
         android:largeHeap="true"

1
您能解释一下这实际上是什么吗?仅仅告诉人们添加此内容并没有帮助。
Stealth Rabbi

1
这是一个非常糟糕的解决方案。基本上,您不是在尝试解决问题。而是要求android系统为您的应用程序分配更多的堆空间。这将对您的应用程序产生非常严重的影响,例如您的应用程序消耗大量电池电量,因为GC必须在大堆空间中运行以清理内存,并且您的应用程序性能也会变慢。
Prakash

2
那么为什么android允许我们在清单中添加此android:largeHeap =“ true”?现在,您正在挑战Android。
Himanshu Mori


28

我有一个更有效的解决方案,不需要进行任何缩放。只需解码一次位图,然后根据其名称将其缓存在映射中。然后只需根据名称检索位图,然后在ImageView中进行设置。没有什么需要做的了。

这将起作用,因为已解码位图的实际二进制数据未存储在dalvik VM堆中。它存储在外部。因此,每次解码位图时,它都会在VM堆之外分配内存,而该内存不会被GC回收

为了帮助您更好地理解这一点,假设您已将ur图像保存在drawable文件夹中。您只需通过执行getResources()。getDrwable(R.drawable。)来获取图像。这不会每次都解码图像,而是每次调用它时都重新使用已经解码的实例。因此,从本质上讲,它是缓存的。

现在,由于图像位于某个位置的文件中(甚至可能来自外部服务器),因此您有责任缓存已解码的位图实例,以在需要的任何地方重复使用。

希望这可以帮助。


4
“然后根据名称将其缓存在地图中。” 您如何精确缓存图像?
文森特

3
您实际尝试过吗?即使像素数据实际上没有存储在Dalvik堆中,它在本机内存中的大小也会报告给VM,并根据其可用内存进行计数。
ErikR 2011年

3
@Vincent我认为将它们存储在地图中并不难。我建议使用HashMap <KEY,Bitmap> map之类的东西,其中Key可以是源字符串或任何对您有意义的字符串。假设您将路径作为KEY,将其存储为map.put(Path,Bitmap),并通过map.get(Path)接收它
Rafael T

3
如果您要实现图像缓存,则可能需要使用HashMap <String,SoftReference <Bitmap >>,否则无论如何您可能会用完内存-我也不认为“它在VM堆之外分配内存,而GC从未回收它” “是真的,内存被回收,因为我知道这可能只是一个延迟,这就是bitmap.recycle()的用意,以作为提早回收内存的提示……
Dori

28

我已经通过以下方式解决了相同的问题。

Bitmap b = null;
Drawable d;
ImageView i = new ImageView(mContext);
try {
    b = Bitmap.createBitmap(320,424,Bitmap.Config.RGB_565);
    b.eraseColor(0xFFFFFFFF);
    Rect r = new Rect(0, 0,320 , 424);
    Canvas c = new Canvas(b);
    Paint p = new Paint();
    p.setColor(0xFFC0C0C0);
    c.drawRect(r, p);
    d = mContext.getResources().getDrawable(mImageIds[position]);
    d.setBounds(r);
    d.draw(c);

    /*   
        BitmapFactory.Options o2 = new BitmapFactory.Options();
        o2.inTempStorage = new byte[128*1024];
        b = BitmapFactory.decodeStream(mContext.getResources().openRawResource(mImageIds[position]), null, o2);
        o2.inSampleSize=16;
        o2.inPurgeable = true;
    */
} catch (Exception e) {

}
i.setImageBitmap(b);

没关系,但是我在OnCreate中使用多个位图绘制绘制圈并进行活动调用4-5次,因此如何清除位图以及如何删除位图并在活动0nCreate时第二次再次创建。–
ckpatel,

27

这里有两个问题。

  • 位图内存不在VM堆中,而是在本机堆中-请参阅BitmapFactory OOM使我发疯
  • 本机堆的垃圾收集比VM堆慢-因此,每次执行Activity的onPause或onDestroy时,您都必须非常积极地进行bitmap.recycle和bitmap = null

自android 2.3+起它就在VM堆中
FindOut_Quran 16'Apr

27

这对我有用!

public Bitmap readAssetsBitmap(String filename) throws IOException {
    try {
        BitmapFactory.Options options = new BitmapFactory.Options(); 
        options.inPurgeable = true;
        Bitmap bitmap = BitmapFactory.decodeStream(assets.open(filename), null, options);
        if(bitmap == null) {
            throw new IOException("File cannot be opened: It's value is null");
        } else {
            return bitmap;
        }
    } catch (IOException e) {
        throw new IOException("File cannot be opened: " + e.getMessage());
    }
}

20

上面的答案都不对我有用,但是我确实提出了一个可怕的丑陋变通办法来解决问题。我在项目中添加了一个很小的1x1像素图像作为资源,并在调用垃圾回收之前将其加载到ImageView中。我认为可能是ImageView没有释放位图,所以GC从未选择它。这很丑陋,但现在似乎可以使用。

if (bitmap != null)
{
  bitmap.recycle();
  bitmap = null;
}
if (imageView != null)
{
  imageView.setImageResource(R.drawable.tiny); // This is my 1x1 png.
}
System.gc();

imageView.setImageBitmap(...); // Do whatever you need to do to load the image you want.

看起来imageView确实不会自行回收位图。对我有所帮助,谢谢
德米特里·扎伊采夫

@Mike可以添加imageloader的完整代码,也可以给我链接以加载位图图像的链接。如果我在位图上使用回收站,则会显示所有列表视图,但所有项目均显示为空白
TNR 2012年

@Mike您能告诉我是否在调用垃圾回收之前执行imageView = null是否好?
Youddh 2012年

@TNR我想你错过这里要说的是bitmap在上面的代码是先前已显示的图像,需要回收的是,清除它的任何引用,使imageView通过设置一个很小的替代忘记它为好,gc(); 然后,将以上代码中的加载到您的新图片bitmap并显示出来...
TWiStErRob 2014年

错了 在回收位图之前(而不是在其实际显示和使用时),您应该始终清除imageView内容。
FindOut_Quran '16

20

这里的答案很好,但是我想要一个完全可用的课程来解决这个问题。所以我做了一个。

这是我的BitmapHelper类,它是OutOfMemoryError证明的:-)

import java.io.File;
import java.io.FileInputStream;

import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;

public class BitmapHelper
{

    //decodes image and scales it to reduce memory consumption
    public static Bitmap decodeFile(File bitmapFile, int requiredWidth, int requiredHeight, boolean quickAndDirty)
    {
        try
        {
            //Decode image size
            BitmapFactory.Options bitmapSizeOptions = new BitmapFactory.Options();
            bitmapSizeOptions.inJustDecodeBounds = true;
            BitmapFactory.decodeStream(new FileInputStream(bitmapFile), null, bitmapSizeOptions);

            // load image using inSampleSize adapted to required image size
            BitmapFactory.Options bitmapDecodeOptions = new BitmapFactory.Options();
            bitmapDecodeOptions.inTempStorage = new byte[16 * 1024];
            bitmapDecodeOptions.inSampleSize = computeInSampleSize(bitmapSizeOptions, requiredWidth, requiredHeight, false);
            bitmapDecodeOptions.inPurgeable = true;
            bitmapDecodeOptions.inDither = !quickAndDirty;
            bitmapDecodeOptions.inPreferredConfig = quickAndDirty ? Bitmap.Config.RGB_565 : Bitmap.Config.ARGB_8888;

            Bitmap decodedBitmap = BitmapFactory.decodeStream(new FileInputStream(bitmapFile), null, bitmapDecodeOptions);

            // scale bitmap to mathc required size (and keep aspect ratio)

            float srcWidth = (float) bitmapDecodeOptions.outWidth;
            float srcHeight = (float) bitmapDecodeOptions.outHeight;

            float dstWidth = (float) requiredWidth;
            float dstHeight = (float) requiredHeight;

            float srcAspectRatio = srcWidth / srcHeight;
            float dstAspectRatio = dstWidth / dstHeight;

            // recycleDecodedBitmap is used to know if we must recycle intermediary 'decodedBitmap'
            // (DO NOT recycle it right away: wait for end of bitmap manipulation process to avoid
            // java.lang.RuntimeException: Canvas: trying to use a recycled bitmap android.graphics.Bitmap@416ee7d8
            // I do not excatly understand why, but this way it's OK

            boolean recycleDecodedBitmap = false;

            Bitmap scaledBitmap = decodedBitmap;
            if (srcAspectRatio < dstAspectRatio)
            {
                scaledBitmap = getScaledBitmap(decodedBitmap, (int) dstWidth, (int) (srcHeight * (dstWidth / srcWidth)));
                // will recycle recycleDecodedBitmap
                recycleDecodedBitmap = true;
            }
            else if (srcAspectRatio > dstAspectRatio)
            {
                scaledBitmap = getScaledBitmap(decodedBitmap, (int) (srcWidth * (dstHeight / srcHeight)), (int) dstHeight);
                recycleDecodedBitmap = true;
            }

            // crop image to match required image size

            int scaledBitmapWidth = scaledBitmap.getWidth();
            int scaledBitmapHeight = scaledBitmap.getHeight();

            Bitmap croppedBitmap = scaledBitmap;

            if (scaledBitmapWidth > requiredWidth)
            {
                int xOffset = (scaledBitmapWidth - requiredWidth) / 2;
                croppedBitmap = Bitmap.createBitmap(scaledBitmap, xOffset, 0, requiredWidth, requiredHeight);
                scaledBitmap.recycle();
            }
            else if (scaledBitmapHeight > requiredHeight)
            {
                int yOffset = (scaledBitmapHeight - requiredHeight) / 2;
                croppedBitmap = Bitmap.createBitmap(scaledBitmap, 0, yOffset, requiredWidth, requiredHeight);
                scaledBitmap.recycle();
            }

            if (recycleDecodedBitmap)
            {
                decodedBitmap.recycle();
            }
            decodedBitmap = null;

            scaledBitmap = null;
            return croppedBitmap;
        }
        catch (Exception ex)
        {
            ex.printStackTrace();
        }
        return null;
    }

    /**
     * compute powerOf2 or exact scale to be used as {@link BitmapFactory.Options#inSampleSize} value (for subSampling)
     * 
     * @param requiredWidth
     * @param requiredHeight
     * @param powerOf2
     *            weither we want a power of 2 sclae or not
     * @return
     */
    public static int computeInSampleSize(BitmapFactory.Options options, int dstWidth, int dstHeight, boolean powerOf2)
    {
        int inSampleSize = 1;

        // Raw height and width of image
        final int srcHeight = options.outHeight;
        final int srcWidth = options.outWidth;

        if (powerOf2)
        {
            //Find the correct scale value. It should be the power of 2.

            int tmpWidth = srcWidth, tmpHeight = srcHeight;
            while (true)
            {
                if (tmpWidth / 2 < dstWidth || tmpHeight / 2 < dstHeight)
                    break;
                tmpWidth /= 2;
                tmpHeight /= 2;
                inSampleSize *= 2;
            }
        }
        else
        {
            // Calculate ratios of height and width to requested height and width
            final int heightRatio = Math.round((float) srcHeight / (float) dstHeight);
            final int widthRatio = Math.round((float) srcWidth / (float) dstWidth);

            // Choose the smallest ratio as inSampleSize value, this will guarantee
            // a final image with both dimensions larger than or equal to the
            // requested height and width.
            inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
        }

        return inSampleSize;
    }

    public static Bitmap drawableToBitmap(Drawable drawable)
    {
        if (drawable instanceof BitmapDrawable)
        {
            return ((BitmapDrawable) drawable).getBitmap();
        }

        Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);
        drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
        drawable.draw(canvas);

        return bitmap;
    }

    public static Bitmap getScaledBitmap(Bitmap bitmap, int newWidth, int newHeight)
    {
        int width = bitmap.getWidth();
        int height = bitmap.getHeight();
        float scaleWidth = ((float) newWidth) / width;
        float scaleHeight = ((float) newHeight) / height;

        // CREATE A MATRIX FOR THE MANIPULATION
        Matrix matrix = new Matrix();
        // RESIZE THE BIT MAP
        matrix.postScale(scaleWidth, scaleHeight);

        // RECREATE THE NEW BITMAP
        Bitmap resizedBitmap = Bitmap.createBitmap(bitmap, 0, 0, width, height, matrix, false);
        return resizedBitmap;
    }

}

对于使用此方法的任何人:我只是修复了一个错误:“ int scaledBitmapHeight = scaledBitmap.getWidth();” 是错误的(显然。我将其替换为“ int scaledBitmapHeight = scaledBitmap.getHeight();”)
Pascal

19

这对我有用。

Bitmap myBitmap;

BitmapFactory.Options options = new BitmapFactory.Options(); 
options.InPurgeable = true;
options.OutHeight = 50;
options.OutWidth = 50;
options.InSampleSize = 4;

File imgFile = new File(filepath);
myBitmap = BitmapFactory.DecodeFile(imgFile.AbsolutePath, options);

这是在C#monodroid上。您可以轻松更改图像的路径。这里重要的是要设置的选项。


16

这似乎是与社区共享我的实用程序类以加载和处理图像的合适位置,欢迎您使用和自由修改它。

package com.emil;

import java.io.IOException;
import java.io.InputStream;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;

/**
 * A class to load and process images of various sizes from input streams and file paths.
 * 
 * @author Emil http://stackoverflow.com/users/220710/emil
 *
 */
public class ImageProcessing {

    public static Bitmap getBitmap(InputStream stream, int sampleSize, Bitmap.Config bitmapConfig) throws IOException{
        BitmapFactory.Options options=ImageProcessing.getOptionsForSampling(sampleSize, bitmapConfig);
        Bitmap bm = BitmapFactory.decodeStream(stream,null,options);
        if(ImageProcessing.checkDecode(options)){
            return bm;
        }else{
            throw new IOException("Image decoding failed, using stream.");
        }
    }

    public static Bitmap getBitmap(String imgPath, int sampleSize, Bitmap.Config bitmapConfig) throws IOException{
        BitmapFactory.Options options=ImageProcessing.getOptionsForSampling(sampleSize, bitmapConfig);
        Bitmap bm = BitmapFactory.decodeFile(imgPath,options);
        if(ImageProcessing.checkDecode(options)){
            return bm;
        }else{
            throw new IOException("Image decoding failed, using file path.");
        }
    }

    public static Dimensions getDimensions(InputStream stream) throws IOException{
        BitmapFactory.Options options=ImageProcessing.getOptionsForDimensions();
        BitmapFactory.decodeStream(stream,null,options);
        if(ImageProcessing.checkDecode(options)){
            return new ImageProcessing.Dimensions(options.outWidth,options.outHeight);
        }else{
            throw new IOException("Image decoding failed, using stream.");
        }
    }

    public static Dimensions getDimensions(String imgPath) throws IOException{
        BitmapFactory.Options options=ImageProcessing.getOptionsForDimensions();
        BitmapFactory.decodeFile(imgPath,options);
        if(ImageProcessing.checkDecode(options)){
            return new ImageProcessing.Dimensions(options.outWidth,options.outHeight);
        }else{
            throw new IOException("Image decoding failed, using file path.");
        }
    }

    private static boolean checkDecode(BitmapFactory.Options options){
        // Did decode work?
        if( options.outWidth<0 || options.outHeight<0 ){
            return false;
        }else{
            return true;
        }
    }

    /**
     * Creates a Bitmap that is of the minimum dimensions necessary
     * @param bm
     * @param min
     * @return
     */
    public static Bitmap createMinimalBitmap(Bitmap bm, ImageProcessing.Minimize min){
        int newWidth, newHeight;
        switch(min.type){
        case WIDTH:
            if(bm.getWidth()>min.minWidth){
                newWidth=min.minWidth;
                newHeight=ImageProcessing.getScaledHeight(newWidth, bm);
            }else{
                // No resize
                newWidth=bm.getWidth();
                newHeight=bm.getHeight();
            }
            break;
        case HEIGHT:
            if(bm.getHeight()>min.minHeight){
                newHeight=min.minHeight;
                newWidth=ImageProcessing.getScaledWidth(newHeight, bm);
            }else{
                // No resize
                newWidth=bm.getWidth();
                newHeight=bm.getHeight();
            }
            break;
        case BOTH: // minimize to the maximum dimension
        case MAX:
            if(bm.getHeight()>bm.getWidth()){
                // Height needs to minimized
                min.minDim=min.minDim!=null ? min.minDim : min.minHeight;
                if(bm.getHeight()>min.minDim){
                    newHeight=min.minDim;
                    newWidth=ImageProcessing.getScaledWidth(newHeight, bm);
                }else{
                    // No resize
                    newWidth=bm.getWidth();
                    newHeight=bm.getHeight();
                }
            }else{
                // Width needs to be minimized
                min.minDim=min.minDim!=null ? min.minDim : min.minWidth;
                if(bm.getWidth()>min.minDim){
                    newWidth=min.minDim;
                    newHeight=ImageProcessing.getScaledHeight(newWidth, bm);
                }else{
                    // No resize
                    newWidth=bm.getWidth();
                    newHeight=bm.getHeight();
                }
            }
            break;
        default:
            // No resize
            newWidth=bm.getWidth();
            newHeight=bm.getHeight();
        }
        return Bitmap.createScaledBitmap(bm, newWidth, newHeight, true);
    }

    public static int getScaledWidth(int height, Bitmap bm){
        return (int)(((double)bm.getWidth()/bm.getHeight())*height);
    }

    public static int getScaledHeight(int width, Bitmap bm){
        return (int)(((double)bm.getHeight()/bm.getWidth())*width);
    }

    /**
     * Get the proper sample size to meet minimization restraints
     * @param dim
     * @param min
     * @param multipleOf2 for fastest processing it is recommended that the sample size be a multiple of 2
     * @return
     */
    public static int getSampleSize(ImageProcessing.Dimensions dim, ImageProcessing.Minimize min, boolean multipleOf2){
        switch(min.type){
        case WIDTH:
            return ImageProcessing.getMaxSampleSize(dim.width, min.minWidth, multipleOf2);
        case HEIGHT:
            return ImageProcessing.getMaxSampleSize(dim.height, min.minHeight, multipleOf2);
        case BOTH:
            int widthMaxSampleSize=ImageProcessing.getMaxSampleSize(dim.width, min.minWidth, multipleOf2);
            int heightMaxSampleSize=ImageProcessing.getMaxSampleSize(dim.height, min.minHeight, multipleOf2);
            // Return the smaller of the two
            if(widthMaxSampleSize<heightMaxSampleSize){
                return widthMaxSampleSize;
            }else{
                return heightMaxSampleSize;
            }
        case MAX:
            // Find the larger dimension and go bases on that
            if(dim.width>dim.height){
                return ImageProcessing.getMaxSampleSize(dim.width, min.minDim, multipleOf2);
            }else{
                return ImageProcessing.getMaxSampleSize(dim.height, min.minDim, multipleOf2);
            }
        }
        return 1;
    }

    public static int getMaxSampleSize(int dim, int min, boolean multipleOf2){
        int add=multipleOf2 ? 2 : 1;
        int size=0;
        while(min<(dim/(size+add))){
            size+=add;
        }
        size = size==0 ? 1 : size;
        return size;        
    }

    public static class Dimensions {
        int width;
        int height;

        public Dimensions(int width, int height) {
            super();
            this.width = width;
            this.height = height;
        }

        @Override
        public String toString() {
            return width+" x "+height;
        }
    }

    public static class Minimize {
        public enum Type {
            WIDTH,HEIGHT,BOTH,MAX
        }
        Integer minWidth;
        Integer minHeight;
        Integer minDim;
        Type type;

        public Minimize(int min, Type type) {
            super();
            this.type = type;
            switch(type){
            case WIDTH:
                this.minWidth=min;
                break;
            case HEIGHT:
                this.minHeight=min;
                break;
            case BOTH:
                this.minWidth=min;
                this.minHeight=min;
                break;
            case MAX:
                this.minDim=min;
                break;
            }
        }

        public Minimize(int minWidth, int minHeight) {
            super();
            this.type=Type.BOTH;
            this.minWidth = minWidth;
            this.minHeight = minHeight;
        }

    }

    /**
     * Estimates size of Bitmap in bytes depending on dimensions and Bitmap.Config
     * @param width
     * @param height
     * @param config
     * @return
     */
    public static long estimateBitmapBytes(int width, int height, Bitmap.Config config){
        long pixels=width*height;
        switch(config){
        case ALPHA_8: // 1 byte per pixel
            return pixels;
        case ARGB_4444: // 2 bytes per pixel, but depreciated
            return pixels*2;
        case ARGB_8888: // 4 bytes per pixel
            return pixels*4;
        case RGB_565: // 2 bytes per pixel
            return pixels*2;
        default:
            return pixels;
        }
    }

    private static BitmapFactory.Options getOptionsForDimensions(){
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds=true;
        return options;
    }

    private static BitmapFactory.Options getOptionsForSampling(int sampleSize, Bitmap.Config bitmapConfig){
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = false;
        options.inDither = false;
        options.inSampleSize = sampleSize;
        options.inScaled = false;
        options.inPreferredConfig = bitmapConfig;
        return options;
    }
}

16

在我的一个应用程序中,我需要从拍摄照片Camera/Gallery。如果用户单击“摄像机”中的图像(可能是2MP,5MP或8MP),则图像大小会因kBs 而异MB。如果图像大小小于(或高达1-2MB)以上代码工作正常,但如果我的图像大小大于4MB或5MB,则OOM出现在框架中:(

然后我努力解决了这个问题,最后我对Fedor做了以下改进(感谢Fedor做出了如此出色的解决方案)代码:)

private Bitmap decodeFile(String fPath) {
    // Decode image size
    BitmapFactory.Options opts = new BitmapFactory.Options();
    /*
     * If set to true, the decoder will return null (no bitmap), but the
     * out... fields will still be set, allowing the caller to query the
     * bitmap without having to allocate the memory for its pixels.
     */
    opts.inJustDecodeBounds = true;
    opts.inDither = false; // Disable Dithering mode
    opts.inPurgeable = true; // Tell to gc that whether it needs free
                                // memory, the Bitmap can be cleared
    opts.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

    BitmapFactory.decodeFile(fPath, opts);

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

    // Find the correct scale value. 
    int scale = 1;

    if (opts.outHeight > REQUIRED_SIZE || opts.outWidth > REQUIRED_SIZE) {

        // Calculate ratios of height and width to requested height and width
        final int heightRatio = Math.round((float) opts.outHeight
                / (float) REQUIRED_SIZE);
        final int widthRatio = Math.round((float) opts.outWidth
                / (float) REQUIRED_SIZE);

        // Choose the smallest ratio as inSampleSize value, this will guarantee
        // a final image with both dimensions larger than or equal to the
        // requested height and width.
        scale = heightRatio < widthRatio ? heightRatio : widthRatio;//
    }

    // Decode bitmap with inSampleSize set
    opts.inJustDecodeBounds = false;

    opts.inSampleSize = scale;

    Bitmap bm = BitmapFactory.decodeFile(fPath, opts).copy(
            Bitmap.Config.RGB_565, false);

    return bm;

}

我希望这将有助于哥们面临同样的问题!

有关更多信息,请参阅


14

我只是在几分钟前遇到了这个问题。我通过在管理Listview适配器方面做得更好而解决了它。我以为使用的数百张50x50px图像是个问题,事实证明,每次显示该行时,我都试图为自己的自定义视图充气。只需通过测试以查看行是否已膨胀,就可以消除此错误,并且我正在使用数百个位图。这实际上是一个Spinner,但是基本适配器对ListView的工作都相同。这个简单的修复程序也极大地提高了适配器的性能。

@Override
public View getView(final int position, View convertView, final ViewGroup parent) {

    if(convertView == null){
        LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        convertView = inflater.inflate(R.layout.spinner_row, null);
    }
...

3
我对此深表谢意!看到这个之前,我一直在追寻错误的问题。但是给您的问题是:由于每个或我的列表行都有唯一的名称和照片,因此我必须使用convertView数组保留每个行的值。我看不到使用单个变量如何允许您这样做。我想念什么吗?
PeteH

13

我整天都在测试这些解决方案,而对我来说唯一有效的方法就是上述获取图像并手动调用GC的方法,我知道这不是必需的,但这是唯一有效的方法当我将我的应用置于重负载测试下时,在活动之间进行切换。我的应用在列表视图中的缩略图视图中有一个缩略图列表(让我们说活动A),当您单击其中一个图像时,它将带您到另一个活动(让我们说活动B),该活动显示了该项目的主图像。当我在两个活动之间来回切换时,最终会出现OOM错误,并且该应用程序将强制关闭。

当我到达列表视图的一半时,它将崩溃。

现在,当我在活动B中实现以下内容时,我可以毫无问题地遍历整个列表视图,并且不断前进……而且速度很快。

@Override
public void onDestroy()
{   
    Cleanup();
    super.onDestroy();
}

private void Cleanup()
{    
    bitmap.recycle();
    System.gc();
    Runtime.getRuntime().gc();  
}

爱您的解决方案!我也花了数小时来解决这个错误,真烦人!编辑:可悲的是,当我在横向模式下更改屏幕方向时,问题仍然存在...
Xarialon 2012年

最终,这对我有所帮助:-BitmapFactory.Options options = new BitmapFactory.Options(); options.InPurgeable = true; options.InSampleSize = 2;
user3833732 2013年

13

仅在Android模拟器中会发生此问题。我在模拟器中也遇到了这个问题,但是当我检入设备时,它运行良好。

因此,请检查设备。它可以在设备中运行。


12

我的2分钱:我使用以下位图解决了OOM错误:

a)将我的图像缩放2倍

b)在我的自定义适配器中将Picasso库用于ListView,并在getView中进行如下调用:Picasso.with(context).load(R.id.myImage).into(R.id.myImageView);


我很高兴您提到毕加索,因为它使加载图像更加容易。特别是远程存储的。
Chrispix

12

对于从SdCard中选择的每个图像或可绘制的位图对象,请使用这些代码。

Resources res = getResources();
WindowManager window = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
Display display = window.getDefaultDisplay();
@SuppressWarnings("deprecation")
int width = display.getWidth();
@SuppressWarnings("deprecation")
int height = display.getHeight();
try {
    if (bitmap != null) {
        bitmap.recycle();
        bitmap = null;
        System.gc();
    }
    bitmap = Bitmap.createScaledBitmap(BitmapFactory
        .decodeFile(ImageData_Path.get(img_pos).getPath()),
        width, height, true);
} catch (OutOfMemoryError e) {
    if (bitmap != null) {
        bitmap.recycle();
        bitmap = null;
        System.gc();
    }
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inPreferredConfig = Config.RGB_565;
    options.inSampleSize = 1;
    options.inPurgeable = true;
    bitmapBitmap.createScaledBitmap(BitmapFactory.decodeFile(ImageData_Path.get(img_pos)
        .getPath().toString(), options), width, height,true);
}
return bitmap;

使用ImageData_Path.get(img_pos).getPath()的图像路径。


12

通常android设备的堆大小只有16MB(根据设备/操作系统的不同,请参阅post Heap Sizes),如果您正在加载图像并且大小超过16MB,则会抛出内存不足异常,而不是将Bitmap用于,加载来自SD卡,资源或什至来自网络的图像尝试使用getImageUri,加载位图需要更多内存,或者如果使用该位图完成工作,则可以将位图设置为null。


1
如果setImageURI仍然得到例外,然后请参阅本stackoverflow.com/questions/15377186/...
马赫什

11

这里的所有解决方案都需要设置IMAGE_MAX_SIZE。这限制了设备使用更强大的硬件,如果图像尺寸太小,则在高清屏幕上看起来很难看。

我提出了一种可与我的Samsung Galaxy S3和其他一些设备(包括功能较弱的设备)配合使用的解决方案,当使用功能更强大的设备时,图像质量会更好。

要点是要计算为特定设备上的应用程序分配的最大内存,然后将比例设置为尽可能小而不超过此内存。这是代码:

public static Bitmap decodeFile(File f)
{
    Bitmap b = null;
    try
    {
        // Decode image size
        BitmapFactory.Options o = new BitmapFactory.Options();
        o.inJustDecodeBounds = true;

        FileInputStream fis = new FileInputStream(f);
        try
        {
            BitmapFactory.decodeStream(fis, null, o);
        }
        finally
        {
            fis.close();
        }

        // In Samsung Galaxy S3, typically max memory is 64mb
        // Camera max resolution is 3264 x 2448, times 4 to get Bitmap memory of 30.5mb for one bitmap
        // If we use scale of 2, resolution will be halved, 1632 x 1224 and x 4 to get Bitmap memory of 7.62mb
        // We try use 25% memory which equals to 16mb maximum for one bitmap
        long maxMemory = Runtime.getRuntime().maxMemory();
        int maxMemoryForImage = (int) (maxMemory / 100 * 25);

        // Refer to
        // http://developer.android.com/training/displaying-bitmaps/cache-bitmap.html
        // A full screen GridView filled with images on a device with
        // 800x480 resolution would use around 1.5MB (800*480*4 bytes)
        // When bitmap option's inSampleSize doubled, pixel height and
        // weight both reduce in half
        int scale = 1;
        while ((o.outWidth / scale) * (o.outHeight / scale) * 4 > maxMemoryForImage)
        scale *= 2;

        // Decode with inSampleSize
        BitmapFactory.Options o2 = new BitmapFactory.Options();
        o2.inSampleSize = scale;
        fis = new FileInputStream(f);
        try
        {
            b = BitmapFactory.decodeStream(fis, null, o2);
        }
        finally
        {
            fis.close();
        }
    }
    catch (IOException e)
    {
    }
    return b;
}

我将此位图使用的最大内存设置为最大分配内存的25%,您可能需要根据自己的需要进行调整,并确保该位图已清理完毕并且在使用完后不留在内存中。通常,我使用此代码执行图像旋转(源位图和目标位图),因此我的应用需要同时在内存中加载2个位图,而25%的位图为我提供了良好的缓冲区,并且在执行图像旋转时不会耗尽内存。

希望这对外面的人有帮助。


11

OutofMemoryException通过调用等不能完全解决这些问题System.gc()

通过参考活动生命周期

活动状态由操作系统本身确定,取决于每个进程的内存使用情况和每个进程的优先级。

您可以考虑所使用的每个位图图片的大小和分辨率。我建议减小尺寸,重新采样以降低分辨率,请参考画廊的设计(一幅小图片PNG和一幅原始图片。)


11

此代码将有助于从drawable加载大位图

public class BitmapUtilsTask extends AsyncTask<Object, Void, Bitmap> {

    Context context;

    public BitmapUtilsTask(Context context) {
        this.context = context;
    }

    /**
     * Loads a bitmap from the specified url.
     * 
     * @param url The location of the bitmap asset
     * @return The bitmap, or null if it could not be loaded
     * @throws IOException
     * @throws MalformedURLException
     */
    public Bitmap getBitmap() throws MalformedURLException, IOException {       

        // Get the source image's dimensions
        int desiredWidth = 1000;
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;

        BitmapFactory.decodeResource(context.getResources(), R.drawable.green_background , options);

        int srcWidth = options.outWidth;
        int srcHeight = options.outHeight;

        // Only scale if the source is big enough. This code is just trying
        // to fit a image into a certain width.
        if (desiredWidth > srcWidth)
            desiredWidth = srcWidth;

        // Calculate the correct inSampleSize/scale value. This helps reduce
        // memory use. It should be a power of 2
        int inSampleSize = 1;
        while (srcWidth / 2 > desiredWidth) {
            srcWidth /= 2;
            srcHeight /= 2;
            inSampleSize *= 2;
        }
        // Decode with inSampleSize
        options.inJustDecodeBounds = false;
        options.inDither = false;
        options.inSampleSize = inSampleSize;
        options.inScaled = false;
        options.inPreferredConfig = Bitmap.Config.ARGB_8888;
        options.inPurgeable = true;
        Bitmap sampledSrcBitmap;

        sampledSrcBitmap =  BitmapFactory.decodeResource(context.getResources(), R.drawable.green_background , options);

        return sampledSrcBitmap;
    }

    /**
     * The system calls this to perform work in a worker thread and delivers
     * it the parameters given to AsyncTask.execute()
     */
    @Override
    protected Bitmap doInBackground(Object... item) {
        try { 
          return getBitmap();
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}

很好,您认为使用装载程序而不是异步任务会更好吗?
Chrispix

怎么Bitmap.Config.ARGB_565样 如果高质量不是关键。
Hamzeh Soboh,
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.