[关闭]
@linux1s1s 2016-12-30T17:34:30.000000Z 字数 9839 阅读 1983

Base Time-Bitmap

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,所以我们从这三个方面入手:

WHAT

Bitmap是什么?

可以理解为一种数据结构,这个数据结构是通过bit数组来存储特定数据。

Bitmap 究竟占多大内存?

问题:
将下面这张PNG图片(165*54),放到xxhdpi目录下,在魅族 metal (1080*1920)上显示,占用多大空间?

此处输入图片的描述

  1. //BitMap Test xxhdpi + Samsung galaxy s4 1080*1920
  2. BitmapFactory.Options options = new BitmapFactory.Options();
  3. options.inPreferredConfig = Bitmap.Config.ARGB_8888;
  4. Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.arrow_big, options);
  5. Log.i("bitmap", "Bitmap size is: " + bitmap.getByteCount());
  1. ARGB_8888 一个像素占4个字节,所以165*54*4 = 35640B

对比上面的结果35640B,完全符合预期!!!那如果把同样的图片放入hdpi目录又会如何呢?在看答案之前,请仔细思考一炷香的时间。




思考完,我们来看看放入hdpi目录中占用多少内存空间。

此处输入图片的描述

测试结果142560 B是测算35640 B4倍,在没有对Bitmap的decode源码理清楚之前,可能你现在是一脸懵逼的状态,为啥放入不同目录上,在同一个手机上同样一张图片会有截然不同的表现呢?为了搞清楚上面的实验,我们需要两步走:

DisplayMetrics基本概念

简单来说,可以理解为 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的基本信息如下:

  1. DisplayMetrics{density=3.0, width=1080, height=1920, scaledDensity=3.0, xdpi=415.636, ydpi=443.345}
  2. densityDpi is: 480

推算结论

所以我们得到魅族metal的densityDpi为480,而目录xxhdpi对应的densityDpi恰好也是480,所以我们可以大概得出目标densityDpi(显示器的densityDpi)与源densityDpi(源文件的densityDpi)之比为1,于是得出的结果和算术表达式推算出来的结果一致,如下所示:

  1. Scale * Height * Scale * Width * 单位像素占用字节数 = Bitmap占用内存字节数
  2. 这里的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. 1 * 54 * 1 * 165 * 4 = 35640

PNG图片(165*54),放到hdpi目录下,在魅族 metal (1080*1920)上显示,占用多大空间

  1. 2 * 54 * 2 * 165 * 4 = 142560

验证了以上结果以后,我们需要从源码角度进一步证实,接下来看看Bitmap的Decode是如何工作的。

HOW

Bitmap Decode是如何工作的?

上面提及了获取Bitmap内存的方法,我们从这个方法入手查看源码:

  1. public final int getByteCount() {
  2. // int result permits bitmaps up to 46,340 x 46,340
  3. return getRowBytes() * getHeight();
  4. }

先看一下简单的getHeight()方法

  1. /** Returns the bitmap's height */
  2. public final int getHeight() {
  3. if (mRecycled) {
  4. Log.w(TAG, "Called getHeight() on a recycle()'d bitmap! This is undefined behavior!");
  5. }
  6. return mHeight;
  7. }

没有啥难点,难点在getRowBytes()方法

  1. public final int getRowBytes() {
  2. if (mRecycled) {
  3. Log.w(TAG, "Called getRowBytes() on a recycle()'d bitmap! This is undefined behavior!");
  4. }
  5. return nativeRowBytes(mFinalizer.mNativeBitmap);
  6. }

看到Native方法是不是有种想撞墙的赶脚,扔了几年的C++还得拾起来,
nativeRowBytes 对应的函数如下:
Bitmap.cpp

  1. static jint Bitmap_rowBytes(JNIEnv* env, jobject, jlong bitmapHandle) {
  2. SkBitmap* bitmap = reinterpret_cast<SkBitmap*>(bitmapHandle)
  3. return static_cast<jint>(bitmap->rowBytes());
  4. }

我们看到Java层的Bitmap到了C++层变成了SKBitmap,所以我们先来认识一下SKBitmap

头文件SKBitmap.h

  1. /** Return the number of bytes between subsequent rows of the bitmap. */
  2. size_t rowBytes() const { return fRowBytes; }

源文件SkBitmap.cpp

  1. size_t SkBitmap::ComputeRowBytes(Config c, int width) {
  2. return SkColorTypeMinRowBytes(SkBitmapConfigToColorType(c), width);
  3. }
  4. SkImageInfo.h
  5. static int SkColorTypeBytesPerPixel(SkColorType ct) {
  6. static const uint8_t gSize[] = {
  7. 0, // Unknown
  8. 1, // Alpha_8
  9. 2, // RGB_565
  10. 2, // ARGB_4444
  11. 4, // RGBA_8888
  12. 4, // BGRA_8888
  13. 1, // kIndex_8
  14. };
  15. SK_COMPILE_ASSERT(SK_ARRAY_COUNT(gSize) == (size_t)(kLastEnum_SkColorType + 1),
  16. size_mismatch_with_SkColorType_enum);
  17. SkASSERT((size_t)ct < SK_ARRAY_COUNT(gSize));
  18. return gSize[ct];
  19. }
  20. static inline size_t SkColorTypeMinRowBytes(SkColorType ct, int width) {
  21. return width * SkColorTypeBytesPerPixel(ct);
  22. }

小结

好,跟踪到这里,我们发现 ARGB_8888(也就是我们最常用的 Bitmap 的格式)的一个像素占用 4byte,那么 rowBytes 实际上就是 4*width bytes。
那么结论出来了,一张 ARGB_8888 的 Bitmap 占用内存的计算公式
bitmapInRam = bitmapWidth*bitmapHeight *4 bytes

