bitmap总结

主要整理bitmap平时模糊的知识点

bitmap解码方式

解码指的是把图片通过文件读取到内存中,而bitmap正是承载着图片在内存中存储的对象。关于图片的解码方式有如下几种:

  • ALPHA_8:只存储透明度信息,一个像素占用一个字节,一个字节是8位。所以在在该解码方式下,一个像素是占8位。
  • ARGB_4444:每个像素占2个字节,也就是16位,其中A占4位,R占4位,G占4位,B占4位(4+4+4+4),支持alpha通道
    • 从API13开始不推荐使用,在android4.4上面,设置的ARGB_4444会被系统使用ARGB_8888替换
  • ARGB_8888:bitmap编码的默认方式,每像素占4字节,每个通道分别占8位
  • RGB_565:每像素占用2字节,RGB分别占5,6,5位,不支持alpha通道 参考:https://www.ieclipse.cn/2017/06/14/Android/Android-bitmap-config/
  • 上面提到的RGB_565解码方式,为什么是5,6,5呢?
    • RGB_565 是早期 Android 在 内存限制、渲染效率、视觉质量 三者间找到的平衡点。随着设备硬件提升,更高精度的格式(如 ARGB_8888)逐渐成为默认选项,但在特定优化场景中,RGB_565 仍是一个有效的省内存方案。

测试各种解码方式

alt text

  • 对于带有透明的图片:在ALPHA_8下,它是只有透明信息,但是他占用的大小还是一个像素占4个字节,并且展示的图片和ARGB_8888一样的。RGB_565是不带透明通道的,但是带有透明度的图片还是能呈现透明样式的,并且占用内存和ARGB_8888一样的。
  • 对于非透明的图片:RGB_565下内存占用是ARGB_8888的一半。在ALPHA_8下面展示的图片所占用的字节大小和ARGB_8888是一样的。

关于这块为什么带有透明的图片在alpha_8下还是和argb_8888下面一样的,下面文章有讲解:
https://juejin.cn/post/7059206294959292452
alt text 测试了下带透明的图片使用RGB_565的时候,发现解码生成的bitmap的config还是ARGB_8888,说明系统还是会选择合适的颜色模式来解码,通过如下获取最终的解码方式: alt text

bitmap压缩

1.采样率压缩:设置BitmapFactory.Options.inSampleSize,值越大,对应的宽高的像素值越小,如果inSampleSize=2,则内存是原来的4分支1了,因为宽高各减少2倍
2.设置编码格式:BitmapFactory.Options.inPreferredConfig,一般设置有RGB_565,ARGB_8888,RGB_565的时候忽略了透明通道。
3.质量压缩:质量压缩不改变图片的像素,改变图片的位深和透明度,适合二进制图片数据,常见的有PNG、JPEG、WEBP压缩算法。其中PNG压缩算法称为无损压缩,基本不会改变图片占用文件的大小,JPEG会根据quality参数来实现压缩。 经过测试:在PNG压缩算法下,图片不会改变,因此被称为无损压缩

在JPEG压缩算法下,如果是带有透明的图片,由于会忽略透明度,因此会显示成黑色。并且发现保存到本地的文件大小会增大,但是二进制的byte数会随着quality变小而变小。因此带有透明的图片慎用

web压缩算法是一种有损压缩,webp图像的体积要比jpeg格式图像小40%,但是webp的编码时间比jpeg格式长8倍。
https://juejin.cn/post/6844903725081821198#heading-12

bitmap内存获取

  • bitmap的宽(像素个数)*bitmap的高(像素个数)*每个像素所占用的内存
  • bitmap的实际宽:
    • 图片的真实宽*设备的densityDpi/图片所在的drawable的densityDpi
    • 设备的densityDpi:最终会赋值到上BitmapFactory.Options.inTargetDensity,通过如下方式获取设备的densityDpi: alt text
    • 图片所在的drawable的densityDpi:最终会设置到BitmapFactory.Options.inDensity上,通过如下方式获取: alt text
  • 关于设备的dpi和drawable的dpi可以跟着源码看下:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public static Bitmap decodeResource(Resources res, int id, Options opts) {
    Bitmap bm = null;
    InputStream is = null; 
    //获取图片所在drawable的dpi
    final TypedValue value = new TypedValue();
    is = res.openRawResource(id, value);
    
    bm = decodeResourceStream(res, value, is, null, opts);
    return bm;
}

