压缩前先搞明白原理:Bitmap占用的内存大小:
bytes = 原始图片宽*(options.inTargetDensity/options.inDensity)*原始图片长*(options.inTargetDensity/options.inDensity)*每个像素点位数复制代码
inTargetDensity指的是当前手机的密度,inDensity是图片的所在drawable目录生成的密度
使用sample采样率来对Bitmap进行压缩到指定的宽和高(原理不在赘述) 方法相信大家都熟悉,网上一搜都大体类似,下面是具体的方法
// 使用decodeRes方法对资源进行转换,使用InJustDecodeBounds属性置为true,先获图//片高,native层先不返回Bitmap//进行计算sampleSize,然后设为false,再次decode返回Bitmappublic Bitmap compressBitmapIntoThumbnailPic(Context context, int res) { BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; Bitmap bitmap1 = BitmapFactory.decodeResource(context.getResources(), res, options); Log.d("tag", "first bitmap == " + bitmap1); int sampleBitmap = calculateInSampleSize(options, 40, 40); options.inSampleSize = sampleBitmap; options.inJustDecodeBounds = false; Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), res, options); Log.d("tag", "final == " + bitmap.getByteCount() + " target real density " + options.inTargetDensity + " folder density " + options.inDensity); return bitmap; }复制代码
// 对要求的宽高和当前图片宽高对比,取一个最小的比例作为sampleSize private int calculateInSampleSize(BitmapFactory.Options options, int requireW, int requereH) { int sampleSize = 1; int outHeight = options.outHeight; int outWidth = options.outWidth; int rationHeight = Math.round(outHeight / requereH); int rationWidth = Math.round(outWidth / requireW); if (rationHeight > 1 || rationWidth > 1) { sampleSize = rationHeight > rationWidth ? rationWidth : rationHeight; } Log.d("tag", "outHeight = " + outHeight + "outWidth = " + outWidth + " -------- " + sampleSize); return sampleSize; }复制代码
执行上述的代码能正常的将一个大图片进行压缩展示,然后自己看了下源码,发现BitmapFactory最终调用的都是decodeStream(...) 这个方法来处理的,于是想试一下直接处理流来对图片压缩也应该可以,代码见下:
/** * 用流的形式生成Bitmap **/ public Bitmap compressBitmapIntoThumbnailPic(InputStream is) { BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; // 最终返回的Bitmap Bitmap finalBitmap = null; if(Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT){ //设置inJustDecodeBounds在native decode时候只返回尺寸等,Bitmap为空 // 或者将inputStream -> BufferedInputStream 保证某些情况reset不支持 BitmapFactory.decodeStream(is, null, options); int sampleBitmap = calculateInSampleSize(options, 80, 80); options.inSampleSize = sampleBitmap; options.inJustDecodeBounds = false; try { is.reset(); } catch (IOException e) { e.printStackTrace(); } finalBitmap = BitmapFactory.decodeStream(is, null, options); }else {// 4.4包含以后就没有这个is.mark(1024)的大小限制问题了。不会出现OOM if (is.markSupported()) { try { BitmapFactory.decodeStream(is,null,options); int sampleBitmap = calculateInSampleSize(options, 40, 40); options.inSampleSize = sampleBitmap; options.inJustDecodeBounds = false; is.reset(); finalBitmap = BitmapFactory.decodeStream(is, null, options); Log.d("tag", "final == " + finalBitmap.getByteCount() + " target real density " + options.inTargetDensity + " folder density " + options.inDensity); } catch (IOException e) { e.printStackTrace(); } } } return finalBitmap; }复制代码
这里对版本做了一个判断,只是为了调式方便。先说下原因,和上面方法相比而言,多了一个 is.reset , 不加的话直接就返回null 了,即使我们已近设置了inJustDecodeBounds为false, 这就奇怪了,这时候还是看下源码来找答案吧,圈红的部分意思是 : 我们在options里设置要求只返回大小或者流不能被decode的时候就返回null,那就说明是流不能被decode了,然后想到了流被使用过后的位置是会改变的,我们第一次decode的时候流执行了mark方法,下次再执行这个流的时候就必须要使用reset,返回上次标记的内容,这样才能正常使用流。有点恍然大悟~,被自己感动了。
然后我们去看下InputStream的源码是怎么说的,重点是readLimit这个关键字,大体意思是在流支持mark的情况下,调用reset方法会返回给记录的字节数,但是如果超过了这个 阀值,调用reset不能全部的返回,/** * Marks the current position in this input stream. A subsequent call to * thereset
method repositions this stream at the last marked * position so that subsequent reads re-read the same bytes. * *The
readlimit
arguments tells this input stream to * allow that many bytes to be read before the mark position gets * invalidated. * *The general contract of
mark
is that, if the method *markSupported
returnstrue
, the stream somehow * remembers all the bytes read after the call tomark
and * stands ready to supply those same bytes again if and whenever the method *reset
is called. However, the stream is not required to * remember any data at all if more thanreadlimit
bytes are * read from the stream beforereset
is called. * *Marking a closed stream should not have any effect on the stream. * *
The
mark
method ofInputStream
does * nothing. * * @param readlimit the maximum limit of bytes that can be read before * the mark position becomes invalid. * @see java.io.InputStream#reset() */复制代码
在reset的时候容易发生OOM,原因是在流在创建时没有调用mark的话,mark记录的大小肯定是小于整体的,这时候你去调用reset请求的大于mark记录的大小,就会OOM 。
/** * Repositions this stream to the position at the time the *mark
method was last called on this input stream. * *The general contract of
reset
is: * *
- *
- If the method
markSupported
returns *true
, then: * *- If the method
mark
has not been called since * the stream was created, or the number of bytes read from the stream * sincemark
was last called is larger than the argument * tomark
at that last call, then an *IOException
might be thrown. * * - If such an
IOException
is not thrown, then the * stream is reset to a state such that all the bytes read since the * most recent call tomark
(or since the start of the * file, ifmark
has not been called) will be resupplied * to subsequent callers of theread
method, followed by * any bytes that otherwise would have been the next input data as of * the time of the call toreset
.
- If the method
- If the method
markSupported
returns *false
, then: * *- The call to
reset
may throw an *IOException
. * * - If an
IOException
is not thrown, then the stream * is reset to a fixed state that depends on the particular type of the * input stream and how it was created. The bytes that will be supplied * to subsequent callers of theread
method depend on the * particular type of the input stream.
- The call to
The method reset
for class InputStream
* does nothing except throw an IOException
. * * @exception IOException if this stream has not been marked or if the * mark has been invalidated. * @see java.io.InputStream#mark(int) * @see java.io.IOException */ public synchronized void reset() throws IOException { throw new IOException("mark/reset not supported"); }复制代码
上面代码中有区分4.4版本的判断,这里解释下原因:如下图所示,在4.3源码中默认mark的阀值大小为1024K,这样的话对大图片很容易引起OOM ; 在4.4(包含)之后,去掉了这个case,没有具体的1024限制了。这里只是对原理做了简单的研究,实际上我们还是使用Glide之类的框架去做加载图片的工作。但是对原理有些了解还是不错的。
扩展 这里还有俩点疑问,
- 为啥设置了inJustDecodeBounds = true 就返回空的Bitmap ; 2.第一种用decodeResource的方法为啥能成功的返回Bitmap ??? 还是要去源码中找答案:
结尾
有疑问还是要去看源码啊,知其然也要知其所以然·,水平有限,错误之处请帮忙斧正