@linux1s1s
2016-12-30T17:34:30.000000Z
字数 9839
阅读 2011
Base
2016-12
系列博文
Base Time-Http Protocol
Base Time-Bitmap
Base Time-Database
Base Time-Java
Base Time-Design Patterns
Base Time-Java Algorithms
学习技术的三部曲:WHAT、HOW、WHY,所以我们从这三个方面入手:
可以理解为一种数据结构,这个数据结构是通过bit数组来存储特定数据。
问题:
将下面这张PNG图片(165*54
),放到xxhdpi
目录下,在魅族 metal (1080*1920)
上显示,占用多大空间?
//BitMap Test xxhdpi + Samsung galaxy s4 1080*1920
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.ARGB_8888;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.arrow_big, options);
Log.i("bitmap", "Bitmap size is: " + bitmap.getByteCount());
测试结果:
按照常规公式核算一下上面的结果是否符合预期
ARGB_8888 一个像素占4个字节,所以165*54*4 = 35640B
对比上面的结果35640B
,完全符合预期!!!那如果把同样的图片放入hdpi
目录又会如何呢?在看答案之前,请仔细思考一炷香的时间。
思
考
时
间
思考完,我们来看看放入hdpi
目录中占用多少内存空间。
测试结果142560 B
是测算35640 B
的4
倍,在没有对Bitmap的decode源码理清楚之前,可能你现在是一脸懵逼的状态,为啥放入不同目录上,在同一个手机上同样一张图片会有截然不同的表现呢?为了搞清楚上面的实验,我们需要两步走:
简单来说,可以理解为 density 的数值是 1dp=density px;densityDpi 是屏幕每英寸对应多少个点(不是像素点),在 DisplayMetrics 当中,这两个的关系是线性的:
density | densityDpi | 目录 |
---|---|---|
1 | 160 | dpi |
1.5 | 240 | hdpi |
2 | 320 | xhdpi |
3 | 480 | xxhdpi |
3.5 | 560 | xxxhdpi |
4 | 640 | xxxxhdpi |
px(像素):屏幕上的点。
in(英寸):长度单位。
mm(毫米):长度单位。
pt(磅):1/72英寸。
dp(与密度无关的像素):一种基于屏幕密度的抽象单位。在每英寸160点的显示器上,1dp = 1px,理解这个很重要。
dip:与dp相同,多用于android/ophone示例中。
sp(与刻度无关的像素):与dp类似,但是可以根据用户的字体大小首选项进行缩放。
从上面的截图中可以看到魅族metal的基本信息如下:
DisplayMetrics{density=3.0, width=1080, height=1920, scaledDensity=3.0, xdpi=415.636, ydpi=443.345}
densityDpi is: 480
所以我们得到魅族metal的densityDpi为480,而目录xxhdpi对应的densityDpi恰好也是480,所以我们可以大概得出目标densityDpi(显示器的densityDpi)与源densityDpi(源文件的densityDpi)之比为1,于是得出的结果和算术表达式推算出来的结果一致,如下所示:
Scale * Height * Scale * Width * 单位像素占用字节数 = Bitmap占用内存字节数
这里的Scale = targetDensityDpi/sourceDensityDpi
单位像素占用字节数如下所示:
格式 | 描述 | 字节数 |
---|---|---|
ALPHA_8 | 只有一个alpha通道 | 8bit=1Byte |
ARGB_4444 | 这个从API 13开始不建议使用,因为质量太差 | 4*4=16bit=2Byte |
ARGB_8888 | ARGB四个通道,每个通道8bit | 4*8=32bit=4Byte |
RGB_565 | 每个像素占2Byte,其中红色占5bit,绿色占6bit,蓝色占5bit | 5+6+5=16bit=2Byte |
所以我们推算一下上面的实验如下:
TargentDensityDpi = 480
放到xxhdpi
目录下 SourceDensityDpi = 480 Scale = 480/480 = 1
放到hdpi
目录下 SourceDensityDpi = 240 Scale = 480/240 = 2
Height = 54
Width = 165
ARGB_8888 占用 4 Byte
PNG图片(
165*54
),放到xxhdpi
目录下,在魅族 metal (1080*1920)
上显示,占用多大空间
1 * 54 * 1 * 165 * 4 = 35640
PNG图片(
165*54
),放到hdpi
目录下,在魅族 metal (1080*1920)
上显示,占用多大空间
2 * 54 * 2 * 165 * 4 = 142560
验证了以上结果以后,我们需要从源码角度进一步证实,接下来看看Bitmap的Decode是如何工作的。
Bitmap Decode是如何工作的?
上面提及了获取Bitmap内存的方法,我们从这个方法入手查看源码:
public final int getByteCount() {
// int result permits bitmaps up to 46,340 x 46,340
return getRowBytes() * getHeight();
}
先看一下简单的getHeight()方法
/** Returns the bitmap's height */
public final int getHeight() {
if (mRecycled) {
Log.w(TAG, "Called getHeight() on a recycle()'d bitmap! This is undefined behavior!");
}
return mHeight;
}
没有啥难点,难点在getRowBytes()方法
public final int getRowBytes() {
if (mRecycled) {
Log.w(TAG, "Called getRowBytes() on a recycle()'d bitmap! This is undefined behavior!");
}
return nativeRowBytes(mFinalizer.mNativeBitmap);
}
看到Native方法是不是有种想撞墙的赶脚,扔了几年的C++还得拾起来,
nativeRowBytes 对应的函数如下:
Bitmap.cpp
static jint Bitmap_rowBytes(JNIEnv* env, jobject, jlong bitmapHandle) {
SkBitmap* bitmap = reinterpret_cast<SkBitmap*>(bitmapHandle)
return static_cast<jint>(bitmap->rowBytes());
}
我们看到Java层的Bitmap到了C++层变成了SKBitmap,所以我们先来认识一下SKBitmap
头文件SKBitmap.h
/** Return the number of bytes between subsequent rows of the bitmap. */
size_t rowBytes() const { return fRowBytes; }
源文件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);
}
好,跟踪到这里,我们发现 ARGB_8888(也就是我们最常用的 Bitmap 的格式)的一个像素占用 4byte,那么 rowBytes 实际上就是 4*width bytes。
那么结论出来了,一张 ARGB_8888 的 Bitmap 占用内存的计算公式
bitmapInRam = bitmapWidth*bitmapHeight *4 bytes
那么跟踪到这,是不是又一脸懵逼了,为啥和我们验证的不同呢,Scale去哪里了呢?
别着急,我们还有部分代码没有跟踪,如下:
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.arrow_big, options);
进入decodeResource()方法中
public static Bitmap decodeResource(Resources res, int id, Options opts) {
Bitmap bm = null;
InputStream is = null;
try {
final TypedValue value = new TypedValue();
is = res.openRawResource(id, value);
bm = decodeResourceStream(res, value, is, null, opts);
} catch (Exception e) {
/* do nothing.
If the exception happened on open, bm will be null.
If it happened on close, bm is still valid.
*/
} finally {
try {
if (is != null) is.close();
} catch (IOException e) {
// Ignore
}
}
if (bm == null && opts != null && opts.inBitmap != null) {
throw new IllegalArgumentException("Problem decoding into existing bitmap");
}
return bm;
}
该方法主要分为两步对应上面的L7和L9。
我们直接跟踪decodeResourceStream()
方法即可
BitmapFactory.java
/**
* Decode a new Bitmap from an InputStream. This InputStream was obtained from
* resources, which we pass to be able to scale the bitmap accordingly.
*/
public static Bitmap decodeResourceStream(Resources res, TypedValue value,
InputStream is, Rect pad, Options 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) {
opts.inDensity = density;
}
}
if (opts.inTargetDensity == 0 && res != null) {
opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
}
return decodeStream(is, pad, opts);
}
L21,我们看到了熟悉的targetDensityDpi,继续跟踪下去到Native的decode,省略掉其他细节,直接看重点。
BitmapFactory.cpp
static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding, jobject options) {
......
if (env->GetBooleanField(options, gOptions_scaledFieldID)) {
const int density = env->GetIntField(options, gOptions_densityFieldID);//对应hdpi的时候,是240
const int targetDensity = env->GetIntField(options, gOptions_targetDensityFieldID);//三星s6的为640
const int screenDensity = env->GetIntField(options, gOptions_screenDensityFieldID);
if (density != 0 && targetDensity != 0 && density != screenDensity) {
scale = (float) targetDensity / density;
}
}
}
const bool willScale = scale != 1.0f;
......
SkBitmap decodingBitmap;
if (!decoder->decode(stream, &decodingBitmap, prefColorType,decodeMode)) {
return nullObjectReturn("decoder->decode returned false");
}
//这里这个deodingBitmap就是解码出来的bitmap,大小是图片原始的大小
int scaledWidth = decodingBitmap.width();
int scaledHeight = decodingBitmap.height();
if (willScale && decodeMode != SkImageDecoder::kDecodeBounds_Mode) {
scaledWidth = int(scaledWidth * scale + 0.5f);
scaledHeight = int(scaledHeight * scale + 0.5f);
}
if (willScale) {
const float sx = scaledWidth / float(decodingBitmap.width());
const float sy = scaledHeight / float(decodingBitmap.height());
// TODO: avoid copying when scaled size equals decodingBitmap size
SkColorType colorType = 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(SkImageInfo::Make(scaledWidth, scaledHeight,
colorType, decodingBitmap.alphaType()));
if (!outputBitmap->allocPixels(outputAllocator, NULL)) {
return nullObjectReturn("allocation failed for scaled bitmap");
}
// If outputBitmap's pixels are newly allocated by Java, there is no need
// to erase to 0, since the pixels were initialized to 0.
if (outputAllocator != &javaAllocator) {
outputBitmap->eraseColor(0);
}
SkPaint paint;
paint.setFilterLevel(SkPaint::kLow_FilterLevel);
SkCanvas canvas(*outputBitmap);
canvas.scale(sx, sy);
canvas.drawBitmap(decodingBitmap, 0.0f, 0.0f, &paint);
}
......
}
L24,25我们看到了熟悉的scale,我们把他提出来看一下:
scaledWidth = int(scaledWidth * scale + 0.5f);
scaledHeight = int(scaledHeight * scale + 0.5f);
这个算法很显然考虑到精度问题了,width和scale相乘以后加上0.5,然后取整即可,所以我们看完源码以后就得修改之前的公式,因为之前的公式没有考虑经度问题,修改后的公式如下:
int(Scale * Height + 0.5f) * int(Scale * Width + 0.5f)* 单位像素占用字节数 = Bitmap占用内存字节数
这里的Scale = targetDensityDpi/sourceDensityDpi,单位是f浮点型
为什么选择Bitmap?
经过上面对Bitmap的讨论,我们大概了解了Bitmap的基本知识,但是有个问题需要进一步探讨,我们为什么选择Bitmap,它有啥好处腻?
再考虑这个问题前,我们先来看看另外一个问题,就是浪费的问题
浪费有两个部分:
所以我们来看看如何解决上面的浪费问题
对于第一种浪费,最直觉的方案就是可以引入一些文件压缩技术,比如gzip/lzo之类的,对存储的Bitmap文件进行压缩,在加载Bitmap的时候再进行解压,这样可以很好的解决存储空间的浪费,以及加载时I/O的消耗;代价则是压缩/解压缩都需要消耗更多的CPU/内存资源;并且文件压缩技术对第二种浪费也无能为力。因此只有系统有足够多空闲的CPU资源而I/O成为瓶颈的情况下,可以考虑引入文件压缩技术。
那么有没有一些技术可以同时解决这两种浪费呢?好消息是有,那就是Bitmap压缩技术;而常见的压缩技术都是基于RLE(Run Length Encoding,详见http://en.wikipedia.org/wiki/Run-length_encoding)。
RLE编码很简单,比较适合有很多连续字符的数据,比如以下边的Bitmap为例:
可以编码为0,8,2,11,1,2,3,11
其意思是:第一位为0,连续有8个,接下来是2个1,11个0,1个1,2个0,3个1,最后是11个0(当然此处只是对RLE的基本原理解释,实际应用中的编码并不完全是这样的)。
可以预见,对于一个很大的Bitmap,如果里边的数据分布很稀疏(说明有很多大片连续的0),采用RLE编码后,占用的空间会比原始的Bitmap小很多。
同时引入一些对齐的技术,可以让采用RLE编码的Bitmap不需要进行解压缩,就可以直接进行AND/OR/XOR等各类计算;因此采用这类压缩技术的Bitmap,加载到内存后还是以压缩的方式存在,从而可以保证计算时候的低内存消耗;而采用word(计算机的字长,64位系统就是64bit)对齐等技术又保证了对CPU资源的高效利用。因此采用这类压缩技术的Bitmap,保持了Bitmap数据结构最重要的一个特性,就是高效的针对每个bit的逻辑运算。
常见的压缩技术包括BBC(有专利保护),
WAH(http://code.google.com/p/compressedbitset/)
和EWAH(http://code.google.com/p/javaewah/)。在Apache Hive里边使用了EWAH。
参考博文:
Bitmap的秘密 在为什么使用Bitmap这一节完全参考此文,特此说明。
Android 开发绕不过的坑:你的 Bitmap 究竟占多大内存? 在Bitmap 究竟占多大内存?小节中参考并原文转载此文,特此说明。
Android图片编码机制深度解析(Bitmap,Skia,libJpeg)