Android Bitmap 使用

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

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中。

Bitmap内存占用

手动计算

计算Bitmap内存占用分为两种情况:

  1. 使用BitmapFactory.decodeResource()加载本地资源文件的方式

无论是使用decodeResource(Resources res, int id)还是使用decodeResource(Resources res, int id, BitmapFactory.Options opts)其内存占用的计算方式都是: width * height * inTargetDensity / inDensity * inTargetDensity / inDensity * 一个像素所占的内存。

  1. 使用BitmapFactory.decodeResource()以外的方式,计算方式是: width * height *一个像素所占的内存。

所用参数解释一下:

  • width:图片的原始像素宽度。
  • height:图片的原始像素高度。
  • inTargetDensity:目标设备的屏幕密度,例如一台手机的屏>幕密度是640dp,那么inTargetDensity的值就是640dp。
  • inDensity:这个值跟这张图片的放置的目录有关(比如 hdpi >240,xxhdpi 是480)。
  • 一个像素所占的内存:使用Bitmap.Config来描述一个像素所>占用的内存,Bitmap.Config有四个取值,分别是:
  1. ARGB_8888: 每个像素4字节,每个通道8位,四通道共32位,图片质量是最高的,但是占用的内存也是最大的,是 默认设置。
  2. RGB_565:共16位,2字节,只存储RGB值,图片失真小,没有透明度,可用于不需要透明度是图片。
  3. Alpha_8: 只有A通道,没有颜色值,即只保存透明度,共8位,1字节,可用于设置遮盖效果。
  4. ARGB_4444: ,每个通道均占用4位,共16位,2字节,严重失真,基本不使用。

Android API 的方法

getByteCount()

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

getAllocationByteCount()

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

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

getByteCount()与getAllocationByteCount()的区别

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

Bitmap的创建

通常我们可以利用Bitmap的静态方法createBitmap()和BitmapFactory的decode系列静态方法创建Bitmap对象。

Bitmap.createBitmap

主要用于图片的操作,例如图片的缩放,裁剪等。


image.png

image.png

BitmapFactory

image.png

注意:decodeFile 和 decodeResource 其实最终都会调用 decodeStream 方法来解析Bitmap 。有一个特别有意思的事情是,在 decodeResource 调用 decodeStream 之前还会调用 decodeResourceStream 这个方法,这个方法主要对 Options进行处理,在得到opts.inDensity的属性前提下,如果没有对该属性的设定值,那么opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;这个值默认为标准dpi的基值:160。如果没有设定opts.inTargetDensity的值时,opts.inTargetDensity = res.getDisplayMetrics().densityDpi; 该值为当前设备的 densityDpi,这个值是根据你放置在 drawable 下的文件不同而不同的。所以说 decodeResourceStream 这个方法主要对 opts.inDensity 和 opts.inTargetDensity进行赋值。

尽量不要使用setImageBitmap或setImageResource或BitmapFactory.decodeResource来设置一张大图,因为这些函数在完成decode后,最终都是通过java层的createBitmap来完成的,需要消耗更多内存,可以通过BitmapFactory.decodeStream方法,创建出一个bitmap,再将其设为ImageView的 source。

Resource资源加载的方式相当的耗费内存,建议采用通过InputStream ins = resources.openRawResource(resourcesId);然后使用decodeStream代替decodeResource获取Bitmap。这么做的好处是:

  • BitmapFactory.decodeResource 加载的图片可能会经过缩放,该缩放目前是放在 java 层做的,效率比较低,而且需要消耗 java 层的内存。因此,如果大量使用该接口加载图片,容易导致OOM错误。
  • BitmapFactory.decodeStream 不会对所加载的图片进行缩放,相比之下占用内存少,效率更高。

这两个接口各有用处,如果对性能要求较高,则应该使用 decodeStream;如果对性能要求不高,且需要 Android 自带的图片自适应缩放功能,则可以使用 decodeResource。

Bitmap 于 drawable 的相互转换

Bitmap 转 drawable

Drawable newBitmapDrawable = new BitmapDrawable(bitmap);
还可以从BitmapDrawable中获取Bitmap对象
Bitmap bitmap = new BitmapDrawable.getBitmap();

drawable 转 Bitmap

  1. BitmapFactory 中的 decodeResource 方法
