位图分配,使用BitmapFactory.Options.inBitmap引发IllegalArgumentException异常。

21

当将inBitmap设置为true时,我遇到了下一个异常:解码问题造成现有位图无法使用

出现原因:java.lang.IllegalArgumentException: Problem decoding into existing bitmap
位于android.graphics.BitmapFactory.decodeResource(BitmapFactory.java:460)
...

有趣的是,同样的代码在以下设备上运行时会在不同的位置失败:

  • API: 4.4.2, Nexus 4
  • API: 4.3.1, Samsung s3

这是我的代码,与DevBytes: Bitmap Allocation视频中展示的完全一样。

private BitmapFactory.Options options;
private Bitmap reusedBitmap;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    final ImageView imageView = (ImageView) findViewById(R.id.image_view);

    // set the size to option, the images we will load by using this option
    options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    options.inMutable = true;
    BitmapFactory.decodeResource(getResources(), R.drawable.img1, options);

    // we will create empty bitmap by using the option
    reusedBitmap = Bitmap.createBitmap(options.outWidth, options.outHeight, Bitmap.Config.ARGB_8888);

    // set the option to allocate memory for the bitmap
    options.inJustDecodeBounds = false;
    options.inSampleSize = 1;
    options.inBitmap = reusedBitmap;

    // #1
    reusedBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.img1, options);
    imageView.setImageBitmap(reusedBitmap);

    imageView.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {

            options.inBitmap = reusedBitmap;
            // #2
            reusedBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.img2, options);
            imageView.setImageBitmap(reusedBitmap);

        }
    });
}

  • 在Nexus 4上,在 // #1 处的 BitmapFactory.decodeResource() 崩溃了。
  • S3,通过#1显示第一张图片,但在点击图片后在 // #2BitmapFactory.decodeResource() 处崩溃。

一些注意点:

  • 这些图片大小相同。我尝试过使用jpgpng格式,但两者都失败了。
  • 位图是可变的。
  • 我使用了这个 canUseForInBitmap 方法进行检查,正如这里所述

问题:

如何正确使用 inBitmap 属性?

如果您遇到此类问题或发现我犯了什么错误,请留言/回复。任何帮助将不胜感激。如果您知道任何解决方法,那将是太好了。

- 编辑(问题仍然开放) -

很抱歉没有解释我为什么要以这种方式重用位图。
这样做的原因是GC每次决定释放内存时都会锁定。
inBitmap 功能应该帮助我们重复使用位图而不分配新的内存,这将导致GC清理已经分配的内存。

例如,如果我使用这种常见方法:

Log.i("my_tag", "image 1");
imageView.setImageResource(R.drawable.img1);
Log.i("my_tag", "image 2");
imageView.setImageResource(R.drawable.img2);
Log.i("my_tag", "image 3");
imageView.setImageResource(R.drawable.img3);

那么这将是垃圾回收的工作:

I/my_tag  ( 5886): image 1
D/dalvikvm( 5886): GC_FOR_ALLOC freed 91K, 2% free 9113K/9240K, paused 15ms, total 15ms
I/dalvikvm-heap( 5886): Grow heap (frag case) to 19.914MB for 11520016-byte allocation
D/dalvikvm( 5886): GC_FOR_ALLOC freed <1K, 1% free 20362K/20492K, paused 13ms, total 13ms
I/my_tag  ( 5886): image 2
D/dalvikvm( 5886): GC_FOR_ALLOC freed 11252K, 2% free 9111K/9236K, paused 15ms, total 15ms
I/dalvikvm-heap( 5886): Grow heap (frag case) to 19.912MB for 11520016-byte allocation
D/dalvikvm( 5886): GC_FOR_ALLOC freed <1K, 1% free 20361K/20488K, paused 35ms, total 35ms
I/my_tag  ( 5886): image 3
D/dalvikvm( 5886): GC_FOR_ALLOC freed 11250K, 2% free 9111K/9236K, paused 15ms, total 15ms
I/dalvikvm-heap( 5886): Grow heap (frag case) to 19.913MB for 11520016-byte allocation
D/dalvikvm( 5886): GC_FOR_ALLOC freed <1K, 1% free 20361K/20488K, paused 32ms, total 32ms

