澳门新葡萄京娱乐场 3

澳门新葡萄京娱乐场非常全面的 Android Bitmap 知识点梳理

在日常开发中,可以说和Bitmap低头不见抬头见,基本上每个应用都会直接或间接的用到,而这里面又涉及到大量的相关知识。
所以这里把Bitmap的常用知识做个梳理,限于经验和能力,不做太深入的分析。

在上一篇《Android性能优化(四)之内存优化实战》中谈到那个内存中的大胖子Bitmap,Bitmap对内存的影响极大。

一、Bitmap 内存回收

从3.0开始,Bitmap 像素数据和 Bitmap 对象一起存放在 Dalvik
堆中,而在3.0之前,Bitmap 像素数据存放在 Native 内存中。
所以,在3.0之前,Bitmap 像素数据在 Nativie
内存的释放是不确定的,容易内存溢出而 Crash,官方强烈建议调用
recycle()(当然是在确定不需要的时候);而在3.0之后,则是强调Bitmap的复用。
使用 LruCache 对 Bitmap 对象进行缓存,当再次使用到这个 Bitmap
的时候直接获取,而不用重走编码流程。
Android3.0(API 11之后)引入了 BitmapFactory.Options.inBitmap
字段,设置此字段之后解码方法会尝试复用一张存在的 Bitmap 。这意味着
Bitmap
的内存被复用,避免了内存的回收及申请过程,显然性能表现更佳。不过,使用这个字段有几点限制:

  • 声明可被复用的 Bitmap 必须设置 inMutable 为 true;
  • Android4.4(API 19)之前只有格式为
    jpg、png,同等宽高(要求苛刻),inSampleSize 为1的 Bitmap
    才可以复用;
  • Android4.4(API 19)之前被复用的 Bitmap 的 inPreferredConfig
    会覆盖待分配内存的 Bitmap 设置的 inPreferredConfig;
  • Android4.4(API 19)之后被复用的 Bitmap 的内存必须大于需要申请内存的
    Bitmap 的内存;

1. 区别decodeResource()和decodeFile()

这里的区别不是指方法名和参数的区别,而是对于解码后图片尺寸在处理上的区别:

decodeFile()用于读取SD卡上的图,得到的是图片的原始尺寸
decodeResource()用于读取Res、Raw等资源,得到的是图片的原始尺寸 *
缩放系数

可以看的出来,decodeResource()比decodeFile()多了一个缩放系数,缩放系数的计算依赖于屏幕密度,当然这个参数也是可以调整的:

// 通过BitmapFactory.Options的这几个参数可以调整缩放系数
public class BitmapFactory {
    public static class Options {
        public boolean inScaled;     // 默认true
        public int inDensity;        // 无dpi的文件夹下默认160
        public int inTargetDensity;  // 取决具体屏幕
    }
}

我们分具体情况来看,现在有一张720×720的图片:

例如:使用Pixel手机拍摄4048×3036像素(1200W)的照片,如果按ARGB_8888来显示的话,需要48MB的内存空间(4048*3036*4
bytes),这么大的内存消耗极易引发OOM。
本篇文章就来说一说这个大胖子。

二、Bitmap 内存大小

Bitmap 类有两个获取存储 Bitmap 像素所占用内存字节数的方法:

inScaled属性

如果inScaled设置为false,则不进行缩放,解码后图片大小为720×720;
否则请往下看。

如果inScaled设置为true或者不设置,则根据inDensity和inTargetDensity计算缩放系数。

1. Bitmap内存模型

Android Bitmap内存的管理随着系统的版本迭代也有演进:

1.在Android
2.2(API8)之前,当GC工作时,应用的线程会暂停工作,同步的GC会影响性能。而Android2.3之后,GC变成了并发的,意味着Bitmap没有引用的时候其占有的内存会很快被回收。

2.在Android
2.3.3(API10)之前,Bitmap的像素数据存放在Native内存,而Bitmap对象本身则存放在Dalvik
Heap中。Native内存中的像素数据并不会以可预测的方式进行同步回收,有可能会导致内存升高甚至OOM。而在Android3.0之后,Bitmap的像素数据也被放在了Dalvik
Heap中。

getByteCount

getByteCount() 方法是 API12 加入的,代表存储 Bitmap
的像素需要的最少内存。

public final int getByteCount() {
        return getRowBytes() * getHeight();
}

默认情况

把这张图片放到drawable目录下, 默认:

以720p的红米3为例子,缩放系数 = inTargetDensity(具体320 /
inDensity(默认160)= 2 = density,解码后图片大小为1440×1440。
以1080p的MX4为例子,缩放系数 = inTargetDensity(具体480 /
inDensity(默认160)= 3 = density, 解码后图片大小为2160×2160。

2. Bitmap的内存回收

getAllocationByteCount()

从 API19 开始,加入了 getAllocationByteCount() 方法,代表在内存中为
Bitmap 分配的内存大小,代替了 getByteCount() 方法。

public final int getAllocationByteCount() {
        if (mBuffer == null) {
            //mBuffer 代表存储 Bitmap 像素数据的字节数组。
            return getByteCount();
        }
        return mBuffer.length;
    }

在不复用 Bitmap 时,getByteCount() 和 getAllocationByteCount
返回的结果是一样的。
在通过复用 Bitmap 来解码图片时,如果被复用的 Bitmap 的内存比待分配内存的
Bitmap 大,那么 getByteCount()
表示新解码图片占用内存的大小(并非实际内存大小,实际大小是复用的那个Bitmap的大小),getAllocationByteCount()
表示被复用 Bitmap真实占用的内存大小(即 mBuffer 的长度)。

*dpi文件夹的影响

把图片放到drawable或者raw这样不带dpi的文件夹,会按照上面的算法计算。

如果放到xhdpi会怎样呢? 在MX4上,放到xhdpi,解码后图片大小为1080 x
1080。

因为放到有dpi的文件夹,会影响到inDensity的默认值,放到xhdpi为160 x 2 =
320; 所以缩放系数 = 480(屏幕) / 320 (xhdpi) = 1.5;
所以得到的图片大小为1080 x 1080。

2.1 Android2.3.3之前

在Android2.3.3之前推荐使用Bitmap.recycle()方法进行Bitmap的内存回收。

备注:只有当确定这个Bitmap不被引用的时候才能调用此方法,否则会有“Canvas:
trying to use a recycled bitmap”这个错误。

官方提供了一个使用Recycle的实例:使用引用计数来判断Bitmap是否被展示或缓存,判断能否被回收。

Bitmap 大小的计算

上面是获取内存大小的方法,下面是 Bitmap 占用内存的计算公式:

Bitamp 占用内存大小 = 宽度像素 x (inTargetDensity / inDensity) x
高度像素 x (inTargetDensity / inDensity)x 一个像素所占的内存

上面公式中:

  • 宽度像素和高度像素就是 Bitmap 原始的宽度和高度的像素规格;
  • 一个像素所占的内存与图片的色彩模式有关,这个色彩模式在 Bitmap
    类里面通过枚举类 Config 标识:

 public enum Config {

        ALPHA_8     (1),

        RGB_565     (3),

        @Deprecated
        ARGB_4444   (4),

        ARGB_8888   (5);
    }
  • ARGB_8888:每个像素占四个字节,A、R、G、B 分量各占8位,是 Android
    的默认设置;
  • RGB_565:每个像素占两个字节,R分量占5位,G分量占6位,B分量占5位;
  • ARGB_4444:每个像素占两个字节,A、R、G、B分量各占4位,成像效果比较差;
  • Alpha_8: 只保存透明度,共8位,1字节;
  • 在 BitmapFactory 的内部类 Options 有两个成员变量 inDensity 和
    inTargetDensity,其中 inDensity 就 Bitmap 的像素密度,也就是 Bitmap
    的成员变量 mDensity,默认是设备屏幕的像素密度,可以通过
    Bitmap#setDensity(int) 设置,inTargetDensity
    是图片的目标像素密度,在加载图片时就是 drawable 目录的像素密度。
    在从资源目录加载图片时,这个时候调用的是
    BitmapFactory#decodeResource 方法,内部调用的是
    decodeResourceStream 方法:

public static Bitmap decodeResourceStream(Resources res, TypedValue value,
            InputStream is, Rect pad, Options opts) {

        if (opts == null) {
            opts = new Options();
        }

        if (opts.inDensity == 0 && value != null) {
            final int density = value.density;
            if (density == TypedValue.DENSITY_DEFAULT) {
                opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
            } else if (density != TypedValue.DENSITY_NONE) {
                opts.inDensity = density;
            }
        }

        if (opts.inTargetDensity == 0 && res != null) {
            opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
        }

        return decodeStream(is, pad, opts);
    }

会根据设备屏幕像素密度到对应 drawable 目录去寻找图片,这个时候
inTargetDensity/inDensity =
1,图片不会做缩放,宽度和高度就是图片原始的像素规格,如果没有找到,会到其他
drawable 目录去找,这个时候 drawable 的屏幕像素密度就是
inTargetDensity,会根据 inTargetDensity/inDensity
的比例对图片的宽度和高度进行缩放。

手动设置缩放系数

如果你不想依赖于这个系统本身的density,你可以手动设置inDensity和inTargetDensity来控制缩放系数:

BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = false;
options.inSampleSize = 1;
options.inDensity = 160;
options.inTargetDensity = 160;
bitmap = BitmapFactory.decodeResource(getResources(),
        R.drawable.origin, options);
// MX4上,虽然density = 3
// 但是通过设置inTargetDensity / inDensity = 160 / 160 = 1
// 解码后图片大小为720x720
System.out.println("w:" + bitmap.getWidth()
        + ", h:" + bitmap.getHeight());

2.2 Android3.0之后

Android3.0之后,并没有强调Bitmap.recycle();而是强调Bitmap的复用:

三、Bitmap 的创建

Bitmap 有一系列的 createXXX 方法用来创建 Bitmap 对象,但是我们一般都使用
BitmapFactory 的一系列 decodeXXX 方法来生成 Bitmap:

public static Bitmap decodeFile(String pathName, Options opts)
public static Bitmap decodeFile(String pathName)
public static Bitmap decodeResourceStream(Resources res, TypedValue value, InputStream is, Rect pad, Options opts)
public static Bitmap decodeResource(Resources res, int id, Options opts)
public static Bitmap decodeResource(Resources res, int id)
public static Bitmap decodeByteArray(byte[] data, int offset, int length, Options opts)
public static Bitmap decodeByteArray(byte[] data, int offset, int length)
public static Bitmap decodeStream(InputStream is, Rect outPadding, Options opts)
public static Bitmap decodeStream(InputStream is)
public static Bitmap decodeFileDescriptor(FileDescriptor fd, Rect outPadding, Options opts)
public static Bitmap decodeFileDescriptor(FileDescriptor fd)

上面的方法大致可以分为四类:

  1. 从本地文件中解码图片:decodeFile。从本地文件中解压的原始图片并不会对图片进行缩放;
  2. 从资源文件中解码图片:decodeResource。会根据
    inTargetDensity/inDensity 对图片进行缩放;
  3. 从输入流中解码图片:decodeStream。获取网络图片的时候使用此方法,其实从本地文件和资源文件中解压图片,最终调用的也是该方法;
  4. 从字节数组中解码图片:decodeByteArray。这个字节数组是输入流传化为的字节数组;
  5. decodeFileDescriptor。也是从本地文件中解码图片,但是并不是通过流的方式解码,比
    decodeFile 方法省内存;

2. recycle()方法

2.2.1 Save a bitmap for later use

使用LruCache对Bitmap进行缓存,当再次使用到这个Bitmap的时候直接获取,而不用重走编码流程。

澳门新葡萄京娱乐场,四、Bitmap 压缩

官方说法

首先,Android对Bitmap内存(像素数据)的分配区域在不同版本上是有区分的:

As of Android 3.0 (API level 11), the pixel data is stored on the
Dalvik heap along with the associated bitmap.

从3.0开始,Bitmap像素数据和Bitmap对象一起存放在Dalvik堆中,而在3.0之前,Bitmap像素数据存放在Native内存中。
所以,在3.0之前,Bitmap像素数据在Nativie内存的释放是不确定的,容易内存溢出而Crash,官方强烈建议调用recycle()(当然是在确定不需要的时候);而在3.0之后,则无此要求。

参考链接:Managing Bitmap
Memory

2.2.2 Use an existing bitmap

Android3.0(API
11之后)引入了BitmapFactory.Options.inBitmap字段,设置此字段之后解码方法会尝试复用一张存在的Bitmap。这意味着Bitmap的内存被复用,避免了内存的回收及申请过程,显然性能表现更佳。不过,使用这个字段有几点限制:

  • 声明可被复用的Bitmap必须设置inMutable为true;
  • Android4.4(API
    19)之前只有格式为jpg、png,同等宽高(要求苛刻),inSampleSize为1的Bitmap才可以复用;
  • Android4.4(API
    19)之前被复用的Bitmap的inPreferredConfig会覆盖待分配内存的Bitmap设置的inPreferredConfig;
  • Android4.4(API
    19)之后被复用的Bitmap的内存必须大于需要申请内存的Bitmap的内存;
  • Android4.4(API
    19)之前待加载Bitmap的Options.inSampleSize必须明确指定为1。

1.质量压缩

调用 Bitmap#compress 方法:

public boolean compress(CompressFormat format, int quality, OutputStream stream) {
        checkRecycled("Can't compress a recycled bitmap");
        // do explicit check before calling the native method
        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(mNativePtr, format.nativeInt,
                quality, stream, new byte[WORKING_COMPRESS_STORAGE]);
        Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
        return result;
    }

第一个参数 format 标识压缩格式,CompressFormat
是一个枚举类,有三个值,是三种图片格式 JPEG,PNG,WEBP

public enum CompressFormat {
        JPEG    (0),
        PNG     (1),
        WEBP    (2);
    }
  • PNG是一种无损压缩的图像存储格式,相同像素宽高的图像保存为PNG在文件大小上比JPEG往往要大的多,一般是JPEG大小的几倍左右;
  • JPEG是一种有损压缩的图像存储格式,不支持alpha通道,由于它具有高压缩比,在压缩过程中把重复的数据和无关紧要的数据会选择性的丢失,所以如果不需要用到alpha通道,那么大都图片格式都用该格式;
  • Webp图片格式是Google推出的一个支持alpha通道的有损压缩格式,据Google官方表明,同质量情况下Webp图像要比JPEG、PNG图像小25%~45%左右;

第二个参数 quality 表示压缩质量
这种方式是在保持像素的前提下改变图片的位深及透明度等,来达到压缩图片的目的,不会减少图片的像素,它压缩的是存储大小,即你放到
disk 上的大小,但是解码成 bitmap 后占的内存是不变的。

一点讨论

3.0之后官方无recycle()建议,是不是就真的不需要recycle()了呢?

在医生的这篇文章:Bitmap.recycle引发的血案 最后指出:“在不兼容Android2.3的情况下,别在使用recycle方法来管理Bitmap了,那是GC的事!”。文章开头指出了原因在于recycle()方法的注释说明:

/**
 * ... This is an advanced call, and normally need not be called,
 * since the normal GC process will free up this memory when
 * there are no more references to this bitmap.
 */
public void recycle() {}

事实上这个说法是不准确的,是不能作为recycle()方法不调用的依据的。

因为从commit
history中看,这行注释早在08年初始化代码的就有了,但是早期的代码并没有因此不需要recycle()方法了。

澳门新葡萄京娱乐场 1

如果3.0之后真的完全不需要主动recycle(),最新的AOSP源码应该有相应体现,我查了SystemUI和Gallery2的代码,并没有取缔Bitmap的recycle()方法。

所以,我个人认为,如果Bitmap真的不用了,recycle一下又有何妨?

PS:至于医生说的那个bug,显然是一种优化策略,APP开发中加个两个bitmap不相等的判断条件即可。

3. Bitmap占有多少内存?

2.尺寸压缩

前面的 decodeXXX 方法中有一个 BitmapFactory.Options
类型的参数,解码图片时,设置 BitmapFactory.Options 类的
inJustDecodeBounds 属性为 true,可以在 Bitmap
不被加载到内存的前提下,获取 Bitmap 的原始宽高。而设置
BitmapFactory.Options 的 inSampleSize 属性可以真实的压缩 Bitmap
占用的内存,加载更小内存的 Bitmap。
设置 inSampleSize 之后,Bitmap 的宽、高都会缩小 inSampleSize 倍。
inSampleSize 比1小的话会被当做1,任何 inSampleSize
的值会被取接近2的幂值。

3. Bitmap到底占多大内存

这个已经有一篇bugly出品的绝好文章讲的很清楚:

Android 开发绕不过的坑:你的 Bitmap
究竟占多大内存?

3.1 getByteCount()

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

3.色彩模式压缩

Bitmap 的色彩模式默认为 Bitmap.Config.ARGB_8888,可以通过
BitmapFactory.Options.inPreferredConfig
属性来修改解码图片的色彩模式,每个像素占用的字节减少了,宽度和高度像素不变的情况下,占用的内存大小也会减少;

4. inBitmap

BitmapFactory.Options.inBitmap是AndroiD3.0新增的一个属性,如果设置了这个属性则会重用这个Bitmap的内存从而提升性能。

但是这个重用是有条件的,在Android4.4之前只能重用相同大小的Bitmap,Android4.4+则只要比重用Bitmap小即可。

在官方网站有详细介绍,这里列举示例代码的两个方法了解一下:

private static void addInBitmapOptions(BitmapFactory.Options options,
        ImageCache cache) {
    // inBitmap only works with mutable bitmaps, so force the decoder to
    // return mutable bitmaps.
    options.inMutable = true;

    if (cache != null) {
        // Try to find a bitmap to use for inBitmap.
        Bitmap inBitmap = cache.getBitmapFromReusableSet(options);

        if (inBitmap != null) {
            // If a suitable bitmap has been found,
            // set it as the value of inBitmap.
            options.inBitmap = inBitmap;
        }
    }
}

static boolean canUseForInBitmap(
        Bitmap candidate, BitmapFactory.Options targetOptions) {

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
        // From Android 4.4 (KitKat) onward we can re-use
        // if the byte size of the new bitmap is smaller than
        // the reusable bitmap candidate
        // allocation byte count.
        int width = targetOptions.outWidth / targetOptions.inSampleSize;
        int height =
            targetOptions.outHeight / targetOptions.inSampleSize;
        int byteCount = width * height
            * getBytesPerPixel(candidate.getConfig());
        return byteCount <= candidate.getAllocationByteCount();
    }

    // On earlier versions,
    // the dimensions must match exactly and the inSampleSize must be 1
    return candidate.getWidth() == targetOptions.outWidth
        && candidate.getHeight() == targetOptions.outHeight
        && targetOptions.inSampleSize == 1;
}

参考链接:

Managing Bitmap
Memory
Bitmap对象的复用

3.2 getAllocationByteCount()

API19之后,Bitmap加了一个Api:getAllocationByteCount();代表在内存中为Bitmap分配的内存大小。

    public final int getAllocationByteCount() {
        if (mBuffer == null) {
            //mBuffer代表存储Bitmap像素数据的字节数组。
            return getByteCount();
        }
        return mBuffer.length;
    }

4.Matrix

Matrix 矩阵变换,可以对 bitmap 进行非常多的操作,其中一项是对 bitmap
进行等比缩放,这种方式可以精确的缩放到符合我们预期的 bitmap
大小,奥代码如下:

int bitmapWidth = bitmap.getWidth();
int bitmapHeight = bitmap.getHeight();
Matrix matrix = new Matrix();
float rate = computeScaleRate(bitmapWidth, bitmapHeight);
matrix.postScale(rate, rate);
Bitmap result = Bitmap.createBitmap(bitmap, 0, 0, bitmapWidth, bitmapHeight, matrix, true);

5. LRU缓存算法

LRU,Least Recently Used,Discards the least recently used items
first。

在最近使用的数据中,丢弃使用最少的数据。与之相反的还有一个MRU,丢弃使用最多的数据。
这就是著名的局部性原理。

3.3 getByteCount()与getAllocationByteCount()的区别

  • 一般情况下两者是相等的;
  • 通过复用Bitmap来解码图片,如果被复用的Bitmap的内存比待分配内存的Bitmap大,那么getByteCount()表示新解码图片占用内存的大小(并非实际内存大小,实际大小是复用的那个Bitmap的大小),getAllocationByteCount()表示被复用Bitmap真实占用的内存大小(即mBuffer的长度)。(见第5节的示例)。

5.Bitmap#createScaledBitmap

通过
public static Bitmap createScaledBitmap(Bitmap src, int dstWidth, int dstHeight, boolean filter)
直接指定图片压缩后的宽度和高度

实现思路

1.新数据插入到链表头部;
2.每当缓存命中(即缓存数据被访问),则将数据移到链表头部;
3.当链表满的时候,将链表尾部的数据丢弃。

澳门新葡萄京娱乐场 2

4. 如何计算Bitmap占用的内存?

还记得之前我曾言之凿凿的说:不考虑压缩,只是加载一张Bitmap,那么它占用的内存
= width * height * 一个像素所占的内存。
现在想来实在惭愧:说法也对,但是不全对,没有说明场景,同时也忽略了一个影响项:Density。

五、Bitmap 与 Drawable 的转换

LruCache

在Android3.1和support v4中均提供了Lru算法的实现类LruCache。

内部使用LinkedHashMap实现。

4.1 BitmapFactory.decodeResource()

    BitmapFactory.java
    public static Bitmap decodeResourceStream(Resources res, TypedValue value,InputStream is, Rect pad, Options opts) {
        if (opts == null) {
            opts = new Options();
        }
        if (opts.inDensity == 0 && value != null) {
            final int density = value.density;
            if (density == TypedValue.DENSITY_DEFAULT) {
                //inDensity默认为图片所在文件夹对应的密度
                opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
            } else if (density != TypedValue.DENSITY_NONE) {
                opts.inDensity = density;
            }
        }
        if (opts.inTargetDensity == 0 && res != null) {
            //inTargetDensity为当前系统密度。
            opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
        }
        return decodeStream(is, pad, opts);
    }

    BitmapFactory.cpp 此处只列出主要代码。
    static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding, jobject options) {
        //初始缩放系数
        float scale = 1.0f;
        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;
            }
        }
        //原始解码出来的Bitmap;
        SkBitmap decodingBitmap;
        if (decoder->decode(stream, &decodingBitmap, prefColorType, decodeMode)
                != SkImageDecoder::kSuccess) {
            return nullObjectReturn("decoder->decode returned false");
        }
        //原始解码出来的Bitmap的宽高;
        int scaledWidth = decodingBitmap.width();
        int scaledHeight = decodingBitmap.height();
        //要使用缩放系数进行缩放,缩放后的宽高;
        if (willScale && decodeMode != SkImageDecoder::kDecodeBounds_Mode) {
            scaledWidth = int(scaledWidth * scale + 0.5f);
            scaledHeight = int(scaledHeight * scale + 0.5f);
        }    
        //源码解释为因为历史原因;sx、sy基本等于scale。
        const float sx = scaledWidth / float(decodingBitmap.width());
        const float sy = scaledHeight / float(decodingBitmap.height());
        canvas.scale(sx, sy);
        canvas.drawARGB(0x00, 0x00, 0x00, 0x00);
        canvas.drawBitmap(decodingBitmap, 0.0f, 0.0f, &paint);
        // now create the java bitmap
        return GraphicsJNI::createBitmap(env, javaAllocator.getStorageObjAndReset(),
            bitmapCreateFlags, ninePatchChunk, ninePatchInsets, -1);
    }

