• Android O Bitmap 内存分配


      我们知道,一般认为在Android进程的内存模型中,heap分为两部分,一部分是native heap,一部分是Dalvik heap(实际上也是native heap的一部分)。

      Android Bitmap 是一个比较特殊的类,用来加载图片的,而图片的数据部分一般较大,因此在创建Bitmap对象时,Android system 采用的策略是将其分为两个部分,一个是基本信息(如宽度),一个是像素点数据。前者会保存在Dalvik heap中,也就是Bitmap对象所指的空间,后者会单独放一个内存空间里,按照不同的Android系统版本,会放在不同的heap中。

      我们先引用一段Android官方的说法:链接

    On Android 2.3.3 (API level 10) and lower, the backing pixel data for a bitmap is stored in native memory. It is separate from the bitmap itself, which is stored in the Dalvik heap. The pixel data in native memory is not released in a predictable manner, potentially causing an application to briefly exceed its memory limits and crash. As of Android 3.0 (API level 11), the pixel data is stored on the Dalvik heap along with the associated bitmap.

      Android 2.3.3及以前版本,像素点数据是保存在native memory,而bitmap对象是保存在Dalvik heap. 从Android 3.0开始,像素点数据与bitmap对象一起存储在Dalvik heap中。

      但其实按目前来看,官方的说法并不全面,可能是未能及时更新。问题起源于我在项目里做的一个功能。该功能会创建若干个中间Bitmap对象,这些对象都是局部变量,并且在使用过一次之后就不会再用到。但bitmap占用的空间较大,需要考虑到内存问题,其自身提供了recycle方法,每次用完后是否需要主动调用该方法呢?我想这是个问题,所以需要验证下没调用recycle方法会不会导致内存泄露。

      于是我使用MAT来观察内存的使用情况。发现在GC后,没能找到这几个中间bitmap对象的引用,但由于在验证的时候,会有一个其它界面会创建较多的bitmap,我担心会影响我的排查。于是写了个demo验证官方的说法。按道理,我们的应用是基于Android O开发的,应该是符合官网说的“像素点数据与bitmap对象一起存储在Dalvik heap中”, 而且局部变量会很快地被回收,理论上不应该有内存泄露。

    demo1

        void load() {
            for (int i = 0; i < 100; i++) {
                Bitmap bitmaps = BitmapFactory.decodeFile(path);
            }
        }
    

      通过AS3.0的Android Profiler观察,发现情况有些出乎意料。

      代码中重复加载了100次的图片,这个图片的源文件大小大概3MB多,100次循环后,Native 竟然飙升到1.26GB, 应用正常运行,并不会OOM,而Java Heap基本上没变,大概是3M多,由于显示的单位切换成了GB,Java那一栏只能显示到小数点后1位,因此3MB最后显示出来是0。

      为了解开这个出乎意料的结果,我们需要从源码找答案。

      跟踪BitmapFactory.decodeFile(path)方法,最后会调用到nativeDecodeStream方法,该方法对应BitmapFactory.cpp文件中的nativeDecodeStream函数。

    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;
    }
    

      然后再调用 doDecode函数,由于该函数的代码非常长,我这里只贴出与本文相关的比较重要的代码。

    static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding, jobject options) {
        HeapAllocator defaultAllocator;
        RecyclingPixelAllocator recyclingAllocator(reuseBitmap, existingBufferSize);
        ScaleCheckingAllocator scaleCheckingAllocator(scale, existingBufferSize);
        SkBitmap::HeapAllocator heapAllocator;
        SkBitmap::Allocator* decodeAllocator;
        if (javaBitmap != nullptr && willScale) {
            decodeAllocator = &scaleCheckingAllocator;
        } else if (javaBitmap != nullptr) {
            decodeAllocator = &recyclingAllocator;
        } else if (willScale || isHardware) {
            decodeAllocator = &heapAllocator;
        } else {
            decodeAllocator = &defaultAllocator;
        }
    
        SkBitmap decodingBitmap;
        if (!decodingBitmap.setInfo(bitmapInfo) ||
                !decodingBitmap.tryAllocPixels(decodeAllocator, colorTable.get())) {
            return nullptr;
        }
       
        return bitmap::createBitmap(env, defaultAllocator.getStorageObjAndReset(),
                bitmapCreateFlags, ninePatchChunk, ninePatchInsets, -1);
    }
    

      可见,通过tryAllocPixels尝试分配空间,默认采用的是defaultAllocator内存分配器,它的类型是HeapAllocator。

      decodingBitmap.tryAllocPixels函数实际会调用defaultAllocator->allocPixelRef,该函数代码如下

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

      只是简单的调用了android::Bitmap::allocateHeapBitmap,而这个函数是在另一个库下面的(frameworks/base/libs/hwui/hwui/Bitmap.cpp,找了很久才找到)

    static sk_sp<Bitmap> allocateHeapBitmap(size_t size, const SkImageInfo& info, size_t rowBytes,
            SkColorTable* ctable) {
        void* addr = calloc(size, 1);
        if (!addr) {
            return nullptr;
        }
        return sk_sp<Bitmap>(new Bitmap(addr, size, info, rowBytes, ctable));
    }
    

      最终调用的是calloc函数,该函数和malloc是类似,都是直接在native heap上分配空间,返回地址。

      所以结论是:Android O上通过BitmapFactory.decodeFile方法创建的Bitmap,其中的像素点数据集默认在native heap上分配的。

      但是官方为什么会说“像素点数据与bitmap对象一起存储在Dalvik heap中”,我想可能是Android O 改了,然后未及时更新这段文字,因此我们基于Android N再来验证一下。

      同样使用demo1的代码,在Android N(7.1.1)的机器上运行,得到如下结果:

      看起来正常了,符合官方说法,为了确定Android O确实修改了分配Bitmap内存的相关代码,我们来看看Android N的源码。

      BitmapFactory.decode函数。

    static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding, jobject options) {
        JavaPixelAllocator javaAllocator(env);
        RecyclingPixelAllocator recyclingAllocator(reuseBitmap, existingBufferSize);
        ScaleCheckingAllocator scaleCheckingAllocator(scale, existingBufferSize);
        SkBitmap::HeapAllocator heapAllocator;
        SkBitmap::Allocator* decodeAllocator;
        if (javaBitmap != nullptr && willScale) {
            decodeAllocator = &scaleCheckingAllocator;
        } else if (javaBitmap != nullptr) {
            decodeAllocator = &recyclingAllocator;
        } else if (willScale) {
            decodeAllocator = &heapAllocator;
        } else {
            decodeAllocator = &javaAllocator;
        }
        
        SkBitmap decodingBitmap;
        if (!decodingBitmap.setInfo(bitmapInfo) ||
                !decodingBitmap.tryAllocPixels(decodeAllocator, colorTable)) {
            return nullptr;
        }
    
        return GraphicsJNI::createBitmap(env, javaAllocator.getStorageObjAndReset(),
                bitmapCreateFlags, ninePatchChunk, ninePatchInsets, -1);
    }
    

      我们看到默认使用的分配器是JavaPixelAllocator,官方对这个分配器的解释如下,其实已经说得很清楚了,这个分配器就是在java heap中进行内存分配。

    /** Allocator which allocates the backing buffer in the Java heap.

    • Instances can only be used to perform a single allocation, which helps
    • ensure that the allocated buffer is properly accounted for with a
    • reference in the heap (or a JNI global reference).
      */

      接着看JavaPixelAllocator::allocPixelRef。

    bool JavaPixelAllocator::allocPixelRef(SkBitmap* bitmap, SkColorTable* ctable) {
        JNIEnv* env = vm2env(mJavaVM);
    
        mStorage = GraphicsJNI::allocateJavaPixelRef(env, bitmap, ctable);
        return mStorage != nullptr;
    }
    

      再看GraphicsJNI::allocateJavaPixelRef。

    android::Bitmap* GraphicsJNI::allocateJavaPixelRef(JNIEnv* env, SkBitmap* bitmap,
                                                 SkColorTable* ctable) {
        const size_t rowBytes = bitmap->rowBytes();
    
        jbyteArray arrayObj = (jbyteArray) env->CallObjectMethod(gVMRuntime,
                                                                 gVMRuntime_newNonMovableArray,
                                                                 gByte_class, size);
    
        jbyte* addr = (jbyte*) env->CallLongMethod(gVMRuntime, gVMRuntime_addressOf, arrayObj);
        if (env->ExceptionCheck() != 0) {
            return NULL;
        }
    
        android::Bitmap* wrapper = new android::Bitmap(env, arrayObj, (void*) addr,
                info, rowBytes, ctable);
        wrapper->getSkBitmap(bitmap);
        bitmap->lockPixels();
        return wrapper;
    }
    

      我们看到,实际是通过java层进行内存分配,调用了gVMRuntime的gVMRuntime_newNonMovableArray,得到一个字节数组,再调用gVMRuntime_addressOf得到这个数组的地址,然后将地址作为android::Bitmat构造函数参数创建android::Bitma对象,返回该对象。实际上java层的Bitmap对象会有一个long型成员变量保存native的这个Bitmap对象的引用。接着看下具体调用哪个方法。

        c = env->FindClass("java/lang/Byte");
        gByte_class = (jclass) env->NewGlobalRef(
            env->GetStaticObjectField(c, env->GetStaticFieldID(c, "TYPE", "Ljava/lang/Class;")));
    
        gVMRuntime_class = make_globalref(env, "dalvik/system/VMRuntime");
        m = env->GetStaticMethodID(gVMRuntime_class, "getRuntime", "()Ldalvik/system/VMRuntime;");
        gVMRuntime = env->NewGlobalRef(env->CallStaticObjectMethod(gVMRuntime_class, m));
        gVMRuntime_newNonMovableArray = env->GetMethodID(gVMRuntime_class, "newNonMovableArray",
                                                         "(Ljava/lang/Class;I)Ljava/lang/Object;");
        gVMRuntime_addressOf = env->GetMethodID(gVMRuntime_class, "addressOf", "(Ljava/lang/Object;)J");
    

      通过java层的dalvik/system/VMRuntime类的静态方法getRuntime获取一个VMRuntime的实例gVMRuntime,然后调用newNonMovableArray方法获取一个字节数组,最后调用addressOf获取这个字节数组第1个元素(array[0])的地址。实际上newNonMovableArray方法最终也是要调用native方法进行内存分配的,具体调用的是dalvik_system_VMRuntime::VMRuntime_newNonMovableArray函数。最后会通过heap实例,分配一个内存。前面提到,dalvik heap也是native heap的一部分。是因为在启动dalvik vm的时候,会预先在native heap中分配一段内存作为dalvik heap使用,后续java层如果需要请求内存,都会在这个dalvik heap中进行分配,如果dalvik heap空间不够,就先进行GC,GC后如果还不够就会再分配一个更大的空间,如果已经达到上限,就会抛出OOM异常。

      Android N 上Bitmap的像素点数据与bitmap对象都是分配到dalvik heap,而Android O 上Bitmap的像素点数据是分配在native heap中,因此在Android O加载大量的Bitmap并不会导致应用OOM,但是有一点要注意,android O对应用native使用的空间也做了限制(不确定是O新增的还是原来就有),当应用占用的native空间到一定程度时(我本地验证是1.26G),再调用BitmapFactory.decodeFile()方法时,会直接返回null。所以Android O对Bitmap内存分配进行了更新,这对开发者来说其实不影响。在需要加载大量Bitmap的时候,该优化还是要优化,该缓存还是要缓存。只是对于某些将Bitmap通过JNI方式直接在native请求空间的优化方案来说,就失去意义了。

  • 相关阅读:
    API
    Object constructor
    function()
    For语句的衍生对象
    编程语言发展史
    将Paul替换成Ringo
    Document.write和 getElementById(ID)
    姓名,电话号码,邮箱的正则检测
    JavaScript-BOM与DOM
    管理心得
  • 原文地址:https://www.cnblogs.com/xiaji5572/p/7794083.html
Copyright © 2020-2023  润新知