转载请标明出处:http://blog.csdn.net/zhaoyanjun6/article/details/123054991
本文出自【赵彦军的博客】
文章目录
两年前写过一篇文章,Android Bitmap 研究与思考(上篇) 介绍了 bitmap 的基本概念,今天这篇文章就来看看 bitmap 的压缩问题
质量压缩
private fun compressQuality() {
//把 drawable 转成 bitmap
val bm = BitmapFactory.decodeResource(resources, R.drawable.a123)
val bos = ByteArrayOutputStream()
//质量压缩方法,这里100表示不压缩,把压缩后的数据存放到baos中
bm.compress(Bitmap.CompressFormat.JPEG, 70, bos)
val bytes = bos.toByteArray()
//把数据流解码为 bitmap
val mSrcBitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
}
在解码的代码中,还可以这只解码参数 Options
public static Bitmap decodeByteArray(byte[] data, int offset, int length, Options opts) {
设置采样率
val options = BitmapFactory.Options()
//设置采样率,为2的阶乘
options.inSampleSize = 16
我们去解析一个图片,如果太大,就会OOM,我们可以设置压缩比例inSampleSize,但是这个压缩比例设置多少就是个问题,所以我们解析图片可以分为俩个步骤,
-
第一步就是获取图片的宽高,这里要设置
Options.inJustDecodeBounds=true
,这时候decode的bitmap为null,只是把图片的宽高放在Options里. -
第二步就是设置合适的压缩比例 inSampleSize ,这时候获得合适的Bitmap
测量解码完成后的bitmap宽高,解码返回值为null,bitmap 不会加载至内存。
val options = BitmapFactory.Options()
options.inJustDecodeBounds = true
//decodeByteArray 返回值为 null
BitmapFactory.decodeByteArray(bytes, 0, bytes.size, options)
val realWidth = options.outWidth //bitmap的宽度
val realHeight = options.outHeight //bitmap的高度
总结:
- bitmap.compress 质量压缩方案,不会降低bitmap 内存占用。如果保存到磁盘会降低占用空间。
- BitmapFactory.decodeByteArray 解码方案,使用采样率 inSampleSize 采样参数大于 1 会降低内存占用,保存到磁盘会降低磁盘占用空间。
采样率压缩
private fun compressSampling() {
//定义解码参数
val options = BitmapFactory.Options()
//设置采样率,为2的阶乘
options.inSampleSize = 4
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.a123, options)
binding.image2.setImageBitmap(bitmap)
}
采样率压缩其原理其实也是缩放bitamp的尺寸,通过调节其inSampleSize参数,比如调节为2,宽高会为原来的1/2,内存变回原来的1/4.
小例子:当我们把 inSampleSize 设置为 256 的时候,看看什么效果。
可以看到当采样率很大的时候,图片会变模糊。
矩阵缩放
private fun compressMatrix() {
val options = BitmapFactory.Options()
options.inSampleSize = 2
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.a123, options)
val matrix = Matrix()
matrix.setScale(0.25f, 0.25f) //缩放
matrix.setRotate(30f) //旋转
val srcBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
}
放缩法压缩使用的是通过矩阵对图片进行裁剪,也是通过缩放图片尺寸,来达到压缩图片的效果,和采样率的原理一样。
RGB_565压缩
private void compressRGB565() {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.RGB_565;
mSrcBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test, options);
}
这是通过压缩像素占用的内存来达到压缩的效果,一般不建议使用ARGB_4444,因为画质实在是辣鸡,如果对透明度没有要求,建议可以改成RGB_565,相比ARGB_8888将节省一半的内存开销。
createScaledBitmap 压缩
private void compressScaleBitmap() {
Bitmap bm = BitmapFactory.decodeResource(getResources(), R.drawable.test);
mSrcBitmap = Bitmap.createScaledBitmap(bm, 600, 900, true);
bm = null;
}
将图片的大小压缩成用户的期望大小,来减少占用内存。
BitmapFactory.Options 属性介绍
- inSampleSize:这是表示采样大小。用于将图片缩小加载出来的,以免站占用太大内存,适合缩略图。
- inJustDecodeBounds:当inJustDecodeBounds为true时,执行decodeXXX方法时,BitmapFactory只会解析图片的原始宽高信息,并不会真正的加载图片
- inPreferredConfig:用于配置图片解码方式,对应的类型Bitmap.Config。如果非null,则会使用它来解码图片。默认值为是 Bitmap.Config.ARGB_8888
- inBitmap:在Android 3.0开始引入了inBitmap设置,通过设置这个参数,在图片加载的时候可以使用之前已经创建了的Bitmap,以便节省内存,避免再次创建一个Bitmap。在Android4.4,新增了允许inBitmap设置的图片与需要加载的图片的大小不同的情况,只要inBitmap的图片比当前需要加载的图片大就好了。
bitmap 保存为文件
/**
* 保存bitmap到本地
*/
private fun saveBitmap(bitmap: Bitmap, file: File) {
FileOutputStream(file).use {
bitmap.compress(Bitmap.CompressFormat.PNG, 100, it)
it.flush()
}
}
/**
* 保存bitmap到本地
*/
private fun saveBitmap(bitmap: Bitmap, file: File) {
val bos = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.PNG, 100, bos)
FileOutputStream(file).use {
it.write(bos.toByteArray())
}
}
文件转为 bitmap
/**
* 保存bitmap到本地
*/
private fun fileToBitmap(file: File): Bitmap {
FileInputStream(file).use {
return BitmapFactory.decodeStream(it)
}
}
/**
* 保存bitmap到本地
*/
private fun fileToBitmap(file: File): Bitmap {
FileInputStream(file).use {
val data = it.readBytes()
val option = BitmapFactory.Options()
option.inSampleSize = 2
return BitmapFactory.decodeByteArray(data, 0, data.size, option)
}
}
/**
* 保存bitmap到本地
*/
private fun fileToBitmap(file: File): Bitmap {
val option = BitmapFactory.Options()
option.inSampleSize = 2
return BitmapFactory.decodeFile(file.absolutePath, option)
}
网络图片转为bitmap
/**
* 解码网络图片
*/
fun getBitmapFromNet(url: String): Bitmap {
val okhttp = OkHttpClient()
val request = Request.Builder().get().url(url).build()
val call = okhttp.newCall(request)
val response = call.execute()
val stream = response.body()?.byteStream()
val bitmap = BitmapFactory.decodeStream(stream)
response.close()
return bitmap
}
高效加载大位图
大位图加载时的OOM问题,解决方式是通过 inSample 属性创建一个原位图的子采样版本以减低内存。那么这里的采样率 inSample 值如何选取最好呢?
这里我们利用官方推荐的采样率最佳计算方式:基本步骤就是:
①获取位图原尺寸
②获取ImageView即最终图片显示的尺寸
③依据两种尺寸计算采样率(或缩放比例)。
public static int calculateInSampleSize(
BitmapFactory.Options options, int reqWidth, int reqHeight) {
// 位图的原宽高通过options对象获取
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
//当要显示的目标大小和图像的实际大小比较接近时,会产生没必要的采样,先除以2再判断以防止过度采样
while ((halfHeight / inSampleSize) >= reqHeight
&& (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
依据上面的最佳采样率计算方法,进一步可以封装出利用最佳采样率创建子采样版本再创建位图对象的方法,这里以从项目图片资源文件加载Bitmap对象为例:
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
int reqWidth, int reqHeight) {
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
//因为inJustDecodeBounds为true,所以不会创建Bitmap对象只会扫描轮廓从而给options对象的宽高属性赋值
BitmapFactory.decodeResource(res, resId, options);
// 计算最佳采样率
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// 记得将inJustDecodeBounds属性设置回false值
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
bitmap 转为 drawable
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.a123)
val drawable = BitmapDrawable(resources, bitmap)
drawable 转为 bitmap
/**
* drawable转为bitmap
*/
private fun drawableToBitmap(drawable: Drawable): Bitmap {
if (drawable is BitmapDrawable) {
return drawable.bitmap
}
var bitmapWidth = drawable.intrinsicWidth
var bitmapHeight = drawable.intrinsicHeight
bitmapWidth = max(bitmapWidth, 1) //最小为1
bitmapHeight = max(bitmapHeight, 1) //最小为1
val bitmap = Bitmap.createBitmap(bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888)
val canvas = Canvas()
canvas.setBitmap(bitmap)
drawable.setBounds(0, 0, bitmap.width, bitmap.height)
drawable.draw(canvas)
return bitmap
}
调用:
val drawable = ResourcesCompat.getDrawable(resources, R.drawable.a123, null)
val bitmap = drawableToBitmap(drawable)
getResources().getDrawable() 过时的解决方法
public Drawable getDrawable(@DrawableRes int id)
已经过时,推荐使用 public Drawable getDrawable(@DrawableRes int id, @Nullable Theme theme)
.
解决办法:
1、当你这个Drawable不受主题影响时
ResourcesCompat.getDrawable(getResources(), R.drawable.name, null);
2、当你这个Drawable受当前Activity主题的影响时
ContextCompat.getDrawable(getActivity(), R.drawable.name);
3、当你这个Drawable想使用另外一个主题样式时
ResourcesCompat.getDrawable(getResources(), R.drawable.name, anotherTheme);
view 获取bitmap 对象
private fun getBitmap(view: View): Bitmap {
val width = view.width
val height = view.height
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val canvas = Canvas()
canvas.setBitmap(bitmap)
view.draw(canvas)
return bitmap
}
ScrollView 获取 bitmap
private fun getBitmap(scrollVew: ScrollView): Bitmap {
val width = scrollVew.width
var height = 0
scrollVew.forEach {
height += it.height
}
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val canvas = Canvas()
canvas.setBitmap(bitmap)
scrollVew.draw(canvas)
return bitmap
}
bitmap增加水印
/**
* 给bitmap增加水印
*/
private fun AddTimeWatermark(bitmap: Bitmap): Bitmap {
//获取原始图片与水印图片的宽与高
val mBitmapWidth = bitmap.width
val mBitmapHeight = bitmap.height
val canvas = Canvas()
canvas.setBitmap(bitmap)
val mPaint = Paint()
val mFormat = SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(Date())
mPaint.color = Color.RED
mPaint.textSize = 40f
//水印的位置坐标
canvas.drawText(mFormat, mBitmapWidth / 2f, mBitmapHeight / 2f, mPaint)
return bitmap
}
bitmap 旋转
/**
* 旋转bitmap
*/
private fun rotateBitmap(bitmap: Bitmap, degree: Float): Bitmap? {
val width = bitmap.width
val height = bitmap.height
val matrix = Matrix()
matrix.postRotate(degree)
return if (!bitmap.isRecycled) {
return Bitmap.createBitmap(bitmap, 0, 0, width, height, matrix, true)
} else {
null
}
}
效果图:
bitmap 裁剪
裁剪 bitmap 中间区域
/**
* bitmap裁剪
*/
private fun cropBitmap(bitmap: Bitmap): Bitmap? {
val bitmapWidth = bitmap.width
val bitmapHeight = bitmap.height
val width = (bitmapWidth * 0.5).toInt() //裁剪后的bitmap宽度是原来的1/2
val height = (bitmapHeight * 0.5).toInt() //裁剪后的bitmap高度是原来的1/2
return if (!bitmap.isRecycled) {
val startX = (bitmapWidth - width) / 2 //开始x坐标
val startY = (bitmapHeight - height) / 2 //开始y坐标
return Bitmap.createBitmap(bitmap, startX, startY, width, height)
} else {
null
}
}
效果图:
获取本地图片宽高
File file = new File("xxx.png")
BitmapFactory.Options options = new BitmapFactory.Options();
//不把bitmap 加到内存
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(file.getAbsolutePath(), options);
int width = options.outWidth;
int height = options.outHeight;
总结
以上5种就是我们常用的压缩方法了,这里的压缩也只是针对在运行加载的bitmap占用内存的大小。我们在做App内存优化的时候,一般可以从这两个方面入手,一个内存泄漏,另外一个是Bitmap压缩了,在要求像素不高的情况下,可以对Bitmap进行压缩,并且针对一些只使用一次的bitmap,要做好recycle的处理。