此处可以看出:加载一张本地资源图片,那么它占用的内存 = width * height
* nTargetDensity/inDensity * nTargetDensity/inDensity *
一个像素所占的内存。

实验:将长为1024、宽为594的一张图片放在xhdpi的文件夹下,使用魅族MX3手机加载。

        // 不做处理,默认缩放。
        BitmapFactory.Options options = new BitmapFactory.Options();
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.resbitmap, options);
        Log.i(TAG, "bitmap:ByteCount = " + bitmap.getByteCount() + ":::bitmap:AllocationByteCount = " + bitmap.getAllocationByteCount());
        Log.i(TAG, "width:" + bitmap.getWidth() + ":::height:" + bitmap.getHeight());
        Log.i(TAG, "inDensity:" + options.inDensity + ":::inTargetDensity:" + options.inTargetDensity);

        Log.i(TAG,"===========================================================================");

        // 手动设置inDensity与inTargetDensity,影响缩放比例。
        BitmapFactory.Options options_setParams = new BitmapFactory.Options();
        options_setParams.inDensity = 320;
        options_setParams.inTargetDensity = 320;
        Bitmap bitmap_setParams = BitmapFactory.decodeResource(getResources(), R.mipmap.resbitmap, options_setParams);
        Log.i(TAG, "bitmap_setParams:ByteCount = " + bitmap_setParams.getByteCount() + ":::bitmap_setParams:AllocationByteCount = " + bitmap_setParams.getAllocationByteCount());
        Log.i(TAG, "width:" + bitmap_setParams.getWidth() + ":::height:" + bitmap_setParams.getHeight());
        Log.i(TAG, "inDensity:" + options_setParams.inDensity + ":::inTargetDensity:" + options_setParams.inTargetDensity);

        输出:
        I/lz: bitmap:ByteCount = 4601344:::bitmap:AllocationByteCount = 4601344
        I/lz: width:1408:::height:817 // 可以看到此处:Bitmap的宽高被缩放了440/320=1.375倍
        I/lz: inDensity:320:::inTargetDensity:440 // 默认资源文件所处文件夹密度与手机系统密度
        I/lz: ===========================================================================
        I/lz: bitmap:ByteCount = 2433024:::bitmap:AllocationByteCount = 2433024
        I/lz: width:1024:::height:594 // 手动设置了缩放系数为1,Bitmap的宽高都不变
        I/lz: inDensity:320:::inTargetDensity:320

