我们知道,一般认为在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. HDU 1220 简单数学题

    题目大意是 在魔方上找到有多少对小立方块它们之间连接的点不超过两个 因为任意两个立方块之间相连的点就只有0,1,2,4 这样4种情况 那么我们只需要考虑总共的组成立方块对数 sum = C(2 , n ...

  2. [K/3Cloud]在插件中根据条件取消表单打开过程

    新建一个类,继承自动态表单抽象插件类AbstractBillPlugIn,重写PreOpenForm. /// <summary> /// 销售订单 单据维护界面插件 /// </s ...

  3. 选择数字(codevs 3327)

    题目描述 Description 给定一行n个非负整数a[1]..a[n].现在你可以选择其中若干个数,但不能有超过k个连续的数字被选择.你的任务是使得选出的数字的和最大. 输入描述 Input De ...

  4. 杨辉三角的打印(Java)

    // //输入指定的行数,打印杨辉三角 // //每个数等于它上方两数之和. //每行数字左右对称,由1开始逐渐变大. //第n行的数字有n项. // // // //可从打印菱形的思想出发:???? ...

  5. Ubuntu 16.04解决在虚拟终端(Ctrl+Alt+F1)下显示菱形中文乱码问题

    在安装Ubuntu时,如果默认选择了中文,那么以后进去到虚拟终端就会出现菱形的中文乱码. 其实这个是无解的,但是可以通过以下技巧去实现: 1.把系统转成英文的 sudo gedit /etc/defa ...

  6. SQL Server 性能优化实战系列(文章索引) : 桦仔

    http://www.cnblogs.com/gaizai/archive/2012/01/20/2327814.html

  7. angularjs 过滤多组数据

    <html> <head> <script src="angular.min.js"></script> <script ty ...

  8. NodeJS经常使用模块收集

    收集了NodeJS开发中经常使用的一些模块. MVC框架 - Express Express 是轻量灵活的Nodejs Web应用框架.它能够高速地搭建站点. Express框架建立在Nodejs内置 ...

  9. 一个尖括号能干什么,画一个笑脸开始(为了支持交互,它又增添了JavaScript。HTML页面也越来越臃肿。于是CSS便诞生了。API和核心代码的出现使HTML能够访问更复杂的软件功能--支持更高级的交互和云服务集成。这就是今天的HTML5)

    一个尖括号 < 一个尖括号能干什么 < ? 你可以编出一顶帽子 <(:-p 或一张笑脸 :-> 再或者更直接一些 20世纪90年代初,html作为一种简单标记语言面世,用于在互 ...

  10. Python:SMOTE算法——样本不均衡时候生成新样本的算法

    Python:SMOTE算法 直接用python的库, imbalanced-learn imbalanced-learn is a python package offering a number ...