Fork me on GitHub

Android Bitmap 性能专题 - Bitmap 内存从申请到回收

问题

1
2
The formulation of the problem is often more essential than its solution, which may be merely a matter of mathematical or experimental skill.
― Albert Einstein

Q:Bitmap 如何开辟内存?

Q:Bitmap 内存是怎么复用和销毁的?本地资源图片应该怎么去做适配?

Q:该如何去优化这些占用较大内存的本地资源图片?

Q:如何判断和优化 Bitmap OOM,如何 dump 线上内存来做优化分析?

从 Bitmap 的创建说起

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
/**
* Private constructor that must received an already allocated native bitmap
* int (pointer).
*/
// called from JNI
Bitmap(long nativeBitmap, int width, int height, int density,
boolean isMutable, boolean requestPremultiplied,
byte[] ninePatchChunk, NinePatch.InsetStruct ninePatchInsets) {
if (nativeBitmap == 0) {
throw new RuntimeException("internal error: native bitmap is 0");
}

mWidth = width;
mHeight = height;
mIsMutable = isMutable;
mRequestPremultiplied = requestPremultiplied;

mNinePatchChunk = ninePatchChunk;
mNinePatchInsets = ninePatchInsets;
if (density >= 0) {
mDensity = density;
}

mNativePtr = nativeBitmap;
long nativeSize = NATIVE_ALLOCATION_SIZE + getAllocationByteCount();
NativeAllocationRegistry registry = new NativeAllocationRegistry(
Bitmap.class.getClassLoader(), nativeGetNativeFinalizer(), nativeSize);
registry.registerNativeAllocation(this, nativeBitmap);

if (ResourcesImpl.TRACE_FOR_DETAILED_PRELOAD) {
sPreloadTracingNumInstantiatedBitmaps++;
sPreloadTracingTotalBitmapsSize += nativeSize;
}
}

called from JNI 这个解释其实已经很明确了,也就是说这个对象是 Native 层构建返回的。因此我们跟踪到 BitmapFactory.decodeResource() 中去看看:

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
public static Bitmap decodeResource(Resources res, int id, Options opts) {
validate(opts);
Bitmap bm = null;
InputStream is = null;

try {
final TypedValue value = new TypedValue();
is = res.openRawResource(id, value);

bm = decodeResourceStream(res, value, is, null, opts);
} catch (Exception e) {
/* do nothing.
If the exception happened on open, bm will be null.
If it happened on close, bm is still valid.
*/
} finally {
try {
if (is != null) is.close();
} catch (IOException e) {
// Ignore
}
}

if (bm == null && opts != null && opts.inBitmap != null) {
throw new IllegalArgumentException("Problem decoding into existing bitmap");
}

return bm;
}

public static Bitmap decodeResourceStream(@Nullable Resources res, @Nullable TypedValue value,
@Nullable InputStream is, @Nullable Rect pad, @Nullable Options opts) {
validate(opts);
if (opts == 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;
}
}
// 获取当前手机设备的 dpi
if (opts.inTargetDensity == 0 && res != null) {
opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
}

return decodeStream(is, pad, opts);
}

// 省略部分跟踪代码 ......

private static native Bitmap nativeDecodeStream(InputStream is, byte[] storage,
Rect padding, Options opts);

最终调用的是 native 方法 nativeDecodeStream

这里以 Android N 版本为例:

/frameworks/base/core/jni/android/graphics/BitmapFactory.cpp

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
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
static jobject nativeDecodeStream(JNIEnv *env, jobject clazz, jobject is, jbyteArray storage,
jobject padding, jobject options) {
jobject bitmap = NULL;
std::unique_ptr<SkStream> stream(CreateJavaInputStreamAdaptor(env, is, storage));

if (stream.get()) {
std::unique_ptr<SkStreamRewindable> bufferedStream(
SkFrontBufferedStream::Create(stream.release(), SkCodec::MinBufferedBytesNeeded()));
SkASSERT(bufferedStream.get() != NULL);
bitmap = doDecode(env, bufferedStream.release(), padding, options);
}
return bitmap;
}

static jobject doDecode(JNIEnv *env, SkStreamRewindable *stream, jobject padding, jobject options) {
// This function takes ownership of the input stream. Since the SkAndroidCodec
// will take ownership of the stream, we don't necessarily need to take ownership
// here. This is a precaution - if we were to return before creating the codec,
// we need to make sure that we delete the stream.
std::unique_ptr<SkStreamRewindable> streamDeleter(stream);

// Set default values for the options parameters.
int sampleSize = 1;
// 是否只是获取图片的大小
bool onlyDecodeSize = false;
SkColorType prefColorType = kN32_SkColorType;
bool isMutable = false;
float scale = 1.0f;
bool requireUnpremultiplied = false;
jobject javaBitmap = NULL;

// Update with options supplied by the client.
// 解析 options 参数
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_justBoundsFieldID)) {
onlyDecodeSize = true;
}

// initialize these, in case we fail later on
env->SetIntField(options, gOptions_widthFieldID, -1);
env->SetIntField(options, gOptions_heightFieldID, -1);
env->SetObjectField(options, gOptions_mimeFieldID, 0);
// 解析 ColorType ,复用参数等等
jobject jconfig = env->GetObjectField(options, gOptions_configFieldID);
prefColorType = GraphicsJNI::getNativeBitmapColorType(env, jconfig);
isMutable = env->GetBooleanField(options, gOptions_mutableFieldID);
requireUnpremultiplied = !env->GetBooleanField(options, gOptions_premultipliedFieldID);
javaBitmap = env->GetObjectField(options, gOptions_bitmapFieldID);
// 计算缩放的比例
if (env->GetBooleanField(options, gOptions_scaledFieldID)) {
// 获取图片当前 xhdpi 的 density
const int density = env->GetIntField(options, gOptions_densityFieldID);
// 获取当前设备的 dpi
const int targetDensity = env->GetIntField(options, gOptions_targetDensityFieldID);
const int screenDensity = env->GetIntField(options, gOptions_screenDensityFieldID);
if (density != 0 && targetDensity != 0 && density != screenDensity) {
// scale = 当前设备的 dpi / xhdpi 的 density
// scale = 420/320 = 1.3125
scale = (float) targetDensity / density;
}
}
}

// Create the codec.
NinePatchPeeker peeker;
std::unique_ptr<SkAndroidCodec> codec(SkAndroidCodec::NewFromStream(streamDeleter.release(),
280 & peeker));
if (!codec.get()) {
return nullObjectReturn("SkAndroidCodec::NewFromStream returned null");
}

// Do not allow ninepatch decodes to 565. In the past, decodes to 565
// would dither, and we do not want to pre-dither ninepatches, since we
// know that they will be stretched. We no longer dither 565 decodes,
// but we continue to prevent ninepatches from decoding to 565, in order
// to maintain the old behavior.
if (peeker.mPatch && kRGB_565_SkColorType == prefColorType) {
prefColorType = kN32_SkColorType;
}
// 获取当前图片的大小
// Determine the output size.
SkISize size = codec->getSampledDimensions(sampleSize);

int scaledWidth = size.width();
int scaledHeight = size.height();
bool willScale = false;
// 处理 simpleSize 压缩,我们这里没穿,上面默认是 1
// 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;
}

// Set the options and return if the client only wants the size.
if (options != NULL) {
jstring mimeType = encodedFormatToString(env, codec->getEncodedFormat());
if (env->ExceptionCheck()) {
return nullObjectReturn("OOM in encodedFormatToString()");
}
// 设置 options 对象中的 outWidth 和 outHeight
env->SetIntField(options, gOptions_widthFieldID, scaledWidth);
env->SetIntField(options, gOptions_heightFieldID, scaledHeight);
env->SetObjectField(options, gOptions_mimeFieldID, mimeType);
// 如果只是获取大小直接 return null 这里是 nullptr 而不是 NULL
if (onlyDecodeSize) {
return nullptr;
}
}

// Scale is necessary due to density differences.
if (scale != 1.0f) {
willScale = true;
// 计算 scaledWidth 和 scaledHeight
// scaledWidth = 864 * 1.3125 + 0.5f = 1134 + 0.5f = 1134
scaledWidth = static_cast<int>(scaledWidth * scale + 0.5f);
// scaledHeight = 582 * 1.3125 + 0.5f = 763.875 + 0.5f = 764
scaledHeight = static_cast<int>(scaledHeight * scale + 0.5f);
}
// 判断是否有复用的 Bitmap
android::Bitmap *reuseBitmap = nullptr;
unsigned int existingBufferSize = 0;
if (javaBitmap != NULL) {
reuseBitmap = GraphicsJNI::getBitmap(env, javaBitmap);
if (reuseBitmap->peekAtPixelRef()->isImmutable()) {
// 无法重用一个不变的位图图像解码器的目标。
ALOGW("Unable to reuse an immutable bitmap as an image decoder target.");
javaBitmap = NULL;
reuseBitmap = nullptr;
} else {
existingBufferSize = GraphicsJNI::getBitmapAllocationByteCount(env, javaBitmap);
}
}

