缩放和加载位图导致OOM(Android中的OutOfMemoryError)

5
我已经尝试了一千次自己解决这个问题,但始终没有成功。我已经记录并调试了应用程序,并从收集到的信息中得知有一个用于动画的大型精灵图。它是一个3000x1614像素大小的.png文件。在精灵图中有10x6个精灵对于动画至关重要。为了使其尽可能节省内存,在精灵之间没有任何间隙。 这是我的方法,将位图加载、解码和调整大小成用户手机的纵横比与我正在构建游戏的手机相同:
public Pixmap newResizedPixmap(String fileName, PixmapFormat format, double Width, double Height) {
    // TODO Auto-generated method stub
    Config config = null;
    if (format == PixmapFormat.RGB565)
        config = Config.RGB_565;
    else if (format == PixmapFormat.ARGB4444)
        config = Config.ARGB_4444;
    else
        config = Config.ARGB_8888;

    Options options = new Options();
    options.inPreferredConfig = config;
    options.inPurgeable=true;
    options.inInputShareable=true;
    options.inDither=false;

    InputStream in = null;
    Bitmap bitmap = null;
    try {
        //OPEN FILE INTO INPUTSTREAM
        in = assets.open(fileName);
        //DECODE INPUTSTREAM
        bitmap = BitmapFactory.decodeStream(in, null, options);

        if (bitmap == null)
            throw new RuntimeException("Couldn't load bitmap from asset '"+fileName+"'");
    } catch (IOException e) {
        throw new RuntimeException("Couldn't load bitmap from asset '"+fileName+"'");
    } finally {
        if (in != null) {
            try {
                in.close();
            } catch (IOException e) {
            }
        }
    }

    if (bitmap.getConfig() == Config.RGB_565)
        format = PixmapFormat.RGB565;
    else if (bitmap.getConfig() == Config.ARGB_4444)
        format = PixmapFormat.ARGB4444;
    else
        format = PixmapFormat.ARGB8888;

    //THIS IS WHERE THE OOM HAPPENS
    return new AndroidPixmap(Bitmap.createScaledBitmap(bitmap, (int)(Width+0.5f), (int)(Height+0.5f), true), format);
}

以下是错误的LogCat输出:

05-07 12:57:18.570: E/dalvikvm-heap(636): Out of memory on a 29736016-byte allocation.
05-07 12:57:18.570: I/dalvikvm(636): "Thread-89" prio=5 tid=18 RUNNABLE
05-07 12:57:18.580: I/dalvikvm(636):   | group="main" sCount=0 dsCount=0 obj=0x414a3c78 self=0x2a1b1818
05-07 12:57:18.580: I/dalvikvm(636):   | sysTid=669 nice=0 sched=0/0 cgrp=apps handle=705796960
05-07 12:57:18.590: I/dalvikvm(636):   | schedstat=( 14462294890 67436634083 2735 ) utm=1335 stm=111 core=0
05-07 12:57:18.590: I/dalvikvm(636):   at android.graphics.Bitmap.nativeCreate(Native Method)
05-07 12:57:18.590: I/dalvikvm(636):   at android.graphics.Bitmap.createBitmap(Bitmap.java:640)
05-07 12:57:18.590: I/dalvikvm(636):   at android.graphics.Bitmap.createBitmap(Bitmap.java:586)
05-07 12:57:18.590: I/dalvikvm(636):   at android.graphics.Bitmap.createScaledBitmap(Bitmap.java:466)
05-07 12:57:18.590: I/dalvikvm(636):   at com.NAME.framework.impl.AndroidGraphics.newResizedPixmap(AndroidGraphics.java:136)
05-07 12:57:18.590: I/dalvikvm(636):   at com.NAME.GAME.GameScreen.<init>(GameScreen.java:121)
05-07 12:57:18.600: I/dalvikvm(636):   at com.NAME.GAME.CategoryScreen.update(CategoryScreen.java:169)
05-07 12:57:18.600: I/dalvikvm(636):   at com.NAME.framework.impl.AndroidFastRenderView.run(AndroidFastRenderView.java:34)
05-07 12:57:18.600: I/dalvikvm(636):   at java.lang.Thread.run(Thread.java:856)
05-07 12:57:18.620: I/Choreographer(636): Skipped 100 frames!  The application may be doing too much work on its main thread.
05-07 12:57:18.670: W/dalvikvm(636): threadid=18: thread exiting with uncaught exception (group=0x40a13300)
05-07 12:57:18.720: E/AndroidRuntime(636): FATAL EXCEPTION: Thread-89
05-07 12:57:18.720: E/AndroidRuntime(636): java.lang.OutOfMemoryError
05-07 12:57:18.720: E/AndroidRuntime(636):  at android.graphics.Bitmap.nativeCreate(Native Method)
05-07 12:57:18.720: E/AndroidRuntime(636):  at android.graphics.Bitmap.createBitmap(Bitmap.java:640)
05-07 12:57:18.720: E/AndroidRuntime(636):  at android.graphics.Bitmap.createBitmap(Bitmap.java:586)
05-07 12:57:18.720: E/AndroidRuntime(636):  at android.graphics.Bitmap.createScaledBitmap(Bitmap.java:466)
05-07 12:57:18.720: E/AndroidRuntime(636):  at com.NAME.framework.impl.AndroidGraphics.newResizedPixmap(AndroidGraphics.java:136)
05-07 12:57:18.720: E/AndroidRuntime(636):  at com.NAME.GAME.GameScreen.<init>(GameScreen.java:121)
05-07 12:57:18.720: E/AndroidRuntime(636):  at com.NAME.GAME.CategoryScreen.update(CategoryScreen.java:169)
05-07 12:57:18.720: E/AndroidRuntime(636):  at com.NAME.framework.impl.AndroidFastRenderView.run(AndroidFastRenderView.java:34)
05-07 12:57:18.720: E/AndroidRuntime(636):  at java.lang.Thread.run(Thread.java:856)
05-07 12:57:18.847: D/Activity:(636): Pausing...

