• 面试:Bitmap像素内存分配在堆内存还是在native中


    今天面试遇到同学说做过内存优化,于是我一般都会问那 Bitmap 的像素内存存在哪?大多数同学都回答在 java heap 里面,就比较尴尬,理论上你做内存优化,如果连图片这个内存大户内存存在哪都不清楚,实在不太能说得过去。

    Bitmap可以说是安卓里面最常见的内存消耗大户了,我们开发过程中遇到的oom问题很多都是由它引发的。谷歌官方也一直在迭代它的像素内存管理策略。从 Android 2.3.3以前的分配在native上,到2.3 - 7.1之间的分配在java堆上,到8.0之后又回到native上。几度变迁,它的回收方法也在跟着变化。

    Android 2.3.3以前

    2.3.3以前Bitmap的像素内存是分配在natvie上,而且不确定什么时候会被回收。根据官方文档的说法我们需要手动调用Bitmap.recycle()去回收:

    https://developer.android.com/topic/performance/graphics/manage-memory

    在 Android 2.3.3(API 级别 10)及更低版本上,位图的后备像素数据存储在本地内存中。它与存储在 Dalvik 堆中的位图本身是分开的。本地内存中的像素数据并不以可预测的方式释放,可能会导致应用短暂超出其内存限制并崩溃。

    在 Android 2.3.3(API 级别 10)及更低版本上,建议使用 recycle()。如果您在应用中显示大量位图数据,则可能会遇到 OutOfMemoryError 错误。利用 recycle() 方法,应用可以尽快回收内存。

    注意:只有当您确定位图已不再使用时才应该使用 recycle()。如果您调用 recycle() 并在稍后尝试绘制位图,则会收到错误:"Canvas: trying to use a recycled bitmap"。

    2.Android 3.0~Android 7.1

    虽然3.0~7.1的版本Bitmp的像素内存是分配在java堆上的,但是实际是在natvie层进行decode的,而且会在native层创建一个c++的对象和java层的Bitmap对象进行关联。

    从BitmapFactory的源码我们可以看到它一路调用到nativeDecodeStream这个native方法:

    1. // BitmapFactory.java
    2. public static Bitmap decodeFile(String pathName, Options opts) {
    3.     ...
    4.     stream = new FileInputStream(pathName);
    5.     bm = decodeStream(stream, null, opts);
    6.     ...
    7.     return bm;
    8. }
    9. public static Bitmap decodeStream(InputStream is, Rect outPadding, Options opts) {
    10.     ...
    11.     bm = decodeStreamInternal(is, outPadding, opts);
    12.     ...
    13.     return bm;
    14. }
    15. private static Bitmap decodeStreamInternal(InputStream is, Rect outPadding, Options opts) {
    16.     ...
    17.     return nativeDecodeStream(is, tempStorage, outPadding, opts);
    18. }

    nativeDecodeStream实际上会通过jni创建java堆的内存,然后读取io流解码图片将像素数据存到这个java堆内存里面:

    1. // BitmapFactory.cpp
    2. static jobject nativeDecodeStream(JNIEnv* env, jobject clazz, jobject is, jbyteArray storage,
    3.         jobject padding, jobject options) {
    4.     ...
    5.     bitmap = doDecode(env, bufferedStream, padding, options);
    6.     ...
    7.     return bitmap;
    8. }
    9. static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding, jobject options) {
    10.     ...
    11.     // outputAllocator是像素内存的分配器,会在java堆上创建内存给像素数据,可以通过BitmapFactory.Options.inBitmap复用前一个bitmap像素内存
    12.     SkBitmap::Allocator* outputAllocator = (javaBitmap != NULL) ?
    13.             (SkBitmap::Allocator*)&recyclingAllocator : (SkBitmap::Allocator*)&javaAllocator;
    14.     ...
    15.     // 将内存分配器设置给解码器
    16.     decoder->setAllocator(outputAllocator);
    17.     ...
    18.     //解码
    19.     if (decoder->decode(stream, &decodingBitmap, prefColorType, decodeMode)
    20.                 != SkImageDecoder::kSuccess) {
    21.         return nullObjectReturn("decoder->decode returned false");
    22.     }
    23.     ...
    24.     return GraphicsJNI::createBitmap(env, javaAllocator.getStorageObjAndReset(),
    25.             bitmapCreateFlags, ninePatchChunk, ninePatchInsets, -1);
    26. }
    27. // Graphics.cpp
    28. jobject GraphicsJNI::createBitmap(JNIEnv* env, android::Bitmap* bitmap,
    29.         int bitmapCreateFlags, jbyteArray ninePatchChunk, jobject ninePatchInsets,
    30.         int density) {
    31.     // java层的Bitmap对象实际上是natvie层new出来的
    32.     // native层也会创建一个android::Bitmap对象与java层的Bitmap对象绑定
    33.     // bitmap->javaByteArray()代码bitmap的像素数据其实是存在java层的byte数组中
    34.     jobject obj = env->NewObject(gBitmap_class, gBitmap_constructorMethodID,
    35.             reinterpret_cast<jlong>(bitmap), bitmap->javaByteArray(),
    36.             bitmap->width(), bitmap->height(), density, isMutable, isPremultiplied,
    37.             ninePatchChunk, ninePatchInsets);
    38.     ...
    39.     return obj;
    40. }

    我们可以看最后会调用javaAllocator.getStorageObjAndReset()创建一个android::Bitmap类型的native层Bitmap对象,然后通过jni调用java层的Bitmap构造函数去创建java层的Bitmap对象,同时将native层的Bitmap对象保存到mNativePtr:

    1. // Bitmap.java
    2. // Convenience for JNI access
    3. private final long mNativePtr;
    4. /**
    5.  * Private constructor that must received an already allocated native bitmap
    6.  * int (pointer).
    7.  */
    8. // called from JNI
    9. Bitmap(long nativeBitmap, byte[] buffer, int width, int height, int density,
    10.         boolean isMutable, boolean requestPremultiplied,
    11.         byte[] ninePatchChunk, NinePatch.InsetStruct ninePatchInsets) {
    12.     ...
    13.     mNativePtr = nativeBitmap;
    14.     ...
    15. }

    从上面的源码我们也能看出来,Bitmap的像素是存在java堆的,所以如果bitmap没有人使用了,垃圾回收器就能自动回收这块的内存,但是在native创建出来的nativeBitmap要怎么回收呢?从Bitmap的源码我们可以看到在Bitmap构造函数里面还会创建一个BitmapFinalizer去管理nativeBitmap:

    1. /**
    2.  * Private constructor that must received an already allocated native bitmap
    3.  * int (pointer).
    4.  */
    5. // called from JNI
    6. Bitmap(long nativeBitmap, byte[] buffer, int width, int height, int density,
    7.         boolean isMutable, boolean requestPremultiplied,
    8.         byte[] ninePatchChunk, NinePatch.InsetStruct ninePatchInsets) {
    9.     ...
    10.     mNativePtr = nativeBitmap;
    11.     mFinalizer = new BitmapFinalizer(nativeBitmap);
    12.     ...
    13. }

    BitmapFinalizer的原理十分简单。Bitmap对象被销毁的时候BitmapFinalizer也会同步被销毁,然后就可以在BitmapFinalizer.finalize()里面销毁native层的nativeBitmap:

    1. private static class BitmapFinalizer {
    2.     private long mNativeBitmap;
    3.     ...
    4.     BitmapFinalizer(long nativeBitmap) {
    5.         mNativeBitmap = nativeBitmap;
    6.     }
    7.     ...
    8.     @Override
    9.     public void finalize() {
    10.         try {
    11.             super.finalize();
    12.         } catch (Throwable t) {
    13.             // Ignore
    14.         } finally {
    15.             setNativeAllocationByteCount(0);
    16.             nativeDestructor(mNativeBitmap);
    17.             mNativeBitmap = 0;
    18.         }
    19.     }
    20. }

    3.Android 8.0之后

    8.0以后像素内存又被放回了native上,所以依然需要在java层的Bitmap对象回收之后同步回收native的内存。

    虽然BitmapFinalizer同样可以实现,但是Java的finalize方法实际上是不推荐使用的,所以谷歌也换了NativeAllocationRegistry去实现:

    1. /**
    2.  * Private constructor that must received an already allocated native bitmap
    3.  * int (pointer).
    4.  */
    5. // called from JNI
    6. Bitmap(long nativeBitmap, int width, int height, int density,
    7.         boolean isMutable, boolean requestPremultiplied,
    8.     ...
    9.     mNativePtr = nativeBitmap;
    10.     long nativeSize = NATIVE_ALLOCATION_SIZE + getAllocationByteCount();
    11.     NativeAllocationRegistry registry = new NativeAllocationRegistry(
    12.         Bitmap.class.getClassLoader(), nativeGetNativeFinalizer(), nativeSize);
    13.     registry.registerNativeAllocation(this, nativeBitmap);
    14. }

    locationRegistry底层实际上使用了sun.misc.Cleaner,可以为对象注册一个清理的Runnable。当对象内存被回收的时候jvm就会调用它。

    1. import sun.misc.Cleaner;
    2. public Runnable registerNativeAllocation(Object referent, Allocator allocator) {
    3.     ...
    4.     CleanerThunk thunk = new CleanerThunk();
    5.     Cleaner cleaner = Cleaner.create(referent, thunk);
    6.     ..
    7. }
    8. private class CleanerThunk implements Runnable {
    9.     ...
    10.     public void run() {
    11.         if (nativePtr != 0) {
    12.             applyFreeFunction(freeFunction, nativePtr);
    13.         }
    14.         registerNativeFree(size);
    15.     }
    16.     ...
    17. }

    这个Cleaner的原理也很暴力,首先它是一个虚引用,registerNativeAllocation实际上创建了一个Bitmap的虚引用:

    1. // Cleaner.java
    2. public class Cleaner extends PhantomReference {
    3.     ...
    4.     public static Cleaner create(Object ob, Runnable thunk) {
    5.         ...
    6.         return add(new Cleaner(ob, thunk));
    7.     }
    8.     ...
    9.     private Cleaner(Object referent, Runnable thunk) {
    10.         super(referent, dummyQueue);
    11.         this.thunk = thunk;
    12.     }
    13.     ...
    14.     public void clean() {
    15.         ...
    16.         thunk.run();
    17.         ...
    18.     }
    19.     ...
    20. }

    虚引用的话我们都知道需要配合一个ReferenceQueue使用,当对象的引用被回收的时候,jvm就会将这个虚引用丢到ReferenceQueue里面。而ReferenceQueue在插入的时候居然通过instanceof判断了下是不是Cleaner:

    1. // ReferenceQueue.java
    2. private boolean enqueueLocked(Reference<? extends T> r) {
    3.     ...
    4.     if (r instanceof Cleaner) {
    5.         Cleaner cl = (sun.misc.Cleaner) r;
    6.         cl.clean();
    7.         ...
    8.     }
    9.     ...
    10. }

    也就是说Bitmap对象被回收,就会触发Cleaner这个虚引用被丢入ReferenceQueue,而ReferenceQueue里面会判断丢进来的虚引用是不是Cleaner,如果是就调用Cleaner.clean()方法。而clean方法内部就会再去执行我们注册的清理的Runnable。

  • 相关阅读:
    RHEL 6入门之使用 ntsysv 、chkconfig 管理服务
    FeSoG论文笔记
    深入理解Java消息中间件-ActiveMQ
    linux学习-文件搜索
    Qt字符串类应用与常用基本数据类型
    迪拜推出国家元宇宙战略
    嵌入式开发环境之系统烧录
    sklearn快速入门教程:处理分类型数据
    汇编原理 | 二进制、跳转指令、算数运算、
    2023年上半年软考中级数据库系统工程师如何高效备考?难吗?
  • 原文地址:https://blog.csdn.net/cpcpcp123/article/details/125409904