Android Bitmap内存占用及缓存
文章图片来自于https://blog.****.net/happylishang/article/details/80404966
一、衡量单位
1) dpi,dip,dp,sp,px
- dpi,dots per inch,单位英寸上的像素点数
- dip,device independent pixels,设备独立像素
- dp,与dip相同,名称不一样而已
- sp,scale pixels,缩放像素,主要用于字体设置
- px,像素
2)计算方式
dpi = Math.sqrt(Math.pow(width,2)+Math.pow(height, 2))/屏幕对角线长度(英寸)
px = dp * (dpi / 160)
也就是密度 = dpi / 160
名称 | 1dp对应px(屏幕密度 density 大小) | dpi |
---|---|---|
低 - ldpi | 0.75 | 120dpi |
中 - mdpi | 1 | 160dpi |
高 - hdpi | 1.5 | 240dpi |
超高 - xhdpi | 2 | 320dpi |
超超高 - xxhdpi | 3 | 480dpi |
超超超高 - xxxhdpi | 4 | 640dpi |
分别对应项目下的 ldpi、mdpi、hdpi、xdpi、xxdpi、xxxdpi 文件夹。
二、Bitmap究竟占用多少内存
源码BitmapFactory.cpp的doDecode()方法如下:
if (env->GetBooleanField(options, gOptions_scaledFieldID)) {
const int density = env->GetIntField(options, gOptions_densityFieldID);
const int targetDensity = env->GetIntField(options, gOptions_targetDensityFieldID);
const int screenDensity = env->GetIntField(options, gOptions_screenDensityFieldID);
if (density != 0 && targetDensity != 0 && density != screenDensity) {
scale = (float) targetDensity / density;
}
}
// Determine the output size.
SkISize size = codec->getSampledDimensions(sampleSize);
int scaledWidth = size.width();
int scaledHeight = size.height();
bool willScale = false;
// Apply a fine scaling step if necessary.
if (needsFineScale(codec->getInfo().dimensions(), size, sampleSize)) {
willScale = true;
scaledWidth = codec->getInfo().width() / sampleSize;
scaledHeight = codec->getInfo().height() / sampleSize;
}
// Scale is necessary due to density differences.
if (scale != 1.0f) {
willScale = true;
scaledWidth = static_cast<int>(scaledWidth * scale + 0.5f);
scaledHeight = static_cast<int>(scaledHeight * scale + 0.5f);
}
const float sx = scaledWidth / float(decodingBitmap.width());
const float sy = scaledHeight / float(decodingBitmap.height());
SkCanvas canvas(outputBitmap);
canvas.scale(sx, sy);
canvas.drawBitmap(decodingBitmap, 0.0f, 0.0f, &paint);
从源码可以看出scaledWidth经过两次计算,一次是如果sampleSize不等于1的时候计算缩放宽高,等于原宽高分别除以采样倍数;另外一次是如果目标屏幕密度和当前图片所处文件夹的密度不一致的话,计算出:
scale = targetDensity / density,
如果scale不等于1,用第一次计算的
scaledWidth * scale + 0.5,scaledHeight * scale + 0.5,
不过具体在做缩放操作的时候缩放因子等于两次计算之后的宽高分别处以原始宽高。可见对于设置采样率可以节省部分内存。
最后实际的占用大小:
width = (originWidth / sampleSize) * (targetDensity / density) + 0.5
height = (originHeight / sampleSize) * (targetDensity / density) + 0.5
totalSize = width * height * 像素位
(targetDensity是手机实际密度,等于(宽平方 + 高平方)开根号,处于屏幕对角线长度,density是图片在app所处文件的密度见上述表格。)
- ARGB_8888: 每个像素4字节. 共32位,默认设置。
- Alpha_8: 只保存透明度,共8位,1字节。
- ARGB_4444: 共16位,2字节。
- RGB_565:共16位,2字节,只存储RGB值。
getRowBytes()返回的是每行的像素值,乘以高度就是总的像素数,也就是占用内存的大小。
getAllocationByteCount()与getByteCount()的返回值一般情况下都是相等的。
只是在图片 复用的时候,getAllocationByteCount()返回的是复用图像所占内存的大小,getByteCount()返回的是新解码图片占用内存的大小。
三、Bitmap参数
BitmapFactory.Options类的inJustDecodeBounds属性为true,可以在Bitmap不被加载到内存的前提下,获取Bitmap的原始宽高
BitmapFactory.Options的inSampleSize属性可以真实的压缩Bitmap占用的内存,加载更小内存的Bitmap
BitmapFactory.Options的inMutable属性,mutable是易变的意思,主要用于从以解码的bitmap返回一个可修改的bitmap对象。(If set, decode methods will always return a mutable Bitmap instead of an immutable one.)
BitmapFactory.Options的inBitmap 属性,用于图片复用,复用bitmap的大小必须小于被复用的bitmap大小。另外在设置这个属性的时候,inMutable属性必须设置为true。
(The current implementation necessitates that the reused bitmap be mutable, and the resulting reused bitmap will continue to remain mutable even when decoding a resource which would normally result in an immutable bitmap.)
四、Bitmap内存模型
- 1)在Android 2.2 (API level 8)及其以下版本上,垃圾回收线程工作时,APP线程就得暂停,这一特性无疑会降低APP的性能。 Android 2.3开始实现了并发垃圾回收,这意味着一个bitmap对象不再任何被引用持有时,它所占有的内存空间会很快的被回收。
- 2)在Android 2.3.3 (API level 10)及其以下版本上,bitmap的ARGB数据(backing pixel data)是存在native内存里的,而bitmap对象本身是存在Dalvik的堆里的。当bitmap对象不再被引用时,Dalvik的堆里的内存可以被垃圾回收期回收,但是native部分的内存却不会同步被回收。如果需要频繁的加载很多bitmap到内存中,即使Java层已经及时的释放掉不用bitmap,依旧有可能引起OOM。
- 3)从Android 3.0 (API level 11)开始,bitmap的ARGB数据(像素数据)和bitmap对象一起存在Dalvik的堆里了。这样bitmap对象和它的ARGB数据就可以同步回收了。
- 4) Android 3.0 开始引入了BitmapFactory.Options.inBitmap字段。如果设置了这个字段,bitmap在加载数据时可以复用这个字段所指向的bitmap的内存空间。新增的这种内存复用的特性,可以优化掉因旧bitmap内存释放和新bitmap内存申请所带来的性能损耗。但是,内存能够复用也是有条件的。比如,在Android 4.4(API level 19)之前,只有新旧两个bitmap的尺寸一样才能复用内存空间。Android 4.4开始只要旧bitmap的尺寸大于等于新的bitmap就可以复用了。
- 5)在Android 8.0之前,Bitmap的内存分配在dalvik heap,Bitmap中有个byte[] mBuffer,其实就是用来存储像素数据的,很明显它位于java heap中,通过在native层构建Java Bitmap对象的方式,将生成的byte[]传递给Bitmap.java对象(Android 7.0):
static jobject Bitmap_creator(JNIEnv* env, jobject, jintArray jColors,jint offset, jint stride, jint width, jint height,jint configHandle, jboolean isMutable) {
SkColorType colorType = GraphicsJNI::legacyBitmapConfigToColorType(configHandle);
if (NULL != jColors) {
size_t n = env->GetArrayLength(jColors);
if (n < SkAbs32(stride) * (size_t)height) {
doThrowAIOOBE(env);
return NULL;
}
}
// ARGB_4444 is a deprecated format, convert automatically to 8888
if (colorType == kARGB_4444_SkColorType) {
colorType = kN32_SkColorType;
}
SkBitmap bitmap;
bitmap.setInfo(SkImageInfo::Make(width, height, colorType, kPremul_SkAlphaType));
Bitmap* nativeBitmap = GraphicsJNI::allocateJavaPixelRef(env, &bitmap, NULL);
if (!nativeBitmap) {
return NULL;
}
if (jColors != NULL) {
GraphicsJNI::SetPixels(env, jColors, offset, stride,
0, 0, width, height, bitmap);
}
return GraphicsJNI::createBitmap(env, nativeBitmap,
getPremulBitmapCreateFlags(isMutable));
}
再看看allocateJavaPixelRef:
android::Bitmap* GraphicsJNI::allocateJavaPixelRef(JNIEnv* env, SkBitmap* bitmap,SkColorTable* ctable) {
const SkImageInfo& info = bitmap->info();
if (info.colorType() == kUnknown_SkColorType) {
doThrowIAE(env, "unknown bitmap configuration");
return NULL;
}
size_t size;
if (!computeAllocationSize(*bitmap, &size)) {
return NULL;
}
// we must respect the rowBytes value already set on the bitmap instead of
// attempting to compute our own.
const size_t rowBytes = bitmap->rowBytes();
jbyteArray arrayObj = (jbyteArray) env->CallObjectMethod(gVMRuntime, gVMRuntime_newNonMovableArray, gByte_class, size);
if (env->ExceptionCheck() != 0) {
return NULL;
}
SkASSERT(arrayObj);
jbyte* addr = (jbyte*) env->CallLongMethod(gVMRuntime, gVMRuntime_addressOf, arrayObj);
if (env->ExceptionCheck() != 0) {
return NULL;
}
SkASSERT(addr);
android::Bitmap* wrapper = new android::Bitmap(env, arrayObj, (void*) addr,
info, rowBytes, ctable);
wrapper->getSkBitmap(bitmap);
// since we're already allocated, we lockPixels right away
// HeapAllocator behaves this way too
bitmap->lockPixels();
return wrapper;
}
可见,将像素的数据分配给了android::Bitmap* wrapper,对应的arrayObj就是java层的byte[] mBuffer。
流程图如下(来自****用户-看书的小蜗牛):
- 6)在Android 8.0及之后,Bitmap的内存分配在native heap,如下:
//Bitmap.cpp:
static jobject Bitmap_creator(JNIEnv* env, jobject, jintArray jColors,jint offset, jint stride, jint width, jint height,jint configHandle, jboolean isMutable,
jfloatArray xyzD50, jobject transferParameters) {
SkColorType colorType = GraphicsJNI::legacyBitmapConfigToColorType(configHandle);
if (NULL != jColors) {
size_t n = env->GetArrayLength(jColors);
if (n < SkAbs32(stride) * (size_t)height) {
doThrowAIOOBE(env);
return NULL;
}
}
// ARGB_4444 is a deprecated format, convert automatically to 8888
if (colorType == kARGB_4444_SkColorType) {
colorType = kN32_SkColorType;
}
SkBitmap bitmap;
sk_sp<SkColorSpace> colorSpace;
if (colorType != kN32_SkColorType || xyzD50 == nullptr || transferParameters == nullptr) {
colorSpace = GraphicsJNI::colorSpaceForType(colorType);
} else {
SkColorSpaceTransferFn p = GraphicsJNI::getNativeTransferParameters(env, transferParameters);
SkMatrix44 xyzMatrix = GraphicsJNI::getNativeXYZMatrix(env, xyzD50);
colorSpace = SkColorSpace::MakeRGB(p, xyzMatrix);
}
bitmap.setInfo(SkImageInfo::Make(width, height, colorType, kPremul_SkAlphaType, colorSpace));
sk_sp<Bitmap> nativeBitmap = Bitmap::allocateHeapBitmap(&bitmap);
if (!nativeBitmap) {
return NULL;
}
if (jColors != NULL) {
GraphicsJNI::SetPixels(env, jColors, offset, stride, 0, 0, width, height, bitmap);
}
return createBitmap(env, nativeBitmap.release(), getPremulBitmapCreateFlags(isMutable));
}
//Bitmap.cpp:
static sk_sp<Bitmap> allocateHeapBitmap(size_t size, const SkImageInfo& info, size_t rowBytes) {
void* addr = calloc(size, 1);
if (!addr) {
return nullptr;
}
return sk_sp<Bitmap>(new Bitmap(addr, size, info, rowBytes));
}
可以看出,8.0之后,Bitmap像素内存的分配是在native层直接调用calloc,所以其像素分配的是在native heap上, 这也是为什么8.0之后的Bitmap消耗内存可以无限增长,直到耗尽系统内存,也不会提示Java OOM的原因。
流程图如下(来自****用户-看书的小蜗牛):
NativeAllocationRegistry是Android 8.0引入的一种辅助自动回收native内存的一种机制,当Java对象因为GC被回收后,NativeAllocationRegistry可以辅助回收Java对象所申请的native内存。
五、缓存
- 1)如何定制缓存策略
经典的图片加载库fresco采用三级缓存,其中两级内存,一级磁盘,两级内存分为以编码和未编码缓存。分析缓存时主要是指LruCache和DiskLruCache,分别对应内存和磁盘缓存。
- 2)LruCache
LRU是Least Recently Used的缩写,最近最久未使用算法。
核心是LinkedHashMap,双向链表,通过构造函数就可以看出:
public LruCache(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
this.maxSize = maxSize;
this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
}
在定义的时候要定义缓存大小,一般是:
int maxMemorySize = (int) (Runtime.getRuntime().totalMemory() / 1024);
int cacheMemorySize = maxMemorySize / 8;
totalMemory是当前已分配的总内存大小,可变化;
maxMemory是当前可用的最大内存,固定;
getMemoryClass(ActivityManager),build.prop中厂商定义的每个进程分配的最大内存,如下:
String vmHeapSize = SystemProperties.get("dalvik.vm.heapgrowthlimit", "");
如果取不到,取下面这个参数:
String vmHeapSize = SystemProperties.get("dalvik.vm.heapsize", "16m");
第二个参数默认16M。
LinkedHashMap第二个参数表示是否按照访问顺序排列,此处设置为true,表示按照访问顺序排列,否则以插入顺序排列。假设我们从表尾访问数据,在表头删除数据,当访问的数据项在链表中存在时,则将该数据项移动到表尾,否则在表尾新建一个数据项。当链表容量超过一定阈值,则移除表头的数据。
- 3)DiskLruCache
使用示例如下:
File directory = getCacheDir();
int appVersion = 1;
int valueCount = 1;
long maxSize = 10 * 1024;
DiskLruCache diskLruCache = DiskLruCache.open(directory, appVersion, valueCount, maxSize);
DiskLruCache.Editor editor = diskLruCache.edit(String.valueOf(System.currentTimeMillis()));
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(editor.newOutputStream(0));
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.scenery);
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, bufferedOutputStream);
editor.commit();
diskLruCache.flush();
diskLruCache.close();
主要stream处理的是Editor,这个editor封装了Entry对象,是否有出错,是否已经写入缓存。
public final class Editor {
private final Entry entry;
private final boolean[] written;
private boolean hasErrors;
private Editor(Entry entry) {
this.entry = entry;
this.written = (entry.readable) ? null : new boolean[valueCount];
}
}
在未写入缓存之前是不可读的。
Editor在commit数据的时候,如果有错误(很多exception都是hasErrors),会删除缓存,并在journal文件中写入删除状态;否则将文件写入缓存,写入CLEAN状态,另外检查当前大小是否超出设定的最大限制,如果超出的话,就要删除部分缓存,删除逻辑与LruCache一致。
另外DiskLruCache的初始化函数是DiskLruCache.open,在这个函数里面,先创建journal.tmp文件,然后重命名成journal正式文件,这个文件里面写了什么呢?
缓存使用名称为journal的文件,一个典型的journal文件如下:
libcore.io.DiskLruCache
1
100
2
CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
DIRTY 335c4c6028171cfddfbaae1a9c313c52
CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
REMOVE 335c4c6028171cfddfbaae1a9c313c52
DIRTY 1ab96a171faeeee38496d8b330771a7a
CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
READ 335c4c6028171cfddfbaae1a9c313c52
READ 3400330d1dfc7f3f7f4b8d4d803dfcf6
文件开始的五行是文件的头。包含常量字符串"libcore.io.DiskLruCache",磁盘缓存的版本,应用的版本,存储大小,一个空行。
后续每一个子行记录了每一个缓存的状态。每一行含有以空格分割的数值:状态,key,可选指定状态的大小,并不一定在每个状态中都有数值。
- DIRTY
意味着缓存被创建或被更新。每一个成功写入DIRTY状态行后续一定跟着一个CLEAN或REMOVE状态,代表这条缓存可读或已被删除。DIRTY状态后续没有CLEAN或REMOVE状态意味着临时文件需要被删除。
- CLEAN
意味着一个缓存已经被成功发布并且可以从缓存里面访问了。一个发布状态的行后续跟着当前缓存的大小,以空格分割,如果一个key对应多个value,就有多个数值了。
- READ
意味着最近被访问了
- REMOVE
意味着当前缓存被删除了
journal文件在缓存操作发生时追加内容。journal文件在去除重复行的时候可能会发生压缩。在压缩时一个名称为journal.tmp的临时文件被使用到,并且当缓存被打开时这个临时文件应该被删除。
另外每次commit数据的时候,会检查容量,如果缓存数量大于2000或缓存entry数大于指定数量都会触发删除操作:
private boolean journalRebuildRequired() {
final int REDUNDANT_OP_COMPACT_THRESHOLD = 2000;
return redundantOpCount >= REDUNDANT_OP_COMPACT_THRESHOLD
&& redundantOpCount >= lruEntries.size();
}
redundantOpCount等于journal文件中除头部分固定的五行外的文件行数量减去缓存中entry的数量。
文章将同步至微信公众号:Android部落格