Android 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 的内存;

二、Bitmap 内存大小

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

getByteCount

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

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

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 的长度)。

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 的比例对图片的宽度和高度进行缩放。

三、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 方法省内存;

四、Bitmap 压缩

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 后占的内存是不变的。

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

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.Bitmap#createScaledBitmap

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

五、Bitmap 与 Drawable 的转换

Bitmap-->Drawable

通过 BitmapDrawable 的构造方法:

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

Drawable-->Bitmap

根据已有的 Drawable 创建一个新的 Bitmap
public static Bitmap createBitmap(int width, int height, Config config)
BitmapDrawable#getBitmap() 方法
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容