【问题标题】:BitmapFactory.decodeResource returns a mutable Bitmap in Android 2.2 and an immutable Bitmap in Android 1.6BitmapFactory.decodeResource 在 Android 2.2 中返回一个可变位图,在 Android 1.6 中返回一个不可变位图
【发布时间】:2011-05-19 22:38:10
【问题描述】:

我正在开发一个应用程序并在运行 Android 2.2 的设备上对其进行测试。在我的代码中,我使用了使用 BitmapFactory.decodeResource 检索的位图,并且可以通过调用 bitmap.setPixels() 来进行更改。当我在朋友的运行 Android 1.6 的设备上进行测试时,我在对 bitmap.setPixels 的调用中得到了一个 IllegalStateException。在线文档说当位图不可变时,此方法会抛出IllegalStateException。文档没有说明 decodeResource 返回不可变位图,但显然必须如此。

是否可以进行不同的调用来可靠地从应用程序资源获取可变位图,而无需第二个 Bitmap 对象(我可以创建一个相同大小的可变位图并绘制到包装它的 Canvas 中,但这会需要两个大小相同的位图,占用的内存是我预期的两倍)?

【问题讨论】:

    标签: java android bitmap


    【解决方案1】:

    您可以将不可变位图转换为可变位图。

    我找到了一个可接受的解决方案,它只使用一个位图的内存。

    源位图被原始保存(RandomAccessFile)在磁盘上(没有内存),然后源位图被释放,(现在,内存中没有位图),然后,文件信息被加载到另一个位图。这种方式可以制作一个位图副本,每次只将一个位图存储在 RAM 内存中。

    在此处查看完整的解决方案和实施:Android: convert Immutable Bitmap into Mutable

    我对此解决方案进行了改进,现在可以使用任何类型的位图(ARGB_8888、RGB_565 等),并删除临时文件。看我的方法:

    /**
     * Converts a immutable bitmap to a mutable bitmap. This operation doesn't allocates
     * more memory that there is already allocated.
     * 
     * @param imgIn - Source image. It will be released, and should not be used more
     * @return a copy of imgIn, but muttable.
     */
    public static Bitmap convertToMutable(Bitmap imgIn) {
        try {
            //this is the file going to use temporally to save the bytes. 
            // This file will not be a image, it will store the raw image data.
            File file = new File(Environment.getExternalStorageDirectory() + File.separator + "temp.tmp");
    
            //Open an RandomAccessFile
            //Make sure you have added uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
            //into AndroidManifest.xml file
            RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw");
    
            // get the width and height of the source bitmap.
            int width = imgIn.getWidth();
            int height = imgIn.getHeight();
            Config type = imgIn.getConfig();
    
            //Copy the byte to the file
            //Assume source bitmap loaded using options.inPreferredConfig = Config.ARGB_8888;
            FileChannel channel = randomAccessFile.getChannel();
            MappedByteBuffer map = channel.map(MapMode.READ_WRITE, 0, imgIn.getRowBytes()*height);
            imgIn.copyPixelsToBuffer(map);
            //recycle the source bitmap, this will be no longer used.
            imgIn.recycle();
            System.gc();// try to force the bytes from the imgIn to be released
    
            //Create a new bitmap to load the bitmap again. Probably the memory will be available. 
            imgIn = Bitmap.createBitmap(width, height, type);
            map.position(0);
            //load it back from temporary 
            imgIn.copyPixelsFromBuffer(map);
            //close the temporary file and channel , then delete that also
            channel.close();
            randomAccessFile.close();
    
            // delete the temp file
            file.delete();
    
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } 
    
        return imgIn;
    }
    

    【讨论】:

    • 太棒了。感谢您为未来的谷歌员工分享完整的实施。自从我问这个问题以来,我一直在记忆中这样做,但是是的......总是一个潜在的 OutOfMemoryException
    • 为什么要使用外部存储并要求开发者使用新的权限?您应该改用内部存储。
    • @androiddeveloper 你是对的,如果你保存在内部存储中,它可能也可以工作。
    • @Derzu 我在这里创建了一个新帖子来展示如何做到这一点:stackoverflow.com/a/16314940/878126,此外它还支持 android 的一些新功能。
    • @Derzu 我现在已经用不使用内部存储且速度更快的答案更新了我的答案。它使用 JNI 代替并适用于 API 8 及更高版本。
    【解决方案2】:

    使用可变选项 true 将位图复制到自身。这样既不需要额外的内存消耗,也不需要长行代码。

    Bitmap bitmap= BitmapFactory.decodeResource(....);
    bitmap= bitmap.copy(Bitmap.Config.ARGB_8888, true);
    

    【讨论】:

    • 是的,但是大图像上的内存消耗过多。我想我将不得不采取多步骤 - 使用 BitmapFactory.Options.inJustDecodeBounds 获取尺寸,创建一个可变的空位图,并从原始数据的输入流中填充它。让我感到惊讶的是,文档详细说明了那些返回不可变位图的方法,而此方法没有指定(暗示默认为可变)。它在我的 2.2 手机上可以这样工作,但在我好友的 1.6 手机上却不行
    • 你有一些例子吗?我知道它很老的帖子。但仍然希望得到回应。
    • 我在以这种方式复制位图时遇到问题 - 新位图的宽度和高度为 -1,因此无法渲染。
    • 位图未呈现。
    【解决方案3】:

    我们可以首先通过实例化 BitmapFactory.Options 类来设置 BitmapFactory 的选项,然后将名为“inMutable”的选项字段设置为 true,然后将此选项实例传递给 decodeResource。

     BitmapFactory.Options opt = new BitmapFactory.Options();
     opt.inMutable = true;
     Bitmap bp = BitmapFactory.decodeResource(getResources(), R.raw.white, opt);
    

    【讨论】:

    • +1,但仅在 api 级别 11 及更高版本中可用。我的问题是如何向后兼容 1.6 (api 4)。
    • 不知道它不适用于 api 4。我不想这么说,但是,您必须使用“复制”方法进行调整。
    • 是的。我现在的代码中都有 if(!bitmap.isMutable()) 。大声笑
    【解决方案4】:

    这是我创建的一个使用内部存储并且不需要任何新权限的解决方案,基于“Derzu”的想法,以及从蜂窝开始的事实,这是内置的:

    /**decodes a bitmap from a resource id. returns a mutable bitmap no matter what is the API level.<br/>
    might use the internal storage in some cases, creating temporary file that will be deleted as soon as it isn't finished*/
    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
    public static Bitmap decodeMutableBitmapFromResourceId(final Context context, final int bitmapResId) {
        final Options bitmapOptions = new Options();
        if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB)
            bitmapOptions.inMutable = true;
        Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), bitmapResId, bitmapOptions);
        if (!bitmap.isMutable())
            bitmap = convertToMutable(context, bitmap);
        return bitmap;
    }
    
    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    public static Bitmap convertToMutable(final Context context, final Bitmap imgIn) {
        final int width = imgIn.getWidth(), height = imgIn.getHeight();
        final Config type = imgIn.getConfig();
        File outputFile = null;
        final File outputDir = context.getCacheDir();
        try {
            outputFile = File.createTempFile(Long.toString(System.currentTimeMillis()), null, outputDir);
            outputFile.deleteOnExit();
            final RandomAccessFile randomAccessFile = new RandomAccessFile(outputFile, "rw");
            final FileChannel channel = randomAccessFile.getChannel();
            final MappedByteBuffer map = channel.map(MapMode.READ_WRITE, 0, imgIn.getRowBytes() * height);
            imgIn.copyPixelsToBuffer(map);
            imgIn.recycle();
            final Bitmap result = Bitmap.createBitmap(width, height, type);
            map.position(0);
            result.copyPixelsFromBuffer(map);
            channel.close();
            randomAccessFile.close();
            outputFile.delete();
            return result;
        } catch (final Exception e) {
        } finally {
            if (outputFile != null)
                outputFile.delete();
        }
        return null;
    }
    

    另一种选择是使用JNI,以便将数据放入其中,回收原始位图,并使用JNI数据创建一个新的位图,它将(自动)可变,因此与my JNI solution for bitmaps一起,一个可以执行以下操作:

    Bitmap bitmap=BitmapFactory.decodeResource(getResources(),R.drawable.ic_launcher);
    final JniBitmapHolder bitmapHolder=new JniBitmapHolder(bitmap);
    bitmap.recycle();
    bitmap=bitmapHolder.getBitmapAndFree();
    Log.d("DEBUG",""+bitmap.isMutable()); //will return true
    

    但是,我不确定 API 级别的最低要求是什么。它在 API 8 及更高版本上运行良好。

    【讨论】:

      【解决方案5】:

      我知道我迟到了,但我们就是这样避免了这个令人痛苦的 Android 问题,并裁剪和修改了一张只在内存中保存一个副本的图像。

      情况
      我们要处理保存到文件的图像的裁剪版本的像素。由于内存需求很高,我们永远不想在任何给定时间在内存中拥有超过一个图像的副本。

      本应有效但无效的方法
      使用BitmapRegionDecoder 打开图像子部分(我们要裁剪的位),使用inMutable = true 传入BitmapFactory.option,处理像素然后保存到文件。
      尽管我们的应用程序声明 API 最小值为 14,目标为 19,但 BitmapRegionDecoder 正在返回一个不可变的位图,实际上忽略了我们的 BitMapFactory.options

      什么不起作用

      • 使用BitmapFactory(尊重我们的inMutable选项)打开一个可变图像并对其进行裁剪:所有裁剪技术都是非强制性的(需要整个图像的副本一次存在于内存中,即使是垃圾覆盖和回收后立即收集)
      • 使用BitmapRegionDecoder(有效裁剪)打开不可变图像并将其转换为可变图像;所有可用的技术都需要在内存中复制。

      2014 年的最佳解决方案

      • 使用BitmapFactory 作为可变位图打开全尺寸图像,并执行我们的像素操作
      • 将位图保存到文件并从内存中回收(它仍然未裁剪)
      • BitmapRegionDecoder打开保存的位图,只打开要裁剪的区域(现在我们不关心位图是否不可变)
      • 将此位图(已有效裁剪)保存到文件中,覆盖之前保存的位图(未裁剪)

      使用这种方法,我们可以在内存中只有 1 个副本的位图上裁剪和执行像素处理(因此我们可以尽可能避免那些讨厌的 OOM 错误),用 RAM 换取时间,因为我们必须执行额外的 (慢)文件 IO。

      【讨论】:

        【解决方案6】:

        发生这种情况是因为您想通过调用 setHeight()setWidth() 来调整位图大小

        调整任何位图或可绘制对象(Png、Svg、矢量等)的大小

        public Bitmap editMyBitmap(int drawableId, int newHeight, int newWidth) {
                Bitmap myBitmap = BitmapFactory.decodeResource(getResources(), drawableId);
                myBitmap = Bitmap.createScaledBitmap(myBitmap, newWidth, newHeight, false);
                return myBitmap;
            }
        

        用法示例:

        Bitmap facebookIcon = editMyBitmap(R.drawable.facebookImage);
        // now use it anywhere
        imageView.setBitmapImage(facebookIcon); 
        canvas.drawBitmap(facebookIcon, 0, 0, null); 
        

        【讨论】:

          【解决方案7】:

          我知道问题已经解决了,但是呢:

          BitmapFactory.decodeStream(getResources().openRawResource(getResources().getIdentifier("bitmapname", "drawable", context.getPackageName())))

          【讨论】:

          • 如果您已经知道它的名称,为什么还需要使用字符串获取标识符?另外,我现在已经测试了这段代码(在 android 1.6 的模拟器上),它没有工作,所以我不知道它是如何为你工作的。
          猜你喜欢
          • 2011-04-18
          • 1970-01-01
          • 2012-08-02
          • 1970-01-01
          • 2012-09-12
          • 2012-08-09
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多