在平常开发中,能够说和Bitmap低头不见抬头见,基本上每一个应用都会直接或间接的用到,而这里面又涉及到大量的相关知识。 因此这里把Bitmap的经常使用知识作个梳理,限于经验和能力,不作太深刻的分析。java
计算Bitmap内存占用分为两种状况:web
使用BitmapFactory.decodeResource()
加载本地资源文件的方式算法
不管是使用decodeResource(Resources res, int id)
仍是使用decodeResource(Resources res, int id, BitmapFactory.Options opts)
其内存占用的计算方式都是: width * height * inTargetDensity / inDensity * inTargetDensity / inDensity * 一个像素所占的内存。
canvas
使用BitmapFactory.decodeResource()
之外的方式,计算方式是: width * height *一个像素所占的内存。
数组
所用参数解释一下:bash
- 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字节,严重失真,基本不使用。
getByteCount()方法是在API12加入的,表明存储Bitmap的色素须要的最少内存。API19开始getAllocationByteCount()方法代替了getByteCount()。并发
API19以后,Bitmap加了一个Api:getAllocationByteCount();表明在内存中为Bitmap分配的内存大小。函数
public final int getAllocationByteCount() {
if (mBuffer == null) {
//mBuffer表明存储Bitmap像素数据的字节数组。
return getByteCount();
}
return mBuffer.length;
}
复制代码
一般咱们能够利用Bitmap的静态方法createBitmap()
和BitmapFactory
的decode系列静态方法建立Bitmap对象。post
主要用于图片的操做,例如图片的缩放,裁剪等。 性能
注意
: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。这么作的好处是:
这两个接口各有用处,若是对性能要求较高,则应该使用 decodeStream;若是对性能要求不高,且须要 Android 自带的图片自适应缩放功能,则可使用 decodeResource。
Drawable newBitmapDrawable = new BitmapDrawable(bitmap);
还能够从BitmapDrawable中获取Bitmap对象
Bitmap bitmap = new BitmapDrawable.getBitmap();
复制代码
BitmapFactory 中的 decodeResource 方法
Resources res = getResources();
Bitmap bmp = BitmapFactory.decodeResource(res, R.drawable.ic_drawable);
复制代码
将 Drable 对象先转化成 BitmapDrawable ,而后调用 getBitmap 方法 获取
Resource res = gerResource();
Drawable drawable = res.getDrawable(R.drawable.ic_drawable);//获取drawable
BitmapDrawable bd = (BitmapDrawable) drawable;
Bitmap bm = bd.getBitmap();
复制代码
根据已有的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;
}
复制代码
使用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
时占用内存依旧不会减少。可是能够减小咱们存储在本地文件的大小,即放到 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;
}
复制代码
这个方法主要用在图片资源自己较大,或者适当地采样并不会影响视觉效果的条件下,这时候咱们输出的目标可能相对的较小,对图片的大小和分辨率都减少。
**
* 采样率压缩,这个和矩阵来实现缩放有点相似,可是有一个原则是“大图小用用采样,小图大用用矩阵”。
* 也能够先用采样来压缩图片,这样内存小了,但是图的尺寸也小。若是要是用 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;
}
复制代码