关于Bitmap的内存,加载和回收等

Bitmap加载图片

Bitmap的加载离不开BitmapFactory类,关于Bitmap官方介绍:
Creates Bitmap objects from various sources, including files, streams, and byte-arrays.

BitmapFactory类提供了四类方法用来加载Bitmap:

  1. decodeFile(),从文件系统加载。
  2. decodeResource(),资源文件中加载。
  3. decodeStream(),从输入流加载。
  4. decodeByteArray(),从字节数组中加载。

注意:查看源码可以发现,decodeFile()decodeResource()间接调用decodeStream()

Bitmap的内存位置

在Android3.0之前:Bitmap的像素数据存放在Native内存,而Bitmap对象本身则存放在Dalvik Heap中。

从Android3.0开始:Bitmap的内存就全部在Dalvik Heap里了 。

Bitmap的内存回收

在Android3.0之前,需要使用Bitmap.recycle()进行Bitmap的内存回收。

从Android3.0开始,不需要手动回收Bitmap了。

Bitmap的内存复用

从Android3.0开始,在Bitmap中引入了一个新的字段BitmapFactory.Options.inBitmap,设置此字段为true后,解码方法会尝试复用一张存在的Bitmap。这意味着Bitmap的内存被复用,避免了内存的回收及申请过程,显然性能表现更佳。

Android4.4(API 19)之前只有格式为jpg、png,同等宽高(要求苛刻),inSampleSize为1的Bitmap才可以复用。

从Android4.4(API 19)开始被复用的Bitmap的内存大于需要新申请内存的Bitmap的内存就可以了。

使用缓存

LruCache+DiskLruCache

出于对性能和app的考虑,我们肯定是想着第一次从网络中加载到图片之后,能够将图片缓存在内存和sd卡中,这样,我们就不用频繁的去网络中加载图片,可以很好地控制内存问题。

一般都会考虑使用LruCache+DiskLruCache,LruCache作为Bitmap在内存中的存放容器,在sd卡则使用DiskLruCache来统一管理磁盘上的图片缓存

SoftReference+inBitmap

之前提到,可以采用LruCache作为存放Bitmap的容器,而在LruCache中有一个方法值得留意,那就是entryRemoved(),按照文档给出的说法,在LruCache容器满了需要淘汰存放其中的对象腾出空间的时候会调用此方法。

  • 注意:这里只是对象被淘汰出LruCache容器,但并不意味着对象的内存会立即被Dalvik虚拟机回收掉

此时可以在此方法中将Bitmap使用SoftReference包裹起来,并用事先准备好的一个HashSet容器来存放这些即将被回收的Bitmap,有人会问,这样存放有什么意义?

之前我们提到将inmutable设置为true,Bitmap的内存可以被复用,当然肯定要满足之前所说的条件。

解码方法对图片进行decode的时候会检查内存中是否有可复用的Bitmap,避免我们频繁地去SD卡上加载图片而造成系统性能的下降,毕竟从直接从内存中复用要比在SD卡上进行IO操作的效率要高很多。

Bitmap的像素格式

1.ALPHA_8:颜色信息只由透明度组成,占8位

2.ARGB_4444:颜色信息由透明度与R(Red),G(Green),B(Blue)四部分组成,每个部分都占4位,总共占16位

3.ARGB_8888:颜色信息由透明度与R(Red),G(Green),B(Blue)四部分组成,每个部分都占8位,总共占32位。是Bitmap默认的颜色配置信息,也是最占空间的一种配置。

4.RGB_565:颜色信息由R(Red),G(Green),B(Blue)三部分组成,R占5位,G占6位,B占5位,总共占16位

通常我们优化Bitmap时,当需要做性能优化或者防止OOM,我们通常会使用RGB_565,因为ALPHA_8只有透明度,显示一般图片没有意义,Bitmap.Config.ARGB_4444显示图片不清楚,Bitmap.Config.ARGB_8888占用内存最多

Bitmap的内存计算

