Bitmap
Bitmap在绘图中是一个非常重要的格式。在Canvas中就保存着一个Bitmap对象,我们调用Canvas的各种绘图函数,最终还是会绘制到其中的Bitmap上的。
View对应着一个Bitmap,而onDraw()函数中的Canvas参数就是通过这个Bitmap创建出来的。有关Bitmap与Canvas、View、Drawable的关系我们会在本章最末尾讲解。
1、概述
Bitmap在绘图中相关的使用方式有两种:第一种是转化为BitmapDrawable对象使用,第二种是当做画布来使用。
(1)转化为BitmapDrawable对象使用
第一种使用方法很简单,就是直接将Bitmap转换为BitmapDrawable对象,然后转换为Drawable使用,比如下面的代码中将Drawable使用,比如下面的代码中将Drawable设置给ImageView,当做ImageView的数据源:
iv = findViewById(R.id.iv);
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.avator_xizuka);
BitmapDrawable drawable = new BitmapDrawable(bitmap);
iv.setBackgroundDrawable(drawable);
(2)作为画布使用
关于Bitmap作为画布使用已经用了很多次了。
主要有两种方式,一种是在重写onDraw()时拿到该View的Bitmap
第二种是 通过Canvas canvas = new Canvas(bitmap)来创建一个画布。
Bitmap格式
Bitmap是位图,由一个个像素点构成的,所以我们应该知道它是这么储存每个像素点的,并且相关的像素点之间是否能够压缩。
(1)如何存储每个像素点
一张位图所占的内存 = 图片长度(px) * 图片宽度(px) * 一个像素点所占用的字节数。
其中占用的字节数按照Bitmap.Config来设定:
- ALPHA_8:表示8位的Alpha位图,即A=8,表示只存储Alpha,不存储颜色值,1个像素点只占用1个字节,它没有颜色,只有透明度
- ARGB_4444:表示16位ARGB位图,即A、R、G、B各占4位,一个像素点占16位,即2字节
- ARGB_8888:表示32位ARGB位图,各占8位,一个像素点占8*4=32位,即4字节
- RGB_565:表示16位RGB位图,R5位,G6位,B5位,没有透明度,一个像素占5+6+5=16位即2字节
我们一般使用ARGB_8888格式来存储Bitmap,而ARGB_4444画质比较惨不忍睹,所以在API 13中已经被弃用了。
假如对图片没有透明度要求,则可以改成RGB_565的格式,相比ARGB_节省一半的内存开销。
注意一个概念:内存中存储的Bitmap对象与文件中存储的Bitmap图片不是要一个概念。文件中存储的Bitmap图片是经过我们在后面讲到的压缩算法压缩过的;而内存中存储的Bitmap对象是通过BitmapFactory或者Bitmap的Create方法创建的,它保存在内存中,而且具有明确的宽和高,所以,很明显,内存中存储的一个Bitmap对象,它所占的内存大小 = bitmap宽* 高 *每个像素所占内存大小。
2、创建Bitmap的方式之一:BitmapFactory
BitmapFactory用于从各种资源、文件、数据流和字节流中创建Bitmap对象。
BitmapFactory类是一个工具类,提供了大量的函数,这些函数可以用于从不同的数据源中解析、创建Bitmap对象。
这些函数网上都罗列出来了,我也懒得敲了,反正就可以根据不同的方式来解析对应的Bitmap对象。
大概就是:
- decodeResource(Resources res,int id)
见得多了 - decodeFile(String pathName)
根据路径名来加载图片。必须是全路径名 比如 "/data/data/demo.jpg"这样 - decodeByteArray(byte[] data,int offset ,int length)
data:压缩图像数据的字节数组
offset:图像数据偏移量,用于解码器定位从哪里开始解析
length:字节数,从偏移量开始,指定取多少字节进行解析。
一般是从网络上下载一个InputSteam的流,然后转换成byte,再将其转换成bitmap - decodeFileDescriptior(FileDescriptior fd)
通过FileDescriptor对象解析出对应的Bitmap,而FD的一般获取方式是通过构造FileInputStream对象。而FileInputStream又是通过Path拿到的,那为什么我们不直接使用decodeFile直接解析呢?是因为该函数比decodeFile函数更节省内存。 - decodeStream(InputStream in)
直接传入inputstream就能解析了
3、BitmapFactory.Option
之前在《Android开发艺术探索》中对Option有了印象比较深刻的理解,因为这个参数非常大的影响了图片的存储性能,是降低OOM发生概率的一个比较关键的参数,也是实战、面试中一定会碰到的问题。
但之前理解过很多,所以这里只写之前没有遇到过的。
下面是Options常用的部分成员变量:
public boolean inJustDecodeBounds;
public int inSampleSize;
public int inDensity;
public int inTargetDensity;
public int inScreenDensity;
public boolean inScaled;
public Bitmap.Config inPreferredConfig;
public int outWidth;
public int outHeight;
public String outMimeTye;
其中以in开头的就是设置某某参数,以out开头的就是获取某某参数,比如outWidth就是获取Bitmap的宽。
(1)inJustDecodeBounds 获取图片信息
如果将这个字段设置为true,则表示只解析图片信息,不获取图片、不分配内存,能获取的信息有图片的宽高、MIME。宽高通过outWidth outHeight返回,MIME通过outMimeType返回。
我们在压缩图片时就会经常使用这个字段。一般而言,图片过大时,经常会造成OOM。所以,当图片的尺寸大于我们想要的尺寸时,我们就要进行压缩。这个问题的关键在于,如何不将图片加载到内存中,依然可以知道它的尺寸?而这个参数就很好的完成了这个需求。
我们只需将inJustDecodeBounds设置为true,而不需要将图片加载到内存中,就可以得知它的宽高,然后跟我们想要的尺寸进行对比,进行压缩。
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.avator_xizuka, options);
Log.d(TAG, "bitmap:" + bitmap);
Log.d(TAG, "realwidth:" + options.outWidth + " realheight:" + options.outHeight
+ " mimeType:" + options.outMimeType);
可以得出下面打印信息:
看到bitmap为null,但是realwidth和realheight和mimeType都是有值的,这说明没有获取到图片,只是解析了它。
解析是不会占用任何内存的。
(2)inSampleSize
就是采样率,balabalabala
这里有一点,比如ImageView是100100 而图片像素时300400 这个时候我们要图片缩小3倍还是4倍呢
因为inSampleSize本来就是失真处理,所以尽量让失真效果没那么强,这里就是缩小3倍的。(举个例子,也不能设inSampleSize为3的)
(3)加载一个Bitmap文件究竟需要占多少空间
之前说过 Bitmap所占的内存是 “宽 * 高 * 4” (因为Bitmap现在都默认ARGB_8888了所以为4字节)
但是这个公式是不是就是一定适用于所有Android机 所有情况呢?
其实不是。根本原因是屏幕的适配。
在Android最初版本的时候,设计人员就考虑到了以后Android以后会有许许多多的不同分辨率的屏幕,所以就定制了几个比如drawable-ldpi drawable-mdpi。。。资源文件,当图片符合该分辨率,就不用进行拉伸或者压缩,但是dpi不止那么几个啊,市面上的不同dpi手机太多了,肯定有图片和该这些分辨率文件不一致,所以,Android就会对该Bitmap进行动态的拉伸/压缩。
比如一张图片在SD卡下的原图是600800 在Activity使用加载该图片后的内存是2MB,然后将它放入到Drawable-xhdpi下,发现它的宽高被拉伸到了9601200 这个时候通过Activity加载图片就是4MB多了。
总结:
- 不同名称的资源文件是为了适配不同的分辨率,当屏幕分辨率与文件所在资源文件加对应的分辨率相同时,会直接使用图片,否则会对图片进行缩放。
- 当从本地文件SD卡或者内存中加载图片时,不会对图片进行缩放。
(4)inScaled、inDensity、inTargetDensity、inScreenDensity
- inScaled
只有在一种情况下才会对Bitmap进行拉缩放:就是当图片所在资源文件夹所对应的屏幕分辨率与真实显示的屏幕分辨率不同时。
而这个参数表示,在需要缩放时,是否对当前文件进行缩放。如果inScaled设置为false,则不进行缩放,如果设置为true或者不设置,则会对Bitmap进行动态的缩放。 - inDensity
用于设置文件所在资源文件夹的屏幕分辨率 - inTargetDensity
表示真实显示的屏幕分辨率 - inScreenDensity
真实的分辨率,不过该参数很鸡肋,源码中也没有出现过。就很尴尬。
一张图片的缩放比例在这里就是 scale = inTargetDensity / inDensity
所以这两个参数的作用就是:手动设置文件所在资源文件夹的分辨率和真实显示的屏幕分辨率来指定图片的缩放比例。
(5)inPreferredConfig
用来设置存储格式的。可以设置ARGB_8888、RGB_565。。。
4、创建Bitmap方法之二:静态方法
除了用BitmapFactory.decodeXXX函数来加载图片,还可以通过Bitmap自带的静态方法加载图片
就是 createBitmap(...)
createScaledBitmap(...)
createBitmap 的构造函数很多,但都容易理解,这里就不再讲解。
来看下createScaledBitmap(Bitmap src,int dstWidth,int dstHeight,boolean filter)
其中Bitmap src表示要缩放的源图像,dstWidth dstHeight表示缩放后的目标宽高,filter表示是否给图像添加滤波效果
到这里,有关创建Bitmap的方法就介绍VAN了
总结一下:
- 加载图像可以使用BitmapFactory和Bitmap的相关方法。
- Options参数很牛逼,要多多运用
- 如果要裁剪或者缩放图片,则只能使用Bitmap的Create系列函数
- 一定要注意,在加载或者创建Bitmap时,必须要使用try…catch语句捕捉OutOfMemoryError,防止出现OOM。
5、常用函数
(1)copy(Config config,boolean isMutable)
这个函数是根据源图像来创建一个副本,但可以指定副本的像素存储格式。它的两个参数含义为:
config表示存储格式
isMutable表示新创建的Bitmap是否可以更改其中元素。
诶~原来图像的像素还是可以被改变的吗?
其实是的,但是之前所学的加载和创建图片的方法不是每种弄出来的图像的像素都是可以改变的。
我们可以通过下面函数来判断图像像素是否可以更改。true表示可以,false表示不可以。
boolean isMutable();
如果图像的该函数返回了false,你还要用setPixel()等函数来更改,诶,就会报错。
通过BitmapFactory创建出来的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)
createBitmap(DisplayMetrics display,int width,int height,Bitmap.Config config)
大家谨记,对于像素不可以更改的图像,是不能作为画布的,比如下面这个:
Bitmap bmp = BitmapFactory.decodeResources(getRescoures(),R.drawable.xxx);
Canvas canvas = new Canvas(bmp);
canvas.drawColor(Color.RED);
这个时候,因为Bitmap是由工厂创建出来的,所以像素不可以更改,所以这个时候不能作为画布,这样写就会报错。
(2)extractAlpha()
这个函数的作用是Bitmap中抽出Alpha值,生成一幅只含有Alpha值的图像。像素存储的格式是ALPHA_8,有两个构造函数。
分别是:
Bitmap extractAlpha()
Bitmap extractAlpha(Paint paint,int[] offsetXY)
第二个参数中的Paint是具有MaskFilter效果的Paint对象,一般使用BlurMaskFilter模糊效果。
第二个参数为BlurMaskFilter效果的偏移量。但其取值并不一定与最终BlurMaskFilter的模糊半径一致。它只是一个建议值。
(3)分配控件获取
获取Bitmap的分配空间有三个函数。
//API 19引入,获取Bitmap所分配的内存,API 19以上的机器都使用该函数获取
int getAllocationByteCount();
//获取Bitmap分配的内存,在API 12中引入,在12< API <19 则使用该函数
int getByteCount()
//获取每行所分配的内存大小。Bitmap所占内存 = getRowBytes() * bitmap.getHeight() 即所占内存等于每行所占内存乘以行数。
//这个在API 1中引入,所以API 12一下必须用这个函数
int getRowBytes()
所以一般情况下获取内存分配都是:
if(api > kitkat){
return getAllocationByteCount();
}
if(api > HONEYCOMB_MR1){
return getByteCount();
}
return getRowBytes() * bitmap.getHeight();
(4)recycle()、isRecycled()
这两个与图片回收有关的函数
//强制回收Bitmap所占的内存
public void recycle()
//判断当前Bitmap的内存是否被回收
public final boolean isRecycled()
所以如果要回收内存,要这样写
if(bmp != null && !bmp.isRecycled()){
bmp.recycle();
bmp = null;
system.gc(); //提醒系统及时回收内存
}
注意一:使用内存已经被回收的Bitmap会引起Crash
注意二:是否应该使用recycle()函数主动回收内存
在API 10以前Bitmap的像素级数据被存放在Native内存空间中。
这些数据与Bitmap本身是隔离的。Bitmap本身被存在Dalvik堆中。我们无法预测在Native内存中的像素级数据何时会被释放。这就意味着程序容易超过它的内存限制并且Crash,而自API 11开始,像素级数据与Bitmap本身一起被存放在Dalvik堆中,可以通过GC自动回收。
所以API 11以前的版本,要手动调用 recycle(),而之后的版本则不用强制调用该函数。
(5)setDensity()、getDensity()
这个和BitmapFactory中的 inDensity一样。表示该Bitmap适合DPI值。
上面函数一个是set一个是get
该函数只影响缩放效果,不影响Bitmap本身的内存
另外在setDensity后 xml中该ImageView必须要设置 scaleType=“center” 宽高都是自适应 才能显示该效果,否则图片就会根据屏幕大小而缩放。
(6)setPixel()、getPixel()
这两个函数用于针对Bitmap中某个位置的像素进行设置和获取
//该函数用于指定位置像素进行颜色设置。
public void setPixel(int x,int y.int color)
//该函数用于获取指定位置像素的颜色值
public int getPixel(int x,int y)
一般用于图像整体处理,比如遍历图像的每个像素,然后处理。
(7)compress()
这个函数的作用就是压缩图像 它会将压缩过的Bitmap写入指定的输出流中,完整的函数声明如下:
public boolean compress(CompressFormat format ,int quality,OutputStream stream)
参数含义如下:
- CompressFormat format:前面提到,Bitmap压缩格式支持CompressFormat.JPEG、CompressFormat.PNG、CompressFormat.WEBP这三种格式,所以这里的format也就只有着三个取值。需要注意的是,WEBP格式是从API 14开始引入的
- int quality:压缩后的画质,取值为0-100,0表示最低画质压缩,100表示最高画质压缩,对于PNG等无损格式的图片,会忽略此项设置
- OutputStream stream:这是输出值,Bitmap在被压缩后,会以OutputStream的形式在这里输出。
- boolean 返回值:true表示成功压缩,false表示失败。
首先JPEG的压缩算法是一种有损压缩 在压缩的过程中,会改变图像原本的质量。quality参数越小,对原有图片质量的损伤会越大。并且如果JPEG有透明度,则在压缩过程中,将透明度不为100的像素以黑色替代。
PNG压缩算法是一直支持透明度的无损压缩算法。
WEBP则提供了有损压缩与无损压缩的图片文件格式,派生自视频编码格式。谷歌从API 14开始支持WEBP, API 18开始支持无损WEBP和带alpha通道的WEBP。也就是说 14<= API <18 是有损压缩且不支持透明度, 大于18就是无损压缩并且支持透明度。
有损压缩下WEBP图像的体积要小于JPEG40%,但是编码时间要长8倍。
无损压缩下WEBP图像体积比PNG小26%,但压缩时间是PNG的5倍。
所以WEBP是拿压缩时间来换压缩后体积的。
压缩示例这边就不讲了,比较简单,可以上网参考一下。
6、常见问题
(1)为什么对Bitmap的画笔设置了ANTI_ALIAS_FLAG属性,但却无效
我们经常使用两种绘图策略:
一个是在Canvas上直接绘制
另一个是先在Bitmap上绘制,再将Bitmap绘制到Canvas上。
我们来看一下在这两种情况下给Paint设置ANTI_ALIAS_FLAG属性,各有什么效果。
在第一种情况试验时,我们通过给Paint设置抗锯齿,最后发现效果没问题。这是因为默认当onDraw()函数被调用时,系统先将Canvas清空,然后重绘所有内容。
第二种则是效果好一点,但边缘还是明显粗糙的。
我们需要来了解一下ANTI_ALIAS_FLAG属性是这么工作的
简单来说,ANTI_ALIAS_FLAG属性通过混合前景色和背景色来产生平滑的边缘。
而我们在Bitmap上重绘时,像素的颜色会越来越纯粹,从而导致边缘越来越粗糙。这是因为不断重绘下,透明度会越来越深,只要重绘三次,颜色就越来越相近了。
如何解决这个问题:
- 避免重绘
- 在重绘前清空Bitmap
(2)Bitmap与Canvas、View、Drawable的关系
①Bitmap与Canvas
通过Canvas的构造方法我们知道:Canvas中的画布实质上就Bitmap。调用Canvas的各个绘图函数,实质上都是绘制在这个Canvas里保存的Bitmap对象上额度。
②Bitmap与View
在自定义控件时,如果该控件派生View,则都会重写onDraw,而当调用onDraw函数里的参数绘图之后,就会直接表现在View。难道View也是Canvas来显示的?其中保存的也是一个Bitmap?
当然了。源码中的draw方法的canvas就是由bitmap创建出来的
③Bitmap与Drawable
Drawable各子类中的Paint是自带的,可以通过getPaint函数得到
Drawable的Canvas画布使用的是View的,它的内部是没有保存Canvas变量的。