Resources res = getResources();
Bitmap    bmp = BitmapFactory.decodeResource(res, R.drawable.ic_drawable);
  1. 将 Drable 对象先转化成 BitmapDrawable ,然后调用 getBitmap 方法 获取
Resource res      = gerResource();
Drawable drawable = res.getDrawable(R.drawable.ic_drawable);//获取drawable
BitmapDrawable bd = (BitmapDrawable) drawable;
Bitmap bm         = bd.getBitmap();
  1. 根据已有的Drawable创建一个新的Bitmap
public static Bitmap drawableToBitmap(Drawable drawable) {

    int w = drawable.getIntrinsicWidth();
    int h = drawable.getIntrinsicHeight();
    System.out.println("Drawable转Bitmap");
    Bitmap.Config config =
            drawable.getOpacity() != PixelFormat.OPAQUE ? Bitmap.Config.ARGB_8888
                    : Bitmap.Config.RGB_565;
                    
    Bitmap bitmap = Bitmap.createBitmap(w, h, config);
    
    //注意,下面三行代码要用到,否则在View或者SurfaceView里的canvas.drawBitmap会看不到图
    Canvas canvas = new Canvas(bitmap);
    drawable.setBounds(0, 0, w, h);
    drawable.draw(canvas);

    return bitmap;
}

BitmapFactory.Options的属性解析

  • inJustDecodeBounds:如果这个值为 true ,那么在解码的时候将不会返回 Bitmap ,只会返回这个 Bitmap 的尺寸。这个属性的目的是,如果你只想知道一个 Bitmap 的尺寸,但又不想将其加载到内存中时,是一个非常好用的属性。
  • outWidth和outHeight:表示这个 Bitmap 的宽和高,一般和 inJustDecodeBounds 一起使用来获得 Bitmap的宽高,但是不加载到内存
  • inSampleSize:压缩图片时采样率的值,如果这个值大于1,那么就会按照比例(1 / inSampleSize)来缩小 Bitmap 的宽和高。如果这个值为 2,那么 Bitmap 的宽为原来的1/2,高为原来的1/2,那么这个 Bitmap 是所占内存像素值会缩小为原来的 1/4。
  • inDensity:表示这个 Bitmap 的像素密度,对应的是 DisplayMetrics 中的 densityDpi,不是 density。(如果不明白它俩之间的异同,可以看我的 Android 屏幕各种参数的介绍和学习 )
  • inTargetDensity:表示要被新 Bitmap 的目标像素密度,对应的是 DisplayMetrics 中的 densityDpi。
  • inScreenDensity:表示实际设备的像素密度,对应的是 DisplayMetrics 中的 densityDpi。
  • inPreferredConfig:这个值是设置色彩模式,默认值是 ARGB_8888,这个模式下,一个像素点占用 4Byte 。RGB_565 占用 2Byte,ARGB_4444 占用 4Byte(以废弃)。
  • inPremultiplied:这个值和透明度通道有关,默认值是 true,如果设置为 true,则返回的 Bitmap 的颜色通道上会预先附加上透明度通道。
  • inScaled:设置这个Bitmap 是否可以被缩放,默认值是 true,表示可以被缩放。
  • inMutable:若为true,则返回的Bitmap是可变的,可以作为Canvas的底层Bitmap使用。
    若为false,则返回的Bitmap是不可变的,只能进行读操作。
    如果要修改Bitmap,那就必须返回可变的bitmap,例如:修改某个像素的颜色值(setPixel)
  • inBitmap:这个参数用来实现 Bitmap 内存的复用,但复用存在一些限制,具体体现在:在 Android 4.4 之前只能重用相同大小的 Bitmap 的内存,而 Android 4.4 及以后版本则只要后来的 Bitmap 比之前的小即可。使用 inBitmap 参数前,每创建一个 Bitmap 对象都会分配一块内存供其使用,而使用了 inBitmap 参数后,多个 Bitmap 可以复用一块内存,这样可以提高性能。

Bitmap如何复用

image.png

image.png

使用inBitmap能够大大提高内存的利用效率,但是它也有几个限制条件:

  • Bitmap复用首选需要其 mIsMutable 属性为 true , mIsMutable 的表面意思为:易变的

