在Android系统中,图片占用的内存比较大,由于加载图片导致的内存溢出是Android中内存溢出常见场景之一。所以在加载图片的时候,需要对图片进行相应的处理,关于图片的处理,一般情况下就是要对Bitmap进行合适的处理和优化。
在Android应用中,图片的主要存在方式:
以File的形式存在于SD卡中
以Stream的形式存在于内存中
以Bitmap形式存在于内存中
在SD卡中图片占用的内存与以Stream形式大小一样,均小于Bitmap形式下内存占比。即当图片从SD卡中以流的形式加载到内存中时大小是不会发生变化的,但是stream转化为Bitmap时,其大小会突然变大。这也是为什么当加载大量图片时容易出现OOM的主要原因。
BitmapFactory提供了4类方法加载Bitmap对象:
1 2 3 4 1. decodeFile 从文件中加载Bitmap对象 2. decodeResource 从资源中加载Bitmap对象 3. decodeStream 从输入流中加载Bitmap对象 4. decodeByteArray 从字节数组中加载Bitmap对象
其中decodeFile和decodeResource间接的调用了decodeStream方法。
建议阅读:[BitmapFactory][1]。
为什么转化为Bitmap时大小会突然变大呢?
以任意一张图片为例,我本地存了一张分辨率为750*1334,大小为119K。如果将这张图片以bitmap形式加载到内存中,它占用的大小是多少,如何计算呢?它的计算公式:
1 图片的占用内存 = 图片的长度(像素单位) * 图片的宽度(像素单位) * 单位像素所占字节数
其中单位占用字节数的大小是变化的,由BitmapFactory.Options的inPreferredConfig 决定,为Bitmap.Config类型,一般情况下默认为:ARGB_8888。不同类型占字节大小如下表所示:
![此处输入图片的描述][2]
可以计算出此图片以bitmap形式加载消耗内存为:7501334 4=3.81M,还是由于我测试的图片任意找的一张图,现在手机拍的高清图基本上都是2000+ * 2000+ * 4 = 15M+ 了。如果不做任何处理的图片大量使用到我们APP上,结局你懂得!
下面介绍下图片压缩处理的三大基本方式:
尺寸压缩,又分为采样率压缩和缩放压缩。
质量压缩
合理选择Bitmap的像素格式
尺寸压缩:改变宽高,减少像素。
质量压缩:内存不变,压缩转化后的bytes.length减少,适用于传输,png无效。
合理选择Bitmap的像素格式:改变字节数。
尺寸压缩 尺寸压缩会改变图片的尺寸,即压缩图片宽度和高度的像素点,从上面的计算公式我们可以得知,这样会降低图片由Stream转化为bitmap内存的占用,从而一定程度上减少OOM的概率。但是要注意,如果压缩比太大,也会由于像素点降低导致图片失真严重,最后图片由高清成了马赛克。
尺寸压缩主要用在图片资源本身较大,或者适当地采样并不会影响视觉效果的条件下,这时候我们输出的目标可能相对的较小,对图片的大小和分辨率都减小。
尺寸压缩有两种方式:采样率压缩和缩放压缩。
采样率压缩 如何进行尺寸压缩呢?其中一种方式就是合理设置[BitmapFactory.Options][3]的inSampleSize参数来设置所需要加载图片的尺寸。记住,是加载所需要合适的尺寸!例如一个ImageView显示图片,基本上ImageView所需要的尺寸不需要原始图片那么大,这时候我们就需要对图片进行一些必要的处理。按照ImageView尺寸大小,通过BitmapFactory.Options来设置恰当的inSampleSize参数值来压缩图片,显示在ImageView上,既避免了出现了OOM,也提高了Bitmap的加载性能。BitmapFactory提供了加载图片的四个方法都支持BitmapFactory.Options参数,通过他们就可以实现对图片进行压缩处理。
通过BitmapF.Options压缩图片的核心就在于inSampleSize参数,采样率或者采样率。采样率为整数,且为2的n次幂,n可以为0。即采样率为1,处理后的图片尺寸与原图一致。当采样率为2时,即宽、高均为原来的1/2,像素则为原来的1/4.其占有内存也为原来的1/4。当设置的采样率小于1时,其效果与1一样。当设置的inSampleSize大于1,不为2的指数时,系统会向下取一个最接近2的指数的值。
举个例子:
当imageView的尺寸为200300,图片的原始尺寸为600 * 900,则压缩比为3即可。但是如果图片尺寸为1000 * 1800呢?如果采样率设置为5,缩放后的图片尺寸为 200 360,此时还是能接受的。但是如果采样率设置为6,缩放后的图片会小于200 * 300。此时图片会被拉伸导致变形。
压缩图片及获取采样率常规步骤:
1 2 3 4 1. 将BitmapFactory.Options的inJustDecodeBounds参数设置为true加载图片。 2. 从BitmapFactory.Options中获取图片原始的宽高(outWidth/outHeight)值。 3. 根据采样率规则并结合所需要大小计算出合适的inSampleSize。 4. BitmapFactory.Options参数设置为false,然后重新加载图片。
在上面4步处理后图片基本上就是缩放后的图片了。解释一下inJustDecodeBounds参数,当设置为true时,BitmapFactory只会解析图片的原始宽/高信息,不会真正的去加载图片,所以这个操作是轻量级的。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.BitmapFactory; public class ImageUtil { /** * 根据目标View的尺寸压缩图片返回bitmap * @param resources * @param resId * @param width 目标view的宽 * @param height 目标view的高 * @return */ public static Bitmap decodeBitmapFromResource(Resources resources, int resId,int width ,int height){ BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeResource(resources,resId,options); //获取采样率 options.inSampleSize = calculateInSampleSize(options,width,height); options.inJustDecodeBounds = false; return BitmapFactory.decodeResource(resources,resId,options); } /** * 获取采样率 * @param options * @param reqWidth 目标view的宽 * @param reqHeight 目标view的高 * @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){ int halfHeight = originalHeight / 2; int halfWidth = originalWidth / 2; //压缩后的尺寸与所需的尺寸进行比较 while ((halfWidth / inSampleSize) >= reqHeight && (halfHeight /inSampleSize)>=reqWidth){ inSampleSize *= 2; } } return inSampleSize; } }
官方文档方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 /** * 计算压缩的比例 * * @param options 解析图片所需的BitmapFactory.Options * @param minSideLength 调整后图片最小的宽或高值,一般赋值为 -1 * @param maxNumOfPixels 调整后图片的内存占用量上限 * @return */ public static int computeSampleSize(BitmapFactory.Options options, int minSideLength, int maxNumOfPixels) { int initialSize = computeInitialSampleSize(options, minSideLength, maxNumOfPixels); int roundedSize; if (initialSize <= 8) { roundedSize = 1; while (roundedSize < initialSize) { roundedSize <<= 1; } } else { roundedSize = (initialSize + 7) / 8 * 8; } return roundedSize; } /** * 计算原始大小 * * @param options 解析图片所需的BitmapFactory.Options * @param minSideLength 调整后图片最小的宽或高值,一般赋值为 -1 * @param maxNumOfPixels 调整后图片的内存占用量上限 * @return */ private static int computeInitialSampleSize(BitmapFactory.Options options, int minSideLength, int maxNumOfPixels) { double w = options.outWidth; double h = options.outHeight; int lowerBound = (maxNumOfPixels == -1) ? 1 : (int) Math.ceil(Math.sqrt(w * h / maxNumOfPixels)); int upperBound = (minSideLength == -1) ? 128 : (int) Math.min(Math.floor(w / minSideLength), Math.floor(h / minSideLength)); if (upperBound < lowerBound) { // return the larger one when there is no overlapping zone. return lowerBound; } if ((maxNumOfPixels == -1) && (minSideLength == -1)) { return 1; } else if (minSideLength == -1) { return lowerBound; } else { return upperBound; } }
上面示例代码中采用的是decodeResource方法,其它几个方法基本类似.
缩放法压缩 样板代码:
1 2 3 4 Matrix matrix = new Matrix(); matrix.setScale(0.5f, 0.5f); bm = Bitmap.createBitmap(bit, 0, 0, bit.getWidth(),bit.getHeight(), matrix, true); Log.i("wechat", "压缩后图片的大小" + (bm.getByteCount() / 1024 / 1024)+ "M宽度为" + bm.getWidth() + "高度为" + bm.getHeight());
与采样率法类似。放缩法压缩使用的是通过矩阵对图片进行裁剪,也是通过缩放图片尺寸,来达到压缩图片的效果,和采样率的原理一样。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 /** * 矩阵缩放图片 * @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; }
其实除了使用矩阵之外,还可以使用createScaledBitmap方法对Bitmap进行缩放。
1 2 3 bm = Bitmap.createScaledBitmap(bit, 150, 150, true); Log.i("wechat", "压缩后图片的大小" + (bm.getByteCount() / 1024) + "KB宽度为" + bm.getWidth() + "高度为" + bm.getHeight());
这里是将图片压缩成用户所期望的长度和宽度,但是这里要说,如果用户期望的长度和宽度和原图长度宽度相差太多的话,图片会很不清晰。
质量压缩 质量压缩不会改变图片的像素点,即前面我们说到的在转化为bitmap时占用内存变大时状况不会得到改善。因为质量压缩是在保持像素的前提下改变图片的位深及透明度,来达到压缩图片的目的,图片的长,宽,像素都不会改变,那么bitmap所占内存大小是不会变的。
但是质量压缩可以减少我们存储在本地文件的大小,即放到disk上的大小。质量压缩能方便的设置压缩百分比,达到我们需要的大小。还是先看方法:
1 Bitmap.compress(CompressFormat format, int quality, OutputStream stream)
简单解释一下这三个参数,第一个表示Bitmap被压缩成的图片格式[Bitmap.CompressFormat][4];第二个表示压缩的质量控制,范围0~100,很好理解。quality为80,表示压缩为原来80%的质量效果。有些格式,例如png,它是无损的,这个值设置了也是无效的。因为质量压缩是在保持像素前提下改变图片的位深及透明度等来压缩图片的,所以quality值与最后生成的图片的大小并不是线性关系,比如大小为300k的图片,当quality为90时,得到的图片大小并不是为270K。大家通过代码测试一下就会发现了。
质量压缩不会改变图片的像素点,即我们使用完质量压缩后,在转换Bitmap时占用内存依旧不会减小。但是可以减少我们存储在本地文件的大小,即放到disk上的大小。
样板代码:
1 2 3 4 5 val baos = ByteArrayOutputStream() // 质量压缩方法,这里100表示不压缩,把压缩后的数据存放到baos中 bmRaw.compress(Bitmap.CompressFormat.JPEG, 50, baos) val bais = ByteArrayInputStream(baos.toByteArray()) val bmScaled = BitmapFactory.decodeStream(bais, null, null)
说明:
使用JPEG格式的质量压缩
1 bmRaw.compress(Bitmap.CompressFormat.JPEG, 50, baos)
对一张透明图片(png),内存、宽高不变,bytes.length 减少。图片会失去透明度,透明处变黑。
对一张非透明图片(png、jpg),内存、宽高不变,bytes.length 减少。
使用 PNG 格式的质量压缩
1 bmRaw.compress(Bitmap.CompressFormat.PNG, 50, baos)
–
CompressFormat.PNG, PNG 格式是无损的,它无法再进行质量压缩,quality 这个参数就没有作用了,会被忽略,所以最后图片保存成的文件大小不会有变化;
CompressFormat.WEBP ,这个格式是 google 推出的图片格式,它会比 JPEG 更加省空间,经过实测大概可以优化 30% 左右。
核心代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 /** * 质量压缩方法,并不能减小加载到内存时所占用内存的空间,应该是减小的所占用磁盘的空间 * @param image * @param compressFormat * @return */ public static Bitmap compressbyQuality(Bitmap image, Bitmap.CompressFormat compressFormat) { ByteArrayOutputStream baos = new ByteArrayOutputStream(); image.compress(compressFormat, 100, baos);//质量压缩方法,这里100表示不压缩,把压缩后的数据存放到baos中 int quality = 100; while ( baos.toByteArray().length / 1024 > 100) { //循环判断如果压缩后图片是否大于100kb,大于继续压缩 baos.reset();//重置baos即清空baos if(quality > 20){ quality -= 20;//每次都减少20 }else { break; } image.compress(Bitmap.CompressFormat.JPEG, quality, baos);//这里压缩options%,把压缩后的数据存放到baos中 } ByteArrayInputStream isBm = new ByteArrayInputStream(baos.toByteArray());//把压缩后的数据baos存放到ByteArrayInputStream中 BitmapFactory.Options options = new BitmapFactory.Options(); options.inPreferredConfig = Bitmap.Config.RGB_565; Bitmap bmp = BitmapFactory.decodeStream(isBm, null, options);//把ByteArrayInputStream数据生成图片 return bmp; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 /** * 质量压缩并存到SD卡中 * @param bitmap * @param reqSize 需要的大小 * @return */ public static String qualityCompress1(Bitmap bitmap ,int reqSize){ ByteArrayOutputStream baos = new ByteArrayOutputStream(); //这里100表示不压缩,把压缩后的数据存放到baos中 bitmap.compress(Bitmap.CompressFormat.JPEG,100,baos); int options = 95; //如果压缩后的大小超出所要求的,继续压缩 while (baos.toByteArray().length / 1024 > reqSize){ baos.reset(); //每次减少5%质量 if (options>5){//避免出现options<=0 options -=5; } else { break; } bitmap.compress(Bitmap.CompressFormat.JPEG,options,baos); } //存入SD卡中 SimpleDateFormat formatYMD = new SimpleDateFormat("yyyy/MM/dd"); String compressImgUri = LOCAL_URL + "000/" + formatYMD.format(new Date()) + "/" + System.currentTimeMillis() + "a.jpg"; File outputFile = new File(compressImgUri); if (!outputFile.exists()) { outputFile.getParentFile().mkdirs(); } else { outputFile.delete(); } FileOutputStream out = null; try { out = new FileOutputStream(outputFile); } catch (FileNotFoundException e) { e.printStackTrace(); } bitmap.compress(Bitmap.CompressFormat.JPEG, options, out); return outputFile.getPath(); } private void compressQuality() { Bitmap bm = BitmapFactory.decodeResource(getResources(), R.drawable.test); mSrcSize = bm.getByteCount() + "byte"; ByteArrayOutputStream bos = new ByteArrayOutputStream(); bm.compress(Bitmap.CompressFormat.JPEG, 100, bos); byte[] bytes = bos.toByteArray(); mSrcBitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length); }
代码相当简单,前面基本解释过了。
合理选择Bitmap的像素格式 ARG_B8888格式的图片,每像素占用4Byte,而RGB_565则是2Byte。显而易见,不同的像素格式其Bitmap的大小也就不同。
关于像素格式,建议阅读:[Bitmap.Config][5]。
综合使用 压缩并保存图片
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 /** * 压缩并保存图片 * <p> * 图片小于50k * * @param localUrl 本地图片路径(例如:/storage/emulated/0/Pictures/multi_image_20180808_130928.jpg) * @param path 目标文件路径(例如:/storage/emulated/0/zhcx/img/cgzf/) * @param filename 文件名称(例如:33000019_0.jpg) */ public static void saveCgzfPic(String localUrl, String path, String filename) { //获得图片的宽和高,但并不把图片加载到内存当中 BitmapFactory.Options options = new BitmapFactory.Options(); options.inPreferredConfig = Bitmap.Config.RGB_565; options.inJustDecodeBounds = true; BitmapFactory.decodeFile(localUrl, options); options.inSampleSize = computeSampleSize(options, -1, (int) (0.5 * 1024 * 1024)); //使用获取到的inSampleSize再次解析图片 options.inJustDecodeBounds = false; Bitmap bitmap = BitmapFactory.decodeFile(localUrl, options); LogUtil.myD("inSampleSize:" + options.inSampleSize); File rootFile = new File(path); if (!rootFile.exists()) { rootFile.mkdirs(); } File file = new File(rootFile, filename); try { if (!file.exists()) { file.createNewFile(); } ByteArrayOutputStream baos = new ByteArrayOutputStream(); bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos);// 质量压缩方法,这里100表示不压缩,把压缩后的数据存放到baos中 //质量压缩 int quality = 95; while (baos.toByteArray().length / 1024 > 50) { // 循环判断如果压缩后图片是否大于50kb,大于继续压缩 //每次减少5%质量 if (quality > 5) {//避免出现options<=0 quality -= 5; } else { break; } LogUtil.d("caowj", "length:" + baos.toByteArray().length + ",,,quality:" + quality); baos.reset(); // 重置baos即清空baos bitmap.compress(Bitmap.CompressFormat.JPEG, quality, baos);// 这里压缩options%,把压缩后的数据存放到baos中 } LogUtil.d("caowj", "2length:" + baos.toByteArray().length + ",,,2quality:" + quality); //保存图片 FileOutputStream fos = new FileOutputStream(file); bitmap.compress(Bitmap.CompressFormat.JPEG, quality, fos); //// TODO: 2018/8/8 问题:byte数组解析成bitmap后,再次解析成byte数组,变大了,为什么? // ByteArrayInputStream isBm = new ByteArrayInputStream(baos.toByteArray());// 把压缩后的数据baos存放到ByteArrayInputStream中 // L.d("caowj", "3length:" + baos.toByteArray().length); // // Bitmap bitmap2 = BitmapFactory.decodeStream(isBm, null, null);// 把ByteArrayInputStream数据生成图片 // // bitmap2.compress(Bitmap.CompressFormat.JPEG, 100, baos); // L.d("caowj", "sss:" + baos.toByteArray().length); // bitmap2.compress(Bitmap.CompressFormat.JPEG, 100, fos); fos.flush(); fos.close(); if (bitmap.isRecycled()) { bitmap.recycle(); } } catch (Exception e) { e.printStackTrace(); } } /** * 计算压缩的比例 * * @param options 解析图片所需的BitmapFactory.Options * @param minSideLength 调整后图片最小的宽或高值,一般赋值为 -1 * @param maxNumOfPixels 调整后图片的内存占用量上限 * @return */ public static int computeSampleSize(BitmapFactory.Options options, int minSideLength, int maxNumOfPixels) { int initialSize = computeInitialSampleSize(options, minSideLength, maxNumOfPixels); int roundedSize; if (initialSize <= 8) { roundedSize = 1; while (roundedSize < initialSize) { roundedSize <<= 1; } } else { roundedSize = (initialSize + 7) / 8 * 8; } return roundedSize; } /** * 计算原始大小 * * @param options 解析图片所需的BitmapFactory.Options * @param minSideLength 调整后图片最小的宽或高值,一般赋值为 -1 * @param maxNumOfPixels 调整后图片的内存占用量上限 * @return */ private static int computeInitialSampleSize(BitmapFactory.Options options, int minSideLength, int maxNumOfPixels) { double w = options.outWidth; double h = options.outHeight; int lowerBound = (maxNumOfPixels == -1) ? 1 : (int) Math.ceil(Math.sqrt(w * h / maxNumOfPixels)); int upperBound = (minSideLength == -1) ? 128 : (int) Math.min(Math.floor(w / minSideLength), Math.floor(h / minSideLength)); if (upperBound < lowerBound) { // return the larger one when there is no overlapping zone. return lowerBound; } if ((maxNumOfPixels == -1) && (minSideLength == -1)) { return 1; } else if (minSideLength == -1) { return lowerBound; } else { return upperBound; } }
下面的例子主要用到了Bitmap的采样压缩,同时使用到了inBitmap内存复用([深入理解Android Bitmap的各种操作][9],原来的似乎有问题,下面的是自己改动的)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 /** * 采样率压缩,这个和矩阵来实现缩放有点类似,但是有一个原则是“大图小用用采样,小图大用用矩阵”。 * 也可以先用采样来压缩图片,这样内存小了,可是图的尺寸也小。如果要是用 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(); image.compress(compressFormat, 100, baos);//质量压缩方法,这里100表示不压缩,把压缩后的数据存放到baos中 ByteArrayInputStream isBm = new ByteArrayInputStream(baos.toByteArray());//把压缩后的数据baos存放到ByteArrayInputStream中 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{ if (image != null && canUseForInBitmap(image, options)) { options.inBitmap = image; } }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是否能够被复用。 static boolean canUseForInBitmap(Bitmap candidate, BitmapFactory.Options targetOptions) { if (candidate == null || !candidate.isMutable()) { return false; } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { // From Android 4.4 (KitKat) onward we can re-use if the byte size of // the new bitmap is smaller than the reusable bitmap candidate allocation byte count. int width = targetOptions.outWidth / targetOptions.inSampleSize; int height = targetOptions.outHeight / targetOptions.inSampleSize; int byteCount = width * height * getBytesPerPixel(targetOptions.inPreferredConfig); return byteCount <= candidate.getAllocationByteCount(); } // On earlier versions, the dimensions must match exactly and the inSampleSize must be 1 return candidate.getWidth() == targetOptions.outWidth && candidate.getHeight() == targetOptions.outHeight && targetOptions.inSampleSize == 1 && candidate.getConfig() == targetOptions.inPreferredConfig; } /** * A helper function to return the byte usage per pixel of a bitmap based on its configuration. */ static int getBytesPerPixel(Bitmap.Config config) { if (config == Bitmap.Config.ARGB_8888) { return 4; } else if (config == Bitmap.Config.RGB_565) { return 2; } else if (config == Bitmap.Config.ARGB_4444) { return 2; } else if (config == Bitmap.Config.ALPHA_8) { return 1; } return 1; } ``` 拍照或者相册选择图片后,能得到一个图片的路径,首先需要根据图片路径,利用采样率压缩一次,如BitmapUtils.decodeBitmapFromFilePath(“图片路径”, 800,800);如此操作得到的bitmap再压缩为字节输出流,然后写入file,就可以上传服务器了(当然,上传图片到服务器有多种方式)。bitmap再压缩为字节输出流(可以判断字节流大小,再决定是否继续压缩)再写入file,是耗时操作,必须放到子线程中执行。 如下所示:
private String pathSource;//原图文件路径
private String pathCompressed;//压缩后的图片文件路径
private int kb_max = 1000;//压缩到多少KB,不能精确,只能<=kb_max
private int quality_max = 80;//压缩精度,尽量>=50
private int reqWidth = 1000;//期望的图片宽度
private int reqHeight = 1000;//期望的图片高度
final Bitmap bitmap = decodeBitmapFromFilePath(compressFileBean.getPathSource(), compressFileBean.getReqWidth(), compressFileBean.getReqHeight());
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
int quality = 80;
//压缩格式选取JPEG就行了,quality,压缩精度尽量不要低于50,否则影响清晰度
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, byteArrayOutputStream);
while (byteArrayOutputStream.toByteArray().length / 1024 > compressFileBean.getKb_max() && quality > compressFileBean.getQuality_max()) {
// 循环判断如果压缩后图片是否大于kb_max kb,大于继续压缩,
byteArrayOutputStream.reset();
quality -= 10;
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, byteArrayOutputStream);
}
try {
final File fileCompressed = createFile(compressFileBean.getPathCompressed());
FileOutputStream fileOutputStream = new FileOutputStream(fileCompressed);
fileOutputStream.write(byteArrayOutputStream.toByteArray());//写入目标文件
fileOutputStream.flush();
fileOutputStream.close();
byteArrayOutputStream.close();
if (fileCompressed != null && fileCompressed.length() > 0)
runOnUiThread(new Runnable() {
@Override
public void run() {
//压缩成功
compressFileCallback.onCompressFileFinished(fileCompressed, bitmap);
}
});
} catch (final Exception e) {
e.printStackTrace();
runOnUiThread(new Runnable() {
@Override
public void run() {
//压缩失败
compressFileCallback.onCompressFileFailed("压缩图片文件失败" + e.getMessage());
}
});
}
BitmapUtils.compressFile(new BitmapUtils.CompressFileBean.Builder() .setFileSource(file.getAbsolutePath()) .setFileCompressed(getExternalCacheDir() + File.separator + “Feedback” + File.separator + System.currentTimeMillis() + “.jpg”) .setKb_max(100) .setQuality_max(50) .setReqWidth(800) .setReqHeight(800).build(), new BitmapUtils.CompressFileCallback() { @Override public void onCompressFileFinished(File file, Bitmap bitmap) {
// if (feedbackUI.getRvAdapter().getList_bean().size() == 3)
// feedbackUI.getRvAdapter().setHaveFootView(false);
// feedbackUI.getRvAdapter().add(file);
}
@Override
public void onCompressFileFailed(String s) {
}
});
1 2 3 4 5 6 整理的相关工具类[BitmapUtils.java][6]。 图片很多时候压缩的过程比较耗时,不要放到主线程执行。由于质量压缩并不会减少图片转换为bitmap时的内存消耗,如果是为了避免出现OOM,建议选择合适的像素格式同时进行适当的尺寸压缩即可,但是如果是为了减少上传时候的流量消耗,则可以三者结合起来使用,即先选择合适的像素格式进行适当的尺寸压缩,然后再进一步进行质量压缩。 项目地址:[点击下载][7]。
public void reset() 将此字节数组输出流的count字段重置为零,从而丢弃输出流中目前已累积的所有数据输出。
[详解Bitmap尺寸压缩与质量压缩][8]
[深入理解Android Bitmap的各种操作][9]
[Bitmap 的四种压缩方式详解][10]
[bitmap的六种压缩方式,Android图片压缩][11](有代码,有详细打印大小变化)
[Android Bitmap的常用压缩方式][12]
[Bitmap压缩方法][13]
[bitmap 质量压缩并保存到本地][14]
[Android中高效的显示图片 - Bitmap的内存模型][15]
[Android Bitmap压缩的正确姿势][16]
[Java ByteArrayOutputStream类][17]
[1]: https://www.zybuluo.com/946898963/note/1617282
[2]: http://image.c3a3.com/bitmap_config.jpeg
[3]: https://www.zybuluo.com/946898963/note/1669622
[4]: https://www.zybuluo.com/946898963/note/1617161
[5]: https://www.zybuluo.com/946898963/note/1617150
[6]: https://github.com/thinkerzhangyan/BitmapStudy/blob/master/app/src/main/java/com/thinkerzhangyan/bitmapstudy/BitmapUtils.java
[7]: https://github.com/flowsky37/BitmapDemo
[8]: https://blog.csdn.net/flowsky37/article/details/78460117
[9]: https://blog.csdn.net/wanliguodu/article/details/84973846
[10]: https://blog.csdn.net/Gdeer/article/details/100800722?depth_1-utm_source=distribute.pc_relevant.none-task&utm_source=distribute.pc_relevant.none-task
[11]: https://blog.csdn.net/HarryWeasley/article/details/51955467?depth_1-utm_source=distribute.pc_relevant.none-task&utm_source=distribute.pc_relevant.none-task
[12]: https://www.jianshu.com/p/08ed0e3c4e71
[13]: https://blog.csdn.net/zhijiandedaima/article/details/81530145?depth_1-utm_source=distribute.pc_relevant.none-task&utm_source=distribute.pc_relevant.none-task
[14]: https://blog.csdn.net/sinat_28864443/article/details/83184550?depth_1-utm_source=distribute.pc_relevant.none-task&utm_source=distribute.pc_relevant.none-task
[15]: https://www.jianshu.com/p/eadb0ef271b0
[16]: https://blog.csdn.net/confusing_awakening/article/details/93076384?depth_1-utm_source=distribute.pc_relevant.none-task&utm_source=distribute.pc_relevant.none-task
[17]: https://www.runoob.com/java/java-bytearrayoutputstream.html
上一篇:启动白屏补充
下一篇:FrameMetricsListener