虽然使用了缩小的样本大小,但 BitmapFactory.decodeStream 仍然出现了内存不足的问题。

13
我是一位有用的助手,可以为您进行文本翻译。以下是需要翻译的内容:

我已经阅读了许多与解码位图时内存分配问题相关的帖子,但即使使用官方网站提供的代码,仍然无法找到解决以下问题的方法。

这是我的代码:

public static Bitmap decodeSampledBitmapFromResource(InputStream inputStream, int reqWidth, int reqHeight) {

    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    byte[] buffer = new byte[1024];
    int len;
    try {
        while ((len = inputStream.read(buffer)) > -1) {
        baos.write(buffer, 0, len);
        }
        baos.flush();
        InputStream is1 = new ByteArrayInputStream(baos.toByteArray());
        InputStream is2 = new ByteArrayInputStream(baos.toByteArray());

        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeStream(is1, null, options);

        options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
        options.inPurgeable = true;
        options.inInputShareable = true;
        options.inJustDecodeBounds = false;
        options.inPreferredConfig = Bitmap.Config.ARGB_8888;
        return BitmapFactory.decodeStream(is2, null, options);

    } catch (Exception e) {
        e.printStackTrace();

        return null;
    }
}

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

        // Calculate ratios of height and width to requested height and width
        final int heightRatio = Math.round((float) height / (float) reqHeight);
        final int widthRatio = Math.round((float) width / (float) reqWidth);

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

bitmap = decodeSampledBitmapFromResource(inputStream, 600, 600); 

我在这一行代码中遇到了"Out of memory error on a 3250016 - byte allocation" 的内存溢出错误:
return BitmapFactory.decodeStream(is2, null, options);

我认为3.2 MB足够小,可以分配。我错在哪里?我该怎么解决?
编辑
在查看N-Joy的HERE解决方案后,它对于所需大小为300有效,但我的所需大小为800,因此仍然出现错误。

1
请查看这些解决方案 https://dev59.com/peo6XIcBkEYKwwoYQiO7#10408877 - N-JOY
int inSampleSize = 这个值必须是2的倍数;更改它并检查。 - Harish
10个回答

36

方法decodeSampledBitmapFromResource不够内存高效,因为它使用了3个流:ByteArrayOutputStream baos、ByteArrayInputStream is1和ByteArrayInputStream is2,每个都存储着相同的图像流数据(每个流对应一个字节数组)。

当我使用我的设备(LG nexus 4)测试将SD卡上的2560x1600图像解码为目标大小800时,大约需要这么长时间:

03-13 15:47:52.557: E/DecodeBitmap(11177): dalvikPss (beginning) = 1780
03-13 15:47:53.157: E/DecodeBitmap(11177): dalvikPss (decoding) = 26393
03-13 15:47:53.548: E/DecodeBitmap(11177): dalvikPss (after all) = 30401 time = 999

我们可以看到:为了解码一个4096000像素的图像,分配了太多的内存(28.5 MB)。

解决方案:我们读取InputStream并将数据直接存储到一个字节数组中,并使用该字节数组进行后续操作。
示例代码:

public Bitmap decodeSampledBitmapFromResourceMemOpt(
            InputStream inputStream, int reqWidth, int reqHeight) {

        byte[] byteArr = new byte[0];
        byte[] buffer = new byte[1024];
        int len;
        int count = 0;

        try {
            while ((len = inputStream.read(buffer)) > -1) {
                if (len != 0) {
                    if (count + len > byteArr.length) {
                        byte[] newbuf = new byte[(count + len) * 2];
                        System.arraycopy(byteArr, 0, newbuf, 0, count);
                        byteArr = newbuf;
                    }

                    System.arraycopy(buffer, 0, byteArr, count, len);
                    count += len;
                }
            }

            final BitmapFactory.Options options = new BitmapFactory.Options();
            options.inJustDecodeBounds = true;
            BitmapFactory.decodeByteArray(byteArr, 0, count, options);

            options.inSampleSize = calculateInSampleSize(options, reqWidth,
                    reqHeight);
            options.inPurgeable = true;
            options.inInputShareable = true;
            options.inJustDecodeBounds = false;
            options.inPreferredConfig = Bitmap.Config.ARGB_8888;

            int[] pids = { android.os.Process.myPid() };
            MemoryInfo myMemInfo = mAM.getProcessMemoryInfo(pids)[0];
            Log.e(TAG, "dalvikPss (decoding) = " + myMemInfo.dalvikPss);

            return BitmapFactory.decodeByteArray(byteArr, 0, count, options);

        } catch (Exception e) {
            e.printStackTrace();

            return null;
        }
    }

