Bitmap memory recycling mechanism

Posted by WBSKI on Tue, 08 Mar 2022 04:01:59 +0100

The official has also changed this knowledge point twice

Bitmap can be said to be the most common memory consumer in Android. Many oom problems we encounter in the development process are caused by it. Google officials have also been iterating over its pixel memory management strategy. From Android 2.3.3 to 2.3-7.1 on the java heap, and back to native after 8.0. After several changes, its recycling methods are also changing.

Before Android 2.3.3

Before 2.3.3, the pixel memory of Bitmap was allocated on natvie, and it was uncertain when it would be recycled. according to Official documents We need to call bitmap manually Recycle() to recycle:

On Android 2.3.3 (API level 10) and earlier, the backup pixel data of bitmap is stored in local memory. It is separate from the bitmap itself stored in the Dalvik heap. Pixel data in local memory is not released in a predictable way, which may cause the application to briefly exceed its memory limit and crash.

On Android 2.3.3 (API level 10) and lower, recycle() is recommended. If you display a large amount of bitmap data in your application, you may encounter OutOfMemoryError error. Using the recycle() method, the application can reclaim memory as soon as possible.

Note: recycle() should only be used if you are sure that the bitmap is no longer in use. If you call recycle() and try to draw a bitmap later, you will receive an error: "Canvas: trying to use a recycled bitmap".

Android 3.0~Android 7.1

Although the pixel memory of Bitmp in versions 3.0 ~ 7.1 is allocated on the java heap, it is actually decode d in the natvie layer, and a c + + object will be created in the native layer to associate with the Bitmap object in the java layer.

We can see the way from the source code of bitmapdecivedecode to bitmapstream:

// BitmapFactory.java
public static Bitmap decodeFile(String pathName, Options opts) {
    ...
    stream = new FileInputStream(pathName);
    bm = decodeStream(stream, null, opts);
    ...
    return bm;
}

public static Bitmap decodeStream(InputStream is, Rect outPadding, Options opts) {
    ...
    bm = decodeStreamInternal(is, outPadding, opts);
    ...
    return bm;
}

private static Bitmap decodeStreamInternal(InputStream is, Rect outPadding, Options opts) {
    ...
    return nativeDecodeStream(is, tempStorage, outPadding, opts);
}

nativeDecodeStream will actually create the memory of java heap through jni, then read the decoded picture of io stream and save the pixel data into the memory of java heap:

// BitmapFactory.cpp
static jobject nativeDecodeStream(JNIEnv* env, jobject clazz, jobject is, jbyteArray storage,
        jobject padding, jobject options) {
    ...
    bitmap = doDecode(env, bufferedStream, padding, options);
    ...
    return bitmap;
}

static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding, jobject options) {
    ...
    // Outputalocator is the allocator of pixel memory. It will create memory on the java heap for pixel data, which can be accessed through bitmapfactory Options. Inbitmap multiplexes the memory of the previous bitmap pixel
    SkBitmap::Allocator* outputAllocator = (javaBitmap != NULL) ?
            (SkBitmap::Allocator*)&recyclingAllocator : (SkBitmap::Allocator*)&javaAllocator;
    ...
    // Set the memory allocator to the decoder
    decoder->setAllocator(outputAllocator);
    ...
    //decode
    if (decoder->decode(stream, &decodingBitmap, prefColorType, decodeMode)
                != SkImageDecoder::kSuccess) {
        return nullObjectReturn("decoder->decode returned false");
    }
    ...
    return GraphicsJNI::createBitmap(env, javaAllocator.getStorageObjAndReset(),
            bitmapCreateFlags, ninePatchChunk, ninePatchInsets, -1);
}

// Graphics.cpp
jobject GraphicsJNI::createBitmap(JNIEnv* env, android::Bitmap* bitmap,
        int bitmapCreateFlags, jbyteArray ninePatchChunk, jobject ninePatchInsets,
        int density) {

    // The Bitmap object of the java layer is actually generated from the natvie layer new
    // The native layer will also create an android::Bitmap object to bind with the Bitmap object of the java layer
    // Bitmap - > javabytearray() code the pixel data of bitmap actually exists in the byte array of java layer
    jobject obj = env->NewObject(gBitmap_class, gBitmap_constructorMethodID,
            reinterpret_cast<jlong>(bitmap), bitmap->javaByteArray(),
            bitmap->width(), bitmap->height(), density, isMutable, isPremultiplied,
            ninePatchChunk, ninePatchInsets);
    ...
    return obj;
}

We can see that javaallocator will be called in the end Getstorageobjiandreset() creates a native layer Bitmap object of android::Bitmap type, then calls the java layer Bitmap constructor through jni to create the java layer Bitmap object, and saves the native layer Bitmap object to mnateptr:

// Bitmap.java
// Convenience for JNI access
private final long mNativePtr;

/**
 * Private constructor that must received an already allocated native bitmap
 * int (pointer).
 */
// called from JNI
Bitmap(long nativeBitmap, byte[] buffer, int width, int height, int density,
        boolean isMutable, boolean requestPremultiplied,
        byte[] ninePatchChunk, NinePatch.InsetStruct ninePatchInsets) {
    ...
    mNativePtr = nativeBitmap;
    ...
}

From the above source code, we can also see that the pixels of bitmap are stored in java heap, so if no one uses the bitmap, the garbage collector can automatically recycle this memory, but how to recycle the nativeBitmap created in nativeBitmap? From the source code of bitmap, we can see that a BitmapFinalizer will be created in the bitmap constructor to manage the nativeBitmap:

/**
 * Private constructor that must received an already allocated native bitmap
 * int (pointer).
 */
// called from JNI
Bitmap(long nativeBitmap, byte[] buffer, int width, int height, int density,
        boolean isMutable, boolean requestPremultiplied,
        byte[] ninePatchChunk, NinePatch.InsetStruct ninePatchInsets) {
    ...
    mNativePtr = nativeBitmap;
    mFinalizer = new BitmapFinalizer(nativeBitmap);
    ...
}

The principle of BitmapFinalizer is very simple. When the Bitmap object is destroyed, the BitmapFinalizer will also be destroyed synchronously, and then it can be destroyed in BitmapFinalizer Destroy the nativeBitmap of the native layer in finalize():

private static class BitmapFinalizer {
    private long mNativeBitmap;
    ...
    BitmapFinalizer(long nativeBitmap) {
        mNativeBitmap = nativeBitmap;
    }
    ...
    @Override
    public void finalize() {
        try {
            super.finalize();
        } catch (Throwable t) {
            // Ignore
        } finally {
            setNativeAllocationByteCount(0);
            nativeDestructor(mNativeBitmap);
            mNativeBitmap = 0;
        }
    }
}

After Android 8.0

Since 8.0, the pixel memory has been put back on the native, so it is still necessary to recycle the native memory synchronously after the Bitmap object in the java layer is recycled.

Although bitmap finalize r can also be implemented, the finalization method of Java is actually not recommended, so Google also changed the nativeallocation registry to implement it:

/**
 * 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,
    ...
    mNativePtr = nativeBitmap;
    long nativeSize = NATIVE_ALLOCATION_SIZE + getAllocationByteCount();
    NativeAllocationRegistry registry = new NativeAllocationRegistry(
        Bitmap.class.getClassLoader(), nativeGetNativeFinalizer(), nativeSize);
    registry.registerNativeAllocation(this, nativeBitmap);
}

The underlying layer of native allocation registry actually uses sun misc. Cleaner, you can register a cleaned Runnable for the object. When the object memory is reclaimed, the jvm will call it.

import sun.misc.Cleaner;

public Runnable registerNativeAllocation(Object referent, Allocator allocator) {
    ...
    CleanerThunk thunk = new CleanerThunk();
    Cleaner cleaner = Cleaner.create(referent, thunk);
    ..
}

private class CleanerThunk implements Runnable {
    ...
    public void run() {
        if (nativePtr != 0) {
            applyFreeFunction(freeFunction, nativePtr);
        }
        registerNativeFree(size);
    }
    ...
}

The principle of this Cleaner is also very violent. First, it is a virtual reference. registerNativeAllocation actually creates a virtual reference of Bitmap:

// Cleaner.java
public class Cleaner extends PhantomReference {
    ...
    public static Cleaner create(Object ob, Runnable thunk) {
        ...
        return add(new Cleaner(ob, thunk));
    }
    ...
    private Cleaner(Object referent, Runnable thunk) {
        super(referent, dummyQueue);
        this.thunk = thunk;
    }
    ...
    public void clean() {
        ...
        thunk.run();
        ...
    }
    ...
}

We all know that the virtual reference needs to be used with a ReferenceQueue. When the reference of the object is recycled, the jvm will throw the virtual reference into the ReferenceQueue. When inserting, ReferenceQueue judges whether it is a Cleaner through instanceof:

// ReferenceQueue.java
private boolean enqueueLocked(Reference<? extends T> r) {
    ...
    if (r instanceof Cleaner) {
        Cleaner cl = (sun.misc.Cleaner) r;
        cl.clean();
        ...
    }
    ...
}

In other words, when the Bitmap object is recycled, the Cleaner will be triggered, and the virtual reference will be thrown into the ReferenceQueue. In the ReferenceQueue, it will judge whether the dropped virtual reference is a Cleaner. If so, it will call Cleaner Clean() method. In the clean method, we will execute the registered clean Runnable.



Author: Jia WeiLuo
Link: https://www.jianshu.com/p/5c776c01c204
Source: Jianshu
The copyright belongs to the author. For commercial reprint, please contact the author for authorization. For non-commercial reprint, please indicate the source.

Topics: Java Android Android Studio