*本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布
图片下载“不容易”
由于项目对图片加载需求的特殊性,现有图片加载框架无法满足,就自己写了一个简单的图片加载功能,在写的过程中遇到了一些坑,下面就分享下我在图片下载这条线上遇到的坑和怎么解决这些坑的。
下载数据
我们使用如下代码先把图片数据下载到内存里面:
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
connection.setConnectTimeout(TIME_OUT);
connection.setReadTimeout(TIME_OUT);
connection.setRequestMethod("GET");
if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {
InputStream is = connection.getInputStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[10240];// download 10kb every time
int length;
while ((length = is.read(buffer)) > -1) {
baos.write(buffer, 0, length);
}
baos.flush();
is.close();
}
Oh, beautiful code~~ 但是,你中枪了!
试想,你的APP最大可用内存64M,你现在下载的是一个80M的图片会怎么样?Bomb,你的APP炸了,你看,我还没开始(decode)就结束。在Android开发中提起OOM大家立马会想到”Decode Bitmap”,但是OOM并不是Decode Bitmap的专属,这里就说明不decode也会出现OOM。我们得想办法解决这个问题,谁也不想做秒男。既然内存不够用,那我们先把数据保存成文件,然后decodeFile:
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
connection.setConnectTimeout(TIME_OUT);
connection.setReadTimeout(TIME_OUT);
connection.setRequestMethod("GET");
if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {
InputStream is = connection.getInputStream();
File file = new File(mTempFilePath);
if (file.exists()) {
file.delete();
}
file.createNewFile();
FileOutputStream fos = new FileOutputStream(file);
byte[] buffer = new byte[10240];// download 10kb every time
int length;
while ((length = is.read(buffer)) > -1) {
fos.write(buffer, 0, length);
}
fos.flush();
fos.close();
is.close();
}
诶~,这下没有OOM,但是下载耗时多了好多(我做的时候都是2x),原因是每次下载10kb数据就需要写一次文件,IO操作相比内存操作是很耗时的。动作太慢没感觉(其实还是秒男,只是变成了0.5倍速了),不行,我要做真男人。
内存只是不够用并不是不能用了,我们能不能分出5M的内存,先把数据下载到这快内存里面,当这块内存装满时,把这5M的数据拿出来保存到文件里,当然可以,这就是BufferedOutputStream。
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
connection.setConnectTimeout(TIME_OUT);
connection.setReadTimeout(TIME_OUT);
connection.setRequestMethod("GET");
if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {
InputStream is = connection.getInputStream();
File file = new File(mTempFilePath);
if (file.exists()) {
file.delete();
}
file.createNewFile();
// 为了防止文件过大导致OOM,这里直接把数据下载到文件中
FileOutputStream fos = new FileOutputStream(file);
// 5MB buffer
BufferedOutputStream bos = new BufferedOutputStream(fos, 5 * 1024 * 1024);
byte[] buffer = new byte[10240];// download 10kb every time
int length;
while ((length = is.read(buffer)) > -1) {
bos.write(buffer, 0, length);
}
bos.flush();
bos.close();
fos.close();
is.close();
}
这样,下载5M数据,之前要写512(5*1024/10)次文件,现在只需要写一次,速度自然就提上来了。现在才是真男人,速度可以接受,还不会OOM。
那这个5M怎么来的呢?这个大小取决与你要加载的图片大小分布,目的就是为了在内存允许的情况下让大部分的图片只需要一次写操作就能搞定,尽可能的减小由于写操作带来的速度影响。例如,你的APP加载的图片大部分是1M以内,只有极少数大于1M,那这个大小就是1M。
回过头来看这三种方式,其实就是空间换时间、时间换空间、权衡方案。
解析Bitmap
老司机都知道为了防止解析图片时OOM,我们一般先只解析出图片尺寸信息,然后根据尺寸信息和我们实际显示大小来计算出inSampleSize的值,然后去deocde一个缩放过的图片,代码可能是这样的:
private int calculateScaleSize(int originalWidth, int originalHeight, int destWidth, int destHeight) {
int scaleWidth = originalWidth / destWidth;
int scaleHeight = originalHeight / destHeight;
int inSampleSize = Math.max(scaleWidth, scaleHeight);
return inSampleSize;
}
如果你是这样写的,那么你又中枪了(WTF)!假如图片原始大小480x800,你要显示的大小是250x420,你得到的inSampleSize将会是1,因为int/int是取整的,不会对图片做缩放,这可能就是OOM的隐患。
我们在计算inSampleSize的时候应该是向上取整:
private int calculateScaleSize(int originalWidth, int originalHeight, int destWidth, int destHeight) {
int scaleWidth = (int) Math.ceil((originalWidth * 1.0f) / destWidth);
int scaleHeight = (int) Math.ceil((originalHeight * 1.0f) / destHeight);
int inSampleSize = Math.max(scaleWidth, scaleHeight);
return inSampleSize;
}
好啦,剩下的就只需要decodeBitmap啦,强无敌!!!
// calculate inSampleSize
Options options = new Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(mTempFilePath, options);
options.inSampleSize = calculateScaleSize(options.outWidth, options.outHeight, destWidth, destHeight);
// decode bitmap
options.inJustDecodeBounds = false;
Bitmap scaleBitmap = BitmapFactory.decodeFile(mTempFilePath, options);
// delete temp file
file.delete();
我解析出一张美美哒图,你们呢?