JavaPixelAllocator javaAllocator(env);
RecyclingPixelAllocator recyclingAllocator(reuseBitmap, existingBufferSize);
ScaleCheckingAllocator scaleCheckingAllocator(scale, existingBufferSize);
SkBitmap::HeapAllocator heapAllocator;
SkBitmap::Allocator *decodeAllocator;
if (javaBitmap != nullptr && willScale) {
// This will allocate pixels using a HeapAllocator, since there will be an extra
// scaling step that copies these pixels into Java memory. This allocator
// also checks that the recycled javaBitmap is large enough.
decodeAllocator = &scaleCheckingAllocator;
} else if (javaBitmap != nullptr) {
decodeAllocator = &recyclingAllocator;
} else if (willScale) {
// This will allocate pixels using a HeapAllocator, since there will be an extra
// scaling step that copies these pixels into Java memory.
decodeAllocator = &heapAllocator;
} else {
decodeAllocator = &javaAllocator;
}

// Set the decode colorType. This is necessary because we can't always support
// the requested colorType.
SkColorType decodeColorType = codec->computeOutputColorType(prefColorType);

// Construct a color table for the decode if necessary
SkAutoTUnref <SkColorTable> colorTable(nullptr);
SkPMColor *colorPtr = nullptr;
int *colorCount = nullptr;
int maxColors = 256;
SkPMColor colors[256];
if (kIndex_8_SkColorType == decodeColorType) {
colorTable.reset(new SkColorTable(colors, maxColors));

// SkColorTable expects us to initialize all of the colors before creating an
// SkColorTable. However, we are using SkBitmap with an Allocator to allocate
// memory for the decode, so we need to create the SkColorTable before decoding.
// It is safe for SkAndroidCodec to modify the colors because this SkBitmap is
// not being used elsewhere.
colorPtr = const_cast<SkPMColor *>(colorTable->readColors());
colorCount = &maxColors;
}

// Set the alpha type for the decode.
SkAlphaType alphaType = codec->computeOutputAlphaType(requireUnpremultiplied);
// 创建 SkImageInfo 信息,宽,高,ColorType,alphaType
const SkImageInfo decodeInfo = SkImageInfo::Make(size.width(), size.height(), decodeColorType,
alphaType);
SkImageInfo bitmapInfo = decodeInfo;
if (decodeColorType == kGray_8_SkColorType) {
// The legacy implementation of BitmapFactory used kAlpha8 for
// grayscale images (before kGray8 existed). While the codec
// recognizes kGray8, we need to decode into a kAlpha8 bitmap
// in order to avoid a behavior change.
bitmapInfo = SkImageInfo::MakeA8(size.width(), size.height());
}
// 解析 SkBitmap 设置 bitmapInfo,tryAllocPixels 开辟内存,具体分析在后面
SkBitmap decodingBitmap;
if (!decodingBitmap.setInfo(bitmapInfo) ||
!decodingBitmap.tryAllocPixels(decodeAllocator, colorTable)) {
// SkAndroidCodec should recommend a valid SkImageInfo, so setInfo()
// should only only fail if the calculated value for rowBytes is too
// large.
// tryAllocPixels() can fail due to OOM on the Java heap, OOM on the
// native heap, or the recycled javaBitmap being too small to reuse.
return nullptr;
}

// Use SkAndroidCodec to perform the decode.
SkAndroidCodec::AndroidOptions codecOptions;
codecOptions.fZeroInitialized = (decodeAllocator == &javaAllocator) ?
SkCodec::kYes_ZeroInitialized : SkCodec::kNo_ZeroInitialized;
codecOptions.fColorPtr = colorPtr;
codecOptions.fColorCount = colorCount;
codecOptions.fSampleSize = sampleSize;
// 解析获取像素值
SkCodec::Result result = codec->getAndroidPixels(decodeInfo, decodingBitmap.getPixels(),
decodingBitmap.rowBytes(), &codecOptions);
switch (result) {
case SkCodec::kSuccess:
case SkCodec::kIncompleteInput:
break;
default:
return nullObjectReturn("codec->getAndroidPixels() failed.");
}

jbyteArray ninePatchChunk = NULL;
if (peeker.mPatch != NULL) {
if (willScale) {
scaleNinePatchChunk(peeker.mPatch, scale, scaledWidth, scaledHeight);
}

size_t ninePatchArraySize = peeker.mPatch->serializedSize();
ninePatchChunk = env->NewByteArray(ninePatchArraySize);
if (ninePatchChunk == NULL) {
return nullObjectReturn("ninePatchChunk == null");
}

jbyte *array = (jbyte *) env->GetPrimitiveArrayCritical(ninePatchChunk, NULL);
if (array == NULL) {
return nullObjectReturn("primitive array == null");
}

memcpy(array, peeker.mPatch, peeker.mPatchSize);
env->ReleasePrimitiveArrayCritical(ninePatchChunk, array, 0);
}

jobject ninePatchInsets = NULL;
if (peeker.mHasInsets) {
ninePatchInsets = env->NewObject(gInsetStruct_class, gInsetStruct_constructorMethodID,
peeker.mOpticalInsets[0], peeker.mOpticalInsets[1], peeker.mOpticalInsets[2], peeker.mOpticalInsets[3],
peeker.mOutlineInsets[0], peeker.mOutlineInsets[1], peeker.mOutlineInsets[2], peeker.mOutlineInsets[3],
peeker.mOutlineRadius, peeker.mOutlineAlpha, scale);
if (ninePatchInsets == NULL) {
return nullObjectReturn("nine patch insets == null");
}
if (javaBitmap != NULL) {
env->SetObjectField(javaBitmap, gBitmap_ninePatchInsetsFieldID, ninePatchInsets);
}
}
// 构建 SkBitmap 这个才是最终的
SkBitmap outputBitmap;
if (willScale) {
// 如果需要缩放,那需要重新创建一张图片,上面加载的是图片的本身大小
// This is weird so let me explain: we could use the scale parameter
// directly, but for historical reasons this is how the corresponding
// Dalvik code has always behaved. We simply recreate the behavior here.
// The result is slightly different from simply using scale because of
// the 0.5f rounding bias applied when computing the target image size
const float sx = scaledWidth / float(decodingBitmap.width());
const float sy = scaledHeight / float(decodingBitmap.height());

// Set the allocator for the outputBitmap.
SkBitmap::Allocator *outputAllocator;
if (javaBitmap != nullptr) {
outputAllocator = &recyclingAllocator;
} else {
outputAllocator = &javaAllocator;
}

SkColorType scaledColorType = 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.
// 设置 SkImageInfo ,注意这里是 scaledWidth ,scaledHeight
outputBitmap.setInfo(SkImageInfo::Make(scaledWidth, scaledHeight,
scaledColorType, decodingBitmap.alphaType()));
// 开辟当前 Bitmap 图片的内存
if (!outputBitmap.tryAllocPixels(outputAllocator, NULL)) {
// 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 unininitialized pixels in
// outputBitmap. Otherwise we would blend by default, which is not
// what we want.
paint.setXfermodeMode(SkXfermode::kSrc_Mode);
paint.setFilterQuality(kLow_SkFilterQuality);
// decodingBitmap -> 画到 outputBitmap
SkCanvas canvas(outputBitmap);
canvas.scale(sx, sy);
canvas.drawBitmap(decodingBitmap, 0.0f, 0.0f, &paint);
} else {
outputBitmap.swap(decodingBitmap);
}

if (padding) {
if (peeker.mPatch != NULL) {
GraphicsJNI::set_jrect(env, padding,
peeker.mPatch->paddingLeft, peeker.mPatch->paddingTop,
peeker.mPatch->paddingRight, peeker.mPatch->paddingBottom);
} else {
GraphicsJNI::set_jrect(env, padding, -1, -1, -1, -1);
}
}

// If we get here, the outputBitmap should have an installed pixelref.
if (outputBitmap.pixelRef() == NULL) {
return nullObjectReturn("Got null SkPixelRef");
}

if (!isMutable && javaBitmap == NULL) {
// promise we will never change our pixels (great for sharing and pictures)
outputBitmap.setImmutable();
}
// 如果有复用返回原来的 javaBitmap
bool isPremultiplied = !requireUnpremultiplied;
if (javaBitmap != nullptr) {
GraphicsJNI::reinitBitmap(env, javaBitmap, outputBitmap.info(), isPremultiplied);
outputBitmap.notifyPixelsChanged();
// If a java bitmap was passed in for reuse, pass it back
return javaBitmap;
}

int bitmapCreateFlags = 0x0;
if (isMutable) bitmapCreateFlags |= GraphicsJNI::kBitmapCreateFlag_Mutable;
if (isPremultiplied) bitmapCreateFlags |= GraphicsJNI::kBitmapCreateFlag_Premultiplied;
// 没有复用的 Bitmap 创建一个新的 Bitmap
// now create the java bitmap
return GraphicsJNI::createBitmap(env, javaAllocator.getStorageObjAndReset(),
bitmapCreateFlags, ninePatchChunk, ninePatchInsets, -1);
}

jobject GraphicsJNI::createBitmap(JNIEnv *env, android::Bitmap *bitmap,
int bitmapCreateFlags, jbyteArray ninePatchChunk,
jobject ninePatchInsets,
int density) {
bool isMutable = bitmapCreateFlags & kBitmapCreateFlag_Mutable;
bool isPremultiplied = bitmapCreateFlags & kBitmapCreateFlag_Premultiplied;
// The caller needs to have already set the alpha type properly, so the
// native SkBitmap stays in sync with the Java Bitmap.
assert_premultiplied(bitmap->info(), isPremultiplied);

jobject obj = env->NewObject(gBitmap_class, gBitmap_constructorMethodID,
reinterpret_cast<jlong>(bitmap), bitmap->javaByteArray(),
bitmap->width(), bitmap->height(), density, isMutable, isPremultiplied,
ninePatchChunk, ninePatchInsets);
hasException(env); // For the side effect of logging.
return obj;
}