这是使用DDMS的内存泄露报告:
问题嫌疑人1:一个由"dalvik.system.PathClassLoader @ 0x41a06f90"加载的"com.NAME.framework.impl.AndroidPixmap"实例占用了12,393,680(54.30%)字节的内存。内存累积在一个由""加载的"byte[]"实例中。
关键词:dalvik.system.PathClassLoader @ 0x41a06f90、com.NAME.framework.impl.AndroidPixmap、byte[]
详情 »
问题嫌疑人2:由""加载的类"android.content.res.Resources"占用了4,133,808(18.11%)字节的内存。内存累积在一个由""加载的"java.lang.Object[]"实例中。
关键词:java.lang.Object[]、android.content.res.Resources
详情 »
问题嫌疑人3:8个由""加载的"android.graphics.Bitmap"实例占用了2,696,680(11.82%)字节的内存。
最大的实例:•android.graphics.Bitmap @ 0x41462ba0 - 1,048,656(4.59%)字节。•android.graphics.Bitmap @ 0x41a08cf0 - 768,064(3.37%)字节。•android.graphics.Bitmap @ 0x41432990 - 635,872(2.79%)字节。
关键词:android.graphics.Bitmap 详情 »
附加信息: 我使用RGB565或ARGB4444作为图像的配置。我希望对于带有渐变的图像使用ARGB8888,但它会占用太多内存。另一件需要注意的事情是我的Assets类,其中包含游戏使用的所有"Pixmap"。从assets加载的位图存储在这些"Pixmap"中,并在不再需要时(即位图)被删除。也许有更好的存储这些对象的方法吗?请告诉我:
public static Pixmap image1;
public static Pixmap image2;
public static Pixmap image3;
public static Pixmap image4;
public static Pixmap image5;
public static Pixmap image6;
public static Pixmap image7;
public static Pixmap image8;
public static Pixmap image9;
public static Pixmap image10;
public static Pixmap image11;
...

请注意,OOM仅会在分辨率高于我的设备(800x480)的设备上发生,这是因为位图被缩放以适应更大的设备。请注意,这些图像正在画布上绘制,因为我正在开发游戏,对OpenGL不太熟悉。
编辑#1:为了确保您知道我的问题是什么 我在以下行中遇到OOM错误:
Bitmap.createScaledBitmap(bitmap, (int)(Width), (int)(Height), true)

