Android内存分配
Java Head(Dalvik Head),这部分的内存是由Dalvik虚拟机管理,可以通过java的new方法来分配内存;而内存的回收是符合GC Root回收规则。内存的大小受到系统限制,如果使用内存超过App最大可用内存时会抛出OOM错误。
Native Head,这部分内存,不受Dalvik虚拟机管理的,内存的分配和回收是通过C++的方式来创建和释放的,没有自动回收机制。而内存的大小受硬件的限制(手机内存的限制)。
Ashmem(Android匿名共享内存),这部分内存和Native内存区类似,有点不同的是,它是由Android系统底层管理的,Android系统在内存不足时,会回收Ashmem区域中状态是unpin的对象内存,如果不希望对象被回收,可以通过pin来保护一个对象。
Bitmap内存
Bitmap对象的内存分为两部分:
- Bitmap对象
- Bitmap像素数据(即一张图片的数据)。
在Android 2.3.3(API 10)之前,Bitmap的像素数据的内存时分配在Native堆上的,而Bitmap对象的内存则分配在Dalvik堆上的;
由于Native堆上的内存时不受DVM管理的,如果想要回收Bitmap的所占用内存的话,那么需要调用Bitmap.recyle()方法。
而API 10之后呢,谷歌将像素数据的内存分配也移到DVM堆上,由DVM管理,因此在dvm回收前;
只需要保证Bitmap对象不被任何GC Roots强引用就可以回收这部分内存。
Bitmap.Config
Bitmap.Config是影响图片画质的重要因素,单位像素占用字节越大,画质越高。ARGB是一种存储色彩的模式,其中A:透明度;R:红色;G:绿色;B:蓝色
Bitmap.Config | 值 | 描述 | 占用内存(字节) |
---|---|---|---|
Bitmap.Config | ARGB_8888 | 表示32位的ARGB位图 | 4 |
Bitmap.Config | ARGB_4444 | 表示16位的ARGB位图 | 2 |
Bitmap.Config | RGB_565 | 表示16位的RGB位图 | 2 |
Bitmap.Config | ALPHA_8 | 表示16位的Alpha位图 | 1 |
注意:ARGB_8888单位像素点占用内存是最高的,所以该模式下画质最好,虽然ARGB_4444单位像素点占用内存是ARGB_8888的一般,但是画质较差,如果不需要Alpah通道的话,可以使用RGB_565,jpg格式图片是没有Alpha通道的
density,densityDpi,targetDensity的区别
density | densityDpi(dpi) | 分辨率 | res |
---|---|---|---|
1 | 160 | 320 X 533 | mdpi |
1.5 | 240 | 480 X 800 | hdpi |
2 | 320 | 720 X 1280 | xhdpi |
3 | 480 | 1080 X 1920 | xxhdpi |
3.5 | 560 | … | xxxhdpi |
density:密度,指每平方英寸中的像素数,在DisplayMetrics类中属性density的值为dpi/160
densityDpi,单位密度下可存在的点。
Bitmap对象创建
Bitmap的构造方法不是共有的,因此外部不能通过new的方式来创建,不过可以Bitmap的createBitmap方法和BitmapFactory
Bitmap
createBitmap -> nativeCreate
Bitmap中的
BitmapFactory
// resource
BitmapFactory.decodeResource(...)
// 字节数组
BitmapFactory.decodeByteArray()
// 文件
BitmapFactory.decodeFile()
// 流
BitmapFactory.decodeStream()
// FileDescriptor
BitmapFactory.decodeFileDescriptor()
decodeResource流程图
Created with Rapha?l 2.1.0decodeFiledecodeResourceStreamdecodeStream返回Bitmap
decodeResourceStream方法会inDensity和inTargetDensity进行处理,如果inDensity值为0的话,那么则会采用默认的(160dp),
同样,如果inTargetDensity值为0的话,会使用手机系统的density
比如手机的分辨率是720*1280的话,那么手机的density为320,则inTargetDensity=320。
这两个值影响图片最终显示出来是否缩放。
decodeFile流程图
c1=>operation: decodeFile|current
c2=>operation: decodeStream|current
e2=>operation: 返回Bitmap
c1(right)->c6->e2
decodeStream流程图
Created with Rapha?l 2.1.0decodeStream是否是Assets目录下的流nativeDecodeAsset返回BitmapdecodeStreamInternalnativeDecodeStreamyesno
decodeByteArray流程图
Created with Rapha?l 2.1.0decodeByteArraynativeDecodeByteArray
decodeFileDescriptor流程图
Created with Rapha?l 2.1.0decodeFileDescriptornativeIsSeekablenativeDecodeFileDescriptor返回BitmapnativeDecodeStreamyesno
Bitmap占用内存计算
Bitmap占用内存计算 = 图片最终显示出来的宽 * 图片最终显示出来的高 * 图片品质(Bitmap.Config的值)
比如SDcard中A图片的分辨率为300 X 600,使用ARGB_8888的品质加载,那么这张图片占用的内存 = 300 * 600 * 4 = 720000(byte) = 0.686(mb)
Bitmap中哟getByteCount()可以获取图片占用内存字节大小。
注意,为什么计算的公式是图片最终显示出来的宽 * 图片最终显示出来的高,而不是,图片的宽和图片的高呢?
主要是这样的,Android为了适配不同分辨率的机型,对放到不同drawable下的图片,在创建Bitmap的过程中,进行了缩放判断,如果需要缩放的话,
那么最终创建出来的图片宽和高都进行了修改。
ImageView iv = (ImageView) findViewById(R.id.iv);
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.french_girl);
iv.setImageBitmap(bitmap);
bitmap创建流程,decodeResource -> decodeStream -> nativeDecodeStream;可以看到最终是通过jni调用nativeDecodeStream方法来创建Bitmap。
BitmapFactory.cpp
nativeDecodeStream
static jobject nativeDecodeStream(JNIEnv* env, jobject clazz, jobject is, jbyteArray storage,
jobject padding, jobject options) {
jobject bitmap = NULL;
SkAutoTUnref<SkStream> stream(CreateJavaInputStreamAdaptor(env, is, storage));
if (stream.get()) {
SkAutoTUnref<SkStreamRewindable> bufferedStream(SkFrontBufferedStream::Create(stream, 64));
SkASSERT(bufferedStream.get() != NULL);
// 调用doDecode方法创建bitmap
bitmap = doDecode(env, bufferedStream, padding, options, false, false);
}
return bitmap;
}
doDecode
static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding,
jobject options, bool allowPurgeable, bool forcePurgeable = false) {
// ....省略
float scale = 1.0f;
// ....省略
if (options != NULL) {
// ....省略
// 计算出图片是否需要缩放
// density,如果不设置opts.inDensity的话,该值默认为160, 代码查看BitmapFactory中decodeResourceStream方法
// 比如,如果图片放到drawable-hdpi目录下,该值为240,
// targetDensity,如果不设置opts.inTargetDensity的话,该值默认为DisplayMetrics的densityDpi,注意该值是由手机自身设置的
// 比如720 X 1280分辨率的手机,该值为320;1080 X 1920分辨率的手机,该值为480
// scale = (float) targetDensity / density;
// 图片放到drawable-hdpi目录下,手机分辨率为720*1280;scale = 320 / 240 = 1.333
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;
}
}
}
// 判断图片是否需要缩放
const bool willScale = scale != 1.0f;
isPurgeable &= !willScale;
// 图片缩放宽高默认为原图宽高
int scaledWidth = decodingBitmap.width();
int scaledHeight = decodingBitmap.height();
if (willScale && mode != SkImageDecoder::kDecodeBounds_Mode) {
// 计算出缩放后图片的宽高,也就是最终显示出来的宽高
scaledWidth = int(scaledWidth * scale + 0.5f);
scaledHeight = int(scaledHeight * scale + 0.5f);
}
// 更新options
if (options != NULL) {
// 设置图片最终显示出来的宽高为缩放后的宽高
env->SetIntField(options, gOptions_widthFieldID, scaledWidth);
env->SetIntField(options, gOptions_heightFieldID, scaledHeight);
env->SetObjectField(options, gOptions_mimeFieldID,
getMimeTypeString(env, decoder->getFormat()));
}
// ....省略
// 创建Bitmap对象
return GraphicsJNI::createBitmap(env, outputBitmap, javaAllocator.getStorageObj(),
bitmapCreateFlags, ninePatchChunk, layoutBounds, -1);
}
原图大小为165*221,图片放到drawable-hdpi目录下,手机分辨率为720*1280:
ImageView iv = (ImageView) findViewById(R.id.iv);
Bitmap bitmap1 = BitmapFactory.decodeResource(getResources(), R.drawable.french_girl);
iv.setImageBitmap(bitmap1);
Log.d("MYTAG", "getByteCount " + bitmap1.getByteCount());
Log.d("MYTAG", "getRowBytes " + bitmap1.getRowBytes());
Log.d("MYTAG", "getHeight " + bitmap1.getHeight());
Log.d("MYTAG", "getHeight " + bitmap1.getWidth());
输出
MYTAG: getByteCount 259600
MYTAG: getRowBytes 880
MYTAG: getHeight 295
MYTAG: getHeight 220
如果是:图片占用内存 = 图片宽 * 图片高 * Bitmap.Config
那么这张图所占用内存 = 165 * 221 * 4 = 145860(b) = 142.44(kb)
但是打印出出来的值为259600,很明显该Bitmap在创建的时候,进行了缩放
而使用图片占用内存 = 图片最终的宽 * 图片最终的高 * Bitmap.Config
scale = (float) targetDensity / density = 320 / 240 = 1.333
scaledWidth = int(scaledWidth * scale + 0.5f) = 165 * 1.333 + 0.5 = 220
scaledHeight = int(scaledHeight * scale + 0.5f) = 221 * 1.333 + 0.5 = 295
图片占用内存 = 220 * 295 * 4 = 259600
从上面知道,opts.inDensity和opts.inTargetDensity是影响图片最终创建出来的大小,那么如果我将这两个值设置为相同的,
不出意外的话,图片占用内存=图片宽 * 图片高 * Bitmap.Config
原图大小为165*221,图片放到drawable-hdpi目录下,手机分辨率为720*1280:
通过计算,Bitmap占用内存 = 165 * 221 * 4 = 145860
ImageView iv = (ImageView) findViewById(R.id.iv);
BitmapFactory.Options options = new BitmapFactory.Options();
options.inDensity = 160;
options.inTargetDensity = 160;
Bitmap bitmap1 = BitmapFactory.decodeResource(getResources(), R.drawable.french_girl, options);
iv.setImageBitmap(bitmap1);
Log.d("MYTAG", "getByteCount " + bitmap1.getByteCount());
Log.d("MYTAG", "getRowBytes " + bitmap1.getRowBytes());
Log.d("MYTAG", "getHeight " + bitmap1.getHeight());
Log.d("MYTAG", "getHeight " + bitmap1.getWidth());
输出
MYTAG: getByteCount 145860
MYTAG: getRowBytes 660
MYTAG: getHeight 221
MYTAG: getHeight 165
Bitmap加载大图
一张分辨率为 5400 X 3600的图片,使用ARGB_8888的方式加载,那么这张图占用内存= 5400 * 3600 * 4 = 77760000(byte) = 74.15(MB)
毫无疑问,App只要加载这张74.15m的图片,肯定会抛出OOM错误的。
一般情况,我们会设置inSampleSize,inPreferredConfig等来降低图片占用的内存,但是这样的话,图片就变成有损显示了。
如果想要无损显示的话,那么就得使用BitmapRegionDecoder类。
BitmapRegionDecoder:是用来解码一张图片的某个矩形区域,可以通过BitmapRegionDecoder.newInstance方法创建一个BitmapRegionDecoder对象,
然后再通过BitmapRegionDecoder的decodeRegion方法获取图片某一区域的Bitmap。
Bitmap优化
在我看来,Bitmap的优化主要是加快图片的加载速度和降低图片占用内存的大小
加快Bitmap的加载速度
简略的说,图片的显示,无非就是将不同来源的图片文件,加载到Android系统内存中,然后创建Bitmap对象,最后将Bitmap渲染出来。
来源不同的文件,加载的速度是不同的,内存 > 硬盘(本地)> 网络。
因此,我们也应该,尽量将不同来源的图片保存到内存中,因为内存时最快由被系统使用。
这里,我们主要是使用优秀的图片加载框架(比如Picasso,Glide,Fresco等),管理图片,这里不做详细的探讨。
降低Bitmap占用内存的大小
Bitmap占用内存大小 = Bitmap最终的宽度 * Bitmap最终的高度 * Bitmap.Config的值
通过公式,可以看出,对上面3个值,只要任意减少一个值,都可以达到降低占用内存的大小
影响Bitmap.Config:
- inPreferredConfig,该值默认为ARGB_8888,占用4个字节
影响Bitmap最终的宽高:
- inSampleSize,inDensity,inTargetDensity,inScaled,
public Bitmap inBitmap; //是否重用该Bitmap,注意使用条件,Bitmap的大小必须等于inBitmap,inMutable为true
public boolean inMutable; //设置Bitmap是否可以更改
public boolean inJustDecodeBounds; // true时,decode不会创建Bitmap对象,但是可以获取图片的宽高
public int inSampleSize; // 压缩比例,比如=4,代表宽高压缩成原来的1/4,注意该值必须>=1
public Bitmap.Config inPreferredConfig = Bitmap.Config.ARGB_8888; //Bitmap.Config,默认为ARGB_8888
public boolean inPremultiplied; //默认为true,一般不需要修改,如果想要修改图片原始编码数据,那么需要修改
public boolean inDither; //是否抖动,默认为false
public int inDensity; //Bitmap的像素密度
public int inTargetDensity; //Bitmap最终的像素密度(注意,inDensity,inTargetDensity影响图片的缩放度)
public int inScreenDensity; //当前屏幕的像素密度
public boolean inScaled; //是否支持缩放,默认为true,当设置了这个,Bitmap将会以inTargetDensity的值进行缩放
public boolean inPurgeable; //当存储Pixel的内存空间在系统内存不足时是否可以被回收
public boolean inInputShareable; //inPurgeable为true情况下才生效,是否可以共享一个InputStream
public boolean inPreferQualityOverSpeed; //为true则优先保证Bitmap质量其次是解码速度
public int outWidth; //Bitmap最终的宽
public int outHeight; //Bitmap最终的高
public String outMimeType; //
public byte[] inTempStorage; //解码时的临时空间,建议16*1024
inJustDecodeBounds
inJustDecodeBounds属性,设置为true时,decode不会创建Bitmap对象。
如果想要获取Bitmap的宽高,但又不想将Bitmap加载到内存中(比如将一张分辨率非常高的图片,只要加载到内存中,就会抛出OOM),
那么我们必须得inJustDecodeBounds设置为true
使用BitmapFactory创建Bitmap,最终是调用jni中BitmapFactory.cpp中的doDecode方法的。
doDecode
static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding,
jobject options, bool allowPurgeable, bool forcePurgeable = false) {
// mode默认为SkImageDecoder::kDecodePixels_Mode;
SkImageDecoder::Mode mode = SkImageDecoder::kDecodePixels_Mode;
// ...省略
if (options != NULL) {
sampleSize = env->GetIntField(options, gOptions_sampleSizeFieldID);
// 获取inJustDecodeBounds的值,如果是true的话,mode设置为SkImageDecoder::kDecodeBounds_Mode;
if (optionsJustBounds(env, options)) {
mode = SkImageDecoder::kDecodeBounds_Mode;
}
// ...省略
}
// ...省略 中间计算出Bitmap的宽度和高度,并设置到options中
// inJustDecodeBounds为true时,返回null
if (mode == SkImageDecoder::kDecodeBounds_Mode) {
return NULL;
}
// ...省略
return GraphicsJNI::createBitmap(env, outputBitmap, javaAllocator.getStorageObj(),
bitmapCreateFlags, ninePatchChunk, layoutBounds, -1);
}
inSampleSize
inSampleSize,是调整Bitmap压缩比例的,该值必须>=1,比如inSampleSize = 2,那么Bitmap的宽和高都变为原来的1/2
Bitmap.compress方法压缩图片
除了调整inSampleSize,inDensity,inTargetDensity对图片进行压缩外,Bitmap.compress()方法同样也可以对Bitmap进行压缩。
public boolean compress(CompressFormat format, int quality, OutputStream stream)
- CompressFormat format:压缩格式,三种类型:JPEG,PNG,WEBP
- int quality: 压缩品质,该值必须在[0, 100]区间内,值越大,品质越高
- OutputStream stream:压缩成功后,Bitmap输出流
public boolean compress(CompressFormat format, int quality, OutputStream stream) {
checkRecycled("Can‘t compress a recycled bitmap");
if (stream == null) {
throw new NullPointerException();
}
if (quality < 0 || quality > 100) {
throw new IllegalArgumentException("quality must be 0..100");
}
Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "Bitmap.compress");
boolean result = nativeCompress(mFinalizer.mNativeBitmap, format.nativeInt,
quality, stream, new byte[WORKING_COMPRESS_STORAGE]);
Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
return result;
}
compress,首先会进行参数检测,然后调用jni中nativeCompress的方法进行压缩
例子
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
iv = (ImageView) findViewById(R.id.iv);
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.french_girl);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.JPEG, 10, outputStream);
byte[] bytes = outputStream.toByteArray();
bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
iv.setImageBitmap(bitmap);
}
inBitmap 和 inPurgeable
- inBitmap:主要是重用该Bitmap的内存区域,避免多次重复向dvm申请开辟新的内存区域。
- inPurgeable:设置为True,则使用BitmapFactory创建的Bitmap用于存储Pixel的内存空间,在系统内存不足时可以被回收,当应用需要再次访问该Bitmap的Pixel时,系统会再次调用BitmapFactory 的decode方法重新生成Bitmap的Pixel数组。 设置为False时,表示不能被回收
为了更好的理解这两个参数,我们需要理解下Android管理Bitmap内存的过程:
“stop the world”是指发生GC时,除了GC所需要的线程,其他的线程都会处于等待状态,直到GC完毕。
在Android 2.2(API 8)以及之前,DVM发生GC的时候,会引发”stop the world”,这样会导致应用停滞。而在Android 2.3上,Android引入并发GC机制,并发GC机制是不会引发”stop the world”。
在Android2.3.3(API 10)以及之前,Bitmap的像素数据是分配在Native堆上的,想要回收Bitmap,那么必须得调用bitmap.recyle()方法;而之后,Bitmap的像素数据和Bitmap对象一起分配到DVM堆上,由DVM管理,bitmap的回收只需要置为null,不需要调用recyle()方法。
还有另外一点,上面我们说过Android中对象的内存除了可以在DVM堆和Native堆上分配外,还可以在匿名共享内存中分配。
Ashmem上,一般在应用中是无法直接访问的,但是可以通过设置BitmapFactory.Optinons.inPurgeable = true
,创建一个Purgeable(可擦除的) Bitmap,
这样的decode出来的bitmap,其像素数据是分配在Ashmem内存中的。Ashmem内存上的对象有两种状态:pin
和unpin
,当一个对象状态处于pin
状态,可以通过设置
unpin
,这样系统就可以回收对象的内存。
但是存在一个问题,当一个unpin
的bitmap已经被回收,如果再次使用这个bitmap的时候,系统会对它进行重新decode,而decode方法是发生在主线程上的,
这样就有可能产生掉帧现象,因此该做法被Google废弃掉了,建议使用inBitmap
但是使用inBitmap属性,需要主要注意几点:
- inBitmap只能在3.0以后使用,在这之前Bitmap的像素数据是分配在Native堆上的。
- 在SDK 11 - 18之间,创建Bitmap大小必须和重用Bitmap大小一致,比如重用Bitmap的大小为100 * 100,那么创建Bitmap的大小同样也要100 * 100
- 在SDK 19 上以及之后,创建Bitmap大小必须等于或者小于重用Bitmap大小。
- Bitmap的格式必须一样,比如重用Bitmap的格式为ARGB_8888,那么创建的Bitmap格式同样也得是ARGB_8888
参考:
Android 开发绕不过的坑:你的 Bitmap 究竟占多大内存?
Android性能优化:谈谈Bitmap的内存管理与优化
BitmapFactory和Bitmap中Density的作用
Bitmap基本概念及在Android4.4系统上使用BitmapFactory的注意事项
Android Bitmap.setDensity(int density) 和 BitmapDrawable.setTargetDensity()
inDensity,inTargetDensity,inScreenDensity关系详解
Android Training - 高效地显示Bitmap(Lesson 4 - 优化Bitmap的内存使用)