上面的代码看起来比较长,其实是非常简单的,相信大家都能看得懂,这里我对上面的流程再做一些总结:

  1. 解析 java 层传递过来的 Options 的参数,如 simpleSize ,isMutable,javaBitmap 等等,同时计算出 scale 。
  2. 获取当前图片的大小,根据 sampleSize 判断是否需要压缩,同时计算出 scaledWidth ,scaledHeight。
  3. 设置 options 宽高为 scaledWidth ,scaledHeight ,如果只是解析宽高那么就直接返回,也就是 options.inJustDecodeBounds = true 时,但是这里需要注意返回的是,资源图片的宽高并不是 Bitmap 最终的宽高。(我们大部分人对这个有误解)
  4. 创建 native 层的 SkImageInfo 和 SkBitmap ,然后调用 tryAllocPixels 去开辟图片的内存空间,然后调用 getAndroidPixels 去解析像素值 ,这里的 decodingBitmap 也并不是最终需要返回的 Bitmap ,而是原资源图片的 Bitmap 。
  5. 构建需要返回的 outputBitmap ,如果需要缩放那么重新去开辟一块内存空间,如果不需要缩放直接调用 swap 方法即可。最后判断有没有复用的 JavaBitmap ,如果有复用调用 reinitBitmap 然后直接返回,如果没有则调用 createBitmap 去创建一个新的 Bitmap 。

究竟需要多少内存?

通过上面的分析,我们可能会有疑问?我们调用了两次 tryAllocPixels ,那如果加载一张 (1440x2560) 10M 的图片,岂不是需要 20M 的内存?

在 Android 平台上,Bitmap 的内存申请操作通常涉及到 Java 层和 Native 层的协作,而 tryAllocPixels 方法的多次调用可能是由于内部的内存分配和管理流程所致。

在 Android 4.4 及以上版本中,当你尝试为 Bitmap 对象申请内存时,tryAllocPixels 方法可能会在底层进行两次调用。这个行为在底层库中被设计用来提高内存分配的成功率。

第一次调用 tryAllocPixels 旨在检查是否有足够的内存来分配 Bitmap 的像素数据。如果这次调用失败,系统会尝试触发一次垃圾回收(GC)操作,以释放一些已不再使用的内存资源。

第二次调用 tryAllocPixels 发生在垃圾回收之后,此时系统期望能够分配足够的内存来满足 Bitmap 的需求。如果第二次调用仍然失败,那么可能会抛出 OutOfMemoryError 错误。

这种设计的目的是为了提高 Bitmap 内存分配的成功率。由于 Android 平台上的内存管理和垃圾回收机制是相对复杂的,经过多次尝试分配内存和触发垃圾回收,可以增加 Bitmap 成功分配内存的可能性。

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
问题:两次调用需要两倍的内存吗

GPT4: 在处理Android图片加载时,内存管理是一个关键考虑因素。
特别是对于高分辨率的图片,如1440x2560像素大小的图片,
观上可能会担心单纯通过分析像素数和颜色深度得出的内存需求量会加倍,
尤其是在调用像`tryAllocPixels`这样的函数两次时。
这里的关键在于理解`tryAllocPixels`函数的作用及其在Android图片处理中的实际行为。

`tryAllocPixels`是 Android Skia 图形库中的一个函数,用于尝试分配内存以存储图片的像素数据。
当你看到代码中似乎调用了两次`tryAllocPixels`
可能会误以为这意味着图片数据会被加载或复制两次,
从而导致内存使用翻倍。然而,实际情况可能并非如此。

首先,调用`tryAllocPixels`并不总是意味着实际的内存分配。
这个函数的行为取决于多个因素,
包括图片的存储格式、是否已经有足够的内存分配给图片,以及图像解码器的实现等。
在某些情况下,如果图片已经在内存中有一个分配好的空间,
第二次调用可能仅仅是一个重复的操作,而不会实际增加内存消耗。

其次,即使图片数据需要被重新处理或在内存中重新定位,
这也不一定意味着整个图片大小的内存会被再次完整分配。
例如,某些优化操作可能仅仅需要额外的小量内存来处理图片数据,而不是复制整个图片。

最后,Android平台和许多现代操作系统都采用了高效的内存管理和图像处理策略。
这包括图片的压缩存储、延迟解码(只有在显示图片时才完全解码)、以及内存共享和重用机制等。
这些机制意味着即使处理大型图片,系统也能通过优化减少实际的内存占用。

因此,尽管从理论上分析像素和颜色深度可能会让人担心高内存消耗,
实际上Android的图片加载和处理框架设计有多种优化,以保持内存使用的效率。
对于开发者而言,关键是了解并利用这些机制,例如使用适当的图片加载库(如Glide、Picasso等),
它们为图片处理提供了进一步的优化和内存管理策略。

在 Android 中,Bitmap 对象的内存使用量取决于图像的像素数量、每个像素的位深度和像素数据的存储方式。一个 10MB 的图片通常指的是文件大小,而不是图像的像素数量,因此具体的内存使用量会根据图像的属性而有所不同。

假设你有一张 10MB 的图片,我们可以估算一下将其加载为 Bitmap 时可能需要的内存量。请注意,以下是一个粗略的估算,实际内存使用量可能会受到压缩、图像格式和设备性能的影响。

  1. 图像大小: 假设你的图片大小为 10MB,这指的是文件的大小。这并不直接表示图像的像素数量,因为图像可以使用不同的压缩算法和格式存储。通常情况下,JPEG 格式的图像文件较小,而无损格式如 PNG 或 BMP 文件较大。

  2. 解码后的像素数据: 当你将图片解码为 Bitmap 时,其像素数据将被加载到内存中。假设该图片是一个 24 位的彩色图片(每个像素使用 8 位红、绿、蓝通道),那么像素数据的大小将是图像的宽度乘以高度乘以 3(每个像素 3 个通道)字节。这个估算不考虑额外的颜色通道、Alpha 通道等。

所以,一个粗略的估算如下:

假设图片的像素数据大小为 pixelsSize 字节,则需要的内存量约为:pixelsSize + Overhead

OverheadBitmap 对象的开销,它通常取决于设备的位数、对象头信息、内存对齐等因素。对于一个普通的 24 位彩色图片,Overhead 大约在 4-8MB 左右。

需要注意的是,这只是一个估算,并且实际情况可能因为图片格式、加载库的实现、设备性能等因素而有所不同。在实际开发中,你可以使用一些工具来测量实际的内存使用量,例如 Android Studio 的内存分析工具。

Bitmap 的内存申请

Bitmap 的内存申请不同版本间有些许差异,在 3.0-7.0 的 bitmap 像素内存都是存放在 Java heap 中的,而 8.0 以后则是放在 Native heap 中的

8.0 Bitmap 内存申请

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

logMemory();

Bitmap bitmap = Bitmap.createBitmap(1024, 1024 * 500, Bitmap.Config.ARGB_8888);

logMemory();
}

private void logMemory() {
ActivityManager activityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo();
activityManager.getMemoryInfo(memoryInfo);
Log.e("TAG", "AvailMem :" + memoryInfo.availMem / 1024 / 1024);
Log.e("TAG", "lowMemory:" + memoryInfo.lowMemory);
Log.e("TAG", "NativeHeapAllocatedSize :" + Debug.getNativeHeapAllocatedSize() / 1024 / 1024);
}

上面我们创建了一张 2G 大小的 bitmap 我们在 8.0 以下的版本运行是会 OOM 的,而我们在 8.0 以上的版本运行是完全没问题,但 Native 内存多了 2G 的内存。

1
2
3
4
5
6
7
E/TAG: AvailMem :1654
E/TAG: lowMemory:false
E/TAG: NativeHeapAllocatedSize :4

E/TAG: AvailMem :1656
E/TAG: lowMemory:false
E/TAG: NativeHeapAllocatedSize :2052

为什么会有一张大图片的像素数据放在Java层会OOM,放在Native层却不会的情况

在 Android 应用中,一张大图片的像素数据如果放在 Java 层可能会导致 OutOfMemoryError (OOM) 错误,而放在 Native 层不会出现这个问题。这涉及到 Java 层和 Native 层内存管理的不同方式以及底层实现的细节。

  1. Java 层内存管理: 在 Java 层,对象的内存管理由 Java 虚拟机 (JVM) 负责。当你创建一个 Bitmap 对象并加载大图片的像素数据时,这些像素数据会在 Java 堆内存中分配。Android 应用的 Java 堆内存限制通常较小,这是为了确保系统的稳定性,避免应用占用过多内存影响其他应用和系统的运行。Java 堆内存受到限制,当堆内存用尽时就会触发 OutOfMemoryError 错误。由于 Java 层的内存管理涉及垃圾回收和其他 Java 虚拟机机制,加载大量像素数据可能会导致内存碎片和性能问题,最终导致 OOM 错误。

  2. Native 层内存管理: 在 Native 层,像素数据的内存分配和释放由底层操作系统和 C/C++ 代码负责。通过在 Native 层使用本地代码(如 C/C++),你可以更好地控制内存的分配和释放。Native 层不受 Java 堆内存限制,因此可以更灵活地处理大量的像素数据,减少内存碎片问题,并降低 OOM 风险。

7.0 与 8.0 Bitmap 内存申请 tryAllocPixels 方法

通过之前的源码分析可知 bitmap 的内存创建都是通过 tryAllocPixels 方法来申请的,我们通过源码来对比一下他们之间的区别,我们首先来看下 7.0 的代码:

