[关闭]
@TryLoveCatch 2022-05-05T08:30:37.000000Z 字数 5552 阅读 2477

Android知识体系之Bitmap的大小

Android知识体系


前言

dpi和ppi

dpi和ppi基本一样

我认为

dpi是android定义的,是一些固定值,用于系统中px/dp转换的。如下图

我们可以通过代码来获取:

  1. DisplayMetrics dm = getResources().getDisplayMetrics();
  2. int dpi = dm.densityDpi;

ppi是实际的dpi,是实际计算出来的,公式如下,然后根据这个值,在上图中找到最接近的dpi。

举个例子,小米pad,7.9英寸,2048*1536像素,我们运用上面的公式,得到ppi为326,然后根据dpi的图,可知,dpi为320。

decodeResource()和decodeFile()

decodeFile()用于读取SD卡上的图,得到的是图片的原始尺寸
decodeResource()用于读取Res、Raw等资源,得到的是图片的原始尺寸 * 缩放系数

由上可知,decodeResource()比decodeFile()多了一个缩放系数,而这个缩放系数,依赖于屏幕密度和图片缩放的位置,参考上一篇。接下来,我们看一下代码:

  1. // 通过BitmapFactory.Options的这几个参数可以调整缩放系数
  2. public class BitmapFactory {
  3. public static class Options {
  4. public boolean inScaled; // 默认true
  5. public int inDensity; // 无dpi的文件夹下默认160
  6. public int inTargetDensity; // 取决具体屏幕
  7. }
  8. }

注释里面说的很清楚了,我们继续来看。

假设我们有一张720*720的图片。

inScaled属性

如果inScaled设置为false,则不进行缩放,解码后图片大小为720x720; 否则请往下看。
如果inScaled设置为true或者不设置,则根据inDensity和inTargetDensity计算缩放系数。

inDensity属性

取决于图片的位置,默认或drawable下面,inDensity为160,drawable-xhdpi下面,inDensity为320,具体参见下图:

假设这张图片放到drawable目录下,
以1080p的MX4为例子,缩放系数 = inTargetDensity(具体480 / inDensity(默认160)= 3 = density, 解码后图片大小为2160x2160。

假设这张图放到drawable-xhdpi下面,则为480/320 = 1.5,1.5*720 = 1080,所以得到的图片大小为1080*1080

inTargetDensity属性

取决于屏幕密度

借用上面的例子
假设这张图片放到drawable目录下,
以1080p的MX4为例子,缩放系数 = inTargetDensity(具体480 / inDensity(默认160)= 3 = density, 解码后图片大小为2160x2160。

那么,以720p的红米3为例子,缩放系数 = inTargetDensity(具体320 / inDensity(默认160)= 2 = density,解码后图片大小为1440x1440。

手动设置
我们可以通过BitmapFactory.Options手动设置inTargetDensity和inDensity的值,来控制缩放系数。

  1. BitmapFactory.Options options = new BitmapFactory.Options();
  2. options.inJustDecodeBounds = false;
  3. options.inSampleSize = 1;
  4. options.inDensity = 160;
  5. options.inTargetDensity = 160;
  6. bitmap = BitmapFactory.decodeResource(getResources(),
  7. R.drawable.origin, options);
  8. // MX4上,虽然density = 3
  9. // 但是通过设置inTargetDensity / inDensity = 160 / 160 = 1
  10. // 解码后图片大小为720x720
  11. System.out.println("w:" + bitmap.getWidth()
  12. + ", h:" + bitmap.getHeight());

获取bitmap大小

如何得到bitmap的大小呢?Android提供了一个Api,在Bitmap里面:

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

怎么算的

看了getByteCount()的源码,getHeight()我们很熟悉了,返回的就是bitmap高度的像素(px),那么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. }

呵呵呵,JNI?!好吧,你赢了。大体代码如下,爱看不看吧

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

说明一下,gSize数组,仔细一看,是不是很熟悉?其实就是每一个像素点所占的字节数。所以,我们可以得出来:

看了getByteCount(),返回的是width * 单像素点字节数

所以如果一张ARGB_8888的bitmap的大小如下:

width * height * 4bytes

举个例子

一张522*686的 PNG 图片,我把它放到 drawable-xxhdpi 目录下,在三星s6上加载,占用内存2547360B。

但是通过我们的方式计算,结果是1432368B。

这个原因,估计大家也都明白了,我们之前包括上一篇也都说过了,就不再冗述了,drawable-xxhdpi是480,而s6是640,所以

width * (640/480) * height * (640/480) * 4bytes = 2546432B

精度

我们发现我们计算的2546432B和通过getByteCount()得到的已经很接近了,但是还是不一样。继续源码:

  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. }

关键代码:

  1. if (willScale && decodeMode != SkImageDecoder::kDecodeBounds_Mode) {
  2. scaledWidth = int(scaledWidth * scale + 0.5f);
  3. scaledHeight = int(scaledHeight * scale + 0.5f);
  4. }

所以

  1. scaledWidth = int( 522 * 640 / 480f + 0.5) = int(696.5) = 696
  2. scaledHeight = int( 686 * 640 / 480f + 0.5) = int(915.16666…) = 915
  3. 915 * 696 * 4 = 2547360

完美~~~打完收工

结论

bitmap的大小,由以下三个决定:
1. 单像素点字节数
2. 图片文件存放的drawable文件夹位置
3. 手机屏幕密度

参考:
Android Bitmap面面观
Android 开发绕不过的坑:你的 Bitmap 究竟占多大内存?
Android屏幕适配知识
安卓分辨率的相关知识
Android屏幕适配知识
详解Android开发中常用的 DPI / DP / SP
支持多种屏幕

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