学而实习之 不亦乐乎

Android 中 Bitmap 及其用法

2023-08-22 07:46:43

一、Android 中的 Bitmap 类

Bitmap 类是 Android 系统中的图像处理的最重要的类之一。用它可以获取图像文件信息,进行图像剪切、旋转、缩放等操作,并可以指定格式保存图像文件。

Bitmap相关的使用主要有两种:

  1. 给ImageView设置背景
  2. 当做画布来使用

如下代码:

imageView.setImageBitmap(Bitmap bm);
Canvas canvas = new Canvas(Bitmap bm)

二、Bitmap的格式

我们知道Bitmap是位图,是由像素点组成的。Bitmap 中有两个内部枚举类:Config 和 CompressFormat,Config 是用来设置颜色配置信息的,CompressFormat 是用来设置压缩方式的。

1、存储格式

  • Bitmap.Config.ALPHA_8 单位像素占 1 字节,颜色信息只由透明度组成,占8位
  • Bitmap.Config.ARGB_4444 单位像素占 2 字节,颜色信息由rgba四部分组成,每个部分都占4位,总共占16位
  • Bitmap.Config.ARGB_8888 单位像素占 4 字节,颜色信息由rgba四部分组成,每个部分都占8位,总共占32位。是Bitmap默认的颜色配置信息,也是最占空间的一种配置
  • Bitmap.Config.RGB_565 单位像素占 2 字节,颜色信息由rgb三部分组成,R占5位,G占6位,B占5位,总共占16位
  • RGBA_F16 单位像素占 8 字节,Android 8.0新增(更丰富的色彩表现HDR)
  • HARDWARE Android 8.0 新增 (Bitmap直接存储在graphic memory)

一般情况下,我们不会使用 ALPHA_8 他只存储透明度,没啥用处。对于 ARGB_4444 它的画质又太感人了,ARGB_8888 画质高但是占内存,RGB_565 还行,就是不可以设置透明度。

注意以下三点即可:

  1. 一般情况下用 ARGB_8888 格式存储 Bitmap
  2. ARGB_4444 画面惨不忍睹,被弃用
  3. 假如对图片没有透明度要求,可以使用 RGB_565,比ARGB_8888 节省一半的内存开销

2、压缩格式

如果一张和手机屏幕大小一样的Bitmap图片,采用ARGB_8888格式存储需要多大的内存呢?

按照1024*768的分辨率图片来计算,每个像素需要32位也就是4个字节,

result = 1024*768*32B=25165824B=3MB

一张手机屏幕大小的Bitmap图片竟然要3M? 那就不奇怪我的app为什么一直闪退了,只不过用for循环创建了几十个用在滑动列表里面。

所以我们必须要对图片进行压缩呀,压缩格式使用枚举类Bitmap.CompressFormat

Bitmap.CompressFormat.JPEG:采用JPEG压缩算法,是一种有损压缩格式,会在压缩过程中改变图像原本质量,画质越差,对原来的图片质量损伤越大,但是得到的文件比较小,而且JPEG不支持透明度,当遇到透明度像素时,会以黑色背景填充。

Bitmap.CompressFormat.PNG:采用PNG算法,是一种支持透明度的无损压缩格式。

Bitmap.CompressFormat.WEBP:WEBP是一种同时提供了有损压缩和无损压缩的图片文件格式,在14<=api<=17时,WEBP是一种有损压缩格式,而且不支持透明度,在api18以后WEBP是一种无损压缩格式,而且支持透明度,有损压缩时,在质量相同的情况下,WEBP格式的图片体积比JPEG小40%,但是编码时间比JPEG长8倍。在无损压缩时,无损的WEBP图片比PNG压缩小26%,但是WEBP的压缩时间是PNG格式压缩时间的5倍。

三、Bitmap创建方法

我们如何创建一个 Bitamap 对象呢?Google 给我们提供了两种方式:

  1. Bitmap 的静态方法 createBitmap(XX)
  2. BitmapFactory 的 decodeXX 系列静态方法

BitmapFactory提供了多种创建bitmap的静态方法,decodeFile、 decodeResource、decodeStream和decodeByteArray,分别用于支持从文件系统、资源、输入流以及字节数组中加载出一个Bitmap对象,其中decodeFile和decodeResource又间接调用了decodeStream方法,这四类方法最终是在Android的底层实现的,对应着BitmapFactory类的几个native 方法。

1、 Bitmap.Options类

如何高效地加载bitmap?

通过BitmapFactory.Options按一定的采样率来加载缩小后的图片,将缩小后的图片在ImageView中显示,这样就会降低内存占用从而在一定程度上避免OOM,提高了Bitmap 加载时的性能。

采样率解释:

BitmapFactory提供的加载图片的四类方法都支持BitmapFactory.Options参数

通过BitmapFactory.Options 来缩放图片,主要用到了inSampleSize参数,即采样率。

当inSampleSize为1时,采样后的图片大小为图片的原始大小;

当inSampleSize大于1时,比如为2,那么采样后的图片其宽/高均为原图大小的1/2,而像素数为原图的1/4,其占有的内存大小也为原图的1/4。

拿一张1024×1024像素的图片来说,假定采用ARGB8888格式存储,那么它占有的内存为1024×1024×4即4MB,如果inSampleSize为2,那么采样后的图片其内存占用只有512×512×4,即1MB。

采样率同时作用于宽/高,这将导致缩放后的图片大小以采样率的2次方形式递减,即缩放比例为1/ (inSampleSize的2次方)

比如inSampleSize为4,那么缩放比例就是1/16。

有一种特殊情况,那就是当inSampleSize 小于1时,其作用相当于1,即无缩放效果。

try {
    FileInputStream fis = new FileInputStream(filePath);
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;

    // 设置inJustDecodeBounds为true后,再使用decodeFile()等方法,并不会真正的分配空间,
    //即解码出来的Bitmap为null,但是可计算出原始图片的宽度和高度,即options.outWidth和options.outHeight
    BitmapFactory.decodeFileDescriptor(fis.getFD(), null, options);
    float srcWidth = options.outWidth;
    float srcHeight = options.outHeight;
    int inSampleSize = 1;

    if (srcHeight > height || srcWidth > width) {
        if (srcWidth > srcHeight) {
            inSampleSize = Math.round(srcHeight / height);
        } else {
            inSampleSize = Math.round(srcWidth / width);
        }
    }

    options.inJustDecodeBounds = false;
    options.inSampleSize = inSampleSize;

    return BitmapFactory.decodeFileDescriptor(fis.getFD(), null, options);
} catch (Exception e) {
    e.printStackTrace();
}

过程说明:

(1) 将BitmapFactory.Options的inJustDecodeBounds参数设为true并加载图片。该参数为true时,BitmapFactory只会解析图片的原始宽高信息,不会去真正加载图片,同时获取到的信息和图片的位置与程序运行的设备有关

(2) 从BitmapFactory.Options 中取出图片的原始宽高信息,它们对应于outWidth 和outHeight参数。

(3) 根据采样率的规则并结合目标View的所需大小计算出采样率inSampleSize.

(4) 将BitmapFactory.Options 的inJustDecodeBounds 参数设为false, 然后重新加载图片。

2、Bitmap静态方法

//width和height是长和宽单位px,config是存储格式
static Bitmap createBitmap(int width , int height Bitmap.Config config)

// 根据一幅图像创建一份一模一样的实例
static Bitmap createBitmap(Bitmap bm)

//截取一幅bitmap,起点是(x,y),width和height分别对应宽高
static Bitmap createBitmap(Bitmap bm,int x,int y,int width,int height)

//比上面的裁剪函数多了两个参数,Matrix:给裁剪后的图像添加矩阵 boolean filter:是否给图像添加滤波效果
static Bitmap createBitmap(Bitmap bm,int x,int y,int width,int height,Matrix m,boolean filter);

//用于缩放bitmap,dstWidth和dstHeight分别是目标宽高
createScaledBitmap(Bitmap bm,int dstWidth,int dstHeight,boolean filter)

这些方法大致可以分为三类:

【1】根据已有的Bitmap来创建新Bitmap

/**
 * 通过矩阵的方式,返回原始 Bitmap 中的一个不可变子集。新 Bitmap 可能返回的就是原始的 Bitmap,也可能还是复制出来的。
 * 新 Bitmap 与原始 Bitmap 具有相同的密度(density)和颜色空间;
 *
 * @param source   原始 Bitmap
 * @param x        在原始 Bitmap 中 x方向的其起始坐标(你可能只需要原始 Bitmap x方向上的一部分)
 * @param y        在原始 Bitmap 中 y方向的其起始坐标(你可能只需要原始 Bitmap y方向上的一部分)
 * @param width    需要返回 Bitmap 的宽度(px)(如果超过原始Bitmap宽度会报错)
 * @param height   需要返回 Bitmap 的高度(px)(如果超过原始Bitmap高度会报错)
 * @param m        Matrix类型,表示需要做的变换操作
 * @param filter   是否需要过滤,只有 matrix 变换不只有平移操作才有效
 */
public static Bitmap createBitmap(@NonNull Bitmap source, int x, int y, int width, int height,@Nullable Matrix m, boolean filter)

【2】通过像素点数组创建空的Bitmap

/**
 *
 * 返回具有指定宽度和高度的不可变位图,每个像素值设置为colors数组中的对应值。
 * 其初始密度由给定的确定DisplayMetrics。新创建的位图位于sRGB 颜色空间中。
 * @param display  显示将显示此位图的显示的度量标准
 * @param colors   用于初始化像素的sRGB数组
 * @param offset   颜色数组中第一个颜色之前要跳过的值的数量
 * @param stride   行之间数组中的颜色数(必须> = width或<= -width)
 * @param width    位图的宽度
 * @param height   位图的高度
 * @param config   要创建的位图配置。如果配置不支持每像素alpha(例如RGB_565),
 * 那么colors []中的alpha字节将被忽略(假设为FF)
 */
public static Bitmap createBitmap(@NonNull DisplayMetrics display,
            @NonNull @ColorInt int[] colors, int offset, int stride,
            int width, int height, @NonNull Config config)

【3】 创建缩放的Bitmap

/**
 * 对Bitmap进行缩放,缩放成宽 dstWidth、高 dstHeight 的新Bitmap
 */
public static Bitmap createScaledBitmap(@NonNull Bitmap src, int dstWidth, int dstHeight,boolean filter)

3、创建Bitmap的总结

  • 加载图像可以使用BitmapFactory和Bitmap.create系列方法
  • 可以通过Options实现缩放图片,获取图片信息,配置缩放比例等功能
  • 如果需要裁剪或者缩放图片,只能使用create系列函数
  • 注意加载和创建bitmap事通过try catch捕捉OOM异常

 

四、常见函数

1、函数及其参数

copy(Config config,boolean isMutable)
//根据原图像创建一个副本,但可以指定副本的像素存储格式参数含义。
//  config:像素在内存中的存储格式,但可以指定副本的像素存储格式
//  boolean isMutable:新建的bitmap是否可以修改其中的像素值

extractAlpha()//主要作用是从bitmap中获取Alpha值,生成一幅只有Alpha值得图像,存储格式是ALPHA_8

getByteCount()//获取bitmap的字节数
recycle()://不用的bitmap必须要及时回收,以免造成oom
isRecycled()//判断bitmap是否被回收,被收回不可使用会造成crash

综合案例演示

String items[] = {"copy","extractAlpha 1","extractAlpha 2","bitmap大小","recycle","isRecycled()"};
ArrayAdapter<String> adapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item,items);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinner.setAdapter(adapter);

spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {

        @Override
        public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {

                switch (position){
                        case 0:
                                //copy
                                Bitmap bm = BitmapFactory.decodeResource(getResources(), R.drawable.photo);
                                Bitmap copy = bm.copy(Bitmap.Config.ARGB_8888, true);
                                imageView.setImageBitmap(copy);
                                bm.recycle();
                                break;
                        case 1:
                                //extractAlpha 不带参数
                                Bitmap bp = BitmapFactory.decodeResource(getResources(), R.drawable.photo);
                                Bitmap alpha = bp.extractAlpha();
                                imageView.setImageBitmap(alpha);
                                bp.recycle();
                                break;
                        case 2:
                                //extractAlpha 带参数
                                Bitmap bp1 = BitmapFactory.decodeResource(getResources(), R.drawable.photo);
                                Paint paint = new Paint();
                                BlurMaskFilter blurMaskFilter = new BlurMaskFilter(6, BlurMaskFilter.Blur.NORMAL);
                                paint.setMaskFilter(blurMaskFilter);
                                int[] offsetXY = new int[2];
                                Bitmap alpha1 = bp1.extractAlpha(paint, offsetXY);
                                imageView.setImageBitmap(alpha1);
                                break;
                        case 3:
                                //获取bitmap大小
                                Bitmap b = BitmapFactory.decodeResource(getResources(), R.drawable.photo);
                                Toast.makeText(getApplicationContext(), "图片大小为:"+b.getByteCount()+"字节", Toast.LENGTH_SHORT).show();
                                break;
                        case 4:
                                //回收bitmap
                                Bitmap b1 = BitmapFactory.decodeResource(getResources(), R.drawable.photo);
                                b1.recycle();
                                if(b1.isRecycled()){
                                        Toast.makeText(getApplicationContext(), "已经被回收", Toast.LENGTH_SHORT).show();
                                }
                                //isRecycled()判断是否被回收
                                break;
                }
        }

        @Override
        public void onNothingSelected(AdapterView<?> parent) {}
});

2、常用操作

【1】裁剪、缩放、旋转、移动

Matrix matrix = new Matrix();  

// 缩放
matrix.postScale(0.8f, 0.9f);  

// 左旋,参数为正则向右旋
matrix.postRotate(-45);  

// 平移, 在上一次修改的基础上进行再次修改 set 每次操作都是最新的 会覆盖上次的操作
matrix.postTranslate(100, 80);

// 裁剪并执行以上操作
Bitmap bitmap = Bitmap.createBitmap(source, 0, 0, source.getWidth(), source.getHeight(), matrix, true);

【2】保存与释放

Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test);
File file = new File(getFilesDir(),"test.jpg");
if(file.exists()){
    file.delete();
}

try {
    FileOutputStream outputStream=new FileOutputStream(file);
    bitmap.compress(Bitmap.CompressFormat.JPEG,90,outputStream);
    outputStream.flush();
    outputStream.close();
} catch (FileNotFoundException e) {
    e.printStackTrace();
} catch (IOException e) {
    e.printStackTrace();
}

//释放bitmap的资源,这是一个不可逆转的操作
bitmap.recycle();

【3】图片压缩

public static Bitmap compressImage(Bitmap image) {

    if (image == null) {
        return null;
    }

    ByteArrayOutputStream baos = null;
    try {
        baos = new ByteArrayOutputStream();
        image.compress(Bitmap.CompressFormat.JPEG, 100, baos);
        byte[] bytes = baos.toByteArray();
        ByteArrayInputStream isBm = new ByteArrayInputStream(bytes);
        Bitmap bitmap = BitmapFactory.decodeStream(isBm);
        return bitmap;

    } catch (OutOfMemoryError e) {
        e.printStackTrace();
    } finally {
        try {
            if (baos != null) {
                baos.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    return null;
}

五、常见问题

1、Bitmap与Canvas,View,Drawable的关系

2、使用Bitmap如何造成内存溢出的?

个人认为,Bitmap容易造成内存溢出是由于位图较大,一张屏幕大小的ARGB_8888存储格式的图片竟然有24M,如果有几个这种量级的图片在内存中,并且没有及时回收,那会非常容易造成OOM

3、怎么解决或者避免Bitmap内存溢出?

  • 我们可以对位图进行压缩,压缩手段有PNG,JPEG,WEBP
  • 对不使用的Bitmap一定要及时回收。
  • 在创建Bitmap时使用try catch步骤OOM异常,使程序更健壮,即使发生了OOM也不会闪退,造成不好的使用体验.

4、Bitmap与Drawable的转换

【1】 Drawable转换成Bitmap

public static Bitmap drawableToBitmap(Drawable drawable) {  

        // 取 drawable 的长宽  
        int w = drawable.getIntrinsicWidth();  
        int h = drawable.getIntrinsicHeight();  

        // 取 drawable 的颜色格式  
        Bitmap.Config config = drawable.getOpacity() != PixelFormat.OPAQUE ? Bitmap.Config.ARGB_8888                 : Bitmap.Config.RGB_565;  

        // 建立对应 bitmap  
        Bitmap bitmap = Bitmap.createBitmap(w, h, config);  

        // 建立对应 bitmap 的画布  
        Canvas canvas = new Canvas(bitmap);  
        drawable.setBounds(0, 0, w, h);  

        // 把 drawable 内容画到画布中
        drawable.draw(canvas);  

        return bitmap;  
}  

2、Bitmap转换成Drawable

Bitmap bm=Bitmap.createBitmap(xxx);
BitmapDrawable bd= new BitmapDrawable(getResource(), bm);