alt text 上面逻辑是如果没获取到drawable的density,那么就设置为160,否则直接给Options.inDensity设置为density。然后将设备的density设置到Options.inTargetDensity。

接着调用了decodeStream方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
@Nullable
public static Bitmap decodeStream(@Nullable InputStream is, @Nullable Rect outPadding,
        @Nullable Options opts) {
    validate(opts);
    Bitmap bm = null;

    try {
        //如果是asset目录中的图片,则走if
        if (is instanceof AssetManager.AssetInputStream) {
            final long asset = ((AssetManager.AssetInputStream) is).getNativeAsset();
            bm = nativeDecodeAsset(asset, outPadding, opts, Options.nativeInBitmap(opts),
                Options.nativeColorSpace(opts));
        } else {
            //如果是drawable目录下的图片,则走这里
            bm = decodeStreamInternal(is, outPadding, opts);
        }
        setDensityFromOptions(bm, opts);

    return bm;
}

可以看到如果是drawable目录下的图片,则走decodeStreamInternal:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
private static Bitmap decodeStreamInternal(@NonNull InputStream is,
        @Nullable Rect outPadding, @Nullable Options opts) {
    // ASSERT(is != null);
    byte [] tempStorage = null;
    if (opts != null) tempStorage = opts.inTempStorage;
    if (tempStorage == null) tempStorage = new byte[DECODE_BUFFER_SIZE];
    return nativeDecodeStream(is, tempStorage, outPadding, opts,
            Options.nativeInBitmap(opts),
            Options.nativeColorSpace(opts));
}
private static native Bitmap nativeDecodeStream(InputStream is, byte[] storage,
        Rect padding, Options opts, long inBitmapHandle, long colorSpaceHandle);

最终会调用nativeDecodeStream方法,此处通过aosp来查看源码,该c++代码在/frameworks/base/libs/hwui/jni/BitmapFactory.cpp下,找到cpp文件后,然后再看c++层的方法注册:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
static const JNINativeMethod gMethods[] = {
    {   "nativeDecodeStream",
        "(Ljava/io/InputStream;[BLandroid/graphics/Rect;Landroid/graphics/BitmapFactory$Options;JJ)Landroid/graphics/Bitmap;",
        (void*)nativeDecodeStream
    },

    {   "nativeDecodeFileDescriptor",
        "(Ljava/io/FileDescriptor;Landroid/graphics/Rect;Landroid/graphics/BitmapFactory$Options;JJ)Landroid/graphics/Bitmap;",
        (void*)nativeDecodeFileDescriptor
    },

    {   "nativeDecodeAsset",
        "(JLandroid/graphics/Rect;Landroid/graphics/BitmapFactory$Options;JJ)Landroid/graphics/Bitmap;",
        (void*)nativeDecodeAsset
    },

    {   "nativeDecodeByteArray",
        "([BIILandroid/graphics/BitmapFactory$Options;JJ)Landroid/graphics/Bitmap;",
        (void*)nativeDecodeByteArray
    },

    {   "nativeIsSeekable",
        "(Ljava/io/FileDescriptor;)Z",
        (void*)nativeIsSeekable
    },
};

此处对应到第一个方法nativeDecodeStream:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
static jobject nativeDecodeStream(JNIEnv* env, jobject clazz, jobject is, jbyteArray storage,
        jobject padding, jobject options, jlong inBitmapHandle, jlong colorSpaceHandle) {

    jobject bitmap = NULL;
    std::unique_ptr<SkStream> stream(CreateJavaInputStreamAdaptor(env, is, storage));

    if (stream.get()) {
        std::unique_ptr<SkStreamRewindable> bufferedStream(skia::FrontBufferedStream::Make(
                std::move(stream), SkCodec::MinBufferedBytesNeeded()));
        SkASSERT(bufferedStream.get() != NULL);
        bitmap = doDecode(env, std::move(bufferedStream), padding, options, inBitmapHandle,
                          colorSpaceHandle);
    }
    return bitmap;
}
  1. JNIEnv* env
  • 作用

    • JNIEnv 是 JNI 环境指针,提供所有 JNI 函数(如调用 Java 方法、操作 Java 对象等)的接口。 它是线程相关的,每个线程的 JNIEnv 独立,不可跨线程使用。
  • 关键功能

    • 访问 Java 对象:例如通过 GetFieldID、GetMethodID 获取字段或方法。
    • 调用 Java 方法:例如 CallVoidMethod、CallStaticIntMethod。
    • 异常处理:例如 ExceptionCheck、ExceptionDescribe。
    • 内存管理:例如 NewStringUTF 创建字符串,DeleteLocalRef 释放局部引用。
  1. jobject clazz
  • 作用
    • 如果本地方法是 静态方法(static),jobject clazz 表示调用该方法的 Java 类对象(对应 Class 对象)。
    • 如果本地方法是 实例方法(非静态),参数应为 jobject thiz,表示调用该方法的 Java 对象实例。 可以看到调用了另外一个静态方法doDecode:
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
static jobject doDecode(JNIEnv* env, std::unique_ptr<SkStreamRewindable> stream,
                        jobject padding, jobject options, jlong inBitmapHandle,
                        jlong colorSpaceHandle) {
    // Set default values for the options parameters.
    int sampleSize = 1;
    bool onlyDecodeSize = false;
    SkColorType prefColorType = kN32_SkColorType;
    bool isHardware = false;
    bool isMutable = false;
    float scale = 1.0f;
    bool requireUnpremultiplied = false;
    jobject javaBitmap = NULL;
    sk_sp<SkColorSpace> prefColorSpace = GraphicsJNI::getNativeColorSpace(colorSpaceHandle);

    // Update with options supplied by the client.
    if (options != NULL) {
        sampleSize = env->GetIntField(options, gOptions_sampleSizeFieldID);
        // Correct a non-positive sampleSize.  sampleSize defaults to zero within the
        // options object, which is strange.
        if (sampleSize <= 0) {
            sampleSize = 1;
        }

        if (env->GetBooleanField(options, gOptions_scaledFieldID)) {
            //获取图片所在文件夹的density
            const int density = env->GetIntField(options, gOptions_densityFieldID);
            //获取屏幕所在的density
            const int targetDensity = env->GetIntField(options, gOptions_targetDensityFieldID);
            const int screenDensity = env->GetIntField(options, gOptions_screenDensityFieldID);
            if (density != 0 && targetDensity != 0 && density != screenDensity) {
                //得到bitmap的缩放比例
                scale = (float) targetDensity / density;
            }
        }
    }

    // Determine the output size.
    //得到bitmap的大小
    SkISize size = codec->getSampledDimensions(sampleSize);

    int scaledWidth = size.width();
    int scaledHeight = size.height();
    bool willScale = false;

    // Apply a fine scaling step if necessary.
    if (needsFineScale(codec->getInfo().dimensions(), size, sampleSize)) {
        willScale = true;
        scaledWidth = codec->getInfo().width() / sampleSize;
        scaledHeight = codec->getInfo().height() / sampleSize;
    }

    // Scale is necessary due to density differences.
    //根据前面算的缩放比例,得到最终的bitmap大小
    if (scale != 1.0f) {
        willScale = true;
        scaledWidth = static_cast<int>(scaledWidth * scale + 0.5f);
        scaledHeight = static_cast<int>(scaledHeight * scale + 0.5f);
    }
    //算出最终的缩放比例
    const float scaleX = scaledWidth / float(decodingBitmap.width());
    const float scaleY = scaledHeight / float(decodingBitmap.height());


    SkBitmap outputBitmap;
    if (willScale) {
        // Set the allocator for the outputBitmap.
        SkBitmap::Allocator* outputAllocator;
        if (javaBitmap != nullptr) {
            outputAllocator = &recyclingAllocator;
        } else {
            //最终的数据在outputAllocator中
            outputAllocator = &defaultAllocator;
        }

        SkColorType scaledColorType = 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(
                bitmapInfo.makeWH(scaledWidth, scaledHeight).makeColorType(scaledColorType));
        if (!outputBitmap.tryAllocPixels(outputAllocator)) {
            // This should only fail on OOM.  The recyclingAllocator should have
            // enough memory since we check this before decoding using the
            // scaleCheckingAllocator.
            return nullObjectReturn("allocation failed for scaled bitmap");
        }

        SkPaint paint;
        // kSrc_Mode instructs us to overwrite the uninitialized pixels in
        // outputBitmap.  Otherwise we would blend by default, which is not
        // what we want.
        paint.setBlendMode(SkBlendMode::kSrc);
        //最终解码的bitmap在outputBitmap上
        SkCanvas canvas(outputBitmap, SkCanvas::ColorBehavior::kLegacy);
        //使用最终的缩放比例
        canvas.scale(scaleX, scaleY);
        decodingBitmap.setImmutable(); // so .asImage() doesn't make a copy
        canvas.drawImage(decodingBitmap.asImage(), 0.0f, 0.0f,
                         SkSamplingOptions(SkFilterMode::kLinear), &paint);
    } 

    Bitmap* heapBitmap = defaultAllocator.getStorageObjAndReset();
    if (hasGainmap && heapBitmap != nullptr) {
        heapBitmap->setGainmap(std::move(gainmap));
    }
    uirenderer::logBitmapDecode(*heapBitmap);
    // now create the java bitmap
    //创建bitmap
    return bitmap::createBitmap(env, heapBitmap, bitmapCreateFlags, ninePatchChunk, ninePatchInsets,
                                -1);
}

decode中就是关于bitmap的缩放处理,根据option中的targetDensity/density,也就是屏幕的density/drawable的density得到scale,然后根据原始尺寸再乘以该scale得到最终的宽高像素。

关于底层如何解析图片的解码方式,可以接着上面分析分析BitmapFactory.cpp的doDecode时候调用的,在doDecode时候有这么两句:

1
2
3
SkColorType prefColorType = kN32_SkColorType;
prefColorType = GraphicsJNI::getNativeBitmapColorType(env, jconfig);
SkColorType decodeColorType = codec->computeOutputColorType(prefColorType);

首先默认定义了prefColorType为kN32_SkColorType,然后调用了getNativeBitmapColorType方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
SkColorType GraphicsJNI::getNativeBitmapColorType(JNIEnv* env, jobject jconfig) {
    ALOG_ASSERT(env);
    if (NULL == jconfig) {
        return kUnknown_SkColorType;
    }
    ALOG_ASSERT(env->IsInstanceOf(jconfig, gBitmapConfig_class));
    //获取到java层BitmapFactory.Options中的inPreferredConfig(Bitmap.Config)的nativeInt值
    int c = env->GetIntField(jconfig, gBitmapConfig_nativeInstanceID);
    return legacyBitmapConfigToColorType(c);
}

SkColorType GraphicsJNI::legacyBitmapConfigToColorType(jint legacyConfig) {
    const uint8_t gConfig2ColorType[] = {
            kUnknown_SkColorType,  kAlpha_8_SkColorType,
            kUnknown_SkColorType,  // Previously kIndex_8_SkColorType,
            kRGB_565_SkColorType,  kARGB_4444_SkColorType, kN32_SkColorType,
            kRGBA_F16_SkColorType, kN32_SkColorType,       kRGBA_1010102_SkColorType,
    };

    if (legacyConfig < 0 || legacyConfig > kLastEnum_LegacyBitmapConfig) {
        legacyConfig = kNo_LegacyBitmapConfig;
    }
    return static_cast<SkColorType>(gConfig2ColorType[legacyConfig]);
}

在getNativeBitmapColorType方法中获取到BitmapFactory.Options中的inPreferredConfig(Bitmap.Config)的nativeInt值,然后传给了legacyBitmapConfigToColorType,在该方法中获取到对应的SkColorType,其实就是将在java层Bitmap.Config枚举中定义的nativeInt,映射到gConfig2ColorType数组中,然后对应获取到c++层的SkColorType枚举。对应关系如下

java层 c++层
ALPHA_8 kAlpha_8_SkColorType
RGB_565 kRGB_565_SkColorType
ARGB_4444 kARGB_4444_SkColorType
ARGB_8888 kN32_SkColorType
RGBA_F16 kRGBA_F16_SkColorType
HARDWARE kN32_SkColorType
RGBA_1010102 kRGBA_1010102_SkColorType

获取到SkColorType后,接着调用了SkAndroidCodec的computeOutputColorType方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
SkColorType SkAndroidCodec::computeOutputColorType(SkColorType requestedColorType) {
    //如果每个颜色分量的位数超过8位,就认为是高精度
    bool highPrecision = fCodec->getEncodedInfo().bitsPerComponent() > 8;
    //从编码信息中获取颜色深度(例如 8-bit、10-bit)
    uint8_t colorDepth = fCodec->getEncodedInfo().getColorDepth();
    switch (requestedColorType) {
        //如果是kARGB_4444_SkColorType,直接返回kN32_SkColorType(ARGB_8888)
        //原因:ARGB_4444 格式已过时,现代设备更倾向使用 32 位格式(兼容性/性能优化)。
        case kARGB_4444_SkColorType:
            return kN32_SkColorType;
        //如果是kN32_SkColorType,直接不处理
        case kN32_SkColorType:
            break;

        case kAlpha_8_SkColorType:
        case kGray_8_SkColorType:
            //若当前图像颜色类型为 kGray_8,返回 kGray_8,否则跳过
            //kAlpha_8 旧版本中可能用于灰度图,现需兼容性处理
            //仅当原始图像为灰度时,才返回 kGray_8(避免无效转换)
            if (kGray_8_SkColorType == this->getInfo().colorType()) {
                return kGray_8_SkColorType;
            }
            break;
        case kRGB_565_SkColorType:
            //若图像不透明(alphaType == kOpaque),返回 kRGB_565
            //RGB_565 不支持透明度,仅适用于不透明图像
            if (kOpaque_SkAlphaType == this->getInfo().alphaType()) {
                return kRGB_565_SkColorType;
            }
            break;
        case kRGBA_1010102_SkColorType:
            //若颜色深度为 10-bit,返回 kRGBA_1010102,颜色深度表示每个通道占10位,如果是RGB的话,则是30位的二进制,如果是ARGB的话,则是40位的二进制
            //10-bit 深度匹配该格式的高精度需求
            if (colorDepth == 10) {
              return kRGBA_1010102_SkColorType;
            }
            break;

        case kRGBA_F16_SkColorType:
            //强制高精度浮点格式,每一个颜色通道是16位,如果是RGB的话,则是48位的二进制
            return kRGBA_F16_SkColorType;
        default:
            break;
    }
    //如果高进度的则返回kRGBA_F16_SkColorType,如果位深是10的话,则返回kRGBA_1010102_SkColorType,其余的都返回kN32_SkColorType
    return highPrecision ? kRGBA_F16_SkColorType :
        (colorDepth == 10 ? kRGBA_1010102_SkColorType : kN32_SkColorType);
}

这也就解释了在RGB_565解码方式下,如果是带透明的图片,直接使用kN32_SkColorType的解码方式,也就是ARGB_8888。默认的解码方式,如果是不带透明的图片,则使用的是RGB_565。如果使用的是ALPHA_8,不是灰度图像的时候,使用的是ARGB_8888。

关于drawable的densityDpi的值如下: alt text

结论:图片所占内存和drawable的densityDpi成反比。

来源:https://juejin.cn/post/6844904166138069005

bitmap内存分配:
8.0之前的Bitmap像素数据基本存储在Java heap
8.0之后的 Bitmap像素数据基本存储在native heap
参考:https://juejin.cn/post/6844903608887017485

bitmap像素分配:
参考:https://juejin.cn/post/6844903715766272013

Licensed under CC BY-NC-SA 4.0
Built with Hugo
Theme Stack designed by Jimmy