我们知道,一般认为在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 stream(CreateJavaInputStreamAdaptor(env, is, storage));

if (stream.get()) {

std::unique_ptr 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 allocateHeapBitmap(size_t size, const SkImageInfo& info, size_t rowBytes,

SkColorTable* ctable) {

void* addr = calloc(size, 1);

if (!addr) {

return nullptr;

}

return sk_sp(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请求空间的优化方案来说,就失去意义了。

java map 内存分配_Android O Bitmap 内存分配相关推荐

  1. android bitmap 占用内存大小,drawable与bitmap内存占用大小

    1, 比较Drawable与Bitmap占用内存大小 2, 比较BitmapFactory类的decodeResource方法与decodeStream方法的效率 好吧,先来看第1个测试! 以下这个是 ...

  2. 【Android 内存优化】Bitmap 内存占用计算 ( Bitmap 图片内存占用分析 | Bitmap 内存占用计算 | Bitmap 不同像素密度间的转换 )

    文章目录 一.Bitmap 内存占用 二.Bitmap 内存占用计算示例 三.Bitmap 内存占用与像素密度 四.Bitmap 内存占用与像素密度示例 一.Bitmap 内存占用 在 Android ...

  3. java map 内存分配_mapreduce 内存分配

    稍微有点mapreduce使用经验的同学肯定对OOM不陌生,对的,我目前在mapReduce里面遇到的最多的报错也是内存分配出错,所以看到好多hadoop执行脚本里面有好多关于内存的参数,虽然是知道和 ...

  4. java使用ByteBuffer.allocateDirect分配的堆外内存大小查看方法

    一.问题 在java开发中,可以使用ByteBuffer.allocateDirect分配的堆外内存,那么对一个java程序来说,如何实时查看进程的堆外内存大小呢? 二.本机进程 1.使用Jvisua ...

  5. Java虚拟机详解(六)------内存分配

    我们说Java是自动进行内存管理的,所谓自动化就是,不需要程序员操心,Java会自动进行内存分配和内存回收这两方面. 前面我们介绍过如何通过垃圾回收器来回收内存,那么本篇博客我们来聊聊如何进行分配内存 ...

  6. java内存分配空间大小,JVM内存模型及内存分配过程

    一.JVM内存模型 JVM主要管理两种类型内存:堆(Heap)和非堆(Permanent区域). 1.Heap是运行时数据区域,所有类实例和数组的内存均从此处分配.Heap区分两大块,一块是 Youn ...

  7. java内存模型 创建类_JVM内存模型及String对象内存分配

    昨天看了一篇关于<Java后端程序员1年工作经验总结>的文章,其中有一段关于String和StringBuffer的描述,对于执行结果仍然把握不准,趁此机会也总结了下JVM内存模型. 1. ...

  8. Java虚拟机-第二篇-GC算法与内存分配策略

    2019独角兽企业重金招聘Python工程师标准>>> GC引入 在Java的运行时数据区中,程序计数器.虚拟机栈.本地方法栈三个区域都是线程私有的,随线程而生,随线程而灭,在方法结 ...

  9. 我的世界java 内存_我的世界如何分配内存

    如果你在运行Minecraft时出现内存错误等问题,你可能需要给Minecraft分配更多内存来解决运行故障.如果你玩的是新版本的Minecraft,那么你可以从启动器里直接分配内存(RAM).如果你 ...

最新文章

  1. 中国水果加工行业产销格局与开发价值分析报告2022版
  2. 2018年春阅读计划---阅读笔记4
  3. Dapr微服务应用开发系列4:状态管理构件块
  4. flex.css快速入门,极速布局
  5. 队列处理高并发_高并发场景下缓存处理的一些思路
  6. 山东大学linux实验报告,山东大学操作系统实验四
  7. 原型模式 java 深浅_Java设计模式——原型模式
  8. android自定义tab的分隔符,TabView中的分隔符
  9. 每天看到那么徒步直播的朋友,他们靠什么生活呢?
  10. 浏览器内存泄漏问题的跟踪与解决(转)
  11. 2089. 找出数组排序后的目标下标
  12. linux tomcat reload,linux-tomcat安装配置
  13. springboot框架搭建
  14. StarUML Choice控件写上文字Text
  15. delphi mysql ado_delphi2010利用ADO连接MySQL数据库
  16. SSM房屋租赁系统,房屋合租系统 租房系统 SpringBoot租房系统
  17. 非越狱逆向开发总结文档(含iOS Extension)
  18. 如何提升 B站 等级?
  19. 求空间两条直线之间的距离
  20. as 贪食蛇小游戏(一)

热门文章

  1. php cms多城市版,[转载]齐博CMS多城市版改为单城市版以及设置伪静态规则
  2. 运营入门——超级运营术
  3. apxs编译php.so,PHP编译安装报错Sorry, I cannot run apxs.
  4. 袁国宝:续航1000公里,特斯拉跟不跟?
  5. Mysql之IN 和 Exists 用法
  6. 自然语言推理与自然语言解释
  7. jdbc-(ResultSet.TYPE_SCROLL_INSENSITIVE,ResultSet.CONCUR_READ_ONLY)总结
  8. 史上最全的 Spring 面试题和答案
  9. 灰犀牛之2017年8月21日,四川九寨沟发生7.0级地震
  10. java IO流进阶 模拟上传头像系统详解