Volley/Xutils对大图片处理算法源码分析
Volley/Xutils对大图片处理算法源码分析
图片处理算法源码分析
这几個开源框架,都可以对图片进行请求加载,那么它们又是怎么样处理大图片的呢?在大图片的面前,它们到底怎么防止OOM的呢?
其实,处理大的bitmap來說,核心方法就是设置这個采样率啦。但是这個采样率不能硬编码,我们需要动态地进行计算,而这几大框架也是动态计算。他们的计算方式都不同,但结果是差不多的,目的就是为了取一個合适的采样率。
理论:第一次读取图片,不写入内存,直接获取到图片的宽高,再通过用户设置的宽高,和这個拉伸形式來计算期望的宽高,结合这四個参数來找出最适合的采样率,或者直接根据屏幕大小,控件大小來计算最佳采样率。
相关开源框架的获取地址
Android-volley
然后呢,这里的重点不是怎么使用,而是看看怎么处理大图片的。
但說到大图片,我们还是需要知道Volley怎么从服务器中请求图片,先不說大!
看码:
String url = "https://imgs.sunofbeaches.com/group1/M00/00/05/rBsADV2rEz-AIzSoAABi-6nfiqs456.png";
//1、创建一個图片的请求
int maxWidth = 0;//0为默认的请求宽度
int maxHeight = 0;//0为默认的请求高度,后面再解释吧!
Bitmap.Config decodeConfig = Bitmap.Config.ARGB_4444;//配置
ImageView.ScaleType scaleType = ImageView.ScaleType.FIT_XY;//拉伸形式
ImageRequest imageRequest = new ImageRequest(url, new Response.Listener<Bitmap>() {
@Override
public void onResponse(Bitmap response) {
//结果回调
mImage.setImageBitmap(response);
}
}, maxWidth, maxHeight, scaleType, decodeConfig, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
//错误回调
Log.e(TAG, "errorMsg:" + error.getMessage());
}
});
//2、创建请求队列
RequestQueue queue = Volley.newRequestQueue(this);
//3、添加到队列中执行
queue.add(imageRequest);
目的很简单,也就只是去加载一张图片而已。
好,到这里的话,已经可以加载一张图片吧,那么我们进行细看吧!
首先是这個创建图片的请求:
public ImageRequest(String url, Response.Listener<Bitmap> listener, int maxWidth, int maxHeight,
ScaleType scaleType, Config decodeConfig, Response.ErrorListener errorListener)
这個构造方法又长又臭是吧,那我们就看看它们是什么意思吧!到底什么玩意呀!
@param url URL of the image
图片的URL
@param listener Listener to receive the decoded bitmap
结果回调 ,也就是请求数据成功了,这里可以获取到bitmap
@param maxWidth Maximum width to decode this bitmap to, or zero for none
最大的宽度,图片最大的宽度,用于计算这個采样率Options,可以为0默认宽度
@param maxHeight Maximum height to decode this bitmap to, or zero for none
最大的高度,也是用于计算采样率的,当它为0的时候,就是默认的高度
@param scaleType The ImageViews ScaleType used to calculate the needed image size.
这個拉伸类型,用于计算这個需要的图片大小
@param decodeConfig Format to decode the bitmap to
图片的编码格式
@param errorListener Error listener, or null to ignore errors
错误的回调 ,当请求图片发生错误的时候,就会执行这里。
好,那么基本上把上面的参数解释了一次。其中两個回调不需要再深入說明了吧,就那么回事。
一個一個來,不用急不用抢,每個都有奖!
先是宽和高吧,这其实是用于计算采样率的。我们点去看这個构造函数:
public ImageRequest(String url, Response.Listener<Bitmap> listener, int maxWidth, int maxHeight,
ScaleType scaleType, Config decodeConfig, Response.ErrorListener errorListener) {
super(Method.GET, url, errorListener);
setRetryPolicy(
new DefaultRetryPolicy(IMAGE_TIMEOUT_MS, IMAGE_MAX_RETRIES, IMAGE_BACKOFF_MULT));
mListener = listener;
mDecodeConfig = decodeConfig;
mMaxWidth = maxWidth;
mMaxHeight = maxHeight;
mScaleType = scaleType;
}
我们一点也不惊讶地发现,这個mMaxWidth = maxWidth;mMaxHeight = maxHeight;,再寻根问底,用到这個mMaxWidth和mMaxHeight的地方也不多。找到这样的代码
private Response<Bitmap> doParse(NetworkResponse response) {
byte[] data = response.data;
BitmapFactory.Options decodeOptions = new BitmapFactory.Options();
Bitmap bitmap = null;
//当我们传进來的最大宽度为最小宽度为0的时候,就会使用一個沒有设置采样率大小的decodeOptions
if (mMaxWidth == 0 && mMaxHeight == 0) {
decodeOptions.inPreferredConfig = mDecodeConfig;
bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
} else {
//第一次读取图片的时候,不存进内存(true为不存进内存,false则存进内存)
decodeOptions.inJustDecodeBounds = true;
//第一次读取图片
BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
//获取到图片的实际宽高
int actualWidth = decodeOptions.outWidth;
int actualHeight = decodeOptions.outHeight;
//计算期望的宽高
int desiredWidth = getResizedDimension(mMaxWidth, mMaxHeight,
actualWidth, actualHeight, mScaleType);
int desiredHeight = getResizedDimension(mMaxHeight, mMaxWidth,
actualHeight, actualWidth, mScaleType);
//设置第二次读取的时候,把图片存进内存
decodeOptions.inJustDecodeBounds = false;
//设置采样率
decodeOptions.inSampleSize =
findBestSampleSize(actualWidth, actualHeight, desiredWidth, desiredHeight);
//第二次读取到设置采样率后的bitmap
Bitmap tempBitmap =
BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
//后面的代码是对图片进行返回
if (tempBitmap != null && (tempBitmap.getWidth() > desiredWidth ||
tempBitmap.getHeight() > desiredHeight)) {
bitmap = Bitmap.createScaledBitmap(tempBitmap,
desiredWidth, desiredHeight, true);
tempBitmap.recycle();
} else {
bitmap = tempBitmap;
}
}
if (bitmap == null) {
return Response.error(new ParseError(response));
} else {
return Response.success(bitmap, HttpHeaderParser.parseCacheHeaders(response));
}
那么对于上面这些代码呢,很简单,对吧!但这不是重点,还要深入的话,就是对这個采样率的计算啦!我们可以看到,这里面有几個方法需要再进去看看的:
getResizedDimension();
还有就是:
findBestSampleSize();
这两個到底什么玩意呢?
第一個getResizedDimension();
这個方法呢,其实就是把图片的真实高度和宽度,还有我们设定的宽高扔进去,然后算出期望的高度和宽度。再把这两個期望的高度和宽度,扔个第二個方法
findBestSampleSize();
然后,计算出最适合的采样率。
/**
* Scales one side of a rectangle to fit aspect ratio.
这個方法用于计算缩放一边來适合这個比率,本质上这個方法是计算以宽为标准还是以高为标准好,
当然,这里还不能判断,还要通過另外一個方法判断findBestSampleSize();
*
* @param maxPrimary Maximum size of the primary dimension (i.e. width for
* max width), or zero to maintain aspect ratio with secondary
* dimension
直接理解为第一個最大的参数即可(可以是高/宽)
* @param maxSecondary Maximum size of the secondary dimension, or zero to
* maintain aspect ratio with primary dimension
第二個最大的参数
* @param actualPrimary Actual size of the primary dimension
和第一個参数对应的宽或者高(如果第一個参数传进的是mMaxHeight,那么这個参数则是实际的高,宽同理)
* @param actualSecondary Actual size of the secondary dimension
和第二個参数对应的宽或者高,如果
* @param scaleType The ScaleType used to calculate the needed image size.
*/
private static int getResizedDimension(int maxPrimary, int maxSecondary, int actualPrimary,
int actualSecondary, ScaleType scaleType) {
// 如果我们传进來的maxHeight和这個maxWidth都为0的话,那么就返回传进来的实际宽/高(传进來的是高就返回高,是宽就反回宽)
if ((maxPrimary == 0) && (maxSecondary == 0)) {
return actualPrimary;
}
// 如果这個伸缩类型是这個拉伸XY,也就是铺满控件那种嘛
if (scaleType == ScaleType.FIT_XY) {
//如果这個传进來的第一個最大的高/宽为0
if (maxPrimary == 0) {
//就返回实际传进的宽/高
return actualPrimary;
}
//否则就给出传进來的第一個参数
return maxPrimary;
}
// 如果传进来的第一個参数为0
if (maxPrimary == 0) {
//计算比率,这就是它的算法啦
double ratio = (double) maxSecondary / (double) actualSecondary;
return (int) (actualPrimary * ratio);
}
if (maxSecondary == 0) {
return maxPrimary;
}
//计算比率
double ratio = (double) actualSecondary / (double) actualPrimary;
//默认为第一個参数的值,也就是最大的宽度或者高度
int resized = maxPrimary;
// 还是根据拉伸的类型來判断计算方法和返回的结果
if (scaleType == ScaleType.CENTER_CROP) {
if ((resized * ratio) < maxSecondary) {
resized = (int) (maxSecondary / ratio);
}
return resized;
}
//用小的來计算
if ((resized * ratio) > maxSecondary) {
resized = (int) (maxSecondary / ratio);
}
return resized;
}
这個方法其实是根据前面算出的期望宽度和高度來计算这個最佳的采样率!
findBestSampleSize();
/**
* @param actualWidth Actual width of the bitmap
图片的实际宽度
* @param actualHeight Actual height of the bitmap
图片的实际高度
* @param desiredWidth Desired width of the bitmap
图片的期望宽度
* @param desiredHeight Desired height of the bitmap
图片的期望高度
*/
// Visible for testing.
static int findBestSampleSize(int actualWidth, int actualHeight, int desiredWidth, int desiredHeight) {
//算出实际宽度和期望宽度的比率
double wr = (double) actualWidth / desiredWidth;
//算出实际高度和期望高度的比率
double hr = (double) actualHeight / desiredHeight;
//这两個比率中取小的
double ratio = Math.min(wr, hr);
//这里是为了找到一個适合的采样率,可经修改,开心就好!把心仪的采样率返回即可!
float n = 1.0f;
while ((n * 2) <= ratio) {
n *= 2;
}
return (int) n;
}
好啦,到这里,Volley对大图片的处理,基本上搞定啦,核心的方法就两個,一个是计算期望的宽高,另外一个是计算采样率。而其他几個框架也一样,核心算法就是在这個采样率的计算。目的还是一样的,只是算法不一样而已。虽然我们一张大图片也沒有请求,但我们还是搞定了是吧,哈哈!接下來的两個框架,再也不需要去写请求图片了,直接找到对应的采样率计算即可!
BitmapUtils
在3.0以后的xutils呢,不再是用BitmapUtils了。直接这样子:
x.image().bind(ImageView view, String url, ImageOptions options);
它直接使用了一個独立的ImageOptions來控制图片的大小,当传入的为空的时候,就直接用一個单例模式返回一個默认的Options。
那么3.0以下的呢:
在这個类BitmapDecoder下,我们可以看到很多种形式的代码,但他们的计算方式是一样的,只是来源不一样。
看码:
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, BitmapSize maxSize, Bitmap.Config config) {
synchronized (lock) {
//省略代码....
}
}
public static Bitmap decodeSampledBitmapFromFile(String filename, BitmapSize maxSize, Bitmap.Config config) {
synchronized (lock) {
//省略代码....
}
}
public static Bitmap decodeSampledBitmapFromDescriptor(FileDescriptor fileDescriptor, BitmapSize maxSize, Bitmap.Config config) {
synchronized (lock) {
//省略代码....
}
}
public static Bitmap decodeSampledBitmapFromByteArray(byte[] data, BitmapSize maxSize, Bitmap.Config config) {
synchronized (lock) {
//省略代码....
}
}
public static Bitmap decodeResource(Resources res, int resId) {
synchronized (lock) {
//省略代码....
}
}
public static Bitmap decodeFile(String filename) {
synchronized (lock) {
//省略代码....
}
}
public static Bitmap decodeFileDescriptor(FileDescriptor fileDescriptor) {
synchronized (lock) {
//省略代码....
}
}
public static Bitmap decodeByteArray(byte[] data) {
synchronized (lock) {
//省略代码....
}
}
以上代码只是来源不一样而已,那么对图片的缩放是一样的。怎么样呢,我们抠其中一段代码出來看看吧:
//创建一個options
final BitmapFactory.Options options = new BitmapFactory.Options();
//设置这個不存进内存
options.inJustDecodeBounds = true;
//让这個内存可以即使回收
options.inPurgeable = true;
options.inInputShareable = true;
//获取到这個图片
BitmapFactory.decodeResource(res, resId, options);
//计算这個采样率,所以,所以这個核心方法又來了:calculateInSampleSize
options.inSampleSize = calculateInSampleSize(options, maxSize.getWidth(), maxSize.getHeight());
//第二次读取,就需要写进内存了。是不是和前面volley的一毛一样呢
options.inJustDecodeBounds = false;
if (config != null) {
options.inPreferredConfig = config;
}
try {
return BitmapFactory.decodeResource(res, resId, options);
} catch (Throwable e) {
LogUtils.e(e.getMessage(), e);
return null;
}
好啦,我们也看到重点了是吧,其实每一种有simple的路径,都会调用这個方法:
calculateInSampleSize(BitmapFactory.Options options, int maxWidth, int maxHeight)
计算这個图片的采样率,那么算法就在其中。到底和volley的相不相同呢?
public static int calculateInSampleSize(BitmapFactory.Options options, int maxWidth, int maxHeight) {
//这里其实是获取到默认的高度和宽度,也就是图片的实际高度和宽度
final int height = options.outHeight;
final int width = options.outWidth;
//默认采样率为1,也就是不变嘛。
int inSampleSize = 1;
//===============核心算法啦====================
if (width > maxWidth || height > maxHeight) {
if (width > height) {
inSampleSize = Math.round((float) height / (float) maxHeight);
} else {
inSampleSize = Math.round((float) width / (float) maxWidth);
}
final float totalPixels = width * height;
final float maxTotalPixels = maxWidth * maxHeight * 2;
while (totalPixels / (inSampleSize * inSampleSize) > maxTotalPixels) {
inSampleSize++;
}
}
//=============核心算法end================
return inSampleSize;
}
OK,到这里的话,我们把空上Xutils的也搞定的,如果你平时用到的话,直接复制即可,里面的每一句话你也能看懂。然后呢,俗话說:熟读唐诗三百首,不会作,也会Ctrl+C是吧。这样子,你的经验又长了,这是多么可怕的一件事呢!