Android Bitmap 初探

最近一段时间的开发中和Bitmap接触较多,就Bitmap的使用有了一些新的认识,如何对Bitmap进行压缩,减小内存占用有了一些总结。javascript

背景

社交类(或者说是包含用户系统)的APP基本上都会包含用户自定义头像的功能,可让用户从相册选择或拍摄一张图片做为本身的头像,这样才能显现出每一个人的个性嘛!每一个用户的手机里各类各样不可描述的照片,从尺寸到大小各不相同,所以如何把用户选择的图片正确的加载到ImageView里就成了一件值得探讨的事情。好了,废话不说,下面就让咱们一步步揭开Bitmap的神秘面纱。java

从相册加载一张图片

咱们先从简单的入手,看看从手机相册加载一张图片到ImageView的正确方式。android

咱们就以上图为列,这张图片在我手机里的信息以下:git

能够看到,图片大小不足1M。那么把他加载到手机内存中时又会发生什么呢?github

打开相册加载图片

/** * 打开手机相册 */
    private void selectFromGalley() {
        Intent intent = new Intent();
        intent.setType("image/*");
        intent.setAction(Intent.ACTION_GET_CONTENT);
        intent.addCategory(Intent.CATEGORY_OPENABLE);
        startActivityForResult(intent, REQUEST_CODE_PICK_FROM_GALLEY);
    }复制代码

在Android 中打开相册是一件很是方便的事情,选择好图片以后就能够在onActivityResult中接收这张图片测试

if (resultCode == Activity.RESULT_OK) {
                    Uri uri = data.getData();
                    if (uri != null) {
                        ProcessResult(uri);
                    }
                }复制代码

根据Uri获得Bitmapui

@TargetApi(Build.VERSION_CODES.KITKAT)
    private void ProcessResult(Uri destUrl) {
        String pathName = FileHelper.stripFileProtocol(destUrl.toString());
        showBitmapInfos(pathName);
        Bitmap bitmap = BitmapFactory.decodeFile(pathName);
        if (bitmap != null) {
            mImageView.setImageBitmap(bitmap);
            float count = bitmap.getByteCount() / M_RATE;
            float all = bitmap.getAllocationByteCount() / M_RATE;
            String result = "这张图片占用内存大小:\n" +
                    "bitmap.getByteCount()== " + count + "M\n" +
                    "bitmap.getAllocationByteCount()= " + all + "M";
            info.setText(result);
            Log.e(TAG, result);
            bitmap = null;
        } else {
            T.showLToast(mContext, "fail");
        }
    }

    /** * 获取Bitmap的信息 * @param pathName */
    private void showBitmapInfos(String pathName) {
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeFile(pathName, options);
        int width = options.outWidth;
        int height = options.outHeight;

        Log.e(TAG, "showBitmapInfos: \n" +
                "width=: " + width + "\n" +
                "height=: " + height);
        options.inJustDecodeBounds = false;
    }复制代码

这里的处理很简单,须要注意的一点是onActivityResult 方法中返回的Intent返回的图片地址是一个Uri类型,包含具体协议,为了方便使用BitmapFactory的decode方法,须要将这个个Uri类型的地址转换为普通的地址,stripFileProtocol具体实现可参考源码spa

showBitmapInfos 这个方法就是很简单,就是获取一下所要加载图片的信息。这里主要仍是靠inJustDecodeBounds 这个参数,当此参数为true时,BitmapFactory 只会解析图片的原始宽/高信息,并不会去真正的加载图片。3d

咱们看一下输出日志及内存变化:日志

关于getByteCount和getAllocationByteCount的区别,这里暂时不讨论,只要知道他们均可以获取Bitmap占用内存大小

能够看到,因为这张图片是放在手机内部SD卡上,因此showBitmapInfos 解析后获取的图片宽高信息和以前是一致的,宽x高为 2160x1920。看到所占用的内存 15M,是否是有点意外,一张658KB 的加载后竟然要占这么大的内存。在看一下monitor检测的内存变化,在20s后选择图片后,占用内存有了一个明显的上升。占用这么大的内存,显然是很差的。可能不少人和我同样,在这个时候想到的第一个词是压缩图片,把图片变小他占的内存不就会变小了吗?好,那就压缩图片

压缩图片

压缩图片方案一(Compress)

由于咱们要处理的是Bitmap,首先从他自带的方法出发,果真找到了一个compress方法。

