避免位图内存不足错误的建议

Posted

技术标签:

【中文标题】避免位图内存不足错误的建议【英文标题】:Suggestions to avoid bitmap Out of Memory error 【发布时间】:2012-12-23 12:06:12 【问题描述】:

我正在开发一个安卓应用程序。该应用程序有一个包含大量图像的视图。我有一个错误,我会尽量提供尽可能多的信息,希望有人能给我一些建议。

该应用程序在所有本地测试中运行良好。但是,我收到了很多来自用户的崩溃:java.lang.OutOfMemoryError: bitmap size exceeds VM budget

这是堆栈跟踪

0       java.lang.OutOfMemoryError: bitmap size exceeds VM budget
1   at  android.graphics.Bitmap.nativeCreate(Native Method)
2   at  android.graphics.Bitmap.createBitmap(Bitmap.java:507)
3   at  android.graphics.Bitmap.createBitmap(Bitmap.java:474)
4   at  android.graphics.Bitmap.createScaledBitmap(Bitmap.java:379)
5   at  android.graphics.BitmapFactory.finishDecode(BitmapFactory.java:498)
6   at  android.graphics.BitmapFactory.decodeStream(BitmapFactory.java:473)
7   at  android.graphics.BitmapFactory.decodeResourceStream(BitmapFactory.java:336)
8   at  android.graphics.BitmapFactory.decodeResource(BitmapFactory.java:359)
9   at  android.graphics.BitmapFactory.decodeResource(BitmapFactory.java:385)

我最大的问题是即使在旧设备上我也无法在本地重现该问题。

我已经实现了很多东西来尝试解决这个问题:

    没有内存泄漏:我确保完全没有内存泄漏。当我不需要视图时,我删除了它们。我还回收了所有位图并确保垃圾收集器正常工作。我在onDestroy() 方法中实现了所有必要的步骤 正确缩放的图像尺寸:在获取图像之前,我会获取其尺寸并计算inSampleSize堆大小:我还在获取图像之前检测最大堆大小并确保有足够的空间。如果不够,我会相应地重新缩放图像。

计算正确 inSampleSize 的代码

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)
      
         if(width > height)
         
            inSampleSize = Math.round((float) height / (float) reqHeight);
         
         else
         
            inSampleSize = Math.round((float) width / (float) reqWidth);
         
      
      return inSampleSize;
   

获取位图的代码

    // decodes image and scales it to reduce memory consumption
   private static Bitmap decodeFile(File file, int newWidth, int newHeight)
   // target size
      try
      

         Bitmap bmp = MediaStore.Images.Media.getBitmap(getContext().getContentResolver(), Uri.fromFile(file));
         if(bmp == null)
         
            // avoid concurrence
            // Decode image size
            BitmapFactory.Options option = new BitmapFactory.Options();

            // option = getBitmapOutput(file);

            option.inDensity = res.getDisplayMetrics().densityDpi < DisplayMetrics.DENSITY_HIGH ? 120 : 240;
            option.inTargetDensity = res.getDisplayMetrics().densityDpi;

            if(newHeight > 0 && newWidth > 0) 
                option.inSampleSize = calculateInSampleSize(option, newWidth, newWidth);

            option.inJustDecodeBounds = false;
            byte[] decodeBuffer = new byte[12 * 1024];
            option.inTempStorage = decodeBuffer;
            option.inPurgeable = true;
            option.inInputShareable = true;
            option.inScaled = true;

            bmp = BitmapFactory.decodeStream(new FileInputStream(file), null, option);
            if(bmp == null)
            
               return null;
            

         
         else
         
            int inDensity = res.getDisplayMetrics().densityDpi < DisplayMetrics.DENSITY_HIGH ? 120 : 240;
            int inTargetDensity = res.getDisplayMetrics().densityDpi;
            if(inDensity != inTargetDensity)
            
               int newBmpWidth = (bmp.getWidth() * inTargetDensity) / inDensity;
               int newBmpHeight = (bmp.getHeight() * inTargetDensity) / inDensity;
               bmp = Bitmap.createScaledBitmap(bmp, newBmpWidth, newBmpHeight, true);
            
         

         return bmp;
      
      catch(Exception e)
      
         Log.e("Error calling Application.decodeFile Method params: " + Arrays.toString(new Object[]file ), e);
      
      return null;
   

根据旧设备的堆大小计算图像大小的代码

