@linux1s1s
2016-12-30T09:34:30.000000Z
字数 9839
阅读 2374
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*1920BitmapFactory.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,340return 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.hstatic int SkColorTypeBytesPerPixel(SkColorType ct) {static const uint8_t gSize[] = {0, // Unknown1, // Alpha_82, // RGB_5652, // ARGB_44444, // RGBA_88884, // BGRA_88881, // 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的时候,是240const int targetDensity = env->GetIntField(options, gOptions_targetDensityFieldID);//三星s6的为640const 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 sizeSkColorType 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)