private Bitmap getCompressedBitmap(Bitmap bitmap) {
        try {
            //建立一个用于存储压缩后Bitmap的文件
            File compressedFile = FileHelper.createFileByType(mContext, destType, "compressed");
            Uri uri = Uri.fromFile(compressedFile);
            OutputStream os = getContentResolver().openOutputStream(uri);
            Bitmap.CompressFormat format = destType == FileHelper.JPEG ?
                    Bitmap.CompressFormat.JPEG : Bitmap.CompressFormat.PNG;
            boolean success = bitmap.compress(format, compressRate, os);
            if (success) {
                T.showLToast(mContext, "success");
            }

            final String pathName = FileHelper.stripFileProtocol(uri.toString());
            showBitmapInfos(pathName);
            bitmap = BitmapFactory.decodeFile(pathName);
            os.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return bitmap;
    }复制代码

bitmap.compress(format, compressRate, os) 会按照指定的格式和压缩比例将压缩后的bitmap写入到os 所对应的文件中。compressRate的取值在0-100之间,0表示压缩到最小尺寸。

在ProcessResult方法中,咱们获取bitmap后,首先经过上述方法将bitmap压缩,而后在显示到ImageView中。咱们看一下,压缩事后的状况。

上面的日志,第一个showBitmapInfos 显示的是选择的图片经过BitmapFactory解析后的信息,第二个showBitmapInfos
显示的压缩后图片的宽高信息,最后很意外,咱们的压缩方法彷佛没起到做用,占用的内存没有任何变化,依旧是15M。
难道是compress方法没生效吗?其实否则,至少从UI上看compress的确生效了, 当compressRate=0时,懒羊羊的图片显示到ImageView上时已经很是不清晰了,失真很是严重。那么究竟是为何呢?

这里就得从概念上提及,一开始咱们提到了这张懒羊羊的图片大小时658KB,这是它在手机存储空间所占的大小,而当咱们在选择这张图片,并解析为Bitmap时,他所站的15MB是在内存中所占的大小;而compress方法只能压缩前一种大小,也就是所使用Bitmap的compress方法只是压缩他在存储空间的大小,结果就是致使图片失真;而不能改变他在内存中所占用的大小

那么怎样才能让Bitmap所占用的内存变小呢?这就的从Bitmap占用内存的计算方法入手,在这篇文章中已经对bitmap所占用内存大小作了深刻分析,从中咱们能够得出结论,决定一张图片所占内存大小的因素是图片的宽高和Bitmap的格式。这里咱们加载的时候对Bitmap格式未作更改,也就是默认的ARGB_8888,所以咱们就得从宽高入手,得出以下的压缩方案。

压缩图片方案二 (Crop)

private void CropTheImage(Uri imageUrl) {
        Intent cropIntent = new Intent("com.android.camera.action.CROP");
        cropIntent.setDataAndType(imageUrl, "image/*");
        cropIntent.putExtra("cropWidth", "true");
        cropIntent.putExtra("outputX", cropTargetWidth);
        cropIntent.putExtra("outputY", cropTargetHeight);
        File copyFile = FileHelper.createFileByType(mContext, destType, String.valueOf(System.currentTimeMillis()));
        copyUrl = Uri.fromFile(copyFile);
        cropIntent.putExtra("output", copyUrl);
        startActivityForResult(cropIntent, REQUEST_CODE_CROP_PIC);
    }复制代码

这里调用了系统自带的图片裁剪控件,并建立了一个copyFile 的文件,裁剪事后的图片的地址指向就是这个文件所对应的地址。
当cropTargetWidth=1080,cropTargetHeight=920时,咱们看一下日志:

能够看到,Bitmap所占用的内存终于变小了,并且因为在裁剪时宽高各缩小了1/2,整个内存的占用也是缩小了1/4,变成了3.9M左右。同时图片在手机存储空间也变小了。

固然,这里要注意的是,com.android.camera.action.CROP 中两个参数 "outputX" 和"outputY",决定了压缩后图片的大小,所以当这两个值的大小超过原始图片的大小时,内存占用反而会增长,这一点应该很好理解,因此需确保传递合适的值,不然会拔苗助长。

图片压缩方案三 (Sample )

采用Sample,也就是是采样的方式压缩图片以前,咱们首先须要了解一下inSampleSize 这个参数。

inSampleSize 是BitmapFactory.Options 的一个参数,当他为1时,采样后的图片大小为图片原始大小;当inSampleSize 为2时,那么采样后的图片其宽/高均为原图大小的1/2,而像素数为原图的1/4,其占有的内存大小也为原图的1/4。inSampleSize 的取值应该是2的指数。

private Bitmap getRealCompressedBitmap(String pathName, int reqWidth, int reqHeight) {
        Bitmap bitmap;
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeFile(pathName, options);
        int width = options.outWidth / 2;
        int height = options.outHeight / 2;
        int inSampleSize = 1;

        while (width / inSampleSize >= reqWidth && height / inSampleSize >= reqHeight) {
            inSampleSize = inSampleSize * 2;
        }

        options.inSampleSize = inSampleSize;
        options.inJustDecodeBounds = false;
        bitmap = BitmapFactory.decodeFile(pathName, options);
        showBitmapInfos(pathName);
        return bitmap;
    }复制代码

能够以下调用这个方法:

if (needSample) {
                bitmap = getRealCompressedBitmap(pathName, 200, 200);
            }复制代码

咱们但愿将2160x1920像素的原图压缩到200x200 像素的大小,所以在getRealCompressedBitmap方法中,经过while循环inSampleSize的值最终为8,所以内存占用率将变为原来的1/64,这是一个很大的降幅。咱们看一下日志,看看究竟是否可以如咱们所愿:

能够看到,使用这种方法进行图片压缩后,增长的内存只有0.24M,几乎能够忽略不计了。固然前提是咱们要使用的图片的确不须要很大,好比这里,须要用这张图片做为用户头像的话,那么将原图缩略成200x200 px的大小是没有问题的。

三种方案对比

上面提到的三种压缩方案,经过对比能够发现,第一种方案适用于进行纯粹的文件压缩,而不适用进行图像处理压缩;第二种方案压缩方案适用于进行图像编辑时的压缩,就像手机自带相册的编辑功能,能够随着裁剪区域的大小进行最终的压缩;第三种方案相对来讲,适应性较强,各类场景都会符合。

从Camera 获取Bitmap

有时候,咱们除了从相册获取图片以外,还能够经过手机自带的相机拍摄图片。

private void openCamera() {
        Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        //建立一个临时文件夹存储拍摄的照片
        File file = FileHelper.createFileByType(mContext, destType, "test");
        imageUrl = Uri.fromFile(file);
        takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, imageUrl);
        if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
            startActivityForResult(takePictureIntent, REQUEST_CODE_TAKE_PIC_CAMERA);
        }
    }复制代码