可以看出:

  1. 不使用Bitmap复用时,getByteCount()与getAllocationByteCount()的值是一致的;
  2. 默认情况下使用魅族MX3、在xhdpi的文件夹下,inDensity为320,inTargetDensity为440,内存大小为4601344;而4601344
    = 1024 * 594 * (440 / 320)* (440 / 320)* 4。
  3. 手动设置inDensity与inTargetDensity,使其比例为1,内存大小为2433024;2433024
    = 1024 * 594 * 1 * 1 * 4。

Bitmap–>Drawable

通过 BitmapDrawable 的构造方法:

@Deprecated
public BitmapDrawable(Bitmap bitmap)
public BitmapDrawable(Resources res, Bitmap bitmap)

DiskLruCache

LruCache的所有对象和数据都是在内存中(或者说LinkedHashMap中),而DiskLruCache是磁盘缓存,不过它的实现要稍微复杂一点。

使用DiskLruCache后就不用担心文件或者图片太多占用过多磁盘空间,它能把那些不常用的图片自动清理掉。

DiskLruCache系统中并没有正式提供,需要另外下载: DiskLruCache

4.2 BitmapFactory.decodeFile()

与BitmapFactory.decodeResource()的调用链基本一致,但是少了默认设置density和inTargetDensity(与缩放比例相关)的步骤,也就没有了缩放比例这一说。

