Anroid应用中经常会有从网上拉取图片的需求,拉图片说简单也很好做,说难也是很费力的,虽然网上的方案很多,开源框架也不少,但具体的实现还是得看需求。下面分享一下我在项目中用到的两种拉图片方案。
1. 少量图片
如果图片少量,使用框架就显得冗余,直接下载就更简洁一些。
public static boolean downloadImage(String url, String savePath) {
LogUtil.v(TAG, "url=" + url + "; savepath=" + savePath);
if (TextUtils.isEmpty(url) || TextUtils.isEmpty(savePath)) {
return false;
}
// 初始化下载路径,删除已存在的文件以及生成目录
String tempPath = savePath + "_temp";
checkFilePath(savePath);
checkFilePath(tempPath);
// 拉取资源,具体实现网上很多
HttpEntity entity = getHttpEntity(url);
if (entity == null) {
return false;
}
// 检查剩余空间
long imgLength = entity.getContentLength();
long spaceLeft = SdCardSize.getUsableSpace(savePath);
LogUtil.v(TAG, "space left =" + spaceLeft);
if (imgLength <= 0 || spaceLeft < imgLength*3) {
LogUtil.v(TAG, "imgLength*3=" + imgLength*3);
return false;
}
try {
InputStream stream = entity.getContent();
LogUtil.i(TAG, "imgLength = " + imgLength);
// 将stream写入到临时文件,如果写出成功,则重命名为目标文件名
// 确保图片的完整性
if (imgLength == writeImageFile(stream, tempPath)
&& FileUtil.renameFile(tempPath, savePath)) {
return true;
} else {
LogUtil.w(TAG, "failed to write image file");
FileUtil.deleteFile(tempPath);
}
} catch (IllegalStateException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return false;
}
此方法仅仅是下载图片,返回是否下载成功,这个功能应该需求不多,因为只是下载了图片,并没有返回bitmap对象,便于直接使用。如果需要直接使用图片的话在拿到inputstream时就可以直接用BitmapFactory.decodeStream(is)得到bitmap对象;
2. 大量图片
对于需要拉取大量图片的需求,上面的方法不是很好用,主要是线程的封装,以及优先级,还有缓存的处理。在这里根据需求修改volley来实现。
//在volley ImageRequest上封装了一层,volley的request需要两个listener,
//而拉取图片只需要关注是否拉取成功,所以只有一个回调
public void fetchImage(final String url, final String savePath, int width, int height,
final IFetchDataListener<Bitmap> listener) {
ImageRequest request = new ImageRequest(url, new Listener<Bitmap>() {
@Override
public void onResponse(Bitmap response) {
LogUtil.v(TAG, "fetchImage ByteCount=" + response.getByteCount());
// 返回拉取的bitmap
listener.onFetchFinished(response);
}
}, width, height, ScaleType.FIT_XY, Config.ARGB_8888, new ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
LogUtil.e(TAG, "fetchImage url=" + url);
LogUtil.e(TAG, "fetchImage error=" + error);
// volley拉取失败后,再采用第一种方法拉取一遍
ThreadManager.getPoolProxy().execute(new Runnable() {
@Override
public void run() {
final Bitmap bitmap = ImageFetcher.getImage(url, savePath);
LogUtil.v(TAG, "bitmap from imagefetcher:" + bitmap);
// 将拉取的结果返回给UI线程
new InnerHandle().post(new Runnable() {
@Override
public void run() {
listener.onFetchFinished(bitmap);
}
});
}
});
}
});
mQueue.add(request);
}
之所以使用volley,是因为volley对于request以及消息的管理机制很不错,自己实现的很难完善,所以就直接采用volley。在volley拉取失败后又用土办法重新拉一遍是因为实际情况如此,volley拉取时一直都是返回404错误,但是此时土方法每次都能拉取成功,就把两者结合起来了。
public static Bitmap getImage(String url, String savePath) {
LogUtil.v(TAG, "url=" + url + ";savePath=" + savePath);
if (TextUtils.isEmpty(savePath)) {
return null;
}
// 从本地文件读取图片
Bitmap bitmap = getImageByPath(savePath);
if (bitmap != null) {
return bitmap;
}
// 从网络拉取
if (downloadImage(url, savePath)) {
return getImageByPath(savePath);
}
return null;
}
修改volley磁盘缓存方案
volley在将缓存写入磁盘时会将数据附带的信息也写入同一个文件,这样导致我们并不能直接使用volley在磁盘缓存的文件,根据我最开始的方法可以看出,我是需要直接使用图片文件。所以修改了volley的磁盘缓存DiskBasedCache。
public synchronized void put(String key, Entry entry) {
pruneIfNeeded(entry.data.length);
File file = getFileForKey(key);
try {
FileOutputStream fos = new FileOutputStream(file);
CacheHeader e = new CacheHeader(key, entry);
//将写头信息的部分注释掉,这样写入的文件就是可以直接使用的数据了
// boolean success = e.writeHeader(fos);
// if (!success) {
// fos.close();
// VolleyLog.d("Failed to write header for %s",
// file.getAbsolutePath());
// throw new IOException();
// }
fos.write(entry.data);
fos.close();
putEntry(key, e);
return;
} catch (IOException e) {
}
boolean deleted = file.delete();
if (!deleted) {
VolleyLog.d("Could not clean up file %s", file.getAbsolutePath());
}
}
同样的,还需要修改get方法,不然volley自己就用不了缓存文件了。
public synchronized Entry get(String key) {
CacheHeader entry = mEntries.get(key);
// if the entry does not exist, return.
if (entry == null) {
return null;
}
VolleyLog.v("getEntry key=" + key);
File file = getFileForKey(key);
CountingInputStream cis = null;
try {
cis = new CountingInputStream(new BufferedInputStream(
new FileInputStream(file)));
//将读取头信息的部分去掉,这样volley就可以直接读取缓存数据了
// CacheHeader.readHeader(cis); // eat header
// byte[] data = streamToBytes(cis, (int) (file.length()-cis.bytesRead));
byte[] data = streamToBytes(cis, (int) file.length());
return entry.toCacheEntry(data);
} catch (IOException e) {
VolleyLog.d("%s: %s", file.getAbsolutePath(), e.toString());
remove(key);
return null;
} catch (NegativeArraySizeException e) {
VolleyLog.d("%s: %s", file.getAbsolutePath(), e.toString());
remove(key);
return null;
} finally {
if (cis != null) {
try {
cis.close();
} catch (IOException ioe) {
return null;
}
}
}
}
以上的修改可以使用了,但是在重启应用之后,发现缓存不管用了,原来在volley启动的时候会初始话缓存,也就是initialize();
volley的做法是通过读取缓存目录下所有的文件的头信息,来初始化缓存的,但是我刚才已经把缓存文件的头信息都去掉了,所以我们得采用另外的方式来记录头信息,便于volley初始化。
public synchronized void initialize() {
if (!mRootDirectory.exists()) {
if (!mRootDirectory.mkdirs()) {
VolleyLog.e("Unable to create cache dir %s",
mRootDirectory.getAbsolutePath());
}
return;
}
//将mEntries对象用Gson写入到mEntryMapJsonPath路径下json文件,
//在初始化时读取文件并用gson读取为Map<String, CacheHeader>对象
StringBuilder str = FileUtil.readFile(mEntryMapJsonPath);
if (TextUtils.isEmpty(str)) {
return;
}
try {
Map<String, CacheHeader> entries = new Gson().fromJson(str.toString(),
new TypeToken<Map<String, CacheHeader>>(){}.getType());
mEntries.putAll(entries);
VolleyLog.v("entries=" + entries);
} catch (Exception e) {
}
// File[] files = mRootDirectory.listFiles();
// if (files == null) {
// return;
// }
// for (File file : files) {
// BufferedInputStream fis = null;
// try {
// fis = new BufferedInputStream(new FileInputStream(file));
// CacheHeader entry = CacheHeader.readHeader(fis);
// entry.size = file.length();
// putEntry(entry.key, entry);
// } catch (IOException e) {
// if (file != null) {
// file.delete();
// }
// } finally {
// try {
// if (fis != null) {
// fis.close();
// }
// } catch (IOException ignored) {
// }
// }
// }
}
这样实现的话就需要在增加和删除缓存时更新json文件
private void putEntry(String key, CacheHeader entry) {
if (!mEntries.containsKey(key)) {
mTotalSize += entry.size;
} else {
CacheHeader oldEntry = mEntries.get(key);
mTotalSize += (entry.size - oldEntry.size);
}
mEntries.put(key, entry);
saveCachedJsonToFile();
}
private void removeEntry(String key) {
CacheHeader entry = mEntries.get(key);
if (entry != null) {
mTotalSize -= entry.size;
mEntries.remove(key);
}
saveCachedJsonToFile();
}
private void saveCachedJsonToFile() {
synchronized (mEntries) {
String json = new Gson().toJson(mEntries);
FileUtil.writeFileWithBackUp(mEntryMapJsonPath, json, false);
}
}
到这里就完成了volley的修改。。。。
图片三级缓存
public Bitmap getBitmapFromCache(String key) {
checkCacheParams();
Bitmap bitmap = null;
// 从lrucache,即硬连接获取图片
synchronized (mLruCache) {
bitmap = mLruCache.remove(key);
if (bitmap != null) {
// found,move the file to the last of LinkedHashMap
mLruCache.put(key, bitmap);
LogUtil.v(TAG, "getBitmapFrom mLruCache");
return bitmap;
} else {
LogUtil.w(TAG, "getBitmapFrom mLruCache failed");
}
}
// 从软链接获取图片
synchronized (mSoftCache) {
SoftReference<Bitmap> bitmapReference = mSoftCache.remove(key);
if (bitmapReference != null) {
bitmap = bitmapReference.get();
if (bitmap != null) {
// if hit, move bm to lrucache
addBitmapToCache(key, bitmap);
LogUtil.v(TAG, "getBitmapFrom mSoftCache");
return bitmap;
} else {
LogUtil.w(TAG, "getBitmapFrom mSoftCache failed");
}
} else {
LogUtil.w(TAG, "getBitmapFrom mSoftCache failed");
}
}
// 从本地文件获取图片
synchronized (mFileCache) {
bitmap = mFileCache.getImageByURL(key);
if (bitmap != null) {
addBitmapToCache(key, bitmap);
LogUtil.v(TAG, "getBitmapFrom mFileCache");
return bitmap;
} else {
LogUtil.w(TAG, "getBitmapFrom mFileCache failed");
}
}
return null;
}
这就是传说中的三级缓存,呵呵!
初始化是酱紫的:
public void initCacheParams(String cacheDir, int maxCacheByteSize) {
mFileCache = new ImageFileCache(cacheDir);
mLruCache = new LruCache<String, Bitmap>(maxCacheByteSize) {
@Override
protected int sizeOf(String key, Bitmap value) {
if (value != null) {
return value.getRowBytes() * value.getHeight();
} else {
return 0;
}
}
// 缓存占用内存超过设定的最大值时移除lru的缓存
@Override
protected void entryRemoved(boolean evicted, String key,
Bitmap oldValue, Bitmap newValue) {
if (oldValue != null && mSoftCache != null) {
// 将移除的缓存加入到软连接
mSoftCache.put(key, new SoftReference<Bitmap>(oldValue));
}
}
};
mSoftCache = new LinkedHashMap<String, SoftReference<Bitmap>>(
SOFT_CACHE_SIZE, 0.75f, true) {
private static final long serialVersionUID = 6040103833179403725L;
// 当缓存的数量超过设定的最大值,或者内存不够用时,从软链接移除最先加入的缓存
@Override
protected boolean removeEldestEntry(
Entry<String, SoftReference<Bitmap>> eldest) {
if (size() > SOFT_CACHE_SIZE) {
return true;
}
return false;
}
};
}
添加缓存就很简单啦:
public void addBitmapToCache(String key, Bitmap bitmap) {
checkCacheParams();
if (bitmap != null) {
synchronized (mLruCache) {
mLruCache.put(key, bitmap);
}
}
}
仅仅是把缓存加入到lru,若缓存增加就会把以前的缓存移除到二级缓存(软链接),但是从软链接移除后并没有第三级缓存。
注意,这里并没有第三级缓存,网上很多实现在这里加入了第三级,就是把bitmap写入文件。思路上并没有问题,问题是将bitmap写入缓存文件时很容易出问题,很多实现是把bitmap按jpg或png格式压缩并写文件。然后在下次读缓存时再把缓存文件加载为bitmap,如此循环多次后,缓存文件会越来越大,并且图片越来越不清晰,原因是bitmap转jpg或png时是有损压缩,这样说就明白了吧,缓存文件不需要每次都去写,只需要第一次写入原文件,以后每次取缓存时直接读原文件就OK了。但是这样也会引入一个新的问题,就是缓存文件如何管理,因为缓存文件不能一直增加,这个我还没有找到简便的方法,以后再补上吧!