加载图像到Bitmap对象时出现奇怪的OutOfMemory问题

1386

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

ListView上的图像预览是使用游标和ListAdapter完成的。这使得它非常简单,但我不确定如何在飞行中放置调整大小的图像(即较小的位大小而不是像素作为图像按钮的src)。因此,我只调整了从手机相机拍摄的图像的大小。

问题是当它尝试返回并重新启动第二个活动时,会出现OutOfMemoryError

  • 有没有一种简单的方法可以逐行构建列表适配器,其中我可以在飞行中调整大小(按位)?

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

  • 我知道我可以进行带外调整大小并保存我的图像,但这并不是我想做的事情,但一些关于那方面的示例代码会很好。

一旦我禁用了ListView上的图像,它就可以正常工作了。

顺便说一句:这就是我做的方式:

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.imagefilename 是一个 ButtonImage

以下是我的 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 

当显示图片时,我也遇到了一个新的错误:

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

9
我通过避免使用Bitmap.decodeStream或decodeFile方法,改用BitmapFactory.decodeFileDescriptor方法来解决这个问题。 - Fraggle
2
我几周前也遇到了类似的问题,通过将图像缩小到最佳点来解决它。我在我的博客http://codingjunkiesforum.wordpress.com/2014/06/12/outofmemory-due-to-large-bitmap-handling-in-android/中写了完整的方法,并上传了完整的示例项目,其中包含OOM易感代码与OOM证明代码https://github.com/shailendra123/BitmapHandlingDemo。 - Shailendra Singh Rajawat
6
这个问题的被接受答案正在元社区上讨论。 - rene
2
阅读这篇博客文章:http://codingaffairs.blogspot.com/2016/07/processing-bitmap-and-memory-management.html - Developine
5
这是由于糟糕的Android架构造成的。它应该像iOS和UWP一样自动调整图像大小。我不需要亲自做这些事情。Android开发人员习惯了那种困难,认为它可以正常工作。 - Access Denied
显示剩余3条评论
44个回答

18
在我的一个应用程序中,我需要从相机/相册中拍摄照片。如果用户从相机中点击图像(可能是2MP、5MP或8MP),则图像大小从kB到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;

}

希望这能帮助遇到同样问题的伙伴们!

了解更多,请参考this


16

这个问题只出现在Android模拟器中。我也在模拟器中遇到过这个问题,但当我在设备上检查时,它可以正常工作。

因此,请在设备上进行检查。它可能会在设备上运行。


16

我几分钟前遇到了这个问题。通过更好地管理我的ListView适配器来解决了它。我以为问题是我使用的数百个50x50像素图像,后来发现我每次显示行时都会尝试膨胀自定义视图。仅通过测试看是否已经膨胀了该行,我就消除了这个错误,并且我正在使用数百个位图。实际上,这是用于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

15
我花了整整一天的时间测试这些解决方案,唯一有效的方法是采用上述获取图片并手动调用GC的方法。我知道这不应该是必要的,但当我在我的应用程序中进行重负载测试并在活动之间切换时,这是唯一有效的方法。我的应用程序在一个列表视图(假设为活动A)中有一组缩略图像,当您单击其中一个图像时,它会带您进入另一个活动(假设为活动B),显示该项的主要图像。当我在两个活动之间来回切换时,最终会出现OOM错误并导致应用程序强制关闭。
当我滚动到列表视图的中途时,它就会崩溃。
现在,当我在活动B中实现以下内容时,我可以顺利地浏览整个列表视图,并且速度非常快。
@Override
public void onDestroy()
{   
    Cleanup();
    super.onDestroy();
}

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

15

一般而言,安卓设备的堆大小只有16MB(因设备/操作系统而异,请参见Heap Sizes),如果您加载的图像超过16MB,则会引发内存不足异常。与其使用位图来加载图像,建议从SD卡、资源甚至网络中尝试使用getImageUri,因为加载位图需要更多内存,或者在使用完位图后将其设置为空。


1
如果setImageURI仍然出现异常,请参考此链接http://stackoverflow.com/questions/15377186/decode-file-from-sdcard-android-to-avoid-out-of-memory-error-due-to-large-bitmap。 - Mahesh

15

在这里提供的所有解决方案都需要设置一个IMAGE_MAX_SIZE。这会限制那些具备更强硬件的设备,而如果图像尺寸太小,在高清屏幕上看起来也不好看。

我想了一个解决方案,适用于我的三星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%的缓冲区可以很好地避免在执行图像旋转时内存不足。

希望这能帮助某些人..


15

使用这些代码可以将选择自SD卡或drawable中的每个图像转换为位图对象。

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


14

这段代码可以帮助从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;
    }
}

14

我的建议是:通过以下方法解决位图内存不足错误:

a)将图像缩放2倍

b)在自定义ListView适配器中使用Picasso库,并在getView中进行一次调用,如下所示:Picasso.with(context).load(R.id.myImage).into(R.id.myImageView);


14

“OutofMemoryException”无法通过调用“System.gc()”等方式完全解决。

参考Activity Life Cycle

活动状态由操作系统根据每个进程的内存使用情况和每个进程的优先级确定。

您可以考虑所使用的每个位图图片的大小和分辨率。我建议将其缩小并重新采样到较低的分辨率,参考相册的设计(一个小的PNG图片和一个原始图片)。


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