先附上裁剪篇和选取篇的链接,结合本文食用风味更佳~
选取篇;裁剪篇
压缩目标
在讲压缩之前先要明确我们的目标
- 对图片进行处理,使其满足我们对图片分辨率的要求;
- 尽可能减小图片文件的大小,来节省上传时间和用户流量;
- 避免oom;
压缩图片相关函数
明确目标之后首先我们来编写我们可能需要用到的函数
读取图片
从file或者uri中读取bitmap的这一步,我们要对图片进行第一次的处理。生成bitmap可以使用这个方法BitmapFactory.decodeStream()
,由于目标图片可能分辨率很大,如果这里不进行处理很容易造成oom。
这里我们可以利用两个方式来降低bitmap所占用的内存。
inSampleSize
利用BitmapFactory.Options
中的inSampleSize
属性可以减小图片的分辨率。若inSampleSize=x
得到的bitmap属性就是原始分辨率的1/x。inSampleSize值只能为2的倍数。也就是说当inSampleSize
的值为2,4,6,8的时候才有用。这种方式并不能精确的得到我们想要的分辨率,但是作为初步的压缩还是非常合适的。
那么计算inSampleSize
的值可以先获取原始图片的大小,再根据我们自己的目标大小来进行初步压缩。要获取原始图片大小,我们可以利用BitmapFactory.Options
的inJustDecodeBounds
属性。
inPreferredConfig
利用BitmapFactory.Options
中的inPreferredConfig
属性可以改变图片的默认模式,bitmap有如下四种模式
模式 | 组成 | 占用内存 |
---|---|---|
ALPHA_8 | Alpha由8位组成 | 一个像素占用1个字节 |
ARGB_4444 | 4个4位组成即16位 | 一个像素占用2个字节 |
ARGB_8888 | 4个8位组成即32位 | 一个像素占用4个字节 |
RGB_565 | R为5位,G为6位,B为5位共16位 | 一个像素占用2个字节 |
Android默认的图片模式为ARGB_8888,但是如果我们的图片不需要太高的质量并且没有透明通道。我们完全可以使用RGB_565这种模式。
完整代码
/**
* @param
* @param uri
* @param targetWidth 限制宽度
* @param targetHeight 限制高度
* @return
* @throws Exception
*/
public static Bitmap getBitmapFromUri(Context context, Uri uri, float targetWidth, float targetHeight) throws Exception {
Bitmap bitmap = null;
InputStream input = context.getContentResolver().openInputStream(uri);
BitmapFactory.Options onlyBoundsOptions = new BitmapFactory.Options();
onlyBoundsOptions.inJustDecodeBounds = true;
onlyBoundsOptions.inDither = true;
onlyBoundsOptions.inPreferredConfig = Bitmap.Config.RGB_565;
BitmapFactory.decodeStream(input, null, onlyBoundsOptions);
if (input != null) {
input.close();
}
//获取原始图片大小
int originalWidth = onlyBoundsOptions.outWidth;
int originalHeight = onlyBoundsOptions.outHeight;
if ((originalWidth == -1) || (originalHeight == -1))
return null;
float widthRatio = originalWidth / targetWidth;
float heightRatio = originalHeight / targetHeight;
//计算压缩值
float ratio = widthRatio > heightRatio ? widthRatio : heightRatio;
if (ratio < 1)
ratio = 1;
BitmapFactory.Options bitmapOptions = new BitmapFactory.Options();
bitmapOptions.inSampleSize = (int) ratio;
bitmapOptions.inDither = true;
bitmapOptions.inPreferredConfig = Bitmap.Config.RGB_565;
input = context.getContentResolver().openInputStream(uri);
//实际获取图片
bitmap = BitmapFactory.decodeStream(input, null, bitmapOptions);
if (input != null) {
input.close();
}
return bitmap;
}
处理bitmap
bitmap的处理比较简单,我们可以使用Android系统为我们提供的函数extractThumbnail(Bitmap source, int width, int height)
,这个函数的内部实现很有意思,有空大家可以先看看。而如果我们的压缩要保证图片的等比例处理,需要合理的去计算新的width和height。计算方法如下
float widthRadio = (float) bitmap.getWidth() /(float) maxWidth;
float heightRadio = (float) bitmap.getHeight() / (float)maxHeight;
float radio = widthRadio > heightRadio ? widthRadio : heightRadio;
if (radio > 1) {
bitmap = ThumbnailUtils.extractThumbnail(bitmap, (int) (bitmap.getWidth() / radio), (int) (bitmap.getHeight() / radio));
}
保存bitmap并压缩文件大小
得到了合适分辨率的bitmap,我们接下来就需要对图片的大小进行压缩和保存了。接下来问题就来了,一张图片应该占用多大的空间呢?我的办法就是引入一个参数表明1像素占用的大小来处理图片。压缩图片大小我们可以使用bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos);
注意只有jpeg格式的图片才能被这个函数压缩!第二个参数表明图片的压缩质量,100表示不压缩。我们无法估算这个参数对图片最终大小的影响,所以我们只能采用循环的方式来处理我们的图片。
/**
* @param image
* @param outputStream
* @param limitSize 单位byte 由单位像素占用大小计算得出
* @throws IOException
*/
public static void compressImage(Bitmap image, OutputStream outputStream, float limitSize) throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
image.compress(Bitmap.CompressFormat.JPEG, 100, baos);
int options = 100;
//ignoreSize 以下的图片不进行压缩,发现小图的的压缩效率不高而且质量损毁的十分严重。
while (baos.toByteArray().length > limitSize&&baos.toByteArray().length> ignoreSize) {
baos.reset();
image.compress(Bitmap.CompressFormat.JPEG, options, baos);
//每次减少的量,可以进行调整。由于compress这个函数占用时间很长所以我们应当尽量减少循环次数
options -= 15;
Log.i("lzc","currentSize"+(baos.toByteArray().length/1024));
}
image.compress(Bitmap.CompressFormat.JPEG, options, outputStream);
baos.close();
outputStream.close();
}
压缩
编写完成压缩相关函数,接下来我们就要考虑这些函数的调用方式了。很明显这些操作都是耗时操作,不能放在主线程中执行。而且我们有压缩多张图片的需求,考虑到内存问题,我们应该使用service单独开进程来对图片压缩。
另外,多张图片的压缩是顺序,还是并发执行的问题值得我们考虑。顺序执行可以减少内存占用而并发执行可以减少压缩时间。
我选择了并发执行,毕竟压缩之后还要紧接上传,不宜让用户等待过久。我们来建立我们的service开启线程池来处理压缩图片流程。
创建service时建立线程池
@Override
public void onCreate() {
super.onCreate();
fileHashtable.clear();
executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE,
9, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
每次startServcie向线程池增加一个事件
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
executorService.execute(new PressRunnable(intent));
return super.onStartCommand(intent, flags, startId);
}
处理事件和压缩
//压缩图片线程
private class PressRunnable implements Runnable {
private Intent intent;
public PressRunnable(Intent intent) {
this.intent = intent;
}
@Override
public void run() {
onHandleIntent(intent);
}
}
//处理intent得到参数
protected void onHandleIntent(Intent intent) {
if (intent != null) {
final String action = intent.getAction();
if (ACTION_FOO.equals(action)) {
final Uri uri = intent.getParcelableExtra(EXTRA_PARAM1);
final ChoicePhotoManager.Option option = (ChoicePhotoManager.Option) intent.getSerializableExtra(EXTRA_PARAM2);
realCount = intent.getIntExtra(EXTRA_PARAM3, 1);
int position = intent.getIntExtra(EXTRA_PARAM4, 0);
handleActionFoo(uri, option, position);
}
}
}
//压缩图片
private void handleActionFoo(Uri uri, ChoicePhotoManager.Option option, int position) {
File file = new File(FileUntil.UriToFile(uri, this));
if (!file.exists())
return;
//创建新文件
File newFile = FileUntil.createTempFile(FileName + UUID.randomUUID() + ".jpg");
//压缩图片并保存到新文件
FileUntil.compressImg(this, file, newFile, option.pressRadio, option.maxWidth, option.maxHeight);
//读取原始图片的旋转信息,并给以现有图片
FileUntil.setFilePictureDegree(newFile, FileUntil.readPictureDegree(file.getPath()));
fileHashtable.put(position, newFile);
synchronized (PressImgService.class) {
count++;
if (realCount == count) {
callFinish();
}
}
}
//压缩完毕关闭servcie 回传压缩后图片的uri
private void callFinish() {
Intent intent = new Intent();
intent.setAction(callbackReceiver);
Uri[] uris = new Uri[fileHashtable.keySet().size()];
for (Map.Entry<Integer, File> integerFileEntry : fileHashtable.entrySet()) {
uris[integerFileEntry.getKey()] = Uri.fromFile(integerFileEntry.getValue());
}
for (int i = 0; i < realCount; i++) {
Log.i("lzc", "position---asd" + i);
}
intent.putExtra("data", uris);
sendBroadcast(intent);
fileHashtable.clear();
count = 0;
stopSelf();
}
注意
上面的代码很长,但是要注意的只有两点。
- 要注意读取之前文件的旋转信息,并赋值给新的文件。这样,新的图片才能得到正确的旋转角度。
用到的函数如下。
//读取文件的旋转信息
public static int readPictureDegree(String path) {
int degree = 0;
try {
ExifInterface exifInterface = new ExifInterface(path);
int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
switch (orientation) {
case ExifInterface.ORIENTATION_ROTATE_90:
degree = 90;
break;
case ExifInterface.ORIENTATION_ROTATE_180:
degree = 180;
break;
case ExifInterface.ORIENTATION_ROTATE_270:
degree = 270;
break;
}
} catch (IOException e) {
e.printStackTrace();
}
return degree;
}
//为文件设置旋转信息
public static void setFilePictureDegree(File file, int degree) {
try {
ExifInterface exifInterface = new ExifInterface(file.getPath());
int orientation = ExifInterface.ORIENTATION_NORMAL;
switch (degree) {
case 90:
orientation = ExifInterface.ORIENTATION_ROTATE_90;
break;
case 180:
orientation = ExifInterface.ORIENTATION_ROTATE_180;
break;
case 270:
orientation = ExifInterface.ORIENTATION_ROTATE_270;
break;
}
exifInterface.setAttribute(ExifInterface.TAG_ORIENTATION, orientation + "");
exifInterface.saveAttributes();
} catch (IOException e) {
e.printStackTrace();
}
}
2.由于是多线程并发,所以我们需要对几个关键模块加上同步锁。第一个是多张图片完成压缩记的计数
synchronized (PressImgService.class) {
count++;
if (realCount == count) {
callFinish();
}
第二个地方是我们图片读取的地方,否则会产生多张图拼接到一起的问题。
synchronized (PressImgService.class) {
bitmap = getBitmapFromUri(context, Uri.fromFile(file), maxWidth, maxHeight);
}
这样我们细数图片上传功能用到的知识点的三篇文章就全部讲完了。
撒花,完结!
细数图片上传功能用到的知识点(图片选取&拍照篇)
细数图片上传功能用到的知识点(裁剪篇)
细数图片上传功能用到的知识点(图片压缩篇)