除了加载本地资源文件的解码方法会默认使用资源所处文件夹对应密度和手机系统密度进行缩放之外,别的解码方法默认都不会。此时Bitmap默认占用的内存
= width * height *
一个像素所占的内存。这也就是上面4.1开头讲的需要注意场景。

Drawable–>Bitmap

6. 计算inSampleSize

使用Bitmap节省内存最重要的技巧就是加载合适大小的Bitmap,因为以现在相机像素,很多照片都巨无霸的大,这些大图直接加载到内存,最容易OOM。

加载合适的Bitmap需要先读取Bitmap的原始大小,按缩小了合适的倍数的大小进行加载。

那么,这个缩小的倍数的计算就是inSampleSize的计算。

// 根据maxWidth, maxHeight计算最合适的inSampleSize
public static int $sampleSize(BitmapFactory.Options options,
        int maxWidth, int maxHeight) {
    // raw height and width of image
    int rawWidth = options.outWidth;
    int rawHeight = options.outHeight;

    // calculate best sample size
    int inSampleSize = 0;
    if (rawHeight > maxHeight || rawWidth > maxWidth) {
        float ratioWidth = (float) rawWidth / maxWidth;
        float ratioHeight = (float) rawHeight / maxHeight;
        inSampleSize = (int) Math.min(ratioHeight, ratioWidth);
    }
    inSampleSize = Math.max(1, inSampleSize);

    return inSampleSize;
}