/frameworks/base/core/jni/android/graphics/Bitmap.cpp

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
bool SkBitmap::tryAllocPixels(Allocator *allocator, SkColorTable *ctable) {
HeapAllocator stdalloc;

if (nullptr == allocator) {
allocator = &stdalloc;
}
return allocator->allocPixelRef(this, ctable);
}

bool JavaPixelAllocator::allocPixelRef(SkBitmap *bitmap, SkColorTable *ctable) {
JNIEnv *env = vm2env(mJavaVM);

mStorage = GraphicsJNI::allocateJavaPixelRef(env, bitmap, ctable);
return mStorage != nullptr;
}

android::Bitmap *GraphicsJNI::allocateJavaPixelRef(JNIEnv *env, SkBitmap *bitmap,
SkColorTable *ctable) {
const SkImageInfo &info = bitmap->info();
if (info.colorType() == kUnknown_SkColorType) {
doThrowIAE(env, "unknown bitmap configuration");
return NULL;
}

size_t size;
if (!computeAllocationSize(*bitmap, &size)) {
return NULL;
}

// we must respect the rowBytes value already set on the bitmap instead of
// attempting to compute our own.
const size_t rowBytes = bitmap->rowBytes();

jbyteArray arrayObj = (jbyteArray) env->CallObjectMethod(gVMRuntime,
gVMRuntime_newNonMovableArray,
gByte_class, size);
if (env->ExceptionCheck() != 0) {
return NULL;
}
SkASSERT(arrayObj);
jbyte *addr = (jbyte *) env->CallLongMethod(gVMRuntime, gVMRuntime_addressOf, arrayObj);
if (env->ExceptionCheck() != 0) {
return NULL;
}
SkASSERT(addr);
android::Bitmap *wrapper = new android::Bitmap(env, arrayObj, (void *) addr, info, rowBytes,
ctable);
wrapper->getSkBitmap(bitmap);
// since we're already allocated, we lockPixels right away
// HeapAllocator behaves this way too
bitmap->lockPixels();

return wrapper;
}

从上面就可以看到, new android::Bitmap 见:
frameworks/base/core/jni/android/graphics/Bitmap.cpp

1
2
3
4
5
6
7
8
9
10
11
Bitmap::Bitmap(JNIEnv *env, jbyteArray storageObj, void *address,
const SkImageInfo &info, size_t rowBytes, SkColorTable *ctable)
: mPixelStorageType(PixelStorageType::Java) {
env->GetJavaVM(&mPixelStorage.java.jvm);
mPixelStorage.java.jweakRef = env->NewWeakGlobalRef(storageObj);
mPixelStorage.java.jstrongRef = nullptr;
mPixelRef.reset(new WrappedPixelRef(this, address, info, rowBytes, ctable));
// Note: this will trigger a call to onStrongRefDestroyed(), but
// we want the pixel ref to have a ref count of 0 at this point
mPixelRef->unref();
}

address 获取的是 arrayObj 的地址,而 arrayObj 是 jbyteArray 数据类型,也就是说这里是通过 JNI 进入了 Java 世界开辟了内存,好比 Zygote 进入 Java 世界是通过 JNI 调用 com.android.internal.os.ZygoteInit 类的 main 函数是一个道理~ 我们还可以继续跟到 gVMRuntime_newNonMovableArray 中去看看实现,最后是 runtime->GetHeap() 上分配内存也就是 Java heap 内存。

我们还得看下 8.0 的源码,比较一下它与 7.0 之间的区别:
external/skia/src/core/SkBitmap.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
bool SkBitmap::tryAllocPixels(Allocator *allocator, SkColorTable *ctable) {
HeapAllocator stdalloc;

if (nullptr == allocator) {
allocator = &stdalloc;
}
return allocator->allocPixelRef(this, ctable);
}

bool HeapAllocator::allocPixelRef(SkBitmap *bitmap, SkColorTable *ctable) {
mStorage = android::Bitmap::allocateHeapBitmap(bitmap, ctable);
return !!mStorage;
}

allocateHeapBitmap方法会最终new Bitmap,分配内存 ,见:
/frameworks/base/libs/hwui/hwui/Bitmap.cpp

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
sk_sp <Bitmap> Bitmap::allocateHeapBitmap(SkBitmap *bitmap, SkColorTable *ctable) {
return allocateBitmap(bitmap, ctable, &android::allocateHeapBitmap);
}

static sk_sp <Bitmap> allocateBitmap(SkBitmap *bitmap, SkColorTable *ctable, AllocPixeRef alloc) {
const SkImageInfo &info = bitmap->info();
if (info.colorType() == kUnknown_SkColorType) {
LOG_ALWAYS_FATAL("unknown bitmap configuration");
return nullptr;
}

size_t size;

// we must respect the rowBytes value already set on the bitmap instead of
// attempting to compute our own.
const size_t rowBytes = bitmap->rowBytes();
if (!computeAllocationSize(rowBytes, bitmap->height(), &size)) {
return nullptr;
}

auto wrapper = alloc(size, info, rowBytes, ctable);
if (wrapper) {
wrapper->getSkBitmap(bitmap);
// since we're already allocated, we lockPixels right away
// HeapAllocator behaves this way too
bitmap->lockPixels();
}
return wrapper;
}

Bitmap 内存模型

0-2.3 3.0-4.4 5.0-7.1 8.0
Bitmap对象 Java Heap Java Heap Java Heap
像素数据 Native Heap Java Heap Native Heap
迁移原因 解决Native Bitmap内存泄漏 共享整个系统的内存减少OOM

Android Bitmap 回收机制源代码分析 (从2.3到7.0,8.0)

android 2.3.3 和 更低的版本

在 Android 2.3.3 之前开发者必须手动调用 recycle 方法去释放 Native 内存,因为那个时候管理Bitmap内存比较复杂,需要手动维护引用计数器

官网解释:

1
2
3
4
5
On Android 2.3.3 (API level 10) and lower, using recycle() is recommended. If you're displaying large amounts of bitmap data in your app, you're likely to run into OutOfMemoryError errors. The recycle()method allows an app to reclaim memory as soon as possible.
Caution: You should use recycle() only when you are sure that the bitmap is no longer being used. If you call recycle() and later attempt to draw the bitmap, you will get the error: "Canvas: trying to use a recycled bitmap".
The following code snippet gives an example of calling recycle(). It uses reference counting (in the variables mDisplayRefCount and mCacheRefCount) to track whether a bitmap is currently being displayed or in the cache. The code recycles the bitmap when these conditions are met:
The reference count for both mDisplayRefCount and mCacheRefCount is 0.
The bitmap is not null, and it hasn't been recycled yet.

在 Android 2.3.3 以后不需要开发者主动调用 recycle 方法来回收内存了,但 Android K,L,M,N,O 版本上,都还能看到 recycle 方法,为什么没有干掉呢? 调用它会不会真正的释放内存呢?既然不需要手动释放 Native Bitmap ,那 Native 层的对象是怎么自动释放的?我们先来看下 7.0 和 8.0 中 recycle 的方法实现。

7.0 和 8.0 中 recycle 的方法实现

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
/**
* Free the native object associated with this bitmap, and clear the
* reference to the pixel data. This will not free the pixel data synchronously;
* it simply allows it to be garbage collected if there are no other references.
* The bitmap is marked as "dead", meaning it will throw an exception if
* getPixels() or setPixels() is called, and will draw nothing. This operation
* cannot be reversed, so it should only be called if you are sure there are no
* further uses for the bitmap. This is an advanced call, and normally need
* not be called, since the normal GC process will free up this memory when
* there are no more references to this bitmap.
*/

/*
* 释放与此位图关联的本机对象,并清除对像素数据的引用。这不会立即释放像素数据;
* 它只是允许在没有其他引用时进行垃圾回收。该位图被标记为"无效",这意味着如果调用
* getPixels()或setPixels(),它将抛出异常,
* 并且不会绘制任何内容。此操作无法撤销,因此只有在确保不再需要位图时才应调用此方法。
* 这是一个高级调用,通常不需要调用,因为正常的垃圾回收过程将在不再引用该位图时释放此内
* 存。
*/
public void recycle() {
if (!mRecycled && mNativePtr != 0) {
if (nativeRecycle(mNativePtr)) {
// return value indicates whether native pixel object was actually recycled.
// false indicates that it is still in use at the native level and these
// objects should not be collected now. They will be collected later when the
// Bitmap itself is collected.

// 返回值指示本机像素对象是否实际已被回收。
// false 表示它仍在本机级别上使用,现在不应收集这些对象。它们将在位图本身被回收时稍后收集。
mNinePatchChunk = null;
}
mRecycled = true;
}
}

private static native boolean nativeRecycle(long nativeBitmap);

都是调用了native方法,下面看一下native方法

8.0 见:
/xref/frameworks/base/core/jni/android/graphics/Bitmap.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static jboolean Bitmap_recycle(JNIEnv *env, jobject, jlong bitmapHandle) {
LocalScopedBitmap bitmap(bitmapHandle);
bitmap->freePixels();
return JNI_TRUE;
}

