深入理解Android Bitmap的各种操作

文章目录

    • 一、Bitmap
      • 1.1 Bitmap的创建
        • 1.1.1 根据已有的Bitmap来创建新Bitmap
        • 1.1.2 通过像素点数组创建空的Bitmap
        • 1.1.3 创建缩放的Bitmap
    • 二、BitmapFactory
      • 2.1 创建Bitmap的方法
      • 2.2 BitmapFactory.Options的属性解析
    • 三、计算Bitmap的大小
      • 3.1 Android API 的方法
      • 3.2 手动计算
    • 四、Bitmap的缩放
      • 4.1 质量压缩
      • 4.2 采样压缩
      • 4.3 使用矩阵
      • 4.4 合理选择Bitmap的像素格式
      • 4.5 综合优化

Android 开发中,经常和 Bitmap 打交道,不知道你是否真正理解 Bitmap?接下来让我们一起走进 Bitmap 的世界。

一、Bitmap

  Bitmap 代表一个位图,BitmapDrawable 里封装的图片就是一个 Bitmap 对象。开发者为了把一个 Bitmap 对象包装成 BitmapDrawable 对象,可以调用 BitmapDrawable 的构造器:

BitmapDrawable bitmapDrawable = new BitmapDrawable(bitmap);

如果需要获取 BitmapDrawable 所包装的 Bitmap 对象,则可以用 BitmapDrawablegetBitmap() 的方法:

Bitmap bitmap = bitmapDrawable.getBitmap();

1.1 Bitmap的创建

  在 Bitmap 类中, Bitmap 构造方法默认权限,而且这个类也是 final 的,所以我们无法 new 一个 Bitmap 对象。可以根据静态方法 createBitmap 来创建 Bitmap 类。这些重载的方法有13个,他们分别是:
深入理解Android Bitmap的各种操作_第1张图片
这些方法大致可以分为三类(本文所用的源码是 android - 26):

1.1.1 根据已有的Bitmap来创建新Bitmap

/**
* 通过矩阵的方式,返回原始 Bitmap 中的一个不可变子集。新 Bitmap 可能返回的就是原始的 Bitmap,也可能还是复制出来的。
* 新 Bitmap 与原始 Bitmap 具有相同的密度(density)和颜色空间;
*
* @param source   原始 Bitmap
* @param x        在原始 Bitmap 中 x方向的其起始坐标(你可能只需要原始 Bitmap x方向上的一部分)
* @param y        在原始 Bitmap 中 y方向的其起始坐标(你可能只需要原始 Bitmap y方向上的一部分)
* @param width    需要返回 Bitmap 的宽度(px)(如果超过原始Bitmap宽度会报错)
* @param height   需要返回 Bitmap 的高度(px)(如果超过原始Bitmap高度会报错)
* @param m        Matrix类型,表示需要做的变换操作
* @param filter   是否需要过滤,只有 matrix 变换不只有平移操作才有效
*/
public static Bitmap createBitmap(@NonNull Bitmap source, int x, int y, int width, int height,
            @Nullable Matrix m, boolean filter) 

1.1.2 通过像素点数组创建空的Bitmap

/**
     * 
     * 返回具有指定宽度和高度的不可变位图,每个像素值设置为colors数组中的对应值。
     * 其初始密度由给定的确定DisplayMetrics。新创建的位图位于sRGB 颜色空间中。
     * @param display  显示将显示此位图的显示的度量标准
     * @param colors   用于初始化像素的sRGB数组
     * @param offset   颜色数组中第一个颜色之前要跳过的值的数量
     * @param stride   行之间数组中的颜色数(必须> = width或<= -width)
     * @param width    位图的宽度
     * @param height   位图的高度
     * @param config   要创建的位图配置。如果配置不支持每像素alpha(例如RGB_565),
     * 那么colors []中的alpha字节将被忽略(假设为FF)
     */
    public static Bitmap createBitmap(@NonNull DisplayMetrics display,
            @NonNull @ColorInt int[] colors, int offset, int stride,
            int width, int height, @NonNull Config config) 

