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 仍是一个有效的省内存方案。
测试各种解码方式

- 对于带有透明的图片:在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
测试了下带透明的图片使用RGB_565的时候,发现解码生成的bitmap的config还是ARGB_8888,说明系统还是会选择合适的颜色模式来解码,通过如下获取最终的解码方式:

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:

- 图片所在的drawable的densityDpi:最终会设置到BitmapFactory.Options.inDensity上,通过如下方式获取:

- 关于设备的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;
}
|
上面逻辑是如果没获取到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;
}
|
- JNIEnv* env
-
作用
- JNIEnv 是 JNI 环境指针,提供所有 JNI 函数(如调用 Java 方法、操作 Java 对象等)的接口。
它是线程相关的,每个线程的 JNIEnv 独立,不可跨线程使用。
-
关键功能
- 访问 Java 对象:例如通过 GetFieldID、GetMethodID 获取字段或方法。
- 调用 Java 方法:例如 CallVoidMethod、CallStaticIntMethod。
- 异常处理:例如 ExceptionCheck、ExceptionDescribe。
- 内存管理:例如 NewStringUTF 创建字符串,DeleteLocalRef 释放局部引用。
- 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的值如下:

结论:图片所占内存和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