BitmapFactory.decodeResource及其莫名其妙的内存溢出问题

9
我在解码一个960x926像素的jpg drawable图像资源时出现了奇怪的内存不足错误,分配了3555856字节。该图像仅放置在drawable-xxhdpi(3倍)中,而我正在使用hdpi(1.5倍)设备。 两个问题:
  • 为什么会出现错误,尽管堆中有足够的可用内存?

  • 为hdpi设备分配的应该是((960/2) x (926/2)) x 4 = 888960字节(而不是3555856字节)吗?

有人能向我解释一下吗? 注意:该问题是关于在有22.5MB可用内存(参见日志)的情况下分配3.5MB并导致OOM的原因。 03-18 17:30:15.050 32750-32750/? D/dalvikvm:GC_FOR_ALLOC释放了10809K,内存占用率为49%,总内存为23735K / 46087K,暂停时间为89ms,总时间为89ms。03-18 17:30:15.050 32750-32750/? I/dalvikvm-heap:强制回收了3555856字节分配的软引用。03-18 17:30:15.160 32750-32750/? D/dalvikvm:GC_BEFORE_OOM释放了29K,内存占用率为49%,总内存为23705K / 46087K,暂停时间为103ms,总时间为103ms。03-18 17:30:15.160 32750-32750/? E/dalvikvm-heap:一个3555856字节的分配超出了内存限制。03-18 17:30:15.160 32750-32750/? I/dalvikvm:“main”prio = 5 tid = 1可运行。03-18 17:30:15.160 32750-32750/? I/dalvikvm:| group =“main”sCount = 0 dsCount = 0 obj = 0x418fc6a0 self = 0x4010c008。03-18 17:30:15.160 32750-32750/? I/dalvikvm:| sysTid = 32750 nice = 1 sched = 0/0 cgrp = apps handle = 1075251280。03-18 17:30:15.160 32750-32750/? I/dalvikvm:| schedstat =(0 0 0)utm = 3807 stm = 859 core = 0。03-18 17:30:15.160 32750-32750/? I/dalvikvm:在android.graphics.BitmapFactory.nativeDecodeAsset(本机方法)处。03-18 17:30:15.160 32750-32750/? I/dalvikvm:在android.graphics.BitmapFactory.decodeStream(BitmapFactory.java:636)处。03-18 17:30:15.160 32750-32750/? I/dalvikvm:在android.graphics.BitmapFactory.decodeResourceStream(BitmapFactory.java:484)处。03-18 17:30:15.160 32750-32750/? I/dalvikvm:在android.graphics.BitmapFactory.decodeResource(BitmapFactory.java:512)处。03-18 17:30:15.160 32750-32750/? I/dalvikvm:在android.graphics.BitmapFactory.decodeResource(BitmapFactory.java:542)处。

如果您使用Android模拟器,您设置了多少堆内存? - Khang .NT
@Khang .NT 4 我正在使用一个真实设备 48M 堆大小 - GPack
1
“尽管堆内存足够,为什么我还会遇到错误?” -- 这是因为没有足够大的单一块可以满足您的请求。Dalvik垃圾回收器不是压缩式垃圾回收器,这意味着堆会变得分散。ART的垃圾回收器将压缩堆,以便允许分配更大的块,但只有当应用程序处于后台时才会进行。 - CommonsWare
@CommonsWare 有没有一种方法可以检查堆的分段状态? - GPack
这是我第一次看到堆内存还剩49%的时候出现OOM,我并不完全相信。 - GPack
显示剩余4条评论
5个回答

2
您出现OOM的原因是,当图像解码到内存中时,其位图所占用的空间比图像分辨率要大(几乎是4倍,但不确定确切数值)。
在处理图像时,请注意以下几点:
1. 永远不要在主线程上处理位图。请在后台进行所有解码操作。
2. 总是考虑屏幕大小或您将放置图像的视图大小。例如,如果您的屏幕大小为360X720(一些随机值),则解码具有高于所需大小的分辨率的完整图像不是一个好主意(因为它会在主内存中加载完整的位图)。 因此,在解码时始终进行采样。
请尝试使用以下解决方案:
步骤1:查找屏幕大小
从这里获得:https://dev59.com/hnNA5IYBdhLWcg3wUL5Y
Display display = getWindowManager().getDefaultDisplay();
Point size = new Point();
display.getSize(size);
int width = size.x;
int height = size.y;

步骤1:创建异步任务来解码位图

如果您正在使用内部类作为异步任务,则考虑使用公共静态内部类(以避免内存泄漏问题),并在要加载图像的ImageView中保留弱引用。同时将要解码的图像资源、文件或流传递给构造函数。在下面的代码中,假设您要解码一个资源。还要传递在步骤1中计算出的宽度和高度。

public static class BitmapDecodeTask extends AsyncTask<Void, Void, Bitmap> {
    //the reason to use a weak reference is to protect from memory leak issues.
    private WeakReference<Context> mContextReference;
    private WeakReference<ImageView> mImageViewReference;
    private int mResourceId;
    private int mRequiredWidth;
    private int mRequiredHeight;

    public BitmapDecodeTask(Context context, ImageView imageView, int resourceId, int width, int height) {
        this.mContextReference = new WeakReference<>(context);
        this.mImageViewReference = new WeakReference<>(imageView);
        this.mResourceId = resourceId;
        this.mRequiredWidth = width;
        this.mRequiredHeight = height;
    }

