码迷,mamicode.com
首页 > 其他好文 > 详细

Bitmap的深入理解

时间:2016-07-14 15:28:27      阅读:909      评论:0      收藏:0      [点我收藏+]

标签:

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内存上的对象有两种状态:pinunpin,当一个对象状态处于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 优化(1) - 图片压缩
Android中Bitmap和Drawable
Android 开发绕不过的坑:你的 Bitmap 究竟占多大内存?
Android 高清加载巨图方案 拒绝压缩图片
Android内存优化之OOM
Fresco的内存机制
Fresco学习
Android性能优化:谈谈Bitmap的内存管理与优化
Android性能优化之Bitmap的内存优化
BitmapFactory和Bitmap中Density的作用
Bitmap基本概念及在Android4.4系统上使用BitmapFactory的注意事项
Android Bitmap.setDensity(int density) 和 BitmapDrawable.setTargetDensity()
inDensity,inTargetDensity,inScreenDensity关系详解
那些值得你去细细研究的Drawable适配
Android Training - 高效地显示Bitmap(Lesson 4 - 优化Bitmap的内存使用)

Bitmap的深入理解

标签:

原文地址:http://blog.csdn.net/angel1hao/article/details/51890938

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!