void freePixels() {
mInfo = mBitmap->info();
mHasHardwareMipMap = mBitmap->hasHardwareMipMap();
mAllocationSize = mBitmap->getAllocationByteCount();
mRowBytes = mBitmap->rowBytes();
mGenerationId = mBitmap->getGenerationID();
mIsHardware = mBitmap->isHardware();
// 清空了数据
mBitmap.reset();

7.0 见:
/xref/frameworks/base/core/jni/android/graphics/Bitmap.cpp

1
2
3
4
5
static jboolean Bitmap_recycle(JNIEnv *env, jobject, jlong bitmapHandle) {
LocalScopedBitmap bitmap(bitmapHandle);
bitmap->freePixels();
return JNI_TRUE;
}
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
void Bitmap::doFreePixels() {
switch (mPixelStorageType) {
case PixelStorageType::Invalid:
// already free'd, nothing to do
break;
case PixelStorageType::External:
mPixelStorage.external.freeFunc(mPixelStorage.external.address,
mPixelStorage.external.context);
break;
case PixelStorageType::Ashmem:
munmap(mPixelStorage.ashmem.address, mPixelStorage.ashmem.size);
close(mPixelStorage.ashmem.fd);
break;
case PixelStorageType::Java:
// 只是释放了 Java 层之前创建的引用
JNIEnv *env = jniEnv();
LOG_ALWAYS_FATAL_IF(
mPixelStorage.java.jstrongRef,
"Deleting a bitmap wrapper while there are outstanding strong "
"references! mPinnedRefCount = %d", mPinnedRefCount);
env->DeleteWeakGlobalRef(mPixelStorage.java.jweakRef);
break;
}

if (android::uirenderer::Caches::hasInstance()) {
android::uirenderer::Caches::getInstance().textureCache.releaseTexture(
mPixelRef->getStableID());
}
}

7.0 和 8.0 中 手动调用 recycle 的方法,像素数据会被回收吗

从上面的源码可以看出,如果是
8.0 : 我们手动调用 recycle 方法,数据是会立即释放的,因为像素数据本身就是在 Native 层开辟的。
8.0 以下,就算我们手动调用 recycle 方法,数据也是不会立即释放的,而是 DeleteWeakGlobalRef 交由 Java GC 来回收。

注意:以上的所说的释放数据仅代表释放像素数据,并未释放 Native 层的 Bitmap 对象。

Android M 版本及以前的版本, Bitmap 的内存回收

Android M 版本及以前的版本, Bitmap 的内存回收主要是通过 BitmapFinalizer 来完成的见:
/frameworks/base/graphics/java/android/graphics/Bitmap.java

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
Bitmap(long nativeBitmap, byte[] buffer, int width, int height, int density,
boolean isMutable, boolean requestPremultiplied,
byte[] ninePatchChunk, NinePatchInsetStruct ninePatchInsets) {
if (nativeBitmap == 0) {
throw new RuntimeException("internal error: native bitmap is 0");
}

mWidth = width;
mHeight = height;
mIsMutable = isMutable;
mRequestPremultiplied = requestPremultiplied;
mBuffer = buffer;

mNinePatchChunk = ninePatchChunk;
mNinePatchInsets = ninePatchInsets;
if (density >= 0) {
mDensity = density;
}

mNativePtr = nativeBitmap;
// 这个对象对象来回收
mFinalizer = new BitmapFinalizer(nativeBitmap);
int nativeAllocationByteCount = (buffer == null ? getByteCount() : 0);
mFinalizer.setNativeAllocationByteCount(nativeAllocationByteCount);
}

private static class BitmapFinalizer {
private long mNativeBitmap;

// Native memory allocated for the duration of the Bitmap,
// if pixel data allocated into native memory, instead of java byte[]
private int mNativeAllocationByteCount;

BitmapFinalizer(long nativeBitmap) {
mNativeBitmap = nativeBitmap;
}

public void setNativeAllocationByteCount(int nativeByteCount) {
if (mNativeAllocationByteCount != 0) {
VMRuntime.getRuntime().registerNativeFree(mNativeAllocationByteCount);
}
mNativeAllocationByteCount = nativeByteCount;
if (mNativeAllocationByteCount != 0) {
VMRuntime.getRuntime().registerNativeAllocation(mNativeAllocationByteCount);
}
}

@Override
public void finalize() {
try {
super.finalize();
} catch (Throwable t) {
// Ignore
} finally {
// finalize 这里是 GC 回收该对象时会调用
setNativeAllocationByteCount(0);
nativeDestructor(mNativeBitmap);
mNativeBitmap = 0;
}
}
}

private static native void nativeDestructor(long nativeBitmap);

Android N 和 Android O 后,Bitmap 的内存回收

没有了 BitmapFinalizer 类,但在 new Bitmap 时会注册 native 的 Finalizer 方法见: /frameworks/base/graphics/java/android/graphics/Bitmap.java

使用 NativeAllocationRegistry 回收 native 内存

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
/**
* Private constructor that must received an already allocated native bitmap
* int (pointer).
*/
// called from JNI
Bitmap(long nativeBitmap, int width, int height, int density,
boolean isMutable, boolean requestPremultiplied,
byte[] ninePatchChunk, NinePatch.InsetStruct ninePatchInsets) {
if (nativeBitmap == 0) {
throw new RuntimeException("internal error: native bitmap is 0");
}

mWidth = width;
mHeight = height;
mIsMutable = isMutable;
mRequestPremultiplied = requestPremultiplied;

mNinePatchChunk = ninePatchChunk;
mNinePatchInsets = ninePatchInsets;
if (density >= 0) {
mDensity = density;
}

mNativePtr = nativeBitmap;
long nativeSize = NATIVE_ALLOCATION_SIZE + getAllocationByteCount();
NativeAllocationRegistry registry = new NativeAllocationRegistry(
Bitmap.class.getClassLoader(), nativeGetNativeFinalizer(), nativeSize);
registry.registerNativeAllocation(this, nativeBitmap);
}

Bitmap 的 Java 对象 GC 之后,对应的 native 内存如何回收

NativeAllocationRegistry 的原理与设计思想

NativeAllocationRegistryAndroid 8.0(API 27)引入的一种辅助回收native内存的机制

  • Bitmap 的内存分配分外两块:Java 堆和 native 堆。我们都知道 JVM 有垃圾回收机制,那么当 Bitmap的Java对象GC之后,对应的 native 堆内存会回收吗?

  • 提出问题

    掌握了NativeAllocationRegistry的作用和使用步骤后,很自然地会有一些疑问:

    • 为什么在Java层对象被垃圾回收后,native内存会自动被回收呢?
    • NativeAllocationRegistry是从Android 8.0(API 27)开始引入,那么在此之前,native内存是如何回收的呢?

使用步骤

Android 8.0(API 27)开始,Android中很多地方可以看到NativeAllocationRegistry的身影,我们以Bitmap为例子介绍NativeAllocationRegistry的使用步骤,涉及文件:Bitmap.javaBitmap.hBitmap.cpp

步骤1:创建 NativeAllocationRegistry

首先,我们看看实例化NativeAllocationRegistry的地方,具体在Bitmap的构造函数中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// # Android 8.0

// Bitmap.java

// called from JNI
Bitmap(long nativeBitmap,...){
// 省略其他代码...

// 【分析点 1:native 层需要的内存大小】
long nativeSize = NATIVE_ALLOCATION_SIZE + getAllocationByteCount();
// 【分析点 2:回收函数 nativeGetNativeFinalizer()】
// 【分析点 3:加载回收函数的类加载器:Bitmap.class.getClassLoader()】
NativeAllocationRegistry registry = new NativeAllocationRegistry(
Bitmap.class.getClassLoader(), nativeGetNativeFinalizer(), nativeSize);
// 注册 Java 层对象引用与 native 层对象的地址
registry.registerNativeAllocation(this, nativeBitmap);
}

private static final long NATIVE_ALLOCATION_SIZE = 32;
private static native long nativeGetNativeFinalizer();

可以看到,Bitmap的构造函数(在从JNI中调用)中实例化了NativeAllocationRegistry,并传递了三个参数:

参数 解释
classLoader 加载freeFunction函数的类加载器
freeFunction 回收native内存的native函数直接地址
size 分配的native内存大小(单位:字节)
步骤2:注册对象

紧接着,调用了registerNativeAllocation(...),并传递两个参数:

参数 解释
referent Java层对象的引用
nativeBitmap native层对象的地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Bitmap.java

// called from JNI
Bitmap(long nativeBitmap,...){
// 省略其他代码...
// 注册 Java 层对象引用与 native 层对象的地址
registry.registerNativeAllocation(this, nativeBitmap);
}

// NativeAllocationRegistry.java

public Runnable registerNativeAllocation(Object referent, long nativePtr) {
// 代码省略,下文补充...
}
步骤3:回收内存

完成前面两步后,当Java层对象被垃圾回收后,NativeAllocationRegistry会自动回收注册的native内存。例如,我们加载几张图片,随后释放Bitmap的引用,可以观察到GC之后,native层的内存也自动回收了:

ava
1
2
3
4
5
tv.setOnClickListener{
val map = HashSet<Any>()
for(index in 0 .. 2){
map.add(BitmapFactory.decodeResource(resources,R.drawable.test))
}
  • GC 前的内存分配情况 —— Android 8.0

img

  • GC 后的内存分配情况 —— Android 8.0

img


-

通过分析NativeAllocationRegistry源码,我们将一步步解答这些问题,请继续往下看。


NativeAllocationRegistry 源码分析

现在我们将视野回到到NativeAllocationRegistry的源码,涉及文件:NativeAllocationRegistry.javaNativeAllocationRegistry_Delegate.javalibcore_util_NativeAllocationRegistry.cpp

构造函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// NativeAllocationRegistry.java

public class NativeAllocationRegistry {
// 加载 freeFunction 函数的类加载器
private final ClassLoader classLoader;
// 回收 native 内存的 native 函数直接地址
private final long freeFunction;
// 分配的 native 内存大小(字节)
private final long size;

public NativeAllocationRegistry(ClassLoader classLoader, long freeFunction, long size) {
if (size < 0) {
throw new IllegalArgumentException("Invalid native allocation size: " + size);
}

this.classLoader = classLoader;
this.freeFunction = freeFunction;
this.size = size;
}
}

可以看到,NativeAllocationRegistry的构造函数只是将三个参数保存下来,并没有执行额外操作。以Bitmap为例,三个参数在Bitmap的构造函数中获得,我们继续上一节未完成的分析过程:

  • 分析点 1:native 层需要的内存大小
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Bitmap.java

// 【分析点 1:native 层需要的内存大小】
long nativeSize = NATIVE_ALLOCATION_SIZE + getAllocationByteCount();

public final int getAllocationByteCount() {
if (mRecycled) {
Log.w(TAG, "Called getAllocationByteCount() on a recycle()'d bitmap! "
+ "This is undefined behavior!");
return 0;
}
// 调用 native 方法
return nativeGetAllocationByteCount(mNativePtr);
}

private static final long NATIVE_ALLOCATION_SIZE = 32;

可以看到,nativeSize由固定的32字节加上getAllocationByteCount(),总之,NativeAllocationRegistry需要一个native层内存大小的参数。

  • 分析点 2:回收函数 nativeGetNativeFinalizer()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Bitmap.java

// 【分析点 2:回收函数 nativeGetNativeFinalizer()】
NativeAllocationRegistry registry = new NativeAllocationRegistry(
Bitmap.class.getClassLoader(), nativeGetNativeFinalizer(), nativeSize);

private static native long nativeGetNativeFinalizer();

// Java 层
// ----------------------------------------------------------------------
// native 层

// Bitmap.cpp
static jlong Bitmap_getNativeFinalizer(JNIEnv*, jobject) {
// 转为long
return static_cast<jlong>(reinterpret_cast<uintptr_t>(&Bitmap_destruct));
}

static void Bitmap_destruct(BitmapWrapper* bitmap) {
delete bitmap;
}

可以看到,nativeGetNativeFinalizer()是一个native函数,返回值是一个long,这个值其实相当于Bitmap_destruct()函数的直接地址。很明显,Bitmap_destruct()就是用来回收native层内存的。

那么,Bitmap_destruct()是在哪里调用的呢?继续往下看!

  • 分析点 3:加载回收函数的类加载器
1
2
// Bitmap.java
Bitmap.class.getClassLoader()

另外,NativeAllocationRegistry还需要ClassLoader参数,文档注释指出:**classloader是加载freeFunction所在native库的类加载器**,但是NativeAllocationRegistry内部并没有使用这个参数。这里笔者也不理解为什么需要传递这个参数,如果有知道答案的小伙伴请告诉我一下~

注册对象
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
// Bitmap.java

// 注册 Java 层对象引用与 native 层对象的地址
registry.registerNativeAllocation(this, nativeBitmap);

// NativeAllocationRegistry.java

public Runnable registerNativeAllocation(Object referent, long nativePtr) {
if (referent == null) {
throw new IllegalArgumentException("referent is null");
}
if (nativePtr == 0) {
throw new IllegalArgumentException("nativePtr is null");
}

CleanerThunk thunk;
CleanerRunner result;
try {
thunk = new CleanerThunk();
Cleaner cleaner = Cleaner.create(referent, thunk);
result = new CleanerRunner(cleaner);
registerNativeAllocation(this.size);
} catch (VirtualMachineError vme /* probably OutOfMemoryError */) {
applyFreeFunction(freeFunction, nativePtr);
throw vme;
// Other exceptions are impossible.
// Enable the cleaner only after we can no longer throw anything, including OOME.
thunk.setNativePtr(nativePtr);
return result;
}

可以看到,registerNativeAllocation (...)方法参数是**Java层对象引用与native层对象的地址**。函数体乍一看是有点绕,笔者在这里也停留了好长一会。我们简化一下代码,try-catch代码先省略,函数返回值Runnable暂时用不到也先省略,瘦身后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// NativeAllocationRegistry.java

// (简化)
public void registerNativeAllocation(Object referent, long nativePtr) {
CleanerThunk thunk thunk = new CleanerThunk();
// Cleaner 绑定 Java 对象与回收函数
Cleaner cleaner = Cleaner.create(referent, thunk);
// 注册 native 内存
registerNativeAllocation(this.size);
thunk.setNativePtr(nativePtr);
}

private class CleanerThunk implements Runnable {
// 代码省略,下文补充...
}

看到这里,上文提出的第一个疑问就可以解释了,原来NativeAllocationRegistry内部是利用了sun.misc.Cleaner.java机制,简单来说:使用虚引用得知对象被GC的时机,在GC前执行额外的回收工作

# 举一反三

DirectByteBuffer内部也是利用了Cleaner实现堆外内存的释放的。若不了解,请务必阅读:《Java | 堆内存与堆外内存》

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private class CleanerThunk implements Runnable {
// native 层对象的地址
private long nativePtr;

public CleanerThunk() {
this.nativePtr = 0;
}

public void run() {
if (nativePtr != 0) {
// 【分析点 4:执行内存回收方法】
applyFreeFunction(freeFunction, nativePtr);
// 【分析点 5:注销 native 内存】
registerNativeFree(size);
}
}

public void setNativePtr(long nativePtr) {
this.nativePtr = nativePtr;
}
}

继续往下看,CleanerThunk其实是Runnable的实现类,run()Java层对象被垃圾回收时触发,主要做了两件事:

  • 分析点 4:执行内存回收方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static native void applyFreeFunction(long freeFunction, long nativePtr);

// NativeAllocationRegistry.cpp

typedef void (*FreeFunction)(void*);

static void NativeAllocationRegistry_applyFreeFunction(JNIEnv*,
jclass,
jlong freeFunction,
jlong ptr) {
void* nativePtr = reinterpret_cast<void*>(static_cast<uintptr_t>(ptr));
FreeFunction nativeFreeFunction = reinterpret_cast<FreeFunction>(static_cast<uintptr_t>(freeFunction));
// 调用回收函数
nativeFreeFunction(nativePtr);
}

可以看到,applyFreeFunction(...)最终就是执行到了前面提到的内存回收函数,对于Bitmap就是Bitmap_destruct()

  • 分析点 5:注册 / 注销native内存
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// NativeAllocationRegistry.java

// 注册 native 内存
registerNativeAllocation(this.size);
// 注销 native 内存
registerNativeFree(size);

// 提示:这一层函数其实就是为了将参数转为long
private static void registerNativeAllocation(long size) {
VMRuntime.getRuntime().registerNativeAllocation((int)Math.min(size, Integer.MAX_VALUE));
}

private static void registerNativeFree(long size) {
VMRuntime.getRuntime().registerNativeFree((int)Math.min(size, Integer.MAX_VALUE));
}

VM注册native内存,比便在内存占用达到界限时触发GC,在该native内存回收时,需要向VM注销该内存量


对比 Android 8.0 之前回收 native 内存的方式

前面我们已经分析完NativeAllocationRegistry的源码了,我们看一看在Android 8.0之前,Bitmap是用什么方法回收native内存的,涉及文件: /frameworks/base/graphics/java/android/graphics/Bitmap.java

  • 共同点:
    • 分配的native层内存需要向VM注册 / 注销
    • 通过一个native层的内存回收函数来回收内存
  • 不同点:
    • NativeAllocationRegistry依赖于sun.misc.Cleaner.java
    • BitmapFinalizer依赖于Object#finalize()

我们知道,finalize()Java对象被垃圾回收时会调用,BitmapFinalizer就是利用了这个机制来回收native层内存的。

1
2
3
4
5
6
7
8
以下是关于 finalize() 方法的一些要点:

finalize() 方法的定义: finalize() 是一个在 Object 类中定义的方法,所有的 Java 类都可以选择覆盖它。它的签名为 protected void finalize() throws Throwable。
垃圾回收触发: 当垃圾回收器决定要回收一个对象时(即对象变得不可达),在回收对象之前,会调用对象的 finalize() 方法。这个方法可能会在对象被回收之前执行一些清理操作,例如释放底层资源或资源的解引用。
finalize() 方法的调用时机不确定: 由于垃圾回收的时机是不确定的,因此 finalize() 方法的调用时机也是不确定的。这意味着不能依赖于它来进行资源的及时释放或其他重要的清理操作。
避免过度依赖 finalize(): 由于 finalize() 方法的调用时机不确定,且可能会影响性能,通常不建议过度依赖它。更好的做法是使用显式的资源管理,例如使用 try-finally 块来确保资源的释放。
不同的垃圾回收方式: Java 使用不同的垃圾回收策略,包括标记-清除、标记-压缩等。在不同的垃圾回收方式中,finalize() 方法的调用时机可能会有所不同。
需要注意的是,自从 Java 9 开始,finalize() 方法已经被标记为废弃(deprecated),这意味着在未来的 Java 版本中可能会被移除。推荐的做法是使用更可靠的资源管理和垃圾回收机制,例如使用 AutoCloseable 接口和 try-with-resources 语句来确保资源的正确释放。

再举几个常用的类在Android 8.0之前的源码为例子,原理都大同小异:Matrix.java (before Android 8.0)Canvas.java (before Android 8.0)

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
// Matrix.java

@Override
protected void finalize() throws Throwable {
try {
finalizer(native_instance);
} finally {
super.finalize();
}
}
private static native void finalizer(long native_instance);

// Canvas.java

private final CanvasFinalizer mFinalizer;
private static final class CanvasFinalizer {
private long mNativeCanvasWrapper;

public CanvasFinalizer(long nativeCanvas) {
mNativeCanvasWrapper = nativeCanvas;
}

@Override
protected void finalize() throws Throwable {
try {
dispose();
} finally {
super.finalize();
}
}

public void dispose() {
if (mNativeCanvasWrapper != 0) {
finalizer(mNativeCanvasWrapper);
mNativeCanvasWrapper = 0;
}
}
}

public Canvas() {
// 省略其他代码...
mFinalizer = new CanvasFinalizer(mNativeCanvasWrapper);
}

问题回归

  • NativeAllocationRegistry利用虚引用感知Java对象被回收的时机,来回收native层内存
  • Android 8.0 (API 27)之前,Android通常使用Object#finalize()调用时机来回收native层内存

Bitmap 的内存复用

Bitmap 绝对是我们 Android 开发中最容易引起 OOM 的对象之一,因为其占用的像素数据内存比较大,而加载图片又是很常见的操作。如果不断反复的去开辟和销毁 Bitmap 数据内存,势必可能会引起应用的内存抖动,因此 Google 的开发者也为我们想了一些办法,那就是允许 Bitmap 内存复用,具体如下:

  • 被复用的 Bitmap 必须为 Mutable(通过 BitmapFactory.Options 设置)

    这是因为复用操作涉及对 Bitmap 对象像素数据的修改,而只有 Mutable 的 Bitmap 才允许对其像素数据进行修改。

  • 4.4 之前

    1. 图像格式: 被复用的 Bitmap 和要解码的图像必须具有相同的像素格式,通常是 ARGB_8888 或 RGB_565 格式。这是因为像素格式的不匹配可能导致解码失败或图像显示异常。
    2. 图像尺寸: 被复用的 Bitmap 和要解码的图像必须具有相同的尺寸。如果尺寸不匹配,复用操作将被忽略。
    3. 图像类型: 在 Android 4.4 之前,被复用的 Bitmap 和要解码的图像必须是 JPEG 或 PNG 格式。其他格式的图像将无法被复用。
    4. inSampleSize 设置: BitmapFactory.OptionsinSampleSize 字段必须设置为 1,以表示不进行图像的缩放。这是因为在进行图像缩放的情况下,图像的尺寸和复用的 Bitmap 尺寸可能不匹配。
  • 4.4 以后

    BitmapFactory.OptionsinBitmap 字段用于指定要复用的 Bitmap 对象。通过设置这个字段,你可以告诉解码器将解码后的图像数据存储在已经存在的 Bitmap 中,以便进行复用。

    同时,要注意以下几点:

    1. 被解码的图像的内存大小不应该超过要复用的 Bitmap 的内存大小。
    2. 被解码的图像和要复用的 Bitmap 的像素格式和尺寸应该匹配。
    3. 解码时需要设置 inSampleSize 为 1,以确保图像不会被缩放。
1
2
3
4
5
6
7
8
9
10
11
12
13
// 不复用的写法,消耗内存 32 M
logMemory();
Bitmap bitmap1 = BitmapFactory.decodeResource(getResources(), R.drawable.test2);
Bitmap bitmap2 = BitmapFactory.decodeResource(getResources(), R.drawable.test2);
logMemory();
// 复用的写法,消耗内存 16 M
logMemory();
BitmapFactory.Options options = new BitmapFactory.Options();
options.inMutable = true;
Bitmap bitmap1 = BitmapFactory.decodeResource(getResources(), R.drawable.test2, options);
options.inBitmap = bitmap1;
Bitmap bitmap2 = BitmapFactory.decodeResource(getResources(), R.drawable.test2, options);
logMemory();

在 Glide 中,Bitmap 复用是通过使用 BitmapPool 接口来实现的。BitmapPool 是一个用于管理和复用 Bitmap 对象的接口,它允许 Glide 在加载和显示图像时复用之前分配的 Bitmap 对象,从而减少内存分配和垃圾回收的开销。

以下是简要的 Glide Bitmap 复用的流程:

  1. Glide 首先会尝试从 BitmapPool 中获取一个可复用的 Bitmap 对象。如果 BitmapPool 中存在满足条件的 Bitmap,就会使用它。
  2. 如果 BitmapPool 中没有可复用的 Bitmap,Glide 会根据要加载的图像的尺寸和像素格式创建一个新的 Bitmap 对象。
  3. 加载和显示图像时,Glide 会将 Bitmap 对象放入 BitmapPool,以便以后可以复用。

为了更深入地了解 Glide 中 Bitmap 复用的实现,你可以查阅 Glide 的源代码,并关注以下关键部分:

  1. BitmapPool 接口及其实现类:Glide 使用 BitmapPool 接口来管理 Bitmap 对象的复用。BitmapPool 接口定义了一系列的方法,如 get() 获取可复用的 Bitmap,put() 将 Bitmap 放回池中等。在 Glide 源代码中,你可以查看 LruBitmapPool 和其他的 BitmapPool 实现类。
  2. BitmapImageViewTarget:Glide 使用 BitmapImageViewTarget 来将 Bitmap 显示在 ImageView 中。在这个类中,你可以看到 Glide 如何从 BitmapPool 中获取可复用的 Bitmap 对象。
  3. 图片加载流程:Glide 的图片加载流程涉及到许多类和接口,包括请求管理、数据加载、资源解码等。在这些类中,你可以找到 Glide 如何利用 Bitmap 复用来优化内存使用。

Glide Bitmap 复用源代码分析

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
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
package com.bumptech.glide.load.engine.bitmap_recycle;

import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.ComponentCallbacks2;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.os.Build;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.bumptech.glide.util.Synthetic;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

/**
* An {@link com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool} implementation that uses an
* {@link com.bumptech.glide.load.engine.bitmap_recycle.LruPoolStrategy} to bucket {@link Bitmap}s
* and then uses an LRU eviction policy to evict {@link android.graphics.Bitmap}s from the least
* recently used bucket in order to keep the pool below a given maximum size limit.
*/
public class LruBitmapPool implements BitmapPool {
private static final String TAG = "LruBitmapPool";
private static final Bitmap.Config DEFAULT_CONFIG = Bitmap.Config.ARGB_8888;

private final LruPoolStrategy strategy;
private final Set<Bitmap.Config> allowedConfigs;
private final long initialMaxSize;
private final BitmapTracker tracker;

private long maxSize;
private long currentSize;
private int hits;
private int misses;
private int puts;
private int evictions;

// Exposed for testing only.
LruBitmapPool(long maxSize, LruPoolStrategy strategy, Set<Bitmap.Config> allowedConfigs) {
this.initialMaxSize = maxSize;
this.maxSize = maxSize;
this.strategy = strategy;
this.allowedConfigs = allowedConfigs;
this.tracker = new NullBitmapTracker();
}

/**
* Constructor for LruBitmapPool.
*
* @param maxSize The initial maximum size of the pool in bytes.
*/
public LruBitmapPool(long maxSize) {
this(maxSize, getDefaultStrategy(), getDefaultAllowedConfigs());
}

/**
* Constructor for LruBitmapPool.
*
* @param maxSize The initial maximum size of the pool in bytes.
* @param allowedConfigs A white listed put of {@link android.graphics.Bitmap.Config} that are
* allowed to be put into the pool. Configs not in the allowed put will be rejected.
*/
// Public API.
@SuppressWarnings("unused")
public LruBitmapPool(long maxSize, Set<Bitmap.Config> allowedConfigs) {
this(maxSize, getDefaultStrategy(), allowedConfigs);
}

/** Returns the number of cache hits for bitmaps in the pool. */
public long hitCount() {
return hits;
}

/** Returns the number of cache misses for bitmaps in the pool. */
public long missCount() {
return misses;
}

/** Returns the number of bitmaps that have been evicted from the pool. */
public long evictionCount() {
return evictions;
}

/** Returns the current size of the pool in bytes. */
public long getCurrentSize() {
return currentSize;
}

@Override
public long getMaxSize() {
return maxSize;
}

@Override
public synchronized void setSizeMultiplier(float sizeMultiplier) {
maxSize = Math.round(initialMaxSize * sizeMultiplier);
evict();
}

@Override
public synchronized void put(Bitmap bitmap) {
if (bitmap == null) {
throw new NullPointerException("Bitmap must not be null");
}
if (bitmap.isRecycled()) {
throw new IllegalStateException("Cannot pool recycled bitmap");
}
if (!bitmap.isMutable()
|| strategy.getSize(bitmap) > maxSize
|| !allowedConfigs.contains(bitmap.getConfig())) {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(
TAG,
"Reject bitmap from pool"
+ ", bitmap: "
+ strategy.logBitmap(bitmap)
+ ", is mutable: "
+ bitmap.isMutable()
+ ", is allowed config: "
+ allowedConfigs.contains(bitmap.getConfig()));
}
bitmap.recycle();
return;
}

final int size = strategy.getSize(bitmap);
strategy.put(bitmap);
tracker.add(bitmap);

puts++;
currentSize += size;

if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "Put bitmap in pool=" + strategy.logBitmap(bitmap));
}
dump();

evict();
}

private void evict() {
trimToSize(maxSize);
}

@Override
@NonNull
public Bitmap get(int width, int height, Bitmap.Config config) {
Bitmap result = getDirtyOrNull(width, height, config);
if (result != null) {
// Bitmaps in the pool contain random data that in some cases must be cleared for an image
// to be rendered correctly. we shouldn't force all consumers to independently erase the
// contents individually, so we do so here. See issue #131.
result.eraseColor(Color.TRANSPARENT);
} else {
result = createBitmap(width, height, config);
}

return result;
}

@NonNull
@Override
public Bitmap getDirty(int width, int height, Bitmap.Config config) {
Bitmap result = getDirtyOrNull(width, height, config);
if (result == null) {
result = createBitmap(width, height, config);
}
return result;
}

@NonNull
private static Bitmap createBitmap(int width, int height, @Nullable Bitmap.Config config) {
return Bitmap.createBitmap(width, height, config != null ? config : DEFAULT_CONFIG);
}

@TargetApi(Build.VERSION_CODES.O)
private static void assertNotHardwareConfig(Bitmap.Config config) {
// Avoid short circuiting on sdk int since it breaks on some versions of Android.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return;
}

if (config == Bitmap.Config.HARDWARE) {
throw new IllegalArgumentException(
"Cannot create a mutable Bitmap with config: "
+ config
+ ". Consider setting Downsampler#ALLOW_HARDWARE_CONFIG to false in your"
+ " RequestOptions and/or in GlideBuilder.setDefaultRequestOptions");
}
}