在Bitmap中的意思为: 控制bitmap的setPixel方法能否使用,也就是外界能否修改bitmap的像素。mIsMutable 属性为 true 那么就可以修改Bitmap的像素数据,这样也就可以实现Bitmap对象的复用了。

  • 在SDK 11 -> 18之间,重用的bitmap大小必须是一致的,例如给inBitmap赋值的图片大小为100-100,那么新申请的bitmap必须也为100-100才能够被重用。

  • 被复用的Bitmap必须是Mutable,即inMutable的值为true。违反此限制,不会抛出异常,且会返回新申请内存的Bitmap。

  • 从SDK 19开始,新申请的bitmap大小必须小于或者等于已经赋值过的bitmap大小。违反此限制,将会导致复用失败,抛出异常IllegalArgumentException(Problem decoding into existing bitmap)

  • 新申请的bitmap与旧的bitmap必须有相同的解码格式,例如大家都是8888的,如果前面的bitmap是8888,那么就不能支持4444与565格式的bitmap了,不过可以通过创建一个包含多种典型可重用bitmap的对象池,这样后续的bitmap创建都能够找到合适的“模板”去进行重用。

Bitmap如何压缩

质量压缩

质量压缩不会改变图片的像素点,即我们使用完质量压缩后,在转换Bitmap时占用内存依旧不会减小。但是可以减少我们存储在本地文件的大小,即放到 disk上的大小。

/**
     * 质量压缩方法,并不能减小加载到内存时所占用内存的空间,应该是减小的所占用磁盘的空间
     * @param image
     * @param compressFormat
     * @return
     */
    public static Bitmap compressbyQuality(Bitmap image, Bitmap.CompressFormat compressFormat) {

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        //质量压缩方法,这里100表示不压缩,把压缩后的数据存放到baos中
        image.compress(compressFormat, 100, baos);
        int quality = 100;

        //循环判断如果压缩后图片是否大于100kb,大于继续压缩
        while ( baos.toByteArray().length / 1024 > 100) { 
            baos.reset();//重置baos即清空baos
            if(quality > 10){
                quality -= 20;//每次都减少20
            }else {
                break;
            }
            
            //这里压缩options%,把压缩后的数据存放到baos中
            image.compress(Bitmap.CompressFormat.JPEG,quality,baos);
        }
        
        //把压缩后的数据baos存放到ByteArrayInputStream中
        ByteArrayInputStream isBm = new ByteArrayInputStream(baos.toByteArray());

        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inPreferredConfig = Bitmap.Config.RGB_565;
        
        //把ByteArrayInputStream数据生成图片
        Bitmap bmp = BitmapFactory.decodeStream(isBm, null, options);

        return bmp;
    }

采样压缩

这个方法主要用在图片资源本身较大,或者适当地采样并不会影响视觉效果的条件下,这时候我们输出的目标可能相对的较小,对图片的大小和分辨率都减小。