主线程被锁定了100毫秒以上

如果我没有使用inBitmap选项对资源进行解码,那么同样的情况也会发生。因此问题仍然存在,如何使用这个属性?


你的需求是什么?为什么不直接将图像资源设置到你的ImageView中呢?就像调用imageView.setImageResource(R.drawable.img1)然后imageView.setImageResource(R.drawable.img2)这样。 - clemp6r
在捕获异常后,您能否添加日志记录? - droid
1
@clemp6r,我编辑了问题并解释了为什么setImageResource()不好,并且我正在尝试使用inBitmap标志。 - sromku
6个回答

3
我使用模拟的Nexus 4测试了您的代码。 我有一个默认的ic_launcher.png文件,将它复制并粘贴到drawable-mdpi中两次(通常我这样做)。 我将这两个新文件重命名为与您的代码中的名称相匹配的名称(以便我在这里进行更少的更改)。 当我运行应用程序时,我观察到与您一样的情况。 经过几次尝试之后,我决定将新的png文件复制到其他drawable文件夹中-因此它们存在于以下位置:
  • drawable-hdpi
  • drawable-mdpi
  • drawable-xhdpi
  • drawable-xxhdpi
我运行应用程序,它可以正常工作!
我不确定为什么它确实起作用,但显然与正确的屏幕分辨率/密度有关。

我之前已经问过这个问题了。如果作者只有一个drawable-dpi文件夹,那么在其他dpi上位图会消耗大量内存。 - Gaskoin
我没有看到你之前有问过这个问题。它是在另一个问题中吗? - dragi
我认为作者想要用img2替换img1,两者大小相同(例如200x100px),密度也相同,或者至少在相同的drawable-xxxxx文件夹中。在我看来,问题在于作者没有将img1和img2放在正确的drawable-xxxxx文件夹中,这可能会影响decodeBitmap的使用。 - dragi
这就是我的意思 :) 如果图像的 DPI 较低,Android 可以处理它,但在某些设备上,这样的位图会消耗大量内存。 - Gaskoin
@helleye,我很喜欢你能够重现这个问题并找到解决方案。我的图片在drawable文件夹中,按照你的建议,我将它复制到了drawable-nodpi文件夹中,然后它就开始工作了! - sromku
@sromku 我很高兴能帮忙。我看到它不应该难以复制,所以我试了一下。 - dragi

2
创建一个新的位图然后尝试重复使用它有点奇怪。为什么不让decodeResource在第一次创建时直接创建一个新的位图呢?我猜您在问题#2中遇到的问题是,一旦将ImageView设置为使用该位图,它就无法再次重用它(因为它已经在使用中)。在文档中提到了IllegalArgumentException

如果解码操作无法使用此位图,则解码方法将返回null并抛出IllegalArgumentException。

关于解决方法,您可以尝试保留两个位图并切换ImageView指向的位图,例如:
  1. 解码位图1,将ImageView指向位图1
  2. 解码位图2,将ImageView指向位图2
  3. 从步骤1开始重复

由于我没有从一开始就解释使用此功能的原因,所以我给了你边界,但问题仍然存在。如果您有其他建议,欢迎提出。谢谢。 - sromku
我的观点实际上是关于你在第一次“重用”之前创建位图的问题,所以你可以直接在那个非常初步的时候解码(但之后仍然尝试重用)。另外,你尝试过双缓冲方法了吗? - kabuko
我不太明白...根据你在这里展示的流程,接下来会发生以下事情:1. 解码->gc工作,将位图1设置为imageview->gc工作。2. 同样的操作->gc工作和工作...或者我漏掉了什么。双缓冲方法是我没有尝试过的,听起来很有趣,今天会试一下。 - sromku
哦,也许我没有清楚地表达我的答案。我的意思是在重用位图1并指向位图1的同时解码,在重用位图2并指向位图2的同时解码。这与您目前的方法类似,但不使用双缓冲区来重用您当前正在使用的位图。 - kabuko
这并不奇怪,事实上这是官方开发文档的建议 - m0skit0
重复使用位图是一个建议,没错。但是如果你刚创建了一个位图,却从未使用过最初创建的那个位图,那么“重复使用”就不太适用了。 - kabuko

