关闭

Android开发中解析、创建Bitmap对象时OOM的有效解决方法并附上一些干货

标签: android开发位图OOM解决方案Bitmap对象
8244人阅读 评论(6) 收藏 举报
分类:

先来点鸡汤:
Stay hungry,stay foolish
这句话的的解读:我们必须了解自己的渺小。如果我们不学习,科技发展的速度会让我们五年后被清空。所以,我们必须用初学者谦虚的自觉,饥饿者渴望的求知态度,来拥抱未来的知识。

  • 这几天做的项目中需要从图库选择图片或者拍照生成图片,然后展现在IamgeView控件上。当然,从图库选择图片和拍照选择图片的功能实现起来很简单。直接写上代码:
CharSequence[] items = { "拍照", "图库" };
new AlertDialog.Builder(context).setTitle("请选择:")
            .setIcon(R.drawable.ic_choose_picture)
            .setItems(items, new OnClickListener() {
    public void onClick(DialogInterface dialog,int which) {
            switch (which) {
                case 0:
                // 调用拍照功能
                // 创建File对象,用于存储拍照后的对象
                takePhotoImage = new File(Environment.getExternalStorageDirectory(),"take_photo_image.jpg");
                try {
                    if (takePhotoImage.exists()) {
                    takePhotoImage.delete();
                                                  }
                takePhotoImage.createNewFile();
                    } catch (Exception e) { 
                    e.printStackTrace();
                                    }
                    // 将File对象转换成Uri对象
                    imageUri =Uri.fromFile(takePhotoImage);
                    Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
                    // 指定图片的输出地址
                intent.putExtra(MediaStore.EXTRA_OUTPUT,
                imageUri);
                startActivityForResult(intent, TAKE_PHOTO);
                break;
                case 1:
                // 调用系统图库
                // 创建File对象,用于存储选择图库后的图片
                File choosePhoto = new File(Environment.getExternalStorageDirectory(),"choose_photo.jpg");
                try {
                    if (choosePhoto.exists()) {
                    choosePhoto.delete();
                        }
                    choosePhoto.createNewFile();
                    } catch (Exception e) {
                    e.printStackTrace();
                    }
                    // 将File对象转换成Uri对象
                    imageUri = Uri.fromFile(choosePhoto);
                    Intent intent2 = new Intent("android.intent.action.GET_CONTENT");
                    intent2.setType("image/*");
                    intent2.putExtra("crop", "true");
                    intent2.putExtra("scale", true);
        intent2.putExtra(MediaStore.EXTRA_OUTPUT,imageUri);
            startActivityForResult(intent2, CROP_PHOTO);
                break;
                }
                    }
                        }).show();

以上代码很简单,就是创建一个对话框,有两个item选项。一个是拍照、一个是图库。如下所示:

对话框显示

  • 两个item选项的功能都是为了生成一张图片并设置到ImageView控件上。

  • 拍照和从图库选择图片都先创建了一个File对象,用于存储摄像头拍下的图片或者存储从图库中选择的图片。

  • 然后将它放在手机的根目录下,我的手机是在内存设备的根目录下,这个无所谓。然后调用Uri的fromFile()方法将File对象转换成Uri对象。这个Uri对象标识着图片的唯一地址。

  • 如果点击的是拍照,接着会构建出一个Intent对象,并将这个Intent的action指定为:MediaStore.ACTION_IMAGE_CAPTURE,再调用Intent的putExtra()方法指定图片的输出地址,这里就填入刚刚得到的Uri对象,最后调用startActivityForResult()来启动活动。

  • 因为上面的代码是使用startActivityForResult()来启动活动的,所以拍照成功后会回调onActivityResult()方法,也即拍完照后会有结果返回到onActivityResult()方法中。所以重写该方法就能对拍照后的图片做进一步的处理了。方法如下所示:
/**
     * 在调用startActivityForResult的时候会回调该方法
     */
    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        switch (requestCode) {
        // 拍照
        case TAKE_PHOTO:
            if (resultCode == Activity.RESULT_OK) {
                Intent intent = new Intent("com.android.camera.action.CROP");
                intent.setDataAndType(imageUri, "image/*");
                intent.putExtra("scale", true);
                intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
                startActivityForResult(intent, CROP_PHOTO);
            }
            break;
        // 裁剪图片
        case CROP_PHOTO:
            if (resultCode == Activity.RESULT_OK) {
                try {
                // 压缩Bitmap对象
                    BitmapFactory.Options options = new BitmapFactory.Options();
                    options.inSampleSize = 8;
                    options.inPreferredConfig = Bitmap.Config.RGB_565;
                    options.inPurgeable = true;
                    options.inInputShareable = true;
                          bitmap=BitmapFactory.decodeStream(getActivity().getContentResolver().openInputStream(imageUri),null, options);
                    setImageData(bitmap);
                    if (bitmapEntities.size() == 1) {
                        adapter = new BitmapAdapter(context, bitmapEntities);
                        gv_Picture.setAdapter(adapter);
                    } else {
                        adapter.notifyDataSetChanged();
                    }
                    linear_Picture.addView(view_Picture, 1);

                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            break;
        }
    }
  • 上面说到如果用摄像头拍照成功后会回调onActivityResult()方法,这时候会继续构建Intent对象,把它的action指定为:
    “com.android.camera.action.CROP”
    这个Intent是用于拍出的照片进行裁剪的。因为摄像头比较大,而我们可能只希望截取其中的一小部分。然后给这个Intent设置上一些必要的属性,并再调用startActivityForResult()来启动裁剪程序。裁剪后的图片同样会输出到手机根目录下的图片文件中。

===========================================

  • 如果点击的item选项是图库。那么会调用系统的图库选择图片。在这里,跟拍照时基本的操作没有什么太多的差别。
    都是先创建一个File对象,保存从图库选择的文件。然后构建出一个Intent对象,并将它的action指定为:“android.intent.action.GET_CONTENT”。接着给这个Intent对象设置一些必要的参数,包括是否允许缩放和裁剪、图片的输出位置等。最后调用startActivityForResult()方法,就可以打开相册程序选择照片了。
  • 这里用到了一个小技巧,就是我们在调用startActivityForResult()方法的时候,给第二个参数传入的值仍然是:CROP_PHOTO常量,这样就可以复用之前调用摄像头显示图片的逻辑了,不用再编写第二遍显示图片的逻辑。

  • 好了,以上的两个item选项的操作都讲完了,接下来就是关键的时刻了。
    裁剪操作完成后,程序又会回调到onActivityResult方法中,这个时候就可以利用BItmapFactory的decodeStream()方法将存储在手机根目录下的图片文件解析成Bitmap对象,然后把它设置到ImageView控件上显示出来。

  • 我在这里用的是gridView控件显示一些图片。不过都是一样的,在gridView设置adapter的时候,需要在自定义的Adapter中把Bitmap对象设置到ImageView控件上。一开始设置一两张图片的时候如下所示:

    两张图片时候

    但是放的图的一旦多一点,程序就直接崩溃了,看了一下logcat的错误日志。
    一个醒目的Java.lang.OutOfMemoryError(内存溢出错误),最不想遇见的错误。说心里话,蛋疼。困惑了有段时间,试了很多的方法,终于很好的做了一些有效的图片压缩方法优化内存溢出的问题,但是并不能彻底的解决内存溢出问题。所以有时候我们自己要对图片的张数做一些限制。像钉钉的出差管理的图片选择最多允许选择9张、微信发朋友圈发图片最多允许9张。有时候需要一些限制才能解决一些无法避免的问题。
    上面的发生内存溢出错误的代码定位了一下,直接就定位到下面这一行,如下图所示:

定位错误代码行

该行代码如下:
bitmap=BitmapFactory.decodeStream(getActivity().getContentResolver().openInputStream(imageUri));
为什么这行代码会导致OOM呢?给大家讲讲:该行代码的作用是用于从指定输入流中解析、创建Bitmap对象。但是由于手机系统的内存比较小,如果不停的去解析、创建Bitmap对象,可能由于前面所创建的Bitmap所占用的内存还没有回收,而导致程序运行时引发OutOfMemory错误。所以我们需要将Bitmap对象先进行压缩,使用的时候可以降低对内存的占用,这样就可以有效的解决这个问题。我的解决方法是将以上的代码替换为下图所示的代码:

关键代码

上图所示的各行代码解释如下:

======

BitmapFactory.Options options = new BitmapFactory.Options();

该行代码用来创建一个BitmapFactory.Options对象,且Options 只保存图片尺寸大小,不保存图片到内存。

======

options.inSampleSize = 8;

该行代码是最关键的代码。给大家点进去看一下源码,源码如下所示:

源码图片

源码的注释给大家讲讲。
注释说:如果该值设置为>1,就会请求解析器对原图做二次抽样,即二次解析,返回一个较小的图片用来节省内存。二次抽样样本大小的像素尺寸,对应于一个解码位图的像素。举个例子,如果inSampleSize == 4,会返回一个是原图1/4高度和1/4宽度的图像,和1/16像素的数量。对任何值< = 1的值都用=1来赋值。
以上源码的注释很好的说明了该行代码的关键性。大家要有效避免OOM错误的话一定要记得加上哦!只有这样,才能有效的节省Bitmap对象占用的内存。别忘记了!

======

options.inPreferredConfig = Bitmap.Config.RGB_565;

该行代码是附加上图片的Config参数,解析器或根据当前的参数配置进行对应的解析,这也可以有效减少加载的内存。

======

options.inPurgeable = true;

该行代码作用:由此产生的位图将分配它的像素,这样他们可以被净化系统需要回收的内存。

======

options.inInputShareable = true;

该行代码的作用:inInputShareable属性和inPurgeable 有关,当inPurgeable 属性设置为false的时候,inInputShareable属性就可以忽略。但是如果这两个属性都设置为true的时候,源码的注释这样说:确定位图可以共享一个参考输入数据如果它必须深拷贝的话。好了这都不是什么关键的,不设置也是可以的。

======
最后调用方法:

bitmap=BitmapFactory.decodeStream(getActivity().getContentResolver().openInputStream(imageUri),null, options);

该行代码就是根据options的一些选项设置解析输入流返回一个压缩后的Bitmap对象。这时候使用bitmap的时候就可以有效的避免OOM了。下面附上我的项目的测试图片给大家看,眼见为实嘛!如下图:

这里写图片描述

好了,没有报OOM的错误,9张图片都成功的呈现在界面上。(爆料一下,中间那张图片是我的哦!不要羡慕哥,有时间记得锻炼!你也可以有我的身材! :) )

===========================================

就先说到这里,以后会遇到更多实际开发的小问题,到时候继续学习进步并把一些解决方案分享给大家。
好了说了这么久了。干货上场了!
给大家几点程序员摆脱疲劳的方法(博客上写的,但是我自己认为不错的):
链接地址是:程序员摆脱疲劳的方法

每天进步一点点!加油!

4
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:177388次
    • 积分:2245
    • 等级:
    • 排名:第18947名
    • 原创:63篇
    • 转载:3篇
    • 译文:5篇
    • 评论:81条
    最新评论