我需要帮助!这是我发布这个可恶游戏的最后障碍!


编辑 #2:我尝试过但无效的新方法

我尝试将解码后的位图添加到 LruCache 中再使用它们。我还尝试了一种方法,创建一个位图映射到临时文件中,然后再次加载它。就像这样:

活动

// Get memory class of this device, exceeding this amount will throw an
// OutOfMemory exception.
final int memClass = ((ActivityManager) getSystemService(
        Context.ACTIVITY_SERVICE)).getMemoryClass();

// Use 1/8th of the available memory for this memory cache.
final int cacheSize = 1024 * 1024 * memClass / 8;

mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
    @Override
    protected int sizeOf(String key, Bitmap bitmap) {
        // The cache size will be measured in kilobytes rather than
        // number of items.
        return (bitmap.getRowBytes() * bitmap.getHeight()) / 1024;
    }
};

加载位图

        //Copy the byte to the file
//Assume source bitmap loaded using options.inPreferredConfig = Config.ARGB_8888;
FileChannel channel = randomAccessFile.getChannel();
MappedByteBuffer map = null;
try {
    map = channel.map(MapMode.READ_WRITE, 0, width*height*4);
} catch (IOException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
}
bitmap.copyPixelsToBuffer(map);
//recycle the source bitmap, this will be no longer used.
bitmap.recycle();
//Create a new bitmap to load the bitmap again.
bitmap = Bitmap.createBitmap(width, height, config);
map.position(0);
//load it back from temporary 
bitmap.copyPixelsFromBuffer(map);
//close the temporary file and channel , then delete that also
try {
    channel.close();
    randomAccessFile.close();
} catch (IOException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
}

((AndroidGame) activity).addBitmapToMemoryCache(""+fileName, Bitmap.createScaledBitmap(bitmap, (int)(Width), (int)(Height), true));

return new AndroidPixmap(((AndroidGame) activity).getBitmapFromMemCache(""+fileName), format);

编辑#3:如果您不知道...

我正在放大位图,而不是缩小它们,因此在这种情况下,inSampleSize对我没有帮助。我可以说现在已经尝试过inSampleSize,但所有东西都变得非常模糊,我甚至无法看清哪些东西是什么,即使我将其设置为2!


那么,我该怎么做来解决这个问题?如何能够加载大量经过缩放的位图,同时保持其质量而不会导致OOM错误?


请查看https://dev59.com/puo6XIcBkEYKwwoYTzNK以及所有相关问题。 - EricRobertBrewer
很抱歉,根据我所读的没有什么可以帮到我。对于那些问题,最常建议的解决方案是实现inSampleSize,这会降低图像的质量,而我不想这样做,因为我需要放大这些图像。 - Henrik Söderlund
你需要同时使用多少个位图?如果你必须使用多于一个的话,那么在创建多少个位图后会收到OOM(内存不足)错误?你能否一次只保留一个位图在内存中?另外一个问题是为什么宽度和高度是double类型然后再转换成int类型?(你是否检查过转换是否成功?) - EyalBellisha
我只使用精灵表的1个位图,因为只有一个实例需要它。我同时使用25个不同的位图(比这个小得多),并且在加载之前使用的所有位图都被预先回收,因为它们不再需要。我将宽度和高度转换为int(宽度和高度是双倍的,因为我将图像的原始大小乘以在问题开头提到的比例计算出来),以消除可能存在的小数,因为像素不能分成一半。(实际上我忘记在那里添加+0.5f,但现在已经修复了。) - Henrik Söderlund
我会尝试在完全不同的进程中加载这个位图,只是为了看看它是否可以被加载(当没有其他位图被缓存时)。如果可以完成,那么我会考虑两个选项:将同时缓存的小位图数量减少到最低限度,或者(一个更复杂的解决方案)让一个单独的进程为您保存此位图,并让您逐个访问矩形(我曾经使用过一个服务来实现这一点)。 - EyalBellisha
显示剩余6条评论
6个回答