关于inSampleSize需要注意,它只能是2的次方,否则它会取最接近2的次方的值。

4.3 一个像素占用多大内存?

Bitmap.Config用来描述图片的像素是怎么被存储的?
ARGB_8888: 每个像素4字节. 共32位,默认设置。
Alpha_8: 只保存透明度,共8位,1字节。
ARGB_4444: 共16位,2字节。
RGB_565:共16位,2字节,只存储RGB值。

根据已有的 Drawable 创建一个新的 Bitmap
public static Bitmap createBitmap(int width, int height, Config config)

7. 缩略图

为了节省内存,需要先设置BitmapFactory.Options的inJustDecodeBounds为true,这样的Bitmap可以借助decodeFile方法把高和宽存放到Bitmap.Options中,但是内存占用为空(不会真正的加载图片)。

有了具备高宽信息的Options,结合上面的inSampleSize算法算出缩小的倍数,我们就能加载本地大图的某个合适大小的缩略图了

/**
 * 获取缩略图
 * 支持自动旋转
 * 某些型号的手机相机图片是反的,可以根据exif信息实现自动纠正
 * @return
 */
public static Bitmap $thumbnail(String path,
        int maxWidth, int maxHeight, boolean autoRotate) {

    int angle = 0;
    if (autoRotate) {
        angle = ImageLess.$exifRotateAngle(path);
    }

    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    // 获取这个图片的宽和高信息到options中, 此时返回bm为空
    Bitmap bitmap = BitmapFactory.decodeFile(path, options);
    options.inJustDecodeBounds = false;
    // 计算缩放比
    int sampleSize = $sampleSize(options, maxWidth, maxHeight);
    options.inSampleSize = sampleSize;
    options.inPreferredConfig = Bitmap.Config.RGB_565;
    options.inPurgeable = true;
    options.inInputShareable = true;

    if (bitmap != null && !bitmap.isRecycled()) {
        bitmap.recycle();
    }
    bitmap = BitmapFactory.decodeFile(path, options);

    if (autoRotate && angle != 0) {
        bitmap = $rotate(bitmap, angle);
    }

    return bitmap;
}

系统内置了一个ThumbnailUtils也能生成缩略图,细节上不一样但原理是相同的。

5. Bitmap如何复用?

在上述2.2.2我们谈到了Bitmap的复用,以及复用的限制,Google在《Managing
Bitmap
Memory》中给出了详细的复用Demo:

  1. 使用LruCache和DiskLruCache做内存和磁盘缓存;
  2. 使用Bitmap复用,同时针对版本进行兼容。
    此处我写一个简单的demo,机型魅族MX3,系统版本API21;图片宽1024、高594,进行Bitmap复用的实验;

BitmapFactory.Options options = new BitmapFactory.Options();
// 图片复用,这个属性必须设置;
options.inMutable = true;
// 手动设置缩放比例,使其取整数,方便计算、观察数据;
options.inDensity = 320;
options.inTargetDensity = 320;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.resbitmap, options);
// 对象内存地址;
Log.i(TAG, "bitmap = " + bitmap);
Log.i(TAG, "bitmap:ByteCount = " + bitmap.getByteCount() + ":::bitmap:AllocationByteCount = " + bitmap.getAllocationByteCount());

// 使用inBitmap属性,这个属性必须设置;
options.inBitmap = bitmap;
options.inDensity = 320;
// 设置缩放宽高为原始宽高一半;
options.inTargetDensity = 160;
options.inMutable = true;
Bitmap bitmapReuse = BitmapFactory.decodeResource(getResources(), R.drawable.resbitmap_reuse, options);
// 复用对象的内存地址;
Log.i(TAG, "bitmapReuse = " + bitmapReuse);
Log.i(TAG, "bitmap:ByteCount = " + bitmap.getByteCount() + ":::bitmap:AllocationByteCount = " + bitmap.getAllocationByteCount());
Log.i(TAG, "bitmapReuse:ByteCount = " + bitmapReuse.getByteCount() + ":::bitmapReuse:AllocationByteCount = " + bitmapReuse.getAllocationByteCount());

输出:
I/lz: bitmap = android.graphics.Bitmap@35ac9dd4
I/lz: width:1024:::height:594
I/lz: bitmap:ByteCount = 2433024:::bitmap:AllocationByteCount = 2433024
I/lz: bitmapReuse = android.graphics.Bitmap@35ac9dd4 // 两个对象的内存地址一致
I/lz: width:512:::height:297
I/lz: bitmap:ByteCount = 608256:::bitmap:AllocationByteCount = 2433024
I/lz: bitmapReuse:ByteCount = 608256:::bitmapReuse:AllocationByteCount = 2433024 // ByteCount比AllocationByteCount小

可以看出:

  1. 从内存地址的打印可以看出,两个对象其实是一个对象,Bitmap复用成功;
  2. bitmapReuse占用的内存(608256)正好是bitmap占用内存(2433024)的四分之一;
  3. getByteCount()获取到的是当前图片应当所占内存大小,getAllocationByteCount()获取到的是被复用Bitmap真实占用内存大小。虽然bitmapReuse的内存只有608256,但是因为是复用的bitmap的内存,因而其真实占用的内存大小是被复用的bitmap的内存大小(2433024)。这也是getAllocationByteCount()可能比getByteCount()大的原因。
BitmapDrawable#getBitmap() 方法

8. Matrix变形