1

在您的代码中,您需要将options.inMutable设置为true,然后再进行#1和#2操作。

这听起来可能有点附加,但这些是您踩过的小坑。

希望这可以帮到您。


0
为什么要解码位图?只需执行以下操作:
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    final ImageView imageView = (ImageView) findViewById(R.id.image_view);
    imageView.setImageResource(R.drawable.img1);

    imageView.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            imageView.setImageResource(R.drawable.img2);
        }
    });
}

你确定两张图片的尺寸是一样的吗?根据文档所述。

如果设置了[inBitmap],则采用Options对象的解码方法将尝试在加载内容时重用此位图。如果解码操作无法使用此位图,则解码方法将返回null并引发IllegalArgumentException异常。当前实现需要重用的位图是可变的,并且即使在解码通常会导致不可变位图的资源时,生成的重用位图仍将保持可变。
由于上述约束和可能发生的失败情况,您仍应始终使用解码方法返回的位图,而不是假设重用位图有效。检查返回值是否与Options结构中设置的inBitmap的值匹配将指示是否重用了位图,但在所有情况下,您都应使用解码函数返回的位图以确保使用作为解码目标使用的位图。
使用BitmapFactory时的用法
自KITKAT以来,只要解码后的位图的字节计数小于或等于重用位图的分配字节计数,BitmapFactory就可以重用任何可变位图来解码任何其他位图。这可能是因为内在大小较小,或者其大小在缩放后(用于密度/样本大小)较小。
在KITKAT之前,还适用其他约束:正在解码的图像(无论是作为资源还是作为流)必须为jpeg或png格式。仅支持相同大小的位图,inSampleSize设置为1。此外,重用位图的配置将覆盖inPreferredConfig的设置(如果已设置)。

编辑:

如果您有大型资源位图,请在异步任务中加载它... 更多信息在这里,但可以更简单地完成。


所有图像大小相同。而且,我编辑了问题以解释为什么我要以这种方式解码。谢谢。 - sromku
主要问题是GC锁定时间。GC会锁定所有内容,无论您在哪个线程上运行。一旦我们需要分配内存并且没有足够的空间,GC将运行并尝试释放尽可能多的内存。您的编辑在这种情况下无法帮助。这也不是此问题的正确答案,因为imageView.setImageResource(R.drawable.img1);是必须在UI线程上执行的操作,这就是导致GC锁定的原因。 - sromku
是的,但我写了你可以在后台线程中加载资源。我没有写你应该在非 UI 线程中调用此方法。你的 mdpi 中的 png 有多大(kB)? - Gaskoin
不是的,我的朋友。setImageResource 会解码高分辨率图像,这在内存分配方面非常糟糕。 - FindOut_Quran
我知道,但他是从资源文件夹中读取的,而不是从互联网上读取的。我只是假设他会得到适当缩放的位图,并放置在适当的子文件夹中(例如mdpi,hdpi等),以避免出现任何内存问题。这是相当普遍和良好的做法。当您下载位图并且无法确定其大小时,情况会发生变化,但这是另一个话题。请检查一下,因为我看到这里有很多“理论”。 - Gaskoin

0

关于你在#2遇到的三星S3的问题,我不确定。但是你在Nexus 4上遇到的问题可能是因为你把两个图像放错了dpi drawable文件夹。所以当它尝试解码位图时,找不到资源。

我的手机屏幕密度是hdpi,起初我尝试将两个图像放在drawable-mdpi中,结果遇到了问题#1。所以我改成了drawable-hdpi,然后就可以了。


-4

只需在应用程序标签(AndroidManifest.xml)中添加largeHeap="true"


1
这绝对不是推荐的做法,也不能解决主要问题,即触发垃圾回收。 - mlepage

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