private void calculateImagesSize()
   
      // only for android older than HoneyComb that does not support large heap
      if(Build.VERSION.SDK_INT < Constants.HONEYCOMB)
      
         long maxHeapSize = Runtime.getRuntime().maxMemory();
         long maxImageHeap = maxHeapSize - 10485760;
         if(Application.getResource().getDisplayMetrics().densityDpi >= DisplayMetrics.DENSITY_XHIGH)
         
            maxImageHeap -= 12 * 1048576;
         
         if(maxImageHeap < (30 * 1048576))
         
            int screenHeight = Math.min(Application.getResource().getDisplayMetrics().heightPixels, Application.getResource()
               .getDisplayMetrics().widthPixels);
            long maxImageSize = maxImageHeap / 100;
            long maxPixels = maxImageSize / 4;
            long maxHeight = (long) Math.sqrt(maxPixels / 1.5);
            if(maxHeight < screenHeight)
            
               drawableHeight = (int) maxHeight;
               drawableWidth = (int) (drawableHeight * 1.5);
            
         
      
   

我认为问题出在堆上,也许有时操作系统不允许应用程序使用 maxheapsize。另外我最大的问题是我无法重现该问题,因此当我尝试修复时,我必须稍等片刻,看看用户是否仍然收到错误。

我还可以尝试避免内存不足问题吗?任何建议将不胜感激。非常感谢

【问题讨论】:

拍摄一张非常大的图像并尝试将其与您的代码一起使用。我认为它会崩溃)我不喜欢这一行:Bitmap bmp = MediaStore.Images.Media.getBitmap(getContext().getContentResolver(), Uri.fromFile(file)); 我在另一个问题中写了一个建议总结:***.com/questions/11820266/… @Youssef,我认为你应该看看这个 - ***.com/a/15380872/1433187 我遇到了内存不足错误,那么这个解决方案对我来说非常有效。 这个问题你找到合适的解决方案了吗? @Stebra 不,我没有找到合适的解决方案。但最近我用官方 android 教程上的代码替换了上面的代码。 developer.android.com/training/displaying-bitmaps/index.html 检查该链接,他们有一个很棒的示例,您可以下载。我发现它比我拥有的更好,但仍然出现内存不足的错误。 【参考方案1】:

只需使用此功能解码...这是您错误的完美解决方案..因为我也遇到同样的错误并且我得到了这个解决方案..

public static Bitmap decodeFile(File f,int WIDTH,int HIGHT)
     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_WIDTH=WIDTH;
         final int REQUIRED_HIGHT=HIGHT;
         //Find the correct scale value. It should be the power of 2.
         int scale=1;
         while(o.outWidth/scale/2>=REQUIRED_WIDTH && o.outHeight/scale/2>=REQUIRED_HIGHT)
             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;
 

【讨论】:

您好,感谢您的建议,但我实际上正在做同样的事情,但进行了更高级的检查。我有你拥有的确切代码,但发生了崩溃。所以我修改了它以考虑更高级的东西。但是您的代码中的逻辑存在于我的代码中。谢谢 你用过这个功能吗? 是的,我首先使用它。但我得到了 OutOfMemoryError 所以我把它修改成了我今天的样子。它减少了错误,但它仍然出现 你的图片尺寸是多少? 为什么不使用 o2.inSampleSize = Math.max(o.outHeight,o.outWidth)/imageMaxSize; 而不是 while(o.outWidth/scale/2&gt;=REQUIRED_WIDTH &amp;&amp; o.outHeight/scale/2&gt;=REQUIRED_HIGHT) scale*=2; 根据文档“注意:解码器使用基于 2 的幂的最终值,任何其他值将向下舍入到最接近的幂2。”所以android会自动进行舍入。【参考方案2】:

您好,您必须对文件进行解码。为此尝试以下方法。

  public static Bitmap new_decode(File f) 

        // decode image size

        BitmapFactory.Options o = new BitmapFactory.Options();
        o.inJustDecodeBounds = true;
        o.inDither = false; // Disable Dithering mode

        o.inPurgeable = true; // Tell to gc that whether it needs free memory,
                                // the Bitmap can be cleared

        o.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
        try 
            BitmapFactory.decodeStream(new FileInputStream(f), null, o);
         catch (FileNotFoundException e1) 
            // TODO Auto-generated catch block
            e1.printStackTrace();
        

        // Find the correct scale value. It should be the power of 2.
        final int REQUIRED_SIZE = 300;
        int width_tmp = o.outWidth, height_tmp = o.outHeight;
        int scale = 1;
        while (true) 
            if (width_tmp / 1.5 < REQUIRED_SIZE && height_tmp / 1.5 < REQUIRED_SIZE)
                break;
            width_tmp /= 1.5;
            height_tmp /= 1.5;
            scale *= 1.5;
        

        // decode with inSampleSize
        BitmapFactory.Options o2 = new BitmapFactory.Options();
        // o2.inSampleSize=scale;
        o.inDither = false; // Disable Dithering mode

        o.inPurgeable = true; // Tell to gc that whether it needs free memory,
                                // the Bitmap can be cleared

        o.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
        // return BitmapFactory.decodeStream(new FileInputStream(f), null, o2);
        try 