4
这是一种解决方法,但比直接缩放位图需要更长的时间。
  1. 首先,将原始位图解码为原始大小
  2. 将其分成小位图,本例中为10个位图,宽度= originalWidth / 10,高度= originalHeight。并且每个位图的宽度都缩放到目标/10的大小,然后保存到文件中(我们逐个执行此操作,并在每个步骤后释放内存)。
  3. 释放原始位图
  4. 创建新位图以达到目标大小。
  5. 解码之前保存在文件中的每个位图,并绘制到新位图上。存储新缩放的位图以供将来使用。
这种方法类似于:
public Bitmap newResizedPixmapWorkAround(Bitmap.Config format,
            double width, double height) {
        int[] pids = { android.os.Process.myPid() };
        MemoryInfo myMemInfo = mAM.getProcessMemoryInfo(pids)[0];
        Log.e(TAG, "dalvikPss (beginning) = " + myMemInfo.dalvikPss);

        Config config = null;
        if (format == Bitmap.Config.RGB_565) {
            config = Config.RGB_565;
        } else if (format == Bitmap.Config.ARGB_4444) {
            config = Config.ARGB_4444;
        } else {
            config = Config.ARGB_8888;
        }

        Options options = new Options();
        options.inPreferredConfig = config;
        options.inPurgeable = true;
        options.inInputShareable = true;
        options.inDither = false;

        InputStream in = null;
        Bitmap bitmap = null;
        try {
            // OPEN FILE INTO INPUTSTREAM
            String path = Environment.getExternalStorageDirectory()
                    .getAbsolutePath();
            path += "/sprites.png";
            File file = new File(path);
            in = new FileInputStream(file);
            // DECODE INPUTSTREAM
            bitmap = BitmapFactory.decodeStream(in, null, options);

            if (bitmap == null) {
                Log.e(TAG, "bitmap == null");
            }
        } catch (IOException e) {
            Log.e(TAG, "IOException " + e.getMessage());
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                }
            }
        }

        if (bitmap.getConfig() == Config.RGB_565) {
            format = Bitmap.Config.RGB_565;
        } else if (bitmap.getConfig() == Config.ARGB_4444) {
            format = Bitmap.Config.ARGB_4444;
        } else {
            format = Bitmap.Config.ARGB_8888;
        }

        myMemInfo = mAM.getProcessMemoryInfo(pids)[0];
        Log.e(TAG, "dalvikPss (before separating) = " + myMemInfo.dalvikPss);

        // Create 10 small bitmaps and store into files.
        // After that load each of them and append to a large bitmap

        int desSpriteWidth = (int) (width + 0.5) / 10;
        int desSpriteAppend = (int) (width + 0.5) % 10;
        int desSpriteHeight = (int) (height + 0.5);

        int srcSpriteWidth = bitmap.getWidth() / 10;
        int srcSpriteHeight = bitmap.getHeight();

        Rect srcRect = new Rect(0, 0, srcSpriteWidth, srcSpriteHeight);
        Rect desRect = new Rect(0, 0, desSpriteWidth, desSpriteHeight);

        String pathPrefix = Environment.getExternalStorageDirectory()
                .getAbsolutePath() + "/sprites";

        for (int i = 0; i < 10; i++) {
            Bitmap sprite;
            if (i < 9) {
                sprite = Bitmap.createBitmap(desSpriteWidth, desSpriteHeight,
                        format);
                srcRect.left = i * srcSpriteWidth;
                srcRect.right = (i + 1) * srcSpriteWidth;
            } else { // last part
                sprite = Bitmap.createBitmap(desSpriteWidth + desSpriteAppend,
                        desSpriteHeight, format);
                desRect.right = desSpriteWidth + desSpriteAppend;
                srcRect.left = i * srcSpriteWidth;
                srcRect.right = bitmap.getWidth();
            }
            Canvas canvas = new Canvas(sprite);
            // NOTE: if using this drawBitmap method of canvas do not result
            // good quality scaled image, you can try create tile image with the
            // same size original then use Bitmap.createScaledBitmap()
            canvas.drawBitmap(bitmap, srcRect, desRect, null);

            String path = pathPrefix + i + ".png";
            File file = new File(path);
            try {
                if (file.exists()) {
                    file.delete();
                }
                file.createNewFile();
                FileOutputStream fos = new FileOutputStream(file);
                sprite.compress(Bitmap.CompressFormat.PNG, 100, fos);
                fos.flush();
                fos.close();
                sprite.recycle();
            } catch (FileNotFoundException e) {
                Log.e(TAG,
                        "FileNotFoundException  at tile " + i + " "
                                + e.getMessage());
            } catch (IOException e) {
                Log.e(TAG, "IOException  at tile " + i + " " + e.getMessage());
            }
        }

        bitmap.recycle();

        bitmap = Bitmap.createBitmap((int) (width + 0.5f),
                (int) (height + 0.5f), format);
        Canvas canvas = new Canvas(bitmap);

        desRect = new Rect(0, 0, 0, bitmap.getHeight());

        for (int i = 0; i < 10; i++) {
            String path = pathPrefix + i + ".png";
            File file = new File(path);
            try {
                FileInputStream fis = new FileInputStream(file);
                // DECODE INPUTSTREAM
                Bitmap sprite = BitmapFactory.decodeStream(fis, null, options);
                desRect.left = desRect.right;
                desRect.right = desRect.left + sprite.getWidth();
                canvas.drawBitmap(sprite,  null, desRect, null);
                sprite.recycle();
            } catch (IOException e) {
                Log.e(TAG, "IOException  at tile " + i + " " + e.getMessage());
            }
        }

        myMemInfo = mAM.getProcessMemoryInfo(pids)[0];
        Log.e(TAG, "dalvikPss (before scaling) = " + myMemInfo.dalvikPss);

        // THIS IS WHERE THE OOM HAPPENS
        return bitmap;
    }