@Nullable
private synchronized Bitmap getDirtyOrNull(
int width, int height, @Nullable Bitmap.Config config) {
assertNotHardwareConfig(config);
// Config will be null for non public config types, which can lead to transformations naively
// passing in null as the requested config here. See issue #194.
final Bitmap result = strategy.get(width, height, config != null ? config : DEFAULT_CONFIG);
if (result == null) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Missing bitmap=" + strategy.logBitmap(width, height, config));
}
misses++;
} else {
hits++;
currentSize -= strategy.getSize(result);
tracker.remove(result);
normalize(result);
}
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "Get bitmap=" + strategy.logBitmap(width, height, config));
}
dump();

return result;
}

// Setting these two values provides Bitmaps that are essentially equivalent to those returned
// from Bitmap.createBitmap.
private static void normalize(Bitmap bitmap) {
bitmap.setHasAlpha(true);
maybeSetPreMultiplied(bitmap);
}

@TargetApi(Build.VERSION_CODES.KITKAT)
private static void maybeSetPreMultiplied(Bitmap bitmap) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
bitmap.setPremultiplied(true);
}
}

@Override
public void clearMemory() {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "clearMemory");
}
trimToSize(0);
}

@SuppressLint("InlinedApi")
@Override
public void trimMemory(int level) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "trimMemory, level=" + level);
}
if ((level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND)
|| ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
&& (level >= ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN))) {
clearMemory();
} else if ((level >= ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN)
|| (level == ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL)) {
trimToSize(getMaxSize() / 2);
}
}