压缩格式 CompressFormat

  1. Bitmap.CompressFormat.JPEG
  • 一种有损压缩(JPEG2000既可以有损也可以无损),".jpg"或者".jpeg";
  • 优点:采用了直接色,有丰富的色彩,适合存储照片和生动图像效果;缺点:有损,不适合用来存储logo、线框类图
  1. Bitmap.CompressFormat.PNG
  • 一种无损压缩,".png";
  • PNG 格式是无损的,它无法再进行质量压缩,quality 这个参数就没有作用了,会被忽略,所以最后图片保存成的文件大小不会有变化;
  • 优点:支持透明、无损,主要用于小图标,透明背景等;
  • 缺点:若色彩复杂,则图片生成后文件很大;
  1. Bitmap.CompressFormat.WEBP
  • 以WebP算法进行压缩;
  • Google开发的新的图片格式,同时支持无损和有损压缩,使用直接色。
  • 无损压缩,相同质量的webp比PNG小大约26%;
  • 有损压缩,相同质量的webp比JPEG小25%-34% 支持动图,基本取代gif
  • 缺点:解压速度慢
    **
     * 采样率压缩,这个和矩阵来实现缩放有点类似,但是有一个原则是“大图小用用采样,小图大用用矩阵”。
     * 也可以先用采样来压缩图片,这样内存小了,可是图的尺寸也小。如果要是用 Canvas 来绘制这张图时,再用矩阵放大
     * @param image
     * @param compressFormat
     * @param requestWidth 要求的宽度
     * @param requestHeight 要求的长度
     * @return
     */
    public static Bitmap compressbySample(Bitmap image, Bitmap.CompressFormat compressFormat, int requestWidth, int requestHeight){
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        
        //质量压缩方法,这里100表示不压缩,把压缩后的数据存放到baos中
        image.compress(compressFormat,100,baos);
        
        //把压缩后的数据baos存放到ByteArrayInputStream中
        ByteArrayInputStream isBm = new ByteArrayInputStream(baos.toByteArray());

        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inPreferredConfig = Bitmap.Config.RGB_565;
        options.inPurgeable = true;
        
        //只读取图片的头信息,不去解析真是的位图
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeStream(isBm,null,options);
        options.inSampleSize = calculateInSampleSize(options,requestWidth,requestHeight);
        
        //-------------inBitmap------------------
        options.inMutable = true;
        try{
            Bitmap inBitmap = Bitmap.createBitmap(options.outWidth, options.outHeight, Bitmap.Config.RGB_565);
            if (inBitmap != null && canUseForInBitmap(inBitmap, options)) {
                options.inBitmap = inBitmap;
            }
        }catch (OutOfMemoryError e){
            options.inBitmap = null;
            System.gc();
        }

        //---------------------------------------

        options.inJustDecodeBounds = false;//真正的解析位图
        
        isBm.reset();
        Bitmap compressBitmap;
        try{
            compressBitmap =  BitmapFactory.decodeStream(isBm, null, options);//把ByteArrayInputStream数据生成图片
        }catch (OutOfMemoryError e){
            compressBitmap = null;
            System.gc();
        }

        return compressBitmap;
    }

    /**
     * 采样压缩比例
     * @param options
     * @param reqWidth 要求的宽度
     * @param reqHeight 要求的长度
     * @return
     */
    private static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {

        int originalWidth = options.outWidth;
        int originalHeight = options.outHeight;
        
        int inSampleSize = 1;

        if (originalHeight > reqHeight || originalWidth > reqHeight){
            // 计算出实际宽高和目标宽高的比率
            final int heightRatio = Math.round((float) originalHeight / (float) reqHeight);
            final int widthRatio = Math.round((float) originalWidth / (float) reqWidth);
            // 选择宽和高中最小的比率作为inSampleSize的值,这样可以保证最终图片的宽和高
            // 一定都会大于等于目标的宽和高。
            inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;

        }
        return inSampleSize;
    }

使用矩阵

前面我们采用了采样压缩,Bitmap 所占用的内存是小了,可是图的尺寸也小了。当我们需要尺寸较大时该怎么办?我们要用用 Canvas 绘制怎么办?当然可以用矩阵(Matrix)

/**
     * 矩阵缩放图片
     * @param sourceBitmap
     * @param width 要缩放到的宽度
     * @param height 要缩放到的长度
     * @return
     */
    private Bitmap getScaleBitmap(Bitmap sourceBitmap,float width,float height){
        Bitmap scaleBitmap;
        //定义矩阵对象
        Matrix matrix = new Matrix();
        float scale_x = width/sourceBitmap.getWidth();
        float scale_y = height/sourceBitmap.getHeight();
        matrix.postScale(scale_x,scale_y);

        try {
            scaleBitmap = Bitmap.createBitmap(sourceBitmap,0,0,sourceBitmap.getWidth(),sourceBitmap.getHeight(),matrix,true);
        }catch (OutOfMemoryError e){
            scaleBitmap = null;
            System.gc();
        }
        return scaleBitmap;
    }

喜欢点击+关注哦

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

推荐阅读更多精彩内容

  • 目录介绍 01.如何计算Bitmap占用内存1.1 如何计算占用内存1.2 上面方法计算内存对吗1.3 一个像素占...
    杨充211阅读 4,079评论 1 9
  • 一直以来Bitmap都是开发中很棘手的问题,这个问题就是传说中的OOM(java.lang.OutofMemory...
    M悇芐冋忆阅读 4,727评论 0 11
  • 一、Bitmap 内存回收 从3.0开始,Bitmap 像素数据和 Bitmap 对象一起存放在 Dalvik 堆...
    秀花123阅读 1,822评论 1 7
  • 我是小婷,笔名柠檬。今天是我参加“每天一篇文章”连续写作的第7天,每天一篇文章,皆与最重要的一件事有关(开书店——...
    柠檬nemo阅读 133评论 0 2
  • 之前的我是不怎么做梦的,但是最近很奇怪,经常梦里的内容是可以记个大概的。 前天晚上的梦就特别有意思,在一个战火纷飞...
    小熊的蜂蜜罐子阅读 179评论 0 1