浅析Bitmap占据内存大小

来源:互联网 发布:mysql开启远程连接 编辑:程序博客网 时间:2024/06/03 17:20
April is your lie

浅析Bitmap占据内存大小


        Bitmap的使用是开发时绕不过的坑,使用时要处处留意内存问题,稍有不慎就会报OOM(out of memory)。所以这次我们就研究研究程序中Bitmap到底占据多少内存。

前奏

        比如我们使用一张图片,将其放入到工程目录中,想当然的会以为为这张图片建立的bitmap使用内存大小为:宽×高×像素大小。为了验证这个猜想,我在度娘上随便找了幅图:

素材


        规格是768×1152,大小为153.3KB,格式为支持ARGB四阶的32位色的JPG图片。
        我们猜想,如果按照内存大小计算公式,所占内存应为:768×1152×4=3538944,字节。因为JPG格式是有损压缩格式,所以存储大小比内存大小小多了。
        然后将这张图片放到res/drawable-xhdpi下,通过如下代码计算内存大小:
float density = this.getResources().getDisplayMetrics().density;int dpi = this.getResources().getDisplayMetrics().densityDpi;Log.e(TAG, "density = " + density + "------" + "dpi = " + dpi);Bitmap b = BitmapFactory.decodeResource(getResources(), R.drawable.picture);int w = b.getWidth();int h = b.getHeight();int size = b.getByteCount();int config = b.getConfig().ordinal();Log.e(TAG, "w = " + w + ";" + "h = " + h + ";" + "size = " + size + ";" + "config = " + config);

       测试机器规格为:Google Nexus 5 - 5.1.0 - API 22 - 1080×1920(480dpi)
       打印log如下:

density = 3.0——dpi = 480
w = 1152;h = 1728;size = 7962624;config = 3

excuse me


       Why?How did you do it?这个不按套路出牌啊,宽高明显被拉伸了啊。。。。。。然后我又试了下将这张图片放到了res/drawable-xxhdpi下,打印log如下:

density = 3.0——dpi = 480
w = 768;h = 1152;size = 3538944;config = 3

       这次倒是和理论计算的大小一样了,我们大概猜到了什么。。。。。接着我又把这张图片放到了assets目录下,然后修改了一下获取图片的代码,打印log如下:

density = 3.0——dpi = 480
w = 768;h = 1152;size = 3538944;config = 3

       这次也是和理论值一样的,因为放到assets目录下的图片是不会被压缩的。

       如果多试几次,把图片放入不同目录下再运行几遍,我们也能够总结出规律的。但这些都是现象,我们组的老大也曾经说过:开发人员不要轻易根据现象得出结论…….所以我们也要分析一下本质原因。

求证

       做适配的同学要经常和density、densityDpi搞好关系,简单来说,可以理解为 density 的数值是 1dp=density px;densityDpi 是屏幕每英寸对应多少个点(不是像素点),在 DisplayMetrics 当中,这两个的关系是线性的:

density0.7511.5233.54densityDpi120160240320480560640DpiFolderldpimdpihdpixhdpixxhdpixxxhdpixxxxhdpi

       这些内容每个人应该都知道,先放到这里,方便后面查表。

非压缩计算

       如果图片不被压缩,按照常规计算内存大小方法为:

//Bitmap的getByteCount方法   public final int getByteCount() {       // int result permits bitmaps up to 46,340 x 46,340       return getRowBytes() * getHeight();   }   //Bitmap的getRowBytes方法   public final int getRowBytes() {       return nativeRowBytes(mNativeBitmap);   }private static native int nativeRowBytes(long nativeBitmap);

       getHeight 就是图片的高度(单位:px),getRowBytes 从字面意思看应该是行字节大小。我们往下看,找找JNI实现,查看 frameworks/base/core/jni/android/graphics/Bitmap.cpp文件:

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

       (reinterpret_cast和static_cast是C++经常用到的用来处理无关类型之间转换的强制类型转换符,建议有时间可以研究研究,或者把C++回顾一下,毕竟挺重要的。这里先给个科普文章)
       上一篇关于的弹幕文章提到过,java层的Bitmap对应native层是由skia图形引擎创建的SkBitmap,关于skia这玩意儿东西比较多,不是专业的一时半会儿也玩不转。所以我们还是简单看看,继续往下找SkBitmap:(/external/skia/include/core/SkBitmap.h)

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

       得到上述fRowBytes的大小会在SkBitmap.cpp文件里计算:(/external/skia/src/core/SkBitmap.cpp)