private synchronized void trimToSize(long size) {
while (currentSize > size) {
final Bitmap removed = strategy.removeLast();
// TODO: This shouldn't ever happen, see #331.
if (removed == null) {
if (Log.isLoggable(TAG, Log.WARN)) {
Log.w(TAG, "Size mismatch, resetting");
dumpUnchecked();
}
currentSize = 0;
return;
}
tracker.remove(removed);
currentSize -= strategy.getSize(removed);
evictions++;
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Evicting bitmap=" + strategy.logBitmap(removed));
}
dump();
removed.recycle();
}
}

private void dump() {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
dumpUnchecked();
}
}

private void dumpUnchecked() {
Log.v(
TAG,
"Hits="
+ hits
+ ", misses="
+ misses
+ ", puts="
+ puts
+ ", evictions="
+ evictions
+ ", currentSize="
+ currentSize
+ ", maxSize="
+ maxSize
+ "\nStrategy="
+ strategy);
}

private static LruPoolStrategy getDefaultStrategy() {
final LruPoolStrategy strategy;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
strategy = new SizeConfigStrategy();
} else {
strategy = new AttributeStrategy();
}
return strategy;
}

@TargetApi(Build.VERSION_CODES.O)
private static Set<Bitmap.Config> getDefaultAllowedConfigs() {
Set<Bitmap.Config> configs = new HashSet<>(Arrays.asList(Bitmap.Config.values()));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
// GIFs, among other types, end up with a native Bitmap config that doesn't map to a java
// config and is treated as null in java code. On KitKat+ these Bitmaps can be reconfigured
// and are suitable for re-use.
configs.add(null);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
configs.remove(Bitmap.Config.HARDWARE);
}
return Collections.unmodifiableSet(configs);
}

private interface BitmapTracker {
void add(Bitmap bitmap);

void remove(Bitmap bitmap);
}

@SuppressWarnings("unused")
// Only used for debugging
private static class ThrowingBitmapTracker implements BitmapTracker {
private final Set<Bitmap> bitmaps = Collections.synchronizedSet(new HashSet<Bitmap>());

@Override
public void add(Bitmap bitmap) {
if (bitmaps.contains(bitmap)) {
throw new IllegalStateException(
"Can't add already added bitmap: "
+ bitmap
+ " ["
+ bitmap.getWidth()
+ "x"
+ bitmap.getHeight()
+ "]");
}
bitmaps.add(bitmap);
}

@Override
public void remove(Bitmap bitmap) {
if (!bitmaps.contains(bitmap)) {
throw new IllegalStateException("Cannot remove bitmap not in tracker");
}
bitmaps.remove(bitmap);
}
}

private static final class NullBitmapTracker implements BitmapTracker {

@Synthetic
NullBitmapTracker() {}

@Override
public void add(Bitmap bitmap) {
// Do nothing.
}

@Override
public void remove(Bitmap bitmap) {
// Do nothing.
}
}
}

下面对代码进行逐段分析:

  1. 导入相关的包和类。
  2. 定义了一个名为 “LruBitmapPool” 的类,实现了 “BitmapPool” 接口,用于管理位图内存池。
  3. 定义了一些常量,如默认的位图配置和日志标签。
  4. 声明了一些成员变量,包括内存池策略、允许的位图配置、最大大小、当前大小、命中次数、未命中次数、添加次数和驱逐次数等。
  5. 构造函数,初始化内存池大小、策略和允许的位图配置等。
  6. 提供了一些公开方法用于获取统计信息,如命中次数、未命中次数、驱逐次数、当前大小等。
  7. 实现了 “BitmapPool” 接口中的一些方法,包括 setSizeMultiplier、put、get、getDirty、clearMemory、trimMemory 等。
  8. 定义了一些辅助方法,如 evict、assertNotHardwareConfig、normalize、maybeSetPreMultiplied、getDirtyOrNull、trimToSize、dump 和 dumpUnchecked 等。
  9. 定义了 “BitmapTracker” 接口,其中包含了添加和移除位图的方法。
  10. 定义了一些实现了 “BitmapTracker” 接口的类,如 “ThrowingBitmapTracker” 和 “NullBitmapTracker”。

总体来说,这段代码实现了一个基于 LRU(Least Recently Used)算法的位图内存池,用于管理位图对象的内存分配和回收。它可以跟踪位图的使用情况,限制内存池的大小,通过策略进行位图的管理和驱逐,以及提供一些统计信息用于优化和调试。

Gilde 是如何实现 Bitmap 复用的?

在这段代码中,实现位图复用的核心思想是通过内存池策略(LruPoolStrategy)来管理位图的存储和获取。下面我将详细解释如何实现位图的复用:

  1. put(Bitmap bitmap) 方法:
    当你想要回收一个位图时,会调用这个方法。它会首先判断位图是否为空、是否已经被回收、是否是可变的位图,位图大小是否超过了内存池的最大大小,以及位图的配置是否允许被放入内存池。如果这些条件都满足,它会将位图放入内存池策略中,并更新当前内存池的大小。

  2. get(int width, int height, Bitmap.Config config)getDirty(int width, int height, Bitmap.Config config) 方法:
    当你想要获取一个位图时,会调用这两个方法之一。它首先会尝试从内存池策略中获取满足条件的位图,如果找到则返回该位图,否则会创建一个新的位图并返回。这里的位图创建使用了 Bitmap.createBitmap() 方法,确保了返回的位图是可复用的。

  3. trimToSize(long size) 方法:
    当内存池的当前大小超过了最大限制时,会调用这个方法进行驱逐。它会循环地从内存池策略中移除最近未使用的位图,直到当前内存池大小小于等于指定的大小。

  4. LruPoolStrategy 接口的实现:
    这个接口定义了位图内存池策略的规范,包括位图的添加、获取、移除等操作。在这段代码中,有两个实现类:SizeConfigStrategyAttributeStrategy,分别在不同的 Android 版本上进行位图的管理。

通过这些机制,当位图被回收时,它会被添加到内存池中,下次需要位图时会优先从内存池中获取,以达到位图复用的效果。这样可以减少内存分配和回收的开销,提高性能和效率。


BitMap 创建,内存申请与内存模型相关内容整理参考自

作者:红橙Darren
链接:https://www.jianshu.com/p/8e8ad414237e
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

具体回收细节参考自

版权声明:本文为CSDN博主「shihongyu12345」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/shihongyu12345/article/details/89681948

NativeAllocationRegistry 的原理与设计思想 摘录自

作者:彭旭锐
链接:https://www.jianshu.com/p/6f042f9e47a8
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

部分相关知识来自于 chat-GPT 3.5

,