我已经用我的手机进行了测试,当缩放到1.5时,它可以正常运行而不会出现OOM错误(原始方法会遇到OOM错误)


这看起来非常有趣!精灵图包含了6行中每行10个精灵。总共有60个精灵。这种方法会起作用吗?还是有什么需要改变的地方? - Henrik Söderlund
我简直不敢相信...它居然成功了!不过我有一个问题。我注意到你在代码中的一个注释中提到,如果位图质量不好,我应该再次使用Bitmap.createScaledBitmap()。我该如何在不破坏代码并再次引起OOM的情况下将其实现到你的代码中呢? - Henrik Söderlund
根据我的经验,Bitmap.createScaledBitmap方法提供的缩放质量比Android SDK支持的其他缩放方法(如BitmapFactory.decodeImage或Canvas.drawBitmap)要好,或者至少相等,就像我在上面的代码中使用的那样。因此,如果您发现位图质量不如预期,可以修改循环中创建精灵的代码,如下所示: - Binh Tran
位图精灵; 如果 (i < 9) { srcRect.left = i * srcSpriteWidth; srcRect.right = (i + 1) * srcSpriteWidth; } else { // 最后一部分 srcRect.left = i * srcSpriteWidth; srcRect.right = bitmap.getWidth(); } desRect.right = srcRect.width(); desRect.bottom = srcRect.height(); sprite = Bitmap.createBitmap(srcRect.width(), srcRect.height(), format); Canvas canvas = new Canvas(sprite); canvas.drawBitmap(bitmap, srcRect, desRect, null); - Binh Tran
如果 (i < 9) { sprite = Bitmap.createScaledBitmap(sprite, desSpriteWidth, desSpriteHeight, true); } else { // 最后一部分 sprite = Bitmap.createScaledBitmap(sprite, desSpriteWidth + desSpriteAppend, desSpriteHeight, true); } - Binh Tran
1
我尝试在Canvas.drawBitmap方法中添加Paint.FILTER_BITMAP_FLAG来进行绘制,这也成功了。 - Henrik Söderlund

1
你为什么要放大位图?在绘制时只需按比例缩放即可。
3000x1614相当大,如果你可以放弃对<=2.2的支持;http://developer.android.com/reference/android/graphics/BitmapRegionDecoder.html 可以加载位图的特定部分(精灵)。
你正在将新的位图传递到AndroidPixmap的构造函数中,并且看起来你正在保留它们的静态引用 - 你是如何回收它们的?
作为一个简单的解决方案,您可以在清单中使用<application ... android:largeHeap="true" ...>请求更多内存; http://developer.android.com/guide/topics/manifest/application-element.html#largeHeap 我相信这需要 > =3.0。

我在加载时将它们放大以利用Bitmap.createScaledBitmap(Bitmap, boolean)附带的过滤方法。 整个位图已经尽可能地压缩,并且整个精灵表都是必需的,因为它包含所有必要的精灵。 我在加载其他内容之前回收不需要的部分(例如,当我离开菜单时,位图被回收,然后加载游戏其他部分需要的新位图,反之亦然,以节省内存)。 我已经在使用它,但它不起作用(在一个4.1设备上)。 - Henrik Söderlund
将绘制位图时使用的画笔设置为使用相同的过滤器:http://developer.android.com/reference/android/graphics/Paint.html#setFilterBitmap%28boolean%29 - FunkTheMonk
需要一次性加载整个精灵集吗?如果您所说的压缩是文件压缩,那么在读取位图时并没有什么区别,因为它将在内存中需要未压缩的大小:4444格式大约9兆。 - FunkTheMonk
我所说的压缩是指精灵之间的间距为0像素。整个精灵表一次性使用,是的。 - Henrik Söderlund
我投票支持这个问题,因为在这里 largeHeap="true" 对我有用,但是它会成为所有设备的解决方案吗? - Armando Marques da S Sobrinho

0

试试这个函数。

void scaleBitmap()
{


        BitmapFactory.Options o = new BitmapFactory.Options();
        o.inJustDecodeBounds = true;
        o.inPurgeable = true;
        o.inScaled = true;
        BitmapFactory.decodeStream(new FileInputStream(f), null, o);
        int width_temp = o.outWidth, height_temp = o.outHeight;
        int scale = 1;
        while (true) {
                if (width_temp / 2 < 80 || height_temp / 2 < 80)
                    break;
                width_temp /= 2;
                height_temp /= 2;
                scale *= 2;
            }

            BitmapFactory.Options o2 = new BitmapFactory.Options();
            o2.inSampleSize = scale;
            return BitmapFactory.decodeStream(new FileInputStream(f), null,
                    o2);
}

抱歉,但我正在放大位图,而不是缩小它们,因此inSampleSize只能帮助解决OOM问题,但会进一步恶化质量,所以这不起作用。 - Henrik Söderlund

0

我相信我需要一个ImageView来展示这些例子,但是在我的应用程序中我没有使用。正如帖子中提到的那样,我是在画布上绘制我的位图。 - Henrik Söderlund

-1
将您的图像转换为位图对象。并使用位图类的API来缩放图像,以避免内存溢出错误。
       Bitpmap bm = BitmapFactory.decode ("image file path");
       bm.setDensity(integer);
       Bitmap bm1 = Bitmap.createScaledBitmap (bm, width, height, false);
       imageView.setImageBitmap(bm1);

尝试设置宽度和高度的值,以避免内存泄漏。希望这能帮到你。

不行!因为如果你使用inputstream解码位图 -> 内存溢出!!而且如果图像在路径中比Java VM的可用空间大 -> 内存溢出。为了测试你的解决方案,复制/粘贴decode()多次。 - VincentLamoute

-3

使用 -Xmx VM 参数允许 JVM 使用更多内存。

例如,要允许 JVM 使用 1 GB(1024 MB)的内存:

java -Xmx1024m

OR

使用任何图片上传器并在演示中传递图像URL

1)Nostra的通用图像加载器

2)Fedor的LazyList。以及;

3)Novoda的ImageLoader


1
是的!这是用于堆的!但是你如何将其传递给VM Android? - VincentLamoute
@anthonymasson:你打开了JVM中的链接吗? - Nirav Ranpara
他将如何让Dalvik实现这一点? - snowCrabs
1
我找到了这个:android:largeHeap="true"。有人知道它是否有效吗? - Henrik Söderlund
java -Xmx1024m -> Android 不使用 java jvm。 - Teovald
这不适用于Android。表面上因为它使用的是dalvik虚拟机而不是Java虚拟机。但真正的问题在于它使用的虚拟机已经被创建并运行,并从另一个虚拟机派生 - 因此开发人员无法在虚拟机上设置命令行参数。 - Chris Stratton

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