计算的方法:

public void onButtonClicked(View v) {
        int[] pids = { android.os.Process.myPid() };
        MemoryInfo myMemInfo = mAM.getProcessMemoryInfo(pids)[0];
        Log.e(TAG, "dalvikPss (beginning) = " + myMemInfo.dalvikPss);

        long startTime = System.currentTimeMillis();

        FileInputStream inputStream;
        String filePath = Environment.getExternalStorageDirectory()
                .getAbsolutePath() + "/test2.png";
        File file = new File(filePath);
        try {
            inputStream = new FileInputStream(file);
//          mBitmap = decodeSampledBitmapFromResource(inputStream, 800, 800);
            mBitmap = decodeSampledBitmapFromResourceMemOpt(inputStream, 800,
                    800);
            ImageView imageView = (ImageView) findViewById(R.id.image);
            imageView.setImageBitmap(mBitmap);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
        myMemInfo = mAM.getProcessMemoryInfo(pids)[0];
        Log.e(TAG, "dalvikPss (after all) = " + myMemInfo.dalvikPss
                + " time = " + (System.currentTimeMillis() - startTime));
    }

结果如下:

03-13 16:02:20.373: E/DecodeBitmap(13663): dalvikPss (beginning) = 1823
03-13 16:02:20.923: E/DecodeBitmap(13663): dalvikPss (decoding) = 18414
03-13 16:02:21.294: E/DecodeBitmap(13663): dalvikPss (after all) = 18414 time = 917

使用这个方法,我只得到了一个灰色/黑色的位图,而不是我正在解码的图片,有什么想法为什么会这样? - Donal Rafferty
太棒了,正是我在寻找的解决方案。唯一需要改变的是我没有将文件存储在外部SD卡上,因此只需将它们从我的drawable文件夹移动到raw文件夹中即可。 - SingleWave Games
mAM是什么?它是哪种类型的变量?@Binh Tran - Crishnan Kandada
@Crishnan mAM 是 ActivityManager 的一个实例。 - Binh Tran
任何人如何知道要传递什么分辨率?我不想盲目地使用任意两个数字,因为那会破坏图像的宽高比。我想保持比例并避免内存错误。 - Paul
显示剩余2条评论

4

2

ARGB_8888 使用了更多的内存,因为它需要 Alpha 颜色值,所以我的建议是使用 RGB_565,如此文档所述HERE

注意:与 ARGB_8888 相比,质量会稍微降低。


1
当解码位图时遇到内存不足的问题通常与正在解码的图像大小无关。当然,如果您尝试打开一个5000x5000像素的图像,您将会遇到OutOfMemoryError,但如果是800x800像素的大小,则完全合理且应该可以正常工作。
如果您的设备在处理3.2 MB的图像时出现内存不足的情况,很可能是因为应用程序中存在上下文泄漏。
这是this post的第一部分:
我猜问题不在您的布局中,而是在代码的其他地方。并且可能您在某个地方泄漏了上下文。
这意味着您在不应该使用Activity Context的组件中使用了它,从而防止它们被垃圾回收。因为这些组件通常由活动持有,所以这些活动不会被GC,您的Java堆将快速增长,您的应用程序将在某个时候崩溃。
正如Raghunandan所说,您将需要使用MAT找到被持有的Activity / Component并删除上下文泄漏。
我目前发现检测上下文泄漏的最佳方法是方向更改。例如,旋转您的ActivityMain多次,运行MAT并检查是否只有一个ActivityMain实例。如果有多个实例(与旋转更改一样多),则表示存在上下文泄漏。
多年前我找到了一篇关于使用MAT的好教程good tutorial on using MAT。也许现在有更好的教程。
其他有关内存泄漏的帖子: Android - memory leak or? Out of memory error on android emulator, but not on device

1
你可能还保留了之前的位图引用。我猜测你执行了这段代码多次,但从未执行bitmap.recycle()。内存最终会耗尽。

每当您完成位图操作后,应将其回收。我无法回答该问题,因为我不知道您是如何使用它的。 - Paul Lammertsma
是的,我尝试了你的解决方案,但仍然没有运气 :( 还有其他我错过的东西吗? - Goofy

1
我在位图内存使用方面遇到了很多问题。
结果:
- 大多数设备对图形有限制的堆内存,大多数小型设备仅限于16MB的整体应用程序,而不仅仅是您的应用程序。 - 如果适用,使用4位、8位或16位位图。 - 尽可能从头开始绘制形状,如果可能的话省略位图。

1
使用WebView动态加载任意数量的图片,它是使用NDK(低级别)构建的,因此没有GDI堆内存限制。它运行流畅快速 :)

这是一个很好的替代滚动图片布局的选择,比如ViewFlipper。 - AVEbrahimi

0

看一下这个视频。http://www.youtube.com/watch?v=_CruQY55HOk。不要像视频中建议的那样使用system.gc()。使用MAT分析器查找内存泄漏。我猜返回的位图太大导致了内存泄漏。


即使我删除它也不能解决问题,有没有什么办法,请告诉我,我已经卡了很长时间。 - Goofy
删除不会解决问题。要解决您的问题,请使用MAT分析器查找位图占用了多少内存。我猜它占用了太多空间,导致了内存泄漏。 - Raghunandan
是的,它占用了很多内存,但如何解决呢?这是个大问题。 - Goofy
压缩应该能够减少一些内存使用量。但如果仍然非常庞大以至于导致内存泄漏,恐怕无法解决。尝试对位图进行压缩。 - Raghunandan
如何进行压缩?这样做会降低质量吗? - Goofy
尝试使用Paul Lammertsma的解决方案,在位图不使用时进行回收,以及链接中提供的解决方案https://dev59.com/NWoy5IYBdhLWcg3wo_kn。 - Raghunandan

0

看起来你有一个大图像要显示。

你可以下载图像并保存到SD卡(示例),然后使用此代码从SD卡中显示图像。


假设calculateInSampleSize()返回8,那么这将使用完全相同的内存量作为他当前的实现。 - Paul Lammertsma
不,我不能这样做,因为我有大约200张图片。 - Goofy
但你只需要在第一次下载的时候进行。然后你可以从sdcard上显示图像,这也有助于更快地运行你的应用程序。 - MAC
@MAC 不,有一些问题,比如用户如果从SD卡中删除文件会怎样?而且它将不必要地占用用户SD卡的空间。 - Goofy
但是,如果您要使用这种方式,每次迭代都会浪费带宽和时间。 - MAC
此外,您可以通过将文件夹名称写为.FolderName来使文件夹在图库中隐藏。 - PrincessLeiha

-1

我之前也遇到了同样的问题...但是我通过使用这个函数来解决它,你可以得到所需的宽度和高度比例。

private Bitmap decodeFile(FileInputStream f)
{
    try
    {
        //decode image size
        BitmapFactory.Options o = new BitmapFactory.Options();
        o.inJustDecodeBounds = true;
        BitmapFactory.decodeStream(f,null,o);

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

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

请参考Android内存泄漏错误Android中加载图片到GridView出错


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