Bitmap基本认识
Bitmap在绘图中是非常重要的一个概念。在我们熟知的Canvas中就保存着一个Bitmap对象。当我们调用Canvas的各种绘制函数时,最终时绘制到其中的Bitmap 上的。在重写onDraw()函数时,这个函数中带着一个Canvas对象,只需要调用这个Canvas的绘制方法就能画出各种内容。其实真正的原因是View对应着一个Bitmap,onDraw()函数中的Canvas就是通过这个Bitmap创建出来的。
Bitmap在绘图中的使用
- 转为BitmapDrawable对象使用
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.flower)
val bitmapDrawable = BitmapDrawable(bitmap)
iv.setImageDrawable(bitmapDrawable)
- 当作画布使用
val bitmap = Bitmap.createBitmap(300,300,Bitmap.Config.ARGB_8888)
val mCanvas = Canvas(bitmap)
mCanvas.drawColor(Color.BLUE)
最终蓝色会画在这个宽高都为300的Bitmap上。Bitmap可以保存本地,也可以直接画在View上。
Bitmap的格式
Bitmap是位图,也就是由一个个的像素点构成的。那么就涉及到如果存储像素点以及相关的像素点之间能否压缩,这就涉及到了压缩算法。
1、如何存储像素点
一张位图所占用的内存 = 图片宽 x 图片高 x 一个像素点占用的字节数(宽高的单位为像素)。在Android当中,一个像素点所占用的字节数是由Bitmap.Config中的枚举来表示的:
- ALPHA_8:表示8位alpha位图,即只存储alpha位,不存储颜色值。一个像素点占用一个字节。由于只能存储透明度,不存储颜色,所以一般不用
- ARGB_4444:表示16位ARGB位图,其中A、R、G、B各占4位,一个像素点占用4+4+4+4=16位,也就是2个字节。占用内存小,但是画质很差,已经被弃用。
- ARGB_8888:表示32位ARGB位图,其中A、R、G、B各占8位,一个像素点占用8+8+8+8=32位,也就是4个字节。画质最好,但是内存占用比较大,所以内存吃紧的情况下不建议使用
- RGB_565:表示16位RGB位图,其中R占5位,G占6位,B占5位,一个像素点占用5+6+5=16位,也就是2个字节,但是没有透明度。如果对透明度没有要求则可以使用这种格式
每个色值占用的位数越大,表现出来的色彩就越鲜艳,越逼真。假如透明度A占4位,那么每位要么是0,要么是1,也就是0000,0001,0011......一种有2的4次方,也就是16中。如果是8位则是有2的8次方,也就是256种取值。很明显,取值越多,颜色就越艳丽。
ps:一张图片被加载到内存中所占用内存的大小和在文件中存储的大小是不一样的。在文件中是有压缩格式的。
2、压缩格式
Bitmap的压缩主要使用Bitmap.CompressFormat中的成员表示:
- Bitmap.CompressFormat.JEPG:采用 JPEG 压缩算法,是一种有损压缩格式,即在压缩过程中会改变图像的原本质量。compress()函数中的 quality 参数值越小,画质越差,对图片的原有质量损伤越大,但是得到的图片文件比较小,而且JPEG 不支持 alpha 透明度,当遇到透明度像素,会以黑色背景填充
- Bitmap.CompressFormat.PNG:采用 PNG 压缩算法,是 一种支持透明度的无损压缩格式
- Bitmap.CompressFormat.WEBP:WEBP 种同时提供了有损压缩与无损压缩的图片文件格式,派生自视频编码格式VP8。Google 2010 年发布;从 Android 4 .0(API14)开始支持WEBP ,从 Android4.2.1+ (API 18 )开始支持无损 WEBP 和带 Alpha 通道的 WEBP。也就是说,在 14<=API<=17 时,WEBP是一种有损压缩格式,而且不支持透明度 。API18 以后,WEBP 是一种无损压缩格式,而且支持透明度。在有损压缩时,在质量相同的情况下, WEBP格式图像的体积要比 JPEG 格式图像的体积小 40% ,美中不足的是, WEBP 格式图像的编码时间比 JPEG 格式图像的编码时间长8倍。在无损压缩时,无损的 WEBP 图片比 PNG 图片小26% ,但 WEBP 格式的压缩时 PNG 格式的压缩时间的5倍。所以从整体来讲,WEBP格式是通过牺牲压缩时间来减小产出文件大小的
Bitmap的创建
1 通过BitmapFactory创建Bitmap
BitmapFactory用于从各种文件,资源,数据流,字节数组中创建Bitmap对象。这是一个工具类,提供了大量的函数用于从不同的数据源中解析、创建Bitmap。
public static Bitmap decodeResource(Resources res, int id)
public static Bitmap decodeResource(Resources res, int id, Options opts)
public static Bitmap decodeFile(String pathName)
public static Bitmap decodeFile(String pathName , Options opts)
public static Bitmap decodeByteArray(byte[] data, int offset, int length)
public static Bitmap decodeByteArray(byte[] data , int offset, int length , Options opts)
public static Bitmap decodeFileDescriptor(FileDescriptor fd)
public static Bitmap decodeFileDescriptor (FileDescriptor fd , Rect outPadding, Options opts)
public static Bitmap decodeStream(InputStream is)
public static Bitmap decodeStream(InputStream is, Rect outPadding , Options opts)
public static Bitmap decodeResourceStream(Resources res, TypedValue value , InputStream is, Rect pad, Options opts)
都是一些简单的函数调用,其中用的比较少的可能就是decodeFileDescriptor()了。它的简单使用如下:
String path =”/xxx/xxx/demo.jpg”;
File putStream is== new FileinputStream(path);
bmp = BitmapFactory. decodeFileDescriptor (is.getFD ());
if (bmp == null) {
//TODO 文件不存在
}
我们有了文件的路径了,为什么不直接使用decodeFile()直接获取bitmap呢?这是因为使用decodeFileDescriptor()比decodeFile()方式更节省内存。所以如果内存不是很足的话就使用decodeFileDescriptor()吧。
1.1BitmapFactory.Options
这个参数的作用非常大,可以设置采样率,改变图片的宽高等手段以达到减少像素的目的,从而更有效的防止OOM。
-
inJustDecodeBounds 获取图片信息
将这个字段设置为true表示只解析图片信息,不获取图片,不分配内存。能获取的图片信息有宽、高和图片的MIME类型,分别通过options.outWidth、options.outHeight和options.outMimeType返回。
这个参数一般用在压缩图片的时候。因为图片过大很容易发生OOM,所以当图片的尺寸大于我们需要的尺寸的时候就需要进行压缩。通过不把图片加载到内存中来得到图片的原始宽高信息和目标的尺寸进行对比,如果大于目标尺寸,则压缩之后再进行显示:
val options = BitmapFactory.Options()
options.inJustDecodeBounds = true
BitmapFactory.decodeResource(resources, R.drawable.flower, options)
val btmWidth = options.outWidth
val btmHeight = options.outHeight
-
inSampleSize 压缩图片
这个字段表示采样率,指每隔多少个样本采集一次作为结果。比如设置为4,表示从原来的4个像素中取一个像素作为结果,其他的全丢弃。那么最终图片的宽和高将变为原来的1/4。如果采样率的数值越大,图片则越小,同时图片越失真。
针对sample的取值,一般都是取2的幂次方,如1,2,4,8等等。如果不是2的幂次方,则会向下取整并找到一个最接近的幂次方数。假如sample的值为10,那么则在实际采样中使用8作为采样率。
fun caculateSample(options: BitmapFactory.Options, dstWidth: Int, dstHeight: Int): Int {
var sampleSize = 1
val outWidth = options.outWidth
val outHeight = options.outHeight
if ((outWidth > dstWidth) || (outHeight > dstHeight)) {
val ratioW = (outWidth / dstWidth).toFloat()
val ratioH = (outHeight / dstHeight).toFloat()
sampleSize = min(ratioW, ratioH).toInt()
}
return sampleSize
}
假设目标ImageView的宽高为100 x 100,而图片的宽高为 300 x 400。如果按照高来压缩,那么压缩之后的图片的尺寸应该是 75 x 100,而ImageView的大小为100 x 100,那么显示在ImageView上的时候就要在横向上进行拉伸。本来图片设置采样率之后就会失真,现在再一拉伸,图片就会更失真。所以要按照宽来压缩,保证图片是大于等于目标View的大小的,这样不至于图片太失真,唯一不足的就是内存可能多占一点,但这也比看着一张扭曲的图片要好吧。所以上面的代码中选择了宽高比中最小的作为采样率返回。
1.2加载一张Bitmap究竟要占多少内存
上面提到Bitmap所占的内存计算公式为:宽 x 高 x 每个像素点的字节大小。可往往事情并没有这么简单。这是因为Android系统在加载图片的时候会根据需要动态的缩放图片的尺寸。我们知道在Android项目的资源文件夹下面会有很多的资源文件夹来适配不同屏幕的分辨率。例如:drawable、drawable-ldpi、drawable-mdpi、drawable-hdpi、drawable-xhdpi、drawable-xxhdpi等。这些文件夹和所对应的参数的关系如下:
插入一些关于屏幕各种尺寸单位的关系:
- 屏幕尺寸:手机对角线的物理尺寸,单位英寸。一英寸=2.54cm
- 屏幕分辨率:屏幕在横向和纵向上的像素点数综合。如1920*1080
- px:英文单词pixel的缩写,意为像素,即屏幕上的点。px均为整数,不会出现0.5px的情况。
- dpi:Dots Per Inch的缩写, 意为每英寸上的点数,即屏幕像素密度,也就是每英寸上有多少个像素点。计算方式为屏幕对角线上的像素个数/对角线的长度。例如屏幕分辨率为1920 x 1080,屏幕尺寸为5英寸。则需要先通过勾股定理求出对角线上的像素个数为:2203,再除以屏幕尺寸5,则可计算出屏幕像素密度为440
- dp:也叫dip,设备独立像素,density independent pixels的缩写,Android特有的单位,在Android中规定,以160dpi,也就是屏幕分辨率为320 x 480为基准,1dp = 1px
-
sp:和dp很类似,一般用来设置字体大小,和dp的区别是它可以根据用户的字体大小偏好来缩放
- density:表示dpi与px的换算比例
-
densityDpi:表示在对应的分辨率下每英寸有多少dpi,也就是上面的dpi。
所以屏幕物理尺寸的一英寸长所对应的像素个数应该为density * dpi。假如在xhdpi文件夹下有一张图,而屏幕的真实dpi为640。由于xhdpi对应的dpi是320,那么就需要按照640/320,也就是2倍来放大图片。也就是说一张100 x 200的图片会被放大为200 x 400的大小。内存的话假如是在320dpi的手机上,则为100 x 200 x 每个像素点的大小,而在640dpi的手机上则为200 x 400 x 每个像素点的大小,也就是说在640dpi的手机上占用的内存大小是320dpi手机上的4倍。
图片原始信息
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.timg)
Log.d("----->", "btmW: ${bitmap.width}")
Log.d("----->", "btmH: ${bitmap.height}")
Log.d("----->", "btmSize: ${bitmap.byteCount}")

