Android Bitmap 使用

栏目: IOS · Android · 发布时间: 5年前

内容简介:在日常开发中,可以说和Bitmap低头不见抬头见,基本上每个应用都会直接或间接的用到,而这里面又涉及到大量的相关知识。 所以这里把Bitmap的常用知识做个梳理,限于经验和能力,不做太深入的分析。计算Bitmap内存占用分为两种情况:getByteCount()方法是在API12加入的,代表存储Bitmap的色素需要的最少内存。API19开始getAllocationByteCount()方法代替了getByteCount()。

在日常开发中,可以说和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 * 一个像素所占的内存。

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

所用参数解释一下:

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

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

Android Bitmap 使用
Android Bitmap 使用

BitmapFactory

Android Bitmap 使用

注意decodeFiledecodeResource 其实最终都会调用 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进行赋值。

尽量不要使用 setImageBitmapsetImageResourceBitmapFactory.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);
    复制代码
  2. 将 Drable 对象先转化成 BitmapDrawable ,然后调用 getBitmap 方法 获取

    Resource res      = gerResource();
    Drawable drawable = res.getDrawable(R.drawable.ic_drawable);//获取drawable
    BitmapDrawable bd = (BitmapDrawable) drawable;
    Bitmap bm         = bd.getBitmap();
    复制代码
  3. 根据已有的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如何复用

Android Bitmap 使用
Android Bitmap 使用

使用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

  • Bitmap.CompressFormat.JPEG
    • 一种有损压缩(JPEG2000既可以有损也可以无损),".jpg"或者".jpeg";
    • 优点:采用了直接色,有丰富的色彩,适合存储照片和生动图像效果;缺点:有损,不适合用来存储logo、线框类图
  • Bitmap.CompressFormat.PNG
    • 一种无损压缩,".png";
    • PNG 格式是无损的,它无法再进行质量压缩,quality 这个参数就没有作用了,会被忽略,所以最后图片保存成的文件大小不会有变化;
    • 优点:支持透明、无损,主要用于小图标,透明背景等;
    • 缺点:若色彩复杂,则图片生成后文件很大;
  • 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;
    }
复制代码

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

疯狂HTML 5/CSS 3/JavaScript讲义

疯狂HTML 5/CSS 3/JavaScript讲义

李刚 / 电子工业出版社 / 2012-5-1 / 69.00元

疯狂HTML 5/CSS 3/JavaScript讲义,ISBN:9787121168635,作者:李刚 编著一起来看看 《疯狂HTML 5/CSS 3/JavaScript讲义》 这本书的介绍吧!

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具