Bitmap的深入理解

文章来源于http://blog.csdn.net/angel1hao/article/details/51890938

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 表示8位的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.0 decodeFile decodeResourceStream decodeStream 返回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.0 decodeStream 是否是Assets目录下的流 nativeDecodeAsset 返回Bitmap decodeStreamInternal nativeDecodeStream yes no

decodeByteArray流程图

Created with Raphaël 2.1.0 decodeByteArray nativeDecodeByteArray

decodeFileDescriptor流程图

Created with Raphaël 2.1.0 decodeFileDescriptor nativeIsSeekable nativeDecodeFileDescriptor 返回Bitmap nativeDecodeStream yes no

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的内存使用)

你可能感兴趣的:(android,bitmap)