不一样于从相册选取图片,打开相机以前须要咱们本身定义一个存储图片的临时文件file,这个临时文件既能够在应用的临时存储区也能够在手机存储的临时存储区;经过这个文件就能够生成一个Uri对象,有了这个Uri对象,相机拍摄完照片以后就能够在onActivityResult方法中经过这个Uri获取到Bitmap了。

这里咱们能够试一下,随便用手机拍摄一张图片转为Bitmap加载会占多大的手机内存(以我用的小米手机5为列,拍摄一张图片):

能够看到这张图片的分辨率达到了3456x4608 像素,而他加载到内存是所占的大小竟然达到了60M,这是很是不科学的作法,也是毫无心义的作法,由于咱们的手机可见区域并无这么大,将整张照片彻底加载是没有意义的。所以能够按照以前的压缩方案进行压缩。

bitmap = getRealCompressedBitmap(pathName, screenWidth, screenHeight);复制代码

咱们能够将原来的图片压缩到手机屏幕大小的图片

能够看到占用内存有了明显的减小。

将拍摄的图片添加到手机相册中

有时须要将拍摄出来的照片添加到手机相册中,方便从相册直接查看

private void insertToGallery(Uri imageUrl) {
        Uri galleryUri = Uri.fromFile(new File(FileHelper.getPicutresPath(destType)));
        boolean result = FileHelper.copyResultToGalley(mContext, imageUrl, galleryUri);
        if (result) {
            Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
            mediaScanIntent.setData(galleryUri);
            sendBroadcast(mediaScanIntent);
        }
    }复制代码

copyResultToGalley 方法的实现很简单,就是将imageUri 这个地址的文件复制到galleryUri 这个地址,复制成功后发送一条
action="ACTION_MEDIA_SCANNER_SCAN_FILE" 的广播便可。

好了,关于Bitmap的初探就说到这里,对于上面提到的各类压缩方案,有兴趣的同窗可结合一下demo测试。Github 地址

总结

用了好久的ImageView,发现Bitmap才是Android中图像处理最核心的东西,有不少东西值得去深刻了解。

相关文章
相关标签/搜索