1.1.3 创建缩放的Bitmap

/**
* 对Bitmap进行缩放,缩放成宽 dstWidth、高 dstHeight 的新Bitmap
*/
public static Bitmap createScaledBitmap(@NonNull Bitmap src, int dstWidth, int dstHeight,boolean filter)

二、BitmapFactory

  BitmapFactory 是一个工具类,它提供了大量的方法,这个方法可用于不同的数据源来解析、创建 Bitmap 对象。大概有如下方法:
深入理解Android Bitmap的各种操作_第2张图片

2.1 创建Bitmap的方法

  • decodeByteArray(byte[] data, int offset, int length, Options opts):从指定字节数组的 offset 位置开始,将长度 length 的字节数据解析成 Bitmap 对象。
  • decodeFile(String pathName, Options opts):从 pathName 指定的文件中解析、创建 Bitmap 对象。
  • decodeFileDescriptor(FileDescriptor fd, Rect outPadding, Options opts):用于从 FileDescriptor 对应的文件中解析、创建 Bitmap 对象。
  • decodeResource(Resources res, int id, Options opts):用于给定的资源 ID 从指定资源中解析、创建 Bitmap 对象。
  • decodeStream(InputStream is, Rect outPadding, Options opts):用于从指定输入流中解析、创建 Bitmap 对象。

decodeFiledecodeResource 其实最终都会调用 decodeStream 方法来解析 Bitmap 。有一个特别有意思的事情是,在 decodeResource 调用 decodeStream 之前还会调用 decodeResourceStream 这个方法,接下来让我们看看 decodeResourceStream 方法的源码:

public static Bitmap decodeResourceStream(Resources res, TypedValue value,
            InputStream is, Rect pad, Options opts) {
        validate(opts);
        if (opts == null) {
            opts = new Options();
        }

        if (opts.inDensity == 0 && value != null) {
            final int density = value.density;
            if (density == TypedValue.DENSITY_DEFAULT) {
                opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
            } else if (density != TypedValue.DENSITY_NONE) {
            	  //这里 density 的值如果对应资源目录为 hdpi 的话,就是 240;如果是 xhdpi 则是 320;如果是 xxdpi 则是 480
                opts.inDensity = density;
            }
        }
        
        if (opts.inTargetDensity == 0 && res != null) {
            opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
        }
        
        return decodeStream(is, pad, opts);
    }

  这个方法主要对 Options (详解的属性值在下面)进行处理,在得到 opts.inDensity 的属性前提下,如果没有对该属性的设定值,那么 opts.inDensity = DisplayMetrics.DENSITY_DEFAULT; 这个值默认为标准dpi的基值:160。如果没有设定 opts.inTargetDensity 的值时,opts.inTargetDensity = res.getDisplayMetrics().densityDpi; 该值为当前设备的 densityDpi,这个值是根据你放置在 drawable 下的文件不同而不同的(具体参考 Android 屏幕各种参数的介绍和学习)。
  所以说 decodeResourceStream 这个方法主要对 opts.inDensityopts.inTargetDensity进行赋值。那什么时候使用这个 opts 属性呢?在将参数传入 decodeStream方法,该方法在调用 native 方法进行解析 Bitmap 后会调用 setDensityFromOptions 这个方法:

/**
     * Set the newly decoded bitmap's density based on the Options.
     */
    private static void setDensityFromOptions(Bitmap outputBitmap, Options opts) {
        if (outputBitmap == null || opts == null) return;
		 //opts.inDensity 这个值会因为你放置在 drawable 下不同分辨率的文件夹下而不同
        final int density = opts.inDensity;
        if (density != 0) {
            outputBitmap.setDensity(density);
            final int targetDensity = opts.inTargetDensity;
            if (targetDensity == 0 || density == targetDensity || density == opts.inScreenDensity) {
                return;
            }

            byte[] np = outputBitmap.getNinePatchChunk();
            final boolean isNinePatch = np != null && NinePatch.isNinePatchChunk(np);
            if (opts.inScaled || isNinePatch) {
                outputBitmap.setDensity(targetDensity);
            }
        } else if (opts.inBitmap != null) {
            // bitmap was reused, ensure density is reset
            outputBitmap.setDensity(Bitmap.getDefaultDensity());
        }
    }

这个方法就是把刚刚赋值过的两个属性 inDensityinTargetDensityBitmap 进行赋值,不过并不是直接赋值给 Bitmap 而是判断 inDensity 的值与 inTargetDensity 或 该设备屏幕 Density 不相等 ,而且 opts.inScaled = true 时,条件才成立。具体的计算见:

从上面的源码可以得出一个重要的结论:

  • decodeResource 在解析时会对 Bitmap 根据当前设备屏幕密度 densityDpi 的值进行缩放适配操作,使得解析出来的 Bitmap 与当前设备分辨率匹配,并且一般来说,这时 Bitmap 的大小将比原始的 Bitmap 大。
  • decodeFiledecodeStream 在解析时不会对 Bitmap 进行一系列的屏幕适配,解析出来的将是原始大小的图。

2.2 BitmapFactory.Options的属性解析

在使用 BitmapFactoryOptions 这个静态内部类经常用到,里面有很多经常使用的属性,让我们来看一些比较重要的:

  • inJustDecodeBounds:如果这个值为 true ,那么在解码的时候将不会返回 Bitmap ,只会返回这个 Bitmap 的尺寸。这个属性的目的是,如果你只想知道一个 Bitmap 的尺寸,但又不想将其加载到内存中时,是一个非常好用的属性。
  • outWidth和outHeight:表示这个 Bitmap 的宽和高,一般和 inJustDecodeBounds 一起使用来获得 Bitmap的宽高,但是不加载到内存。
  • inSampleSize:压缩图片时采样率的值,如果这个值大于1,那么就会按照比例(1 / inSampleSize)来缩小 Bitmap 的宽和高。如果这个值为 2,那么 Bitmap 的宽为原来的1/2,高为原来的1/2,那么这个 Bitmap 是所占内存像素值会缩小为原来的 1/4。
  • inDensity:表示这个 Bitmap 的像素密度,对应的是 DisplayMetrics 中的 densityDpi,不是 density。(如果不明白它俩之间的异同,可以看我的 Android 屏幕各种参数的介绍和学习 )
  • inTargetDensity:表示要被新 Bitmap 的目标像素密度,对应的是 DisplayMetrics 中的 densityDpi
  • inScreenDensity:表示实际设备的像素密度,对应的是 DisplayMetrics 中的 densityDpi
  • inPreferredConfig:这个值是设置色彩模式,默认值是 ARGB_8888,这个模式下,一个像素点占用 4ByteRGB_565 占用 2ByteARGB_4444 占用 4Byte(以废弃)。
  • inPremultiplied:这个值和透明度通道有关,默认值是 true,如果设置为 true,则返回的 Bitmap 的颜色通道上会预先附加上透明度通道。
  • inDither:这个值和抖动解码有关,默认值为 false,表示不采用抖动解码。
  • inScaled:设置这个Bitmap 是否可以被缩放,默认值是 true,表示可以被缩放。
  • inPreferQualityOverSpeed:这个值表示是否在解码时图片有更高的品质,仅用于 JPEG 格式。如果设置为 true,则图片会有更高的品质,但是会解码速度会很慢。
  • inBitmap:这个参数用来实现 Bitmap 内存的复用,但复用存在一些限制,具体体现在:在 Android 4.4 之前只能重用相同大小的 Bitmap 的内存,而 Android 4.4 及以后版本则只要后来的 Bitmap 比之前的小即可。使用 inBitmap 参数前,每创建一个 Bitmap 对象都会分配一块内存供其使用,而使用了 inBitmap 参数后,多个 Bitmap 可以复用一块内存,这样可以提高性能。

三、计算Bitmap的大小

3.1 Android API 的方法

在使用 Bitmap 时经常会出现 OOM 的现象(这是多么让人心痛的错误啊),那么一张图片究竟是有多大呢?在 Bitmap 中提供了一个供我们查看 Bitmap 大小的 getByteCount() 方法:

public final int getByteCount() {
        if (mRecycled) {
            Log.w(TAG, "Called getByteCount() on a recycle()'d bitmap! "
                    + "This is undefined behavior!");
            return 0;
        }
        // int result permits bitmaps up to 46,340 x 46,340
        return getRowBytes() * getHeight();
    }

这个是 Android API所给的方法,当我们想进一步看看它是怎么实现的时候却发现 getRowBytes 调用的是 native 方法。那么我们能不能给一张在某个手机上的图片,你就知道 Bitmap 所在内存的大小呢?为了探究 Bitmap 的奥秘,我们去手动计算一张 Bitmap 的大小。

3.2 手动计算

下载了 Android framework 源码,Bitmap 相关的源码在文件下:frameworks\base\core\jni\android\graphics,找到 Bitmap.cpp, getRowBytes() 对应的函数为:

static jint Bitmap_rowBytes(JNIEnv* env, jobject, jlong bitmapHandle) {
    LocalScopedBitmap bitmap(bitmapHandle);
    return static_cast<jint>(bitmap->rowBytes());
}

SkBitmap.cpp

size_t SkBitmap::ComputeRowBytes(Config c, int width) {
    return SkColorTypeMinRowBytes(SkBitmapConfigToColorType(c), width);
}

SkImageInfo.h

static int  SkColorTypeBytesPerPixel(SkColorType ct) {
   static const  uint8_t gSize[] = {
    0,  // Unknown

    1,  // Alpha_8

    2,  // RGB_565

    2,  // ARGB_4444

    4,  // RGBA_8888

    4,  // BGRA_8888

    1,  // kIndex_8

  };
  SK_COMPILE_ASSERT(SK_ARRAY_COUNT(gSize) == (size_t)(kLastEnum_SkColorType + 1),
                size_mismatch_with_SkColorType_enum);
   SkASSERT((size_t)ct < SK_ARRAY_COUNT(gSize));
   return gSize[ct];
}
static inline size_t SkColorTypeMinRowBytes(SkColorType ct, int width) {
    return width * SkColorTypeBytesPerPixel(ct);

}

顺便提一下,Bitmap 本质上是一个 SKBitmap ,而这个 SKBitmap 也是大有来头,它来自 Skia 。这个是 Android 2D 图像引擎,而且也是 flutter 的图像引擎。我们发现 ARGB_8888(也就是我们最常用的 Bitmap 的格式)的一个像素占用 4byte,那么 rowBytes 实际上就是 4*width bytes。那么一行图片所占的内存计算公式:

图片的占用内存 = 图片的长度(像素单位) * 图片的宽度(像素单位) * 单位像素所占字节数

但需要注意的是, 在使用decodeResource 获得的 Bitmap 的时候,上面的计算公式并不准确。让我们来看看原因。decodeStream 会调用 native 方法 nativeDecodeStream 最终会调用BitmapFactory.cppdoDecode函数:

static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding, jobject options) {
    	// .....省略
        if (env->GetBooleanField(options, gOptions_scaledFieldID)) {
        	  //对应不同 dpi 的值不同,这个值跟这张图片的放置的目录有关,如 hdpi 是240;xdpi 是320;xxdpi 是480。
            const int density = env->GetIntField(options, gOptions_densityFieldID);
            //特定手机的屏幕像素密度不同,如华为p20 pro targetDensity是480
            const int targetDensity = env->GetIntField(options, gOptions_targetDensityFieldID);
            const int screenDensity = env->GetIntField(options, gOptions_screenDensityFieldID);
            if (density != 0 && targetDensity != 0 && density != screenDensity) {
            	  //缩放比例(这个是重点)
                scale = (float) targetDensity / density;
            }
        }
    }
	//这里这个deodingBitmap就是解码出来的bitmap,大小是图片原始的大小
	int scaledWidth = size.width();
    int scaledHeight = size.height();
    bool willScale = false;

    // Apply a fine scaling step if necessary.
    if (needsFineScale(codec->getInfo().dimensions(), size, sampleSize)) {
        willScale = true;
        scaledWidth = codec->getInfo().width() / sampleSize;
        scaledHeight = codec->getInfo().height() / sampleSize;
    }
    // Scale is necessary due to density differences.
    //进行缩放后的高度和宽度
    if (scale != 1.0f) {
        willScale = true;
        scaledWidth = static_cast<int>(scaledWidth * scale + 0.5f);//①
        scaledHeight = static_cast<int>(scaledHeight * scale + 0.5f);
    }
    //.......省略
    if (willScale) {
        // This is weird so let me explain: we could use the scale parameter
        // directly, but for historical reasons this is how the corresponding
        // Dalvik code has always behaved. We simply recreate the behavior here.
        // The result is slightly different from simply using scale because of
        // the 0.5f rounding bias applied when computing the target image size
        //sx 和 sy 实际上约等于 scale ,因为在①出可以看出scaledWidth 和 scaledHeight 是由 width 和 height 乘以 scale 得到的。
        const float sx = scaledWidth / float(decodingBitmap.width());
        const float sy = scaledHeight / float(decodingBitmap.height());

        // Set the allocator for the outputBitmap.
        SkBitmap::Allocator* outputAllocator;
        if (javaBitmap != nullptr) {
            outputAllocator = &recyclingAllocator;
        } else {
            outputAllocator = &defaultAllocator;
        }

        SkColorType scaledColorType = colorTypeForScaledOutput(decodingBitmap.colorType());
        // FIXME: If the alphaType is kUnpremul and the image has alpha, the
        // colors may not be correct, since Skia does not yet support drawing
        // to/from unpremultiplied bitmaps.
        outputBitmap.setInfo(
                bitmapInfo.makeWH(scaledWidth, scaledHeight).makeColorType(scaledColorType));
        if (!outputBitmap.tryAllocPixels(outputAllocator, NULL)) {
            // This should only fail on OOM.  The recyclingAllocator should have
            // enough memory since we check this before decoding using the
            // scaleCheckingAllocator.
            return nullObjectReturn("allocation failed for scaled bitmap");
        }

        SkPaint paint;
        // kSrc_Mode instructs us to overwrite the uninitialized pixels in
        // outputBitmap.  Otherwise we would blend by default, which is not
        // what we want.
        paint.setBlendMode(SkBlendMode::kSrc);
        paint.setFilterQuality(kLow_SkFilterQuality); // bilinear filtering

        SkCanvas canvas(outputBitmap, SkCanvas::ColorBehavior::kLegacy);
        canvas.scale(sx, sy);//canvas进行缩放
        canvas.drawBitmap(decodingBitmap, 0.0f, 0.0f, &paint);
    } else {
        outputBitmap.swap(decodingBitmap);
    }
    //.......省略
}

所以,sxsy 约等于 scale 的值,而 scale = (float) targetDensity / density;,那么我们来计算,在一张4000 * 3000 的jpg 图片,我们把它放在 drawable - xhdpi 目录下,在华为 P20 Pro 上加载,这个手机上densityDpi为 480,用 getByteCount 算出占用内存为 108000000。
我们来手动计算:
sx = 480/320,
sy = 480/320,
4000 * sx *3000 * sy * 4 = 108000000

如果你自己算你手机上的可能和 getByteCount 不一致,那是因为精度不一样,大家可以看到上面源码 ①处,scaledWidth = static_cast(scaledWidth * scale + 0.5f); 它是这样算的,所以我们可以用:
scaledWidth = int (480/320 *4000 + 0.5)
scaledHeight = int (480/320 *3000 + 0.5)
Bitmap所占内存空间为:scaledWidth * scaledHeight * 4
所以这时Bitmap所占内存空间的方式为:

图片的占用内存 = 缩放后图片的长度(像素单位) * 缩放后图片的宽度(像素单位) * 单位像素所占字节数

总结:这个方法主要是让我们知道为什么同一张图片放在不同分辨率的文件下,Bitmap 所占内存空间的不同。而且这个计算方式是用 decodeResource 来得到Bitmap 的大小时,才有效。而用 decodeFiledecodeStream直接计算就行,没有缩放宽和高的整个过程。

四、Bitmap的缩放

4.1 质量压缩

  质量压缩不会改变图片的像素点,即我们使用完质量压缩后,在转换 Bitmap 时占用内存依旧不会减小。但是可以减少我们存储在本地文件的大小,即放到 disk上的大小。如果减少 Bitmap 加载到内存的大小,可以用采样压缩。下面是质量压缩的代码:

 /**
     * 质量压缩方法,并不能减小加载到内存时所占用内存的空间,应该是减小的所占用磁盘的空间
     * @param image
     * @param compressFormat
     * @return
     */
    public static Bitmap compressbyQuality(Bitmap image, Bitmap.CompressFormat compressFormat) {

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        image.compress(compressFormat, 100, baos);//质量压缩方法,这里100表示不压缩,把压缩后的数据存放到baos中
        int quality = 100;

        while ( baos.toByteArray().length / 1024 > 100) { //循环判断如果压缩后图片是否大于100kb,大于继续压缩
            baos.reset();//重置baos即清空baos
            if(quality > 10){
                quality -= 20;//每次都减少20
            }else {
                break;
            }
            image.compress(Bitmap.CompressFormat.JPEG, quality, baos);//这里压缩options%,把压缩后的数据存放到baos中

        }
        ByteArrayInputStream isBm = new ByteArrayInputStream(baos.toByteArray());//把压缩后的数据baos存放到ByteArrayInputStream中

        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inPreferredConfig = Bitmap.Config.RGB_565;
        Bitmap bmp = BitmapFactory.decodeStream(isBm, null, options);//把ByteArrayInputStream数据生成图片

        return bmp;
    }

4.2 采样压缩

这个方法主要用在图片资源本身较大,或者适当地采样并不会影响视觉效果的条件下,这时候我们输出的目标可能相对的较小,对图片的大小和分辨率都减小。采样压缩最典型的代码如下:

BitmapFactory.Options options = new Options();
options.inSampleSize = 2;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), resId, options);

4.3 使用矩阵

前面我们采用了采样压缩,Bitmap 所占用的内存是小了,可是图的尺寸也小了。当我们需要尺寸较大时该怎么办?我们要用用 Canvas 绘制怎么办?当然可以用矩阵(Matrix):

/**
     * 矩阵缩放图片
     * @param sourceBitmap
     * @param width 要缩放到的宽度
     * @param height 要缩放到的长度
     * @return
     */
    private Bitmap getScaleBitmap(Bitmap sourceBitmap,float width,float height){
        Bitmap scaleBitmap;
        //定义矩阵对象
        Matrix matrix = new Matrix();
        float scale_x = width/sourceBitmap.getWidth();
        float scale_y = height/sourceBitmap.getHeight();
        matrix.postScale(scale_x,scale_y);

        try {
            scaleBitmap = Bitmap.createBitmap(sourceBitmap,0,0,sourceBitmap.getWidth(),sourceBitmap.getHeight(),matrix,true);
        }catch (OutOfMemoryError e){
            scaleBitmap = null;
            System.gc();
        }
        return scaleBitmap;
    }

4.4 合理选择Bitmap的像素格式

ARG_B8888 格式的图片,每像素占用 4 Byte,而 RGB_565 则是 2 Byte。显而易见,不同的像素格式其Bitmap 的大小也就不同。

格式 单位像素所占字节数 描述
ALPHA_8 1 只有一个alpha通道
ARGB_4444 2 这个从API 13开始不建议使用,因为质量太差
ARGB_8888 4 ARGB四个通道,每个通道8bit
RGB_565 2 每个像素占2Byte,其中红色占5bit,绿色占6bit,蓝色占5bit

4.5 综合优化

下面的例子主要用到了 Bitmap 的采样压缩(这个采样率是根据需求来进行生成的),使用到了inBitmap内存复用和 inJustDecodeBounds (这两个字段上面都有介绍)
下面介绍获取采样的流程:

  1. BitmapFactory.OptionsinJustDecodeBounds 参数设置为 true 并加装图片。
  2. BitmapFactory.Options 中取出图片的原始宽和高,它们对应于 outWidthoutHeight 参数。
  3. 根据采样率的规则并结合目标 View 的所需要大小计算出采样率 inSampleSize
  4. BitmapFactory.OptionsinJustDecodeBounds 参数设为 false ,然后重新加装图片。
/**
     * 采样率压缩,这个和矩阵来实现缩放有点类似,但是有一个原则是“大图小用用采样,小图大用用矩阵”。
     * 也可以先用采样来压缩图片,这样内存小了,可是图的尺寸也小。如果要是用 Canvas 来绘制这张图时,再用矩阵放大
     * @param image
     * @param compressFormat
     * @param requestWidth 要求的宽度
     * @param requestHeight 要求的长度
     * @return
     */
    public static Bitmap compressbySample(Bitmap image, Bitmap.CompressFormat compressFormat, int requestWidth, int requestHeight){
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        image.compress(compressFormat, 100, baos);//质量压缩方法,这里100表示不压缩,把压缩后的数据存放到baos中
        ByteArrayInputStream isBm = new ByteArrayInputStream(baos.toByteArray());//把压缩后的数据baos存放到ByteArrayInputStream中

        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inPreferredConfig = Bitmap.Config.RGB_565;
        options.inPurgeable = true;
        options.inJustDecodeBounds = true;//只读取图片的头信息,不去解析真是的位图
        BitmapFactory.decodeStream(isBm,null,options);
        options.inSampleSize = calculateInSampleSize(options,requestWidth,requestHeight);
        //-------------inBitmap------------------
        options.inMutable = true;
        try{
            Bitmap inBitmap = Bitmap.createBitmap(options.outWidth, options.outHeight, Bitmap.Config.RGB_565);
            if (inBitmap != null && canUseForInBitmap(inBitmap, options)) {
                options.inBitmap = inBitmap;
            }
        }catch (OutOfMemoryError e){
            options.inBitmap = null;
            System.gc();
        }

        //---------------------------------------

        options.inJustDecodeBounds = false;//真正的解析位图
        isBm.reset();
        Bitmap compressBitmap;
        try{
            compressBitmap =  BitmapFactory.decodeStream(isBm, null, options);//把ByteArrayInputStream数据生成图片
        }catch (OutOfMemoryError e){
            compressBitmap = null;
            System.gc();
        }

        return compressBitmap;
    }

    /**
     * 采样压缩比例
     * @param options
     * @param reqWidth 要求的宽度
     * @param reqHeight 要求的长度
     * @return
     */
    private static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {

        int originalWidth = options.outWidth;
        int originalHeight = options.outHeight;
        
        int inSampleSize = 1;

        if (originalHeight > reqHeight || originalWidth > reqHeight){
            // 计算出实际宽高和目标宽高的比率
            final int heightRatio = Math.round((float) originalHeight / (float) reqHeight);
            final int widthRatio = Math.round((float) originalWidth / (float) reqWidth);
            // 选择宽和高中最小的比率作为inSampleSize的值,这样可以保证最终图片的宽和高
            // 一定都会大于等于目标的宽和高。
            inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;

        }
        return inSampleSize;
    }

勤能补拙是良训,一分辛苦一分才。

站在巨人的肩膀上:
计算bitmap占用内存大小
BitmapFactory解析与Bitmap的内存优化
图片基础知识梳理(3) - Bitmap&BitmapFactory 解析
Android中BitmapFactory.Options详解

你可能感兴趣的:(Android)