学过线性代数或者图像处理的同学们一定深知Matrix的强大,很多常见的图像变换一个Matrix就能搞定,甚至更复杂的也是如此。

// Matrix matrix = new Matrix();
//
每一种变化都包括set,pre,post三种,分别为设置、矩阵先乘、矩阵后乘。
平移:matrix.setTranslate()
缩放:matrix.setScale()
旋转:matrix.setRotate()
斜切:matrix.setSkew()

下面我举两个例子说明一下。

6. Bitmap如何压缩?

旋转

借助Matrix的postRotate方法旋转一定角度。

Matrix matrix = new Matrix();
// angle为旋转的角度
matrix.postRotate(angle);
Bitmap rotatedBitmap = Bitmap.createBitmap(originBitmap,
        0,
        0,
        originBitmap.getWidth(),
        originBitmap.getHeight(),
        matrix,
        true);

6.1 Bitmap.compress()

质量压缩:
它是在保持像素的前提下改变图片的位深及透明度等,来达到压缩图片的目的,不会减少图片的像素。进过它压缩的图片文件大小会变小,但是解码成bitmap后占得内存是不变的。

缩放

借助Matrix的postScale方法旋转一定角度。

Matrix matrix = new Matrix();
// scaleX,scaleY分别为为水平和垂直方向上缩放的比例
matrix.postScale(scaleX, scaleY);
Bitmap scaledBitmap = Bitmap.createBitmap(originBitmap,
        0,
        0,
        originBitmap.getWidth(),
        originBitmap.getHeight(),
        matrix,
        true);

Bitmap本身也带了一个缩放方法,不过是把bitmap缩放到目标大小,原理也是用Matrix,我们封装一下:

// 水平和宽度缩放到指定大小,注意,这种情况下图片很容易变形
Bitmap scaledBitmap = Bitmap.createScaledBitmap(originBitmap,
        dstWidth,
        dstHeight,
        true);

通过组合可以实现更多效果。

6.2 BitmapFactory.Options.inSampleSize

9. 裁剪

图片的裁剪的应用场景还是很多的:头像剪切,照片裁剪,圆角,圆形等等。

内存压缩:

  • 解码图片时,设置BitmapFactory.Options类的inJustDecodeBounds属性为true,可以在Bitmap不被加载到内存的前提下,获取Bitmap的原始宽高。而设置BitmapFactory.Options的inSampleSize属性可以真实的压缩Bitmap占用的内存,加载更小内存的Bitmap。
  • 设置inSampleSize之后,Bitmap的宽、高都会缩小inSampleSize倍。例如:一张宽高为2048×1536的图片,设置inSampleSize为4之后,实际加载到内存中的图片宽高是512×384。占有的内存就是0.75M而不是12M,足足节省了15倍。

备注:inSampleSize值的大小不是随便设、或者越大越好,需要根据实际情况来设置。
以下是设置inSampleSize值的一个示例:

public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
        int reqWidth, int reqHeight) {
    // 设置inJustDecodeBounds属性为true,只获取Bitmap原始宽高,不分配内存;
    final BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeResource(res, resId, options);
    // 计算inSampleSize值;
    options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
    // 真实加载Bitmap;
    options.inJustDecodeBounds = false;
    return BitmapFactory.decodeResource(res, resId, options);
}

public static int calculateInSampleSize(
            BitmapFactory.Options options, int reqWidth, int reqHeight) {
    // Raw height and width of image
    final int height = options.outHeight;
    final int width = options.outWidth;
    int inSampleSize = 1;
    if (height > reqHeight || width > reqWidth) {
        final int halfHeight = height / 2;
        final int halfWidth = width / 2;
        // 宽和高比需要的宽高大的前提下最大的inSampleSize
        while ((halfHeight / inSampleSize) >= reqHeight
                && (halfWidth / inSampleSize) >= reqWidth) {
            inSampleSize *= 2;
        }
    }
    return inSampleSize;
}

这样使用:mImageView.setImageBitmap(
decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100,
100));

备注:

  • inSampleSize比1小的话会被当做1,任何inSampleSize的值会被取接近2的幂值。

矩形

矩阵形状的裁剪比较简单,直接用createBitmap方法即可:

Canvas canvas = new Canvas(originBitmap);
draw(canvas);
// 确定裁剪的位置和裁剪的大小
Bitmap clipBitmap = Bitmap.createBitmap(originBitmap,
        left, top,
        clipWidth, clipHeight);

7. 总结

圆角

对于圆角我们需要借助Xfermode和PorterDuffXfermode,把圆角矩阵套在原Bitmap上取交集得到圆角Bitmap。

// 准备画笔
Paint paint = new Paint();
paint.setAntiAlias(true);

// 准备裁剪的矩阵
Rect rect = new Rect(0, 0,
        originBitmap.getWidth(), originBitmap.getHeight());
RectF rectF = new RectF(new Rect(0, 0,
        originBitmap.getWidth(), originBitmap.getHeight()));

Bitmap roundBitmap = Bitmap.createBitmap(originBitmap.getWidth(),
        originBitmap.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(roundBitmap);
// 圆角矩阵,radius为圆角大小
canvas.drawRoundRect(rectF, radius, radius, paint);

// 关键代码,关于Xfermode和SRC_IN请自行查阅
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
canvas.drawBitmap(originBitmap, rect, rect, paint);

1. Bitmap内存模型

  • Android
    2.3.3(API10)之前,Bitmap的像素数据存放在Native内存,而Bitmap对象本身则存放在Dalvik
    Heap中。而在Android3.0之后,Bitmap的像素数据也被放在了Dalvik
    Heap中。

圆形

和上面的圆角裁剪原理相同,不过画的是圆形套在上面。

为了从中间裁剪出圆形,我们需要计算绘制原始Bitmap的left和top值。

int min = originBitmap.getWidth() > originBitmap.getHeight() ?
originBitmap.getHeight() : originBitmap.getWidth();
Paint paint = new Paint();
paint.setAntiAlias(true);
Bitmap circleBitmap = Bitmap.createBitmap(min, min,
    Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(circleBitmap);
// 圆形
canvas.drawCircle(min / 2, min / 2, min / 2, paint);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));

