我们知道,一般认为在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. Codeforces 263A. Appleman and Easy Task

    A. Appleman and Easy Task time limit per test  1 second memory limit per test  256 megabytes input  ...

  2. asp.net mvc 4.0 新特性之移动特性

    asp.net mvc 4.0 新特性之移动特性 为不同的客户端提供不同的视图 手动重写 UserAgent,从而强制使用对应的视图 示例1.演示如何为不同的客户端提供不同的视图Global.asax ...

  3. gif & tools

    gif & tools https://www.cockos.com/licecap/ https://www.cockos.com/licecap/licecap128-install.ex ...

  4. Codeforces Round #260 (Div. 2) D

    D. A Lot of Games time limit per test 1 second memory limit per test 256 megabytes input standard in ...

  5. 转载 - Python里面关于 模块 和 包 和 __init__.py 的一些事

    出处:http://www.cnblogs.com/tqsummer/archive/2011/01/24/1943273.html python中的Module是比较重要的概念.常见的情况是,事先写 ...

  6. java中split以。点和|分割的问题

    问题:想要按照点来切分字符串直接这样 String[] filep=filename.split("."); 结果得到一个空数组 解决方法: 法一:需要转义,改为:(注意是2个\\ ...

  7. PHP中HTTP_X_FORWARDED_FOR 和 REMOTE_ADDR使用详解

    1.REMOTE_ADDR:浏览当前页面的用户计算机的ip地址 2.HTTP_X_FORWARDED_FOR: 浏览当前页面的用户计算机的网关 3.HTTP_CLIENT_IP:客户端的ip 在PHP ...

  8. 网卡MAC地址异常会导致无接受数据包,表现为只有发送没有接收

    遇到一个诡异的问题,一块4口博通千兆网卡中两个正常,两个怎么都没有接受,但是博通的程序网卡自检没有任何问题,最后发现是MAC地址的原因.需要将地址改为正常MAC方可正常通讯. 感觉应该是交换机丢弃了M ...

  9. docker国内镜像拉取和镜像加速registry-mirrors配置修改

    docker国内镜像拉取和镜像加速registry-mirrors配置修改 学习了:http://blog.csdn.net/u014231523/article/details/61197945 站 ...

  10. chosen.jquery.js 使用笔记

    using chosen.jquery.js using chosen.jquery.css html: <label for="MeetingUsersList" clas ...