//          return BitmapFactory.decodeStream(new FileInputStream(f), null,
//                  null);
            Bitmap bitmap= BitmapFactory.decodeStream(new FileInputStream(f), null, null);
            System.out.println(" IW " + width_tmp);
            System.out.println("IHH " + height_tmp);           
               int iW = width_tmp;
                int iH = height_tmp;

               return Bitmap.createScaledBitmap(bitmap, iW, iH, true);

         catch (OutOfMemoryError e) 
            // TODO: handle exception
            e.printStackTrace();
            // clearCache();

            // System.out.println("bitmap creating success");
            System.gc();
            return null;
            // System.runFinalization();
            // Runtime.getRuntime().gc();
            // System.gc();
            // decodeFile(f);
         catch (FileNotFoundException e) 
            // TODO Auto-generated catch block
            e.printStackTrace();
            return null;
        

    

【讨论】:

谢谢@MuhammedRefaat...:) 我不喜欢 System.gc() 解决方案。尝试找出它发生的原因,寻找内存泄漏等等。【参考方案3】:

通过缩小/缩放图像的大小,您可以摆脱内存不足异常, 试试这个

  BitmapFactory.Options options = new BitmapFactory.Options();
  options.inSampleSize = 6; 
  Bitmap receipt = BitmapFactory.decodeFile(photo.toString(),options);  //From File You can customise on your needs. 

【讨论】:

如果提供的图片很大,仍然会出现异常。 在这里你会失去质量【参考方案4】:

我在另一个 *** 问题中写了一个建议摘要:Android: BitmapFactory.decodeStream() out of memory with a 400KB file with 2MB free heap

【讨论】:

【参考方案5】:

实际上问题出在开发操作系统上。在与 ios 不同的 android 中,谷歌人基于相机分辨率开发了这个。位图占用大量内存,特别是对于照片等丰富的图像。不同的相机捕获具有不同像素的图像(不同的手机具有不同的相机像素容量)。在基于该像素的android中,只有捕获的图像才会占用内存。所以很明显,低像素容量的手机不会上传高分辨率图像。 在 android 操作系统中,每个应用程序最多分配 16MB。如果上传的图像超过此值,则会发生 java.lang.OutofMemoryError: bitmap size超出 VM 预算并导致应用程序崩溃。 参考这个 http://developer.android.com/training/displaying-bitmaps/index.html

【讨论】:

【参考方案6】:

如果你想避免OOM,你可以捕捉OOM并增加sampleSize直到图像可以解决:

private Bitmap getBitmapSafely(Resources res, int id, int sampleSize) 
// res = context.getResources(), id = R.drawable.yourimageid
    Bitmap bitmap = null;
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inPurgeable = true;
    options.inSampleSize = sampleSize;
    try 
          bitmap = BitmapFactory.decodeResource(res,
                      id, options);
     catch (OutOfMemoryError oom) 
        Log.w("ImageView", "OOM with sampleSize " + sampleSize, oom);
        System.gc();
        bitmap = getBitmapSafely(res, id, sampleSize + 1);
    

    return bitmap;

希望对你有帮助。

不适合捕捉错误,只是一种解决方法。

【讨论】:

无法捕获OutOfMemoryError @Melllvar 我测试了这些代码,我真的抓住了 OOM。你的意思是不适合抓OOM? 好的,所以我看到了很多相互矛盾的信息(例如,请参阅 this post)。我本人在try/catch(OutOfMemoryError) 内部也发生过不可恢复的崩溃,但我所看到的表明这或多或少是运气的问题(例如,这可能是可能的,但如果您确实从错误中恢复,则有可能错误将使程序处于不确定状态;因此无论如何都会发生崩溃)。无论如何,您的解决方案可能会奏效(我的立场是正确的)——但并非总是如此。 @Melllvar 谢谢,我需要了解更多关于异常和错误之间的差异,我已经编辑了我的帖子。 OutOfMemory 大部分时间永远不会收到catched。只是让应用程序崩溃。

以上是关于避免位图内存不足错误的建议的主要内容,如果未能解决你的问题,请参考以下文章

位图 - 内存不足异常

使用大型数据结构时,避免 Java(eclipse) 中的“内存不足错误”?

从服务器获取位图时内存不足?

Ruby 分块创建 tar 球以避免内存不足错误

增加堆以避免 WEKA 中的内存不足错误

如何避免由于太多ajax调用而导致浏览器内存不足错误