// 居中显示
int left = - (originBitmap.getWidth() - min) / 2;
int top = - (originBitmap.getHeight() - min) / 2;
canvas.drawBitmap(originBitmap, left, top, paint);

从圆角、圆形的处理上我们应该能看的出来绘制任意多边形都是可以的。

2. Bitmap的内存回收

  • 在Android2.3.3之前推荐使用Bitmap.recycle()方法进行Bitmap的内存回收;
  • 在Android3.0之后更注重对Bitmap的复用;

10. 保存Bitmap

很多图片应用都支持裁剪功能,滤镜功能等等,最终还是需要把处理后的Bitmap保存到本地,不然就是再强大的功能也是白忙活了。

public static String $save(Bitmap bitmap,
        Bitmap.CompressFormat format, int quality, File destFile) {
    try {
        FileOutputStream out = new FileOutputStream(destFile);
        if (bitmap.compress(format, quality, out)) {
            out.flush();
            out.close();
        }

        if (bitmap != null && !bitmap.isRecycled()) {
            bitmap.recycle();
        }

        return destFile.getAbsolutePath();
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
    return null;
}

如果想更稳定或者更简单的保存到SDCard的包名路径下,可以再封装一下:

// 保存到本地,默认路径/mnt/sdcard/[package]/save/,用随机UUID命名文件
public static String $save(Bitmap bitmap,
        Bitmap.CompressFormat format, int quality, Context context) {
    if (!Environment.getExternalStorageState()
            .equals(Environment.MEDIA_MOUNTED)) {
        return null;
    }

    File dir = new File(Environment.getExternalStorageDirectory()
            + "/" + context.getPackageName() + "/save/");
    if (!dir.exists()) {
        dir.mkdirs();
    }
    File destFile = new File(dir, UUID.randomUUID().toString());
    return $save(bitmap, format, quality, destFile);
}

3. Bitmap占用内存的计算

  • getByteCount()方法是在API12加入的,代表存储Bitmap的色素需要的最少内存;
    • getAllocationByteCount()在API19加入,代表在内存中为Bitmap分配的内存大小;
  • 在复用Bitmap的情况下,getAllocationByteCount()可能会比getByteCount()大;
  • 计算公式:
    • 对资源文件:width * height * nTargetDensity/inDensity *
      nTargetDensity/inDensity * 一个像素所占的内存;
    • 别的:width * height * 一个像素所占的内存;

11. 巨图加载

巨图加载,当然不能使用常规方法,必OOM。

原理比较简单,系统中有一个类BitmapRegionDecoder:

public static BitmapRegionDecoder newInstance(byte[] data, int offset,
        int length, boolean isShareable) throws IOException {
}
public static BitmapRegionDecoder newInstance(
        FileDescriptor fd, boolean isShareable) throws IOException {
}
public static BitmapRegionDecoder newInstance(InputStream is,
        boolean isShareable) throws IOException {
}
public static BitmapRegionDecoder newInstance(String pathName,
        boolean isShareable) throws IOException {
}

可以按区域加载:

public Bitmap decodeRegion(Rect rect, BitmapFactory.Options options) {
}

微博的大图浏览也是通过这个BitmapRegionDecoder实现的,具体可自行查阅。

4. Bitmap的复用

  • BitmapFactory.Options.inBitmap,针对不同版本复用有不同的限制,见上2.2.2,较多此处不再赘述;

12. 颜色矩阵ColorMatrix

图像处理其实是一门很深奥的学科,所幸Android提供了颜色矩阵ColorMatrix类,可实现很多简单的特效,以灰阶效果为例子:

Bitmap grayBitmap = Bitmap.createBitmap(originBitmap.getWidth(),
        originBitmap.getHeight(), Bitmap.Config.RGB_565);
Canvas canvas = new Canvas(grayBitmap);
Paint paint = new Paint();
ColorMatrix colorMatrix = new ColorMatrix();
// 设置饱和度为0,实现了灰阶效果
colorMatrix.setSaturation(0);
ColorMatrixColorFilter colorMatrixColorFilter =
        new ColorMatrixColorFilter(colorMatrix);
paint.setColorFilter(colorMatrixColorFilter);
canvas.drawBitmap(originBitmap, 0, 0, paint);

除了饱和度,我们还能调整对比度,色相变化等等。

5. Bitmap的压缩

  • Bitmap.compress(),质量压缩,不会对内存产生印象;
  • BitmapFactory.Options.inSampleSize,内存压缩;
    • inSampleSize的比对获取;

13. ThumbnailUtils剖析

ThumbnailUtils是系统提供的一个专门生成缩略图的方法,我专门写了一篇文章分析,内容较多,请移步:理解ThumbnailUtils

6. Glide

  • 查看官方文档以及性能优化典范,Google强烈推荐使用Glide来做Bitmap的加载。

参考:

  • 《Caching
    Bitmaps》
  • 《Handling
    Bitmaps》
  • 《Re-using
    Bitmaps》
  • 《Managing Bitmap
    Memory》
  • 《Loading Large Bitmaps
    Efficiently》

欢迎关注微信公众号:定期分享Java、Android干货!

澳门新葡萄京娱乐场 3

欢迎关注

14. 小结

既然与Bitmap经常打交道,那就把它都理清楚弄明白,这是很有必要的。

难免会有遗漏,欢迎留言,我会酌情补充。

发表评论

电子邮件地址不会被公开。 必填项已用*标注