//计算fRowBytes大小size_t SkBitmap::ComputeRowBytes(Config c, int width) {    return SkColorTypeMinRowBytes(SkBitmapConfigToColorType(c), width);//SkColorTypeMinRowBytes是/SkImageInfo.h的方法;SkBitmapConfigToColorType是SkImagePriv.cpp的方法}//SkImageInfo.h的SkColorTypeMinRowBytes方法static inline size_t SkColorTypeMinRowBytes(SkColorType ct, int width) {    return width * SkColorTypeBytesPerPixel(ct);}//SkImageInfo.h的SkColorTypeBytesPerPixel方法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    };...省略障眼法的宏...    return gSize[ct];}//SkBitmapConfigToColorType是SkImagePriv.cpp的方法SkColorType SkBitmapConfigToColorType(SkBitmap::Config config) {    static const SkColorType gCT[] = {        kUnknown_SkColorType,   // kNo_Config        kAlpha_8_SkColorType,   // kA8_Config        kIndex_8_SkColorType,   // kIndex8_Config        kRGB_565_SkColorType,   // kRGB_565_Config        kARGB_4444_SkColorType, // kARGB_4444_Config        kN32_SkColorType,   // kARGB_8888_Config    };    SkASSERT((unsigned)config < SK_ARRAY_COUNT(gCT));    return gCT[config];}

       跟踪到这里,还记得我们上面大log的地方么。int config = b.getConfig().ordinal()返回的是3,那么在Bitmap.Config里面索引第4个枚举变量:

public enum Config {    ALPHA_8     (1),    RGB_565     (3),    ARGB_4444   (4),    ARGB_8888   (5);//索引第四个是这个    final int nativeInt;    //从这个列表可以看出它与skia支持的图片格式一一对应,但是Android只支持上面4种    private static Config sConfigs[] = {        null, ALPHA_8, null, RGB_565, ARGB_4444, ARGB_8888    };           Config(int ni) {        this.nativeInt = ni;    }    static Config nativeToConfig(int ni) {        return sConfigs[ni];    }}

       依照上面C++文件,我们发现 ARGB_8888(也就是我们最常用的 Bitmap 的格式)的一个像素占用 4byte,那么 rowBytes 实际上就是 4*width bytes。则理论上 ARGB_8888 的 Bitmap 占用内存的计算公式为:

bitmapInRam = bitmapWidth × bitmapHeight × 4 bytes

压缩计算

       如果我们不将图片放到assets目录下,内存大小计算方式就和上面完全不同了。我们读取的是 drawable 目录下面的图片,用的是 decodeResource 方法,该方法本质上就两步:

  • 读取原始资源,这个调用了 Resource.openRawResource 方法,这个方法调用完成之后会对 TypedValue 进行赋值,其中包含了原始资源的 density 等信息;
  • 调用 decodeResourceStream 对原始资源进行解码和适配。这个过程实际上就是原始资源的 density 到屏幕 density 的一个映射。
           原始资源的 density 其实取决于资源存放的目录(比如 xxhdpi 对应的是480),而屏幕 density 的赋值,请看下面这段代码:
  •   public static Bitmap decodeResource(Resources res, int id) {      return decodeResource(res, id, null);  }  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);//对 TypedValue 进行赋值,其中包含了原始资源的 density 等信息          bm = decodeResourceStream(res, value, is, null, opts);      } catch (Exception e) {      ......      } finally{      ......      }......      return bm;  }  public static Bitmap decodeResourceStream(Resources res, TypedValue value,          InputStream is, Rect pad, Options opts) {      if (opts == null) {//opt为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;//这里density的值如果对应资源目录为xhdpi的话,就是320          }      }            if (opts.inTargetDensity == 0 && res != null) {       //请注意,inTargetDensity就是当前的显示密度,比如Google Nexus 5就是480          opts.inTargetDensity = res.getDisplayMetrics().densityDpi;      }            return decodeStream(is, pad, opts);  }  public static Bitmap decodeStream(InputStream is, Rect outPadding, Options opts) {......      bm = decodeStreamInternal(is, outPadding, opts);......      return bm;  }  private static Bitmap decodeStreamInternal(InputStream is, Rect outPadding, Options opts) {......      return nativeDecodeStream(is, tempStorage, outPadding, opts);  }  private static native Bitmap nativeDecodeStream(InputStream is, byte[] storage,          Rect padding, Options opts);

       我们看到 opts 这个值被初始化,而它的构造居然如此简单:

public Options() {    inDither = false;    inScaled = true;    inPremultiplied = true;}

       所以我们就很容易的看到,Option.inScreenDensity 这个值没有被初始化,而实际上后面我们也会看到这个值根本不会用到;我们最应该关心的是什么呢?是 inDensity 和 inTargetDensity,这两个值与下面 cpp 文件里面的 density 和 targetDensity 相对应——重复一下,inDensity 就是原始资源的 density,inTargetDensity 就是屏幕的 density。
       紧接着,用到了 nativeDecodeStream 方法:

static jobject nativeDecodeStream(JNIEnv* env, jobject clazz, jobject is, jbyteArray storage,        jobject padding, jobject options) {    jobject bitmap = NULL;......        bitmap = doDecode(env, bufferedStream, padding, options);     return bitmap;}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);//对应xhdpi的时候,是320        const int targetDensity = env->GetIntField(options, gOptions_targetDensityFieldID);//Google Nexus 5为480        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);}......}

       注意到其中有个 density 和 targetDensity,前者是 decodingBitmap 的 density,这个值跟这张图片的放置的目录有关(比如 xhdpi 是320,xxhdpi 是480),这部分代码我跟了一下,太长了,就不列出来了;targetDensity 实际上是我们加载图片的目标 density,这个值的来源我们已经在前面给出了,就是 DisplayMetrics 的 densityDpi,如果是Google Nexus 5那么这个数值就是480。sx 和sy 实际上是约等于 scale 的,因为 scaledWidth 和 scaledHeight 是由 width 和 height 乘以 scale 得到的。我们看到 Canvas 放大了 scale 倍,然后又把读到内存的这张 bitmap 画上去,相当于把这张 bitmap 放大了 scale 倍。

       然后我们再次验证上面打log的地方,win + r ,输入calc呼出计算器。这里千万不要忘了了精度

float scale = 480/320f = 1.5
int scaledWidth = int(768 * 1.5 + 0.5f) = 1152
int scaledHeight = int(1152 * 1.5 + 0.5f) = 1728

size = 1152 1728 4 = 7962624

       果然和上面log打印的一模一样!因此我们可以得出结论。Bitmap在内存中大小取决于:

  • 色彩格式,前面我们已经提到,如果是 ARG_B8888 那么就是一个像素4个字节,如果是 RGB_565 那就是2个字节
  • 原始文件存放的资源目录(是 hdpi 还是 xxhdpi 等等)
  • 目标屏幕的密度(所以同等条件下,红米在资源方面消耗的内存肯定是要小于三星S6的)

       内存大小计算公式大概为(压缩计算情况下)(已忽略精度):

内存大小 = (设备屏幕dpi / 资源所在目录dpi)^ 2 × 图片原始宽 × 图片原始高 × 像素大小

瞎猜

       上面分析Bitmap.Config时发现Android官方并不完全支持skia图形引擎的所有像素格式,供java层设置的Config只有这么4个:

public enum Config {    // these native values must match up with the enum in SkBitmap.h     ALPHA_8     (1),    RGB_565     (3),    ARGB_4444   (4),    ARGB_8888   (5);        inal int nativeInt;}

       其实 Java 层的枚举变量的 nativeInt 对应的就是 Skia 库当中枚举的索引值;而skia却支持这么多:

//Skbitmap.h文件    enum Config {        kNo_Config,         //!< bitmap has not been configured        kA8_Config,         //!< 8-bits per pixel, with only alpha specified (0 is transparent, 0xFF is opaque)        kIndex8_Config,     //!< 8-bits per pixel, using SkColorTable to specify the colors        kRGB_565_Config,    //!< 16-bits per pixel, (see SkColorPriv.h for packing)        kARGB_4444_Config,  //!< 16-bits per pixel, (see SkColorPriv.h for packing)        kARGB_8888_Config,  //!< 32-bits per pixel, (see SkColorPriv.h for packing)    };

       上述枚举中第三个类型为索引图类型。索引位图,每个像素只占 1 Byte,不仅支持 RGB,还支持 alpha。微软画图工具应该都玩过吧(win + r,输入mspaint),里面的调色板就是索引色盘。
画图工具
       而Android其他的config类型一个像素点占的字节比这个大多了,所以我们有时候能不能也用索引色去悄悄替换原来格式呢?
       我的猜想是,反射构造一个Bitmap.Config枚举对象,然后反射设置nativeInt字段的值为2,猜想代码如下:

Options op = new Options();op.inPreferredConfig = ...反射构建Bitmap.Config相关内容...BitmapFactory.decodeResource(getResources(), R.drawable.picture, op);

       不过我没有实践过,也是瞎猜的,不知道能不能行的通。。。。。。

       但是我对上一篇文章种调skia生成弹幕bitmap处的代码做了修改,修改了DanmakuFlameMaster工程里的NativeBitmapFactory.java文件:

    private static Bitmap createNativeBitmap(int width, int height, Config config, boolean hasAlpha) {//        int nativeConfig = getNativeConfig(config);        int nativeConfig = 2;//直接改为索引色        return android.os.Build.VERSION.SDK_INT == 19 ? createBitmap19(width, height,                nativeConfig, hasAlpha) : createBitmap(width, height, nativeConfig, hasAlpha);    }

       将色彩格式改为索引色,然后重新编译运行。。。。。。然而弹幕压根没出来。。。。。等以后有机会问问ctiao吧,请教一下为何。
       这些瞎猜只能暂时放着,等以后有机会再验证吧。。。。。。


原创粉丝点击