那么跟踪到这,是不是又一脸懵逼了,为啥和我们验证的不同呢,Scale去哪里了呢?
别着急,我们还有部分代码没有跟踪,如下:

  1. Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.arrow_big, options);

进入decodeResource()方法中

  1. public static Bitmap decodeResource(Resources res, int id, Options opts) {
  2. Bitmap bm = null;
  3. InputStream is = null;
  4. try {
  5. final TypedValue value = new TypedValue();
  6. is = res.openRawResource(id, value);
  7. bm = decodeResourceStream(res, value, is, null, opts);
  8. } catch (Exception e) {
  9. /* do nothing.
  10. If the exception happened on open, bm will be null.
  11. If it happened on close, bm is still valid.
  12. */
  13. } finally {
  14. try {
  15. if (is != null) is.close();
  16. } catch (IOException e) {
  17. // Ignore
  18. }
  19. }
  20. if (bm == null && opts != null && opts.inBitmap != null) {
  21. throw new IllegalArgumentException("Problem decoding into existing bitmap");
  22. }
  23. return bm;
  24. }

该方法主要分为两步对应上面的L7和L9。

我们直接跟踪decodeResourceStream()方法即可
BitmapFactory.java

  1. /**
  2. * Decode a new Bitmap from an InputStream. This InputStream was obtained from
  3. * resources, which we pass to be able to scale the bitmap accordingly.
  4. */
  5. public static Bitmap decodeResourceStream(Resources res, TypedValue value,
  6. InputStream is, Rect pad, Options opts) {
  7. if (opts == null) {
  8. opts = new Options();
  9. }
  10. if (opts.inDensity == 0 && value != null) {
  11. final int density = value.density;
  12. if (density == TypedValue.DENSITY_DEFAULT) {
  13. opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
  14. } else if (density != TypedValue.DENSITY_NONE) {
  15. opts.inDensity = density;
  16. }
  17. }
  18. if (opts.inTargetDensity == 0 && res != null) {
  19. opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
  20. }
  21. return decodeStream(is, pad, opts);
  22. }

L21,我们看到了熟悉的targetDensityDpi,继续跟踪下去到Native的decode,省略掉其他细节,直接看重点。

BitmapFactory.cpp

  1. static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding, jobject options) {
  2. ......
  3. if (env->GetBooleanField(options, gOptions_scaledFieldID)) {
  4. const int density = env->GetIntField(options, gOptions_densityFieldID);//对应hdpi的时候,是240
  5. const int targetDensity = env->GetIntField(options, gOptions_targetDensityFieldID);//三星s6的为640
  6. const int screenDensity = env->GetIntField(options, gOptions_screenDensityFieldID);
  7. if (density != 0 && targetDensity != 0 && density != screenDensity) {
  8. scale = (float) targetDensity / density;
  9. }
  10. }
  11. }
  12. const bool willScale = scale != 1.0f;
  13. ......
  14. SkBitmap decodingBitmap;
  15. if (!decoder->decode(stream, &decodingBitmap, prefColorType,decodeMode)) {
  16. return nullObjectReturn("decoder->decode returned false");
  17. }
  18. //这里这个deodingBitmap就是解码出来的bitmap,大小是图片原始的大小
  19. int scaledWidth = decodingBitmap.width();
  20. int scaledHeight = decodingBitmap.height();
  21. if (willScale && decodeMode != SkImageDecoder::kDecodeBounds_Mode) {
  22. scaledWidth = int(scaledWidth * scale + 0.5f);
  23. scaledHeight = int(scaledHeight * scale + 0.5f);
  24. }
  25. if (willScale) {
  26. const float sx = scaledWidth / float(decodingBitmap.width());
  27. const float sy = scaledHeight / float(decodingBitmap.height());
  28. // TODO: avoid copying when scaled size equals decodingBitmap size
  29. SkColorType colorType = colorTypeForScaledOutput(decodingBitmap.colorType());
  30. // FIXME: If the alphaType is kUnpremul and the image has alpha, the
  31. // colors may not be correct, since Skia does not yet support drawing
  32. // to/from unpremultiplied bitmaps.
  33. outputBitmap->setInfo(SkImageInfo::Make(scaledWidth, scaledHeight,
  34. colorType, decodingBitmap.alphaType()));
  35. if (!outputBitmap->allocPixels(outputAllocator, NULL)) {
  36. return nullObjectReturn("allocation failed for scaled bitmap");
  37. }
  38. // If outputBitmap's pixels are newly allocated by Java, there is no need
  39. // to erase to 0, since the pixels were initialized to 0.
  40. if (outputAllocator != &javaAllocator) {
  41. outputBitmap->eraseColor(0);
  42. }
  43. SkPaint paint;
  44. paint.setFilterLevel(SkPaint::kLow_FilterLevel);
  45. SkCanvas canvas(*outputBitmap);
  46. canvas.scale(sx, sy);
  47. canvas.drawBitmap(decodingBitmap, 0.0f, 0.0f, &paint);
  48. }
  49. ......
  50. }

L24,25我们看到了熟悉的scale,我们把他提出来看一下:

  1. scaledWidth = int(scaledWidth * scale + 0.5f);
  2. scaledHeight = int(scaledHeight * scale + 0.5f);

算法小结

这个算法很显然考虑到精度问题了,width和scale相乘以后加上0.5,然后取整即可,所以我们看完源码以后就得修改之前的公式,因为之前的公式没有考虑经度问题,修改后的公式如下:

  1. int(Scale * Height + 0.5f) * int(Scale * Width + 0.5f)* 单位像素占用字节数 = Bitmap占用内存字节数
  2. 这里的Scale = targetDensityDpi/sourceDensityDpi,单位是f浮点型

WHY

为什么选择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)

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注