Bitmap类中有一个方法getByteCount()

    /**
     * Returns the minimum number of bytes that can be used to store this bitmap's pixels.
     *
     * 

As of {@link android.os.Build.VERSION_CODES#KITKAT}, the result of this method can * no longer be used to determine memory usage of a bitmap. See {@link * #getAllocationByteCount()}.

*/
public final int getByteCount() { // int result permits bitmaps up to 46,340 x 46,340 return getRowBytes() * getHeight(); }

还有一个方法getAllocationByteCount()

    /**
     * Returns the size of the allocated memory used to store this bitmap's pixels.
     *
     * 

This can be larger than the result of {@link #getByteCount()} if a bitmap is reused to * decode other bitmaps of smaller size, or by manual reconfiguration. See {@link * #reconfigure(int, int, Config)}, {@link #setWidth(int)}, {@link #setHeight(int)}, {@link * #setConfig(Bitmap.Config)}, and {@link BitmapFactory.Options#inBitmap * BitmapFactory.Options.inBitmap}. If a bitmap is not modified in this way, this value will be * the same as that returned by {@link #getByteCount()}.

* *

This value will not change over the lifetime of a Bitmap.

* * @see #reconfigure(int, int, Config) */
public final int getAllocationByteCount() { if (mBuffer == null) { // native backed bitmaps don't support reconfiguration, // so alloc size is always content size return getByteCount(); } return mBuffer.length; }

通过方法注释我们可以了解到,getByteCount()代表存储Bitmap的色素需要的最少内存,而getAllocationByteCount()代表在内存中为Bitmap分配的内存大小

其实getByteCount()方法是在API12加入的,代表存储Bitmap的色素需要的最少内存。从API19开始getAllocationByteCount()方法代替了getByteCount()

一般情况下getByteCount()和getAllocationByteCount()是相等的。但是Bitmap内存如果复用之后,两者就不一样了

通过复用Bitmap来解码图片,如果被复用的Bitmap的内存比待分配内存的Bitmap大,那么getByteCount()表示新解码图片占用内存的大小(并非实际内存大小,实际大小是复用的那个Bitmap的大小),getAllocationByteCount()表示被复用Bitmap真实占用的内存大小

那getByteCount()和getAllocationByteCount()值是怎么计算出来的呢?

例子

下面就来举个例子计算理论上Bitmap加载一张图片时,所占内存的大小,和getByteCount()的结果比较一下。

假设

一张像素为522*686的PNG图片,把它放到drawable-xxhdpi目录下,在三星s6上加载,getByteCount()的结果是2547360B

推导

第一步

默认的像素格式是ARGB_8888,之前已经说到了ARGB_8888格式下的一个像素点占用32位内存即4个字节

所以结果是:int res = 522*686*41432368

显然和答案不一样啊。

第二步

假设中说把图片放到drawable-xxhdpi目录下,在三星s6上加载。并不是随口一说的,它们也是影响Bitmap所占内存大小的重要因素。

我们读取的是drawable目录下面的图片,用的是decodeResource方法,该方法本质上就两步:

  • 读取原始资源,这个调用了Resource.openRawResource方法,这个方法调用完成之后会对TypedValue进行赋值,其中包含了原始资源的density等信息。

  • 调用decodeResourceStream对原始资源进行解码和适配。这个过程实际上就是原始资源的density到屏幕density的一个映射

原始资源的density其实取决于资源存放的目录(比如xxhdpi对应的是480),而屏幕density的值是和设备的硬件有关的,三星s6的值为640。加载时,原始的资源会自动进行缩放

所以结果是:int res = (522 * 640 / 480) * (686 * 640 / 480) * 42546432

第三步

好像还是差那么一点,其实系统是进行了精度处理。

所以最终结果是:int res = (522 * 640 / 480f + 0.5) * (686 * 640 / 480f + 0.5) * 42547360

inScaled

上面说的缩放和一个参数inScaled有关:

    public static class Options {
        /**
         * Create a default Options object, which if left unchanged will give
         * the same result from the decoder as if null were passed.
         */
        public Options() {
            inDither = false;
            inScaled = true;
            inPremultiplied = true;
        }
        ……
    }

如果inScaled设置为true,就缩放。设置为false,则不进行缩放。默认的值从上面代码可以看到,就是true。

缩放的比例为inTargetDensity / inDensity

但是缩放也只针对资源文件有效,对于其他来源的图片不起效果,我们可以从源码中参数上的解释得知:

        /**
         * The pixel density to use for the bitmap.  This will always result
         * in the returned bitmap having a density set for it (see
         * {@link Bitmap#setDensity(int) Bitmap.setDensity(int)}).  In addition,
         * if {@link #inScaled} is set (which it is by default} and this
         * density does not match {@link #inTargetDensity}, then the bitmap
         * will be scaled to the target density before being returned.
         * 
         * 

If this is 0, * {@link BitmapFactory#decodeResource(Resources, int)}, * {@link BitmapFactory#decodeResource(Resources, int, android.graphics.BitmapFactory.Options)}, * and {@link BitmapFactory#decodeResourceStream} * will fill in the density associated with the resource. The other * functions will leave it as-is and no density will be applied. * * @see #inTargetDensity * @see #inScreenDensity * @see #inScaled * @see Bitmap#setDensity(int) * @see android.util.DisplayMetrics#densityDpi */ public int inDensity;

其中有一句The other functions will leave it as-is and no density will be applied就是这个意思。

以上所说inDensity和inTargetDensity其实是DPI(dots per inch),关于DPI的概念请移步 全面理解Android中的Px,DPI,DIP,Density,Sp等概念。

结论

Bitmap加载资源文件在内存当中占用的大小取决于以下三点:

  • 像素格式,前面我们已经提到,如果是ARGB8888那么就是一个像素4个字节,如果是RGB565那就是2个字节。
  • 原始文件存放的资源目录(是hdpi还是xxhdpi)
  • 目标屏幕的DPI(同等条件下,红米在资源方面消耗的内存肯定是要小于三星S6的)

Bitmap加载其他来源的图片,就和像素格式有关。

减少Bitmap内存占用

合理选择Bitmap的像素格式

不需要透明度的情况下,我们通常使用RGB_565

使用采样

inSampleSize的值必须大于1时才会有效果,且采样率同时作用于宽和高。当inSampleSize=1时,采样后的图片为图片的原始大小。

当inSampleSize=n时,采样后的图片的宽高均为原始图片宽高的1/n,这时像素为原始图片的1/(n*n),占用内存也为原始图片的1/(n*n)。

inSampleSize的取值应该总为2的整数倍,否则会向下取整,取一个最接近2的整数倍,比如inSampleSize=3时,系统会取inSampleSize=2。

假设一张1024*1024,模式为ARGB_8888的图片,inSampleSize=2,原始占用内存大小是4MB,采样后的图片占用内存大小就是(1024/2) * (1024/2 )* 4 = 1MB。

具体用法请参考Bitmap的高效加载(Android开发艺术探索学习笔记)。

  • 图片的质量压缩:上述用inSampleSize压缩是尺寸压缩,Android中还有一种压缩方式叫质量压缩。质量压缩是在保持像素的前提下改变图片的位深及透明度等,来达到压缩图片的目的,经过它压缩的图片文件大小(kb)会有改变,但是导入成Bitmap后占得内存是不变的,宽高也不会改变。因为要保持像素不变,所以它就无法无限压缩,到达一个值之后就不会继续变小了。显然这个方法并不适用与缩略图,其实也不适用于想通过压缩图片减少内存的适用,仅仅适用于想在保证图片质量的同时减少文件大小的情况而已

使用矩阵

我们之前使用inSampleSize对图片进行采样,采样之后内存是小了,可是图的尺寸也小了,我们要用Canvas绘制原始大小的图片该怎么办?就可以使用矩阵:

Matrix matrix = new Matrix();
matrix.preScale(2, 2, 0, 0);
canvas.drawBitmap(bitmap, matrix, paint);

这样,绘制出来的图就是放大以后的效果了,不过占用的内存却仍然是我们采样出来的大小。

如果我要把图片放到ImageView当中呢?一样可以,请看:

Matrix matrix = new Matrix();
matrix.postScale(2, 2, 0, 0);
imageView.setImageMatrix(matrix);
imageView.setScaleType(ScaleType.MATRIX);
imageView.setImageBitmap(bitmap);

参考:
1.Android坑档案:你的Bitmap究竟占多大内存?
2.Android性能优化:谈谈Bitmap的内存管理与优化
3.Android 之Bitmap
4.Android性能优化(五)之细说Bitmap
5.softReference+LruCache优化Android缓存
6.玩转Android Bitmap

你可能感兴趣的:(Android)