Bitmap 代表一个位图,BitmapDrawable 里封装的图片就是一个 Bitmap 对象。开发者为了把一个 Bitmap 对象包装成 BitmapDrawable 对象,可以调用 BitmapDrawable 的构造器:
BitmapDrawable bitmapDrawable = new BitmapDrawable(bitmap);
如果需要获取 BitmapDrawable 所包装的 Bitmap 对象,则可以用 BitmapDrawable 的 getBitmap() 的方法:
Bitmap bitmap = bitmapDrawable.getBitmap();
在 Bitmap 类中, Bitmap 构造方法默认权限,而且这个类也是 final 的,所以我们无法 new 一个 Bitmap 对象。可以根据静态方法 createBitmap 来创建 Bitmap 类。这些重载的方法有13个,他们分别是:
这些方法大致可以分为三类(本文所用的源码是 android - 26):
/**
* 通过矩阵的方式,返回原始 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)
/**
*
* 返回具有指定宽度和高度的不可变位图,每个像素值设置为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)
/**
* 对Bitmap进行缩放,缩放成宽 dstWidth、高 dstHeight 的新Bitmap
*/
public static Bitmap createScaledBitmap(@NonNull Bitmap src, int dstWidth, int dstHeight,boolean filter)
BitmapFactory 是一个工具类,它提供了大量的方法,这个方法可用于不同的数据源来解析、创建 Bitmap 对象。大概有如下方法:
decodeFile 和 decodeResource 其实最终都会调用 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.inDensity 和 opts.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());
}
}
这个方法就是把刚刚赋值过的两个属性 inDensity 和 inTargetDensity 给 Bitmap 进行赋值,不过并不是直接赋值给 Bitmap 而是判断 inDensity 的值与 inTargetDensity 或 该设备屏幕 Density 不相等 ,而且 opts.inScaled = true 时,条件才成立。具体的计算见:
从上面的源码可以得出一个重要的结论:
- decodeResource 在解析时会对 Bitmap 根据当前设备屏幕密度 densityDpi 的值进行缩放适配操作,使得解析出来的 Bitmap 与当前设备分辨率匹配,并且一般来说,这时 Bitmap 的大小将比原始的 Bitmap 大。
- decodeFile、decodeStream 在解析时不会对 Bitmap 进行一系列的屏幕适配,解析出来的将是原始大小的图。
在使用 BitmapFactory 时 Options 这个静态内部类经常用到,里面有很多经常使用的属性,让我们来看一些比较重要的:
在使用 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 的大小。
下载了 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.cpp 的 doDecode函数:
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);
}
//.......省略
}
所以,sx 和 sy 约等于 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 的大小时,才有效。而用 decodeFile、decodeStream直接计算就行,没有缩放宽和高的整个过程。
质量压缩不会改变图片的像素点,即我们使用完质量压缩后,在转换 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;
}
这个方法主要用在图片资源本身较大,或者适当地采样并不会影响视觉效果的条件下,这时候我们输出的目标可能相对的较小,对图片的大小和分辨率都减小。采样压缩最典型的代码如下:
BitmapFactory.Options options = new Options();
options.inSampleSize = 2;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), resId, options);
前面我们采用了采样压缩,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;
}
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 |
下面的例子主要用到了 Bitmap 的采样压缩(这个采样率是根据需求来进行生成的),使用到了inBitmap内存复用和 inJustDecodeBounds (这两个字段上面都有介绍)
下面介绍获取采样的流程:
/**
* 采样率压缩,这个和矩阵来实现缩放有点类似,但是有一个原则是“大图小用用采样,小图大用用矩阵”。
* 也可以先用采样来压缩图片,这样内存小了,可是图的尺寸也小。如果要是用 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详解