    @Override
    protected Bitmap doInBackground(Void... params) {

        Context context = mContextReference.get();

        if(context != null) {
            BitmapFactory.Options options = new BitmapFactory.Options();
            options.inJustDecodeBounds = true;
            BitmapFactory.decodeResource(getResources(), mImageResourceId, options);

            //set inSampleSize
            options.inSampleSize = calculateInSampleSize(options);

            //set inJustDecodeBounds = false;
            options.inJustDecodeBounds = false;

            //decode
            return BitmapFactory.decodeResource(getResources(), mImageResourceId, options);
        }

        return null;
    }

    @Override
    protected void onPostExecute(Bitmap bitmap) {
        //check if imageview is available or not
        ImageView imageView = mImageViewReference.get();

        if(imageView != null && bitmap != null) {
            imageView.setImageBitmap(bitmap);
        }
    }

    public static int calculateInSampleSize(BitmapFactory.Options options) {
        // Raw height and width of image
        final int height = options.outHeight;
        final int width = options.outWidth;
        int inSampleSize = 1;

        if (height > mRequiredHeight || width > mRequiredWidth) {
            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) > mRequiredHeight 
                && (halfWidth / inSampleSize) > reqWidth) {
                inSampleSize *= 2;
            }
        }
        return inSampleSize;
    }
}

参考文献:

位图


请更加仔细地阅读问题:内存没有泄漏,堆中有49%的空闲空间。 - GPack
这是我在回答的第一行中写的。每当您解码图像时,它都会转换为位图,其尺寸大约是图像的4倍。我在某个地方读到过这个信息。所以我只是想告诉您根据所需的宽度和高度进行解码。也就是进行采样解码。在编写解决方案时,我尝试了所有最佳实践,因此提到了内存泄漏和在UI线程之外工作。我并不是说您的代码会导致内存泄漏。(960*926)*4大约等于3555840。希望这回答了您的问题 :) - Ankit Aggarwal
还有@CommonsWare的原因也是正确的。刚才我看了他的评论。 - Ankit Aggarwal
@GPack,你的问题最终结论是什么? - Ankit Aggarwal
抱歉,我无法指定悬赏,因为在我看来,即使它们非常有趣,所有的回复都没有指向我的问题主题:那就是为什么我在有足够空闲内存的情况下会出现OOM错误。只有CommonsWare的评论集中在问题上,并且部分涉及GabeSechen关于自动缩放的回复。 - GPack
好的,没问题:)。这实际上是一个非常常见的问题,我认为每个处理图像的人都可能在某个阶段遇到过这个错误。我很乐意了解确切的原因和解决方案。正如CommonsWare所说,这可能是由于内存碎片化,但我认为系统应该处理这个问题。@GPack如果你发现有什么有趣的东西,请分享一下 :)。 - Ankit Aggarwal

2

1)如果在hdpi文件夹中没有较小的版本,它将使用最接近的匹配。因此,如果不存在hdpi或drawable/版本,则将使用xxhdpi。

2)它不会自动缩放。它将读取完整大小。

3)如果这导致OOM,则可能通常使用太多内存。


你的意思是OOM发生在设备的内存中,而不是在VM堆中吗? - GPack

1
根据Gabe Sechan的建议,我建议使用像makeappicon这样的网站,它允许您自动缩放任何过大的图像。虽然Android应该为您处理这些缩放问题,但这是一个有用的工具。
Mimmo Grottoli关于ARGB字节的说法是正确的,就我所知,没有什么可担心的。
最可能出现此错误的原因是在创建(而不是销毁)位图时涉及到重大的内存泄漏,这是我从以前的一个steg项目中吃过的苦头。
为了清理这个内存,您可以覆盖Activity / Fragment的onDestroy()方法,或者根据需要手动执行清理。
@Override
 public void onDestroy() {
    yourbitmap.recycle();
    yourbitmap = null;
    super.onDestroy();
 }

希望这对您有所帮助。

没有泄漏,问题在于为什么在有22.5MB可用内存的情况下分配3.5MB时会出现OOM(请参见上面的日志)。 - GPack

1

最好使用Glide或Picasso等库来进行所有图像操作(解码、调整大小、下载等):

将路径替换为本地文件夹路径或服务器URL

依赖项

compile 'com.github.bumptech.glide:glide:3.5.2'

使用Glide加载图片
Glide.with (context).load (path).asBitmap().into(imageView);

1
位图在内存中的大小为宽度 x 高度 x 每种颜色的位数。我认为您没有使用任何特定选项,因此每个像素使用 4 个字节(红色 1 个字节,绿色 1 个字节,蓝色 1 个字节,Alpha 1 个字节)。所以:960*926*4=3555840 字节。
关于您的OOM:对我来说不太寻常的是:
强制收集 SoftReferences 以进行 3555856 字节的分配
您存储了分配的位图在哪里?在 Android 上应避免使用软引用。

是的,我知道x4字节规则:第二个问题是关于框架的dpi-drawable文件夹管理,而不是关于位图分配数学。 在可能发生OOM之前强制收集可达对象是VM的通常标准行为,作为释放内存的最后机会。问题在于这里有49%的空闲内存。 - GPack
你是否在某个缓存中缓存位图?第二:你是如何解码位图的?请提供代码行。 - Mimmo Grottoli

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