@946898963
2020-02-28T12:03:47.000000Z
字数 5484
阅读 2264
Bitmap
今天做图像缓存需要计算Bitmap的所占的内存空间,于是研究了下Bitmap关于内存占用的API
1、getRowBytes:Since API Level 1,用于计算位图每一行所占用的内存字节数。
2、getByteCount:Since API Level 12,用于计算位图所占用的内存字节数。
查看Bitmap源码:
public final int getByteCount() {
return getRowBytes() * getHeight();
}
也就是说位图所占用的内存空间数等于位图的每一行所占用的空间数乘以位图的行数。
获取Bitmap大小的工具方法:
/**
* 得到bitmap的大小
*/
public static int getBitmapSize(Bitmap bitmap) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { //API 19
return bitmap.getAllocationByteCount();
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) {//API 12
return bitmap.getByteCount();
}
// 在低版本中用一行的字节x高度
return bitmap.getRowBytes() * bitmap.getHeight(); //earlier version
}
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*widthbytes。那么一行图片所占的内存计算公式:
图片的占用内存 = 图片的长度(像素单位) * 图片的宽度(像素单位) * 单位像素所占字节数
但需要注意的是, 在使用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直接计算就行,没有缩放宽和高的整个过程。
参考链接:
Android 计算Bitmap大小 getRowBytes和getByteCount()