这份日志是在分辨率为1920 x 1080,dpi为420的模拟器上的结果。由于xhdpi对应的dpi为320,则最终加载到内存中的图片宽就为500 * 420 / 320 = 656,刚好和打印的宽一致。
上面是把图片放在不同分辨率的文件夹下。那如果把图片直接放到SD卡当中呢?Android对此的处理是:不进行缩放。原本是多少像素,生成的bitmap就还是多少像素。
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.timg, options)
Log.d("----->", "btmW: ${bitmap.width}")
Log.d("----->", "btmH: ${bitmap.height}")
Log.d("----->", "btmSize: ${bitmap.byteCount}")
val path = "/storage/emulated/0/timg.jpeg"
val btm = BitmapFactory.decodeFile(path)
Log.d("----->", "sd-btmW: ${btm.width}")
Log.d("----->", "sd-btmH: ${btm.height}")
Log.d("----->", "sd-btmSize: ${btm.byteCount}")

总结:
(1)不同名称的资源文件夹是为了适配不同的屏幕分辨的,当屏幕分辨率与所在文件资源文件夹对应的分辨率相同时,直接使用图片,不会对图片进行缩放
(2)当屏幕分辨率与图片所在文件夹对应的分辨率不同时,会进行缩放 ,缩放比例是:屏幕分辨率/文件夹所对应的分辨率
(3)当从本地文件中加载图片时,不会对图片进行缩放
在了解了有关图片缩放的概念之后,就可以更好的学习下面几个BitmapFactory.Options中的其他几个变量:
- isScaled:这个参数表示在需要缩放时是否对图片进行缩放。设置为false,表示不会进行缩放,反之或者不设置,则会根据文件夹和屏幕分辨率动态缩放。
- inDensity:用于设置文件所在文件夹的屏幕分辨率
- inTargetDensity:表示真实显示的屏幕分辨率
val options = BitmapFactory.Options()
options.inDensity = 1
options.inTargetDensity = 2
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.timg, options)
Log.d("----->", "btmW: ${bitmap.width}")
Log.d("----->", "btmH: ${bitmap.height}")
Log.d("----->", "btmSize: ${bitmap.byteCount}")
val path = "/storage/emulated/0/timg.jpeg"
val btm = BitmapFactory.decodeFile(path, options)
Log.d("----->", "sd-btmW: ${btm.width}")
Log.d("----->", "sd-btmH: ${btm.height}")
Log.d("----->", "sd-btmSize: ${btm.byteCount}")

可以看到不管是从资源文件夹还是从SD卡加载都根据我们设置的参数而进行了相应的缩放。这说明如果设置了这两个参数,不管是不是真的屏幕分辨率,都会按照我们设置的进行修改。
- inPreferredConfig:用来设置像素储存格式的。默认使用的是ARGB_8888
2 Bitmap静态方法创建Bitmap
- createBitmap(int width, int height,Bitmap.Config config) 用于创建一个空白的bitmap
- createBitmap(Bitmap src) 根据传入的bitmap对象创建一份完全一样的bitmap,有点类似复制
- createBitmap(Bitmap source, int x, int y, int width, int height) 用于裁剪图像。x,y是裁剪的点的坐标。width和height是所要裁剪的宽和高
- createBitmap(Bitmap source, int x, int y, int width , int height, Matrix m , boolean filter) 和上个函数类似,只不过除了裁剪的功能以外还多了可以给裁剪后的bitmap添加矩阵的功能。最后的filter则表示是否给图像加滤波效果。
- createBitmap (int[] colors, int width, int height , Bitmap.Config config)
- createBitmap (int[] colors, int offset, int stride , int width, int height, Bitmap.Config config)
这两个函数基本不会用到,主要是通过指定每个像素的颜色值来绘制bitmap。colors的数组长度必须要大于width x height。 - createScaledBitmap(Bitmap src, int dstWidth , int dstHeight, boolean filter) 用于缩放bitmap。src是源图像,dstWidth和dstHeight是缩放后的目标宽高
3 Bitmap常用方法
- copy(Config config , boolean isMutable) 创建一个副本,但是可以指定副本的储存格式。isMutable表示新创建的Bitmap对象是否可以更改其中的像素。
- boolean isMutable() 判断当前Bitmap是否是像素可更改的。如果是返回true,反之返回false。如果返回false,而仍要使用setPixel()来设置其中的像素,则会报错。直接通过BitmapFactory加载的Bitmap,而不用BitmapFactory.Options中的isMutable属性设置的话,得到的Bitmap都是像素不可改变的,而只有通过Bitmap中以下几个方法创建的Bitmap才是可改变的:
copy(Bitmap.Config config , boolean isMutable)
createBitmap(int width ,int height , Bitmap.Config config)
createScaledBitmap(Bitmap src , int dstWidth , int dstHeight, boolean filter)
// API 17 中引入
createBitmap (DisplayMetrics display, int width , int height , Bitmap.Config config)
其中createScaledBitmap()函数如果目标宽高和原图像的宽高是一样时,返回源图像,不会创建新的。此时如果源图像时可更改的,那返回的Bitmap就是可更改的。反之就是不可更改的。必须进行缩放之后才会无论源图像是否是可改的,都会返回可更改的。
注意:只有isMutable()返回true的Bitmap才可以作为画布。否则把不可更改的作为画布在上面绘制是会报错的。
- extractAlpha() 从Bitmap中抽取Alpha值。得到的Bitmap只有透明度,没有其他颜色
-
extractAlpha(Paint paint, int[] offsetXY) 和上个函数功能一样。区别就是需要传入一个具有MaskFilter效果的Paint。一般使用BlurMaskFilter。例如使用半径为6的BlurMaskFilter,则图像的上下左右都会多处6px的模糊效果。所以要想完全显示图像,就不可以从(0 , 0)的位置了,需要使用offSetXY偏移到(-6,-6)。
上面是一张带有透明通道的图片,直接画在了屏幕上。下面添加上阴影效果:
paint.maskFilter = BlurMaskFilter(6f, BlurMaskFilter.Blur.NORMAL)
val alphaBitmap = bitmap.extractAlpha(paint, offsetXY)
canvas.drawBitmap(alphaBitmap, 0f, 0f, paint)
canvas.drawBitmap(bitmap, -offsetXY[0].toFloat(), -offsetXY[1].toFloat(), null)

可以看到图片的周围多了一圈红色的光晕效果。
- int getAllocationByteCount () 获取Bitmap所分配的内存,在API19引入,如果在大于API19的机器上则要使用这个函数
- int getByteCount () 也是获取Bitmap所分配的内存,在API12引入,所以API<19且大于12的话使用这个
- getRowBytes() 获取一行所分配的内存大小 若要获得bitmap的内存大小可以使用getRowBytes() x bitmap的高。在API小于12的时候要用这种相乘的方式来获得
- recycle() 回收Bitmap所占的内存。如果已经被回收再次调用这个方法则会报错
- isRecycled() 判断Bitmap有没有被回收
if (bmp ! = null 品品 !bmp.isRecycle()) {
bmp.recycle(); //回收图片所占的内存
bmp = null;
}
- getDensity()、setDensity() 与介绍BitmapFactory提到的inDensity一样,不过这个是Bitmap中的方法。inDensity用于表示该Bitmap适合的dpi,如果屏幕的真实dpi和inDensity不相等时,则会进行缩放。与BitmapFactory不同的是,使用Bitmap中的setDensity()只会改变显示的效果,而不会改变宽和高,BitmapFactory当中是可以改变宽高的。
val mDensity = bitmap.density
Log.d("----->", "before == width: ${bitmap.width} height: ${bitmap.height}")
canvas.drawBitmap(bitmap,0f,0f,null)
bitmap.density = mDensity *2
Log.d("----->", "after == width: ${bitmap.width} height: ${bitmap.height}")
canvas.drawBitmap(bitmap,0f,0f+bitmap.height,null)


- compress(CompressFormat format ,int quality, OutputStream stream) 压缩图像,会将压缩的图片写入到指定的流中。
format指的是压缩格式,就是在文章开头提到的那三种格式,其中WEBP格式是API14引入的。quality表示压缩质量,取值范围0~100,数值越大,压缩质量越高。PNG格式的话会忽略此选项。stream就是压缩后的Bitmap的保存形式,可以用这个输出流把Bitmap写到文件中。此外该函数的返回值为boolean类型。true为压缩成功,反之失败。