我们知道,一般认为在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请求空间的优化方案来说,就失去意义了。

Android O Bitmap 内存分配的更多相关文章

  1. Android系统Bitmap内存分配原理与优化

    一.前言 笔者最近致力于vivo游戏中心稳定性维护,在分析线上异常时,发现有相当一部分是由OutOfMemory引起.谈及OOM,我们一般都会想到内存泄漏,其实,往往还有另外一个因素--图片,如果对图 ...

  2. android 管理Bitmap内存 - 开发文档翻译

    由于本人英文能力实在有限,不足之初敬请谅解 本博客只要没有注明“转”,那么均为原创,转贴请注明本博客链接链接   Managing Bitmap Memory 管理Bitmap内存 In additi ...

  3. 图片系列(6)不同版本上 Bitmap 内存分配与回收原理对比

    请点赞关注,你的支持对我意义重大. Hi,我是小彭.本文已收录到 GitHub · AndroidFamily 中.这里有 Android 进阶成长知识体系,有志同道合的朋友,关注公众号 [彭旭锐] ...

  4. 《Android虚拟机》--内存分配策略

    No1: Java在内存分配时会涉及到以下区域: 寄存器:我们在程序中无法控制 栈:存放基本类型的数据和对象的引用,但对象本身不存放在栈中,而是存放在堆中 堆:存放用new产生的数据 静态域:存放在对 ...

  5. android 防止bitmap 内存溢出

    在android开发过程中经常会处理网络图片发送内存溢出,那么怎么解决这种问题? 思路: 下载到本地 通过网络获取和文件下载存放到手机中目录 代码: // 获取网络 public InputStrea ...

  6. Android单个进程内存分配策略

    android不同设备单个进程可用内存是不一样的,可以查看/system/build.prop文件. # This is a high density device with more memory, ...

  7. 【转】Android中的内存管理--不错不错,避免使用枚举类型

    原文网址:http://android-performance.com/android/2014/02/17/android-manage-memory.html 本文内容翻译自:http://dev ...

  8. android bitmap的内存分配和优化

    首先Bitmap在Android虚拟机中的内存分配,在Google的网站上给出了下面的一段话 大致的意思也就是说,在Android3.0之前,Bitmap的内存分配分为两部分,一部分是分配在Dalvi ...

  9. Android性能优化:谈话Bitmap内存管理和优化

    最近除了那些忙着项目开发的事情,目前正在准备我的论文.短的时间没有写博客,今晚难得想总结.只要有一点时间.因此,为了凑合用,行.唠叨罗嗦,直接进入正题. 从事Android自移动终端的发展,想必是常常 ...

随机推荐

  1. IDA 宏定义

    /* This file contains definitions used by the Hex-Rays decompiler output. It has type definitions an ...

  2. 常州模拟赛d7t2 数组

    题目背景 HJZ 有很多玩具.他最喜欢玩的玩具是一个可以变化的数组. 题目描述 HJZ 的数组初始时有 n 个元素.他可以把一个位置上的数加上或减去一个固定的 数 x. 一天 LJZ 和 HZY 来 ...

  3. [K/3Cloud]关于"选单"操作

    之前有些人对这块有些疑问,比如: 1.选单操作是否和下推基本一样,都是公用同一套单据转换规则,只不过下推是源单推目标单,选单是目标单去选择源单,最终操作结果一样? 2,我想实现选单的时候,选单列表先通 ...

  4. MySQL慢日志切割邮件发送脚本

    #!/bin/bashtime=`date -d yesterday +"%Y-%m-%d"`slowlog='/usr/local/percona/data/slow.log'# ...

  5. Java 实现简答的单链表的功能

    作者:林子木  博客网址:http://blog.csdn.net/wolinxuebin 參考网址:http://blog.csdn.net/sunsaigang/article/details/5 ...

  6. Windows中的时区信息

    时区 Windows API 没有提供直接得到特定时区信息的功能,Windows 系统中的时区信息存储在注册表中下面的位置: HKEY_LOCAL_MACHINE     SOFTWARE      ...

  7. Batch基本知识

    一般情况下,每条命令占据一行: 当然也可以将多条命令用特定符号(如:&:.&&:.|.||等)分隔后写入同一行中: 还有的情况就是像if.for等较高级的命令则要占据几行.几十 ...

  8. 【SCOI 2011】 糖果

    [题目链接] 点击打开链接 [算法] 当x = 1时,连边(a,b,0)和(b,a,0) 当x = 2时,连边(a,b,1) 当x = 3时,连边(b,a,0) 当x = 4时,连边(b,a,1) 当 ...

  9. 第四周 Leetcode 124. Binary Tree Maximum Path Sum (HARD)

    124. Binary Tree Maximum Path Sum 题意:给定一个二叉树,每个节点有一个权值,寻找任意一个路径,使得权值和最大,只需返回权值和. 思路:对于每一个节点 首先考虑以这个节 ...

  10. QT Creater环境搭建

    需要的软件包: 1.qt-win-opensource-4.8.5-mingw.exe //Qt库 2.MinGW-gcc440_1.zip //QT编译器 3.qt-creator-opensour ...