背景:在做App应用时,特别是涉及到要大量获取网络图片在UI上展现,获取网络图片是非常要流量的,通常有效的解决方式就是充分的利用内存缓存和文件缓存<sdcard>,当然也可以将图片信息存储在SQLite本地数据库,这里我主要是通过对内存缓存和SD卡上的文件缓存进行代码封装。
1、首先,定义一个 要显示图片的参数实体类:ImageParams.java ,他包含url<图片url> 和 updateTime<图片的更新时间>两个字段变量。 代码如下:
package com.ice.android.common.util.imagecache;
/**
* 要显示图片的参数实体
* @author ice
*
*/
public class ImageParams {
/** 图片的url */
private String url;
/** 图片的更新时间 */
private String updateTime;
/** 有参构造函数 */
public ImageParams(String url, String updateTime) {
this.url = url;
this.updateTime = updateTime;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getUpdateTime() {
return updateTime;
}
public void setUpdateTime(String updateTime) {
this.updateTime = updateTime;
}
public String getImageKey(){
return this.url + this.updateTime;
}
}
2、其次,就是关键的图片内存缓存类: ImageMemCache.java
在该类中 我们会用到 硬缓存 和 软引用缓存 两者的相互配合来共同达到既有效的缓存图片又不会引起内存溢出的作用。详细代码如下:
package com.ice.android.common.util.imagecache;
import java.lang.ref.SoftReference;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.concurrent.ConcurrentHashMap;
import android.graphics.Bitmap;
import android.util.Log;
/**
* 图片内存缓存 对象
* @author ice
*
*/
public class ImageMemCache {
private static final String TAG = "ImageMemCache";
/** 缓存容量 */
private static final int CACHE_CAPACITY = 15;
/** 存放图片的HashMap 对象 */
private static HashMap<String, Bitmap> mHardBitmapCache;
/** 使用软引用存放图片数据 放入ConcurrentHashMap对象 <这里是否使 WeakHashMap 会更好呢 ?> */
private static ConcurrentHashMap<String, SoftReference<Bitmap>> mSoftBitmapCache = new ConcurrentHashMap<>(CACHE_CAPACITY);
public ImageMemCache(){
/* 这里之所以选用 LinkedHashMap,我想主要是因为第三个参数:排序模式 <true表示为访问顺序 |false表示为插入顺序 >
* 第二个参数 为加载因子
*/
mHardBitmapCache = new LinkedHashMap<String,Bitmap>(CACHE_CAPACITY * 2, 0.5f, true){
private static final long serialVersionUID = 1L;
/**
* 从映射移除最旧的条目 返回 true,否则返回false
*/
@Override
protected boolean removeEldestEntry(Entry<String, Bitmap> eldest) {
if(size() >= CACHE_CAPACITY){
/*
* 将Bitmap从 mHardBitmapCache内存中 转移到 mSoftBitmapCache内存中
*/
mSoftBitmapCache.put(eldest.getKey(), new SoftReference<Bitmap>(eldest.getValue()));
Log.d(TAG, "将数据从HardCache移到SoftCache,url="+eldest.getKey());
return true;
}
return false;
}
};
}
/**
* 从缓存中获取图片数据
* @param mImageParams
* @return
*/
public Bitmap getImageData(ImageParams mImageParams){
String imageKey = mImageParams.getImageKey();
// 先从 mHardBitmapCache 缓存中获取
synchronized (mHardBitmapCache) {
Bitmap bitmap = mHardBitmapCache.get(imageKey);
if(bitmap != null){
mHardBitmapCache.remove(imageKey);
mHardBitmapCache.put(imageKey, bitmap);
Log.d(TAG, "从HardCache中取到数据,url="+imageKey);
return bitmap;
}
}
// 如果mHardBitmapCache中找不到,到mSoftBitmapCache中找
SoftReference<Bitmap> softReference = mSoftBitmapCache.get(imageKey);
if(softReference != null){
Bitmap bitmap = softReference.get();
if(bitmap != null){
// 将图片 移回硬缓存 从软缓存中移除
mHardBitmapCache.put(imageKey, bitmap);
mSoftBitmapCache.remove(imageKey);
Log.d(TAG, "从SoftCache中取到数据,url="+ imageKey);
return bitmap;
}else{
mSoftBitmapCache.remove(imageKey);
}
}
return null;
}
/**
* 将图片添加到内存缓存
* @param imageParams
* @param bitmap
*/
public void addBitmapToCache(ImageParams imageParams,Bitmap bitmap){
if(bitmap != null){
synchronized (mHardBitmapCache) {
mHardBitmapCache.put(imageParams.getImageKey(), bitmap);
Log.d(TAG, "将图片缓存到HardCache,url="+imageParams.getImageKey());
}
}
}
}
3、图片内存缓存类封装好啦,接着就是SD卡上的文件缓存类:ImageFileCache.java
在该类中,我们就是把图片储存在SD卡上,当储存在SD卡上的图片临时缓存文件达到一定的大小时,会删除一定百分比的临时文件以达到手机内存的有效控制。
详细代码如下:
package com.ice.android.common.util.imagecache;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.Comparator;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Environment;
import android.util.Log;
import com.ice.android.common.util.SdcardUtil;
import com.ice.android.common.util.encryption.MD5;
/**
* 文件缓存对象 缓存在sdcard 上
* @author ice
*
*/
public class ImageFileCache {
private static final String TAG = "ImageFileCache";
/** 文件缓存存放目录 */
private static final String CACHE_DIR = Environment.getExternalStorageDirectory().getPath()+"app package/cache/img/";
/** 文件缓存后缀名 */
private static final String CACHE_FILE_SUFFIX = ".cach";
/** 允许最大缓存空间 30M */
private static final long MAX_CHCHE_SPACE = 30 * 1024 * 1024;
/** 最小SD卡 剩余空间(预留给用户做其它的事情) */
private static final long MIN_SDCART_AVAILABLE_SPACE = 30 * 1024 * 1024;
/** 每次清除缓存的百分比 */
private static final float CACHE_REMOVE_FACTOR = 0.4f;
public ImageFileCache(){
// 根据sd卡内存情况 清理部分文件缓存
removeCache();
}
/**
* 从sd卡上获取缓存图片
* @param imageParams
* @return
*/
public Bitmap getImageData(ImageParams imageParams){
final String path = CACHE_DIR + convertUrlToFileName(imageParams.getImageKey());
File file = new File(path);
if(file.exists()){
Bitmap bitmap = null;
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
// 获取这个图片的宽和高
int beHeight = (int) (options.outHeight / (float) 200);
int beWidth = (int) (options.outWidth/ (float) 200);
int scale = beHeight>beWidth?beHeight:beWidth;
options.inSampleSize=scale==0?1:scale;
options.inJustDecodeBounds = false;
bitmap = BitmapFactory.decodeFile(path, options);
if(bitmap == null){
Log.w(TAG, "将文件转换成Bitmap失败,删除此文件,path = " + path);
file.delete();
}else {
Log.w(TAG, "将文件转换成Bitmap成功,path = " + path);
// 修改 缓存文件的最后修改时间
updateFileTime(path);
return bitmap;
}
}
return null;
}
/**
* 将图片数据保存到sd卡
* @param imageParams
* @param mBitmap
* @throws IOException
*/
public void addBitmapToSdcard(ImageParams imageParams,Bitmap mBitmap) {
String imageKey = imageParams.getImageKey();
if(mBitmap == null){
Log.w(TAG, "Bitmap为null,持久化存储失败,url = "+imageKey);
return ;
}
long sdcardAvailableSpace = SdcardUtil.getSdcardAvailableSpace();
// 如果sd卡的可用空间 小于 SD卡最小预留空间
if(sdcardAvailableSpace < MIN_SDCART_AVAILABLE_SPACE){
Log.w(TAG, "SD卡空间不足,不做持持久化存储,sdcardAvailableSpace = " + sdcardAvailableSpace + ", url = "+imageParams.getUrl());
return ;
}
File dir = new File(CACHE_DIR);
if(!dir.exists()){
dir.mkdirs(); // PS: 这里客串一下, 注意 mkdirs() 与 mkdir()方法的区别
}
String fileName = CACHE_DIR + convertUrlToFileName(imageKey);
File file = new File(fileName);
OutputStream os = null;
try {
os = new FileOutputStream(file);
mBitmap.compress(Bitmap.CompressFormat.PNG, 100, os);
os.flush();
os.close();
Log.d(TAG, "图片持久化存储成功,path = "+ fileName + ", url = " + imageKey);
} catch (IOException e) {
Log.d(TAG, "图片持久化存储失败:"+e.getMessage());
}
}
/**
* 根据sd卡内存情况 清理部分文件缓存
* 1.计算存储目录下的文件大小
* 2.当文件总大小大于规定的最大缓存大小或者sdcard剩余空间小于最小SD卡可用空间的规定时,
* 删除特定数量的最近没有被使用的文件
*/
private boolean removeCache() {
if(!SdcardUtil.isSdcardWritable()){
Log.d(TAG, "SD卡不可写,清理缓存失败!");
return false;
}
File dir = new File(CACHE_DIR);
File[] files = dir.listFiles();
if(files == null || files.length == 0){
Log.d(TAG, "木有缓存,无需清理...");
return true;
}
long dirSize = 0;
for(int i = 0; i < files.length; i++){
if(files[i].getName().endsWith(CACHE_FILE_SUFFIX)){
dirSize += files[i].length();
}
}
long sdcardAvailableSpace = SdcardUtil.getSdcardAvailableSpace();
Log.d(TAG, "缓存文件的已有总大小为:" + dirSize + ",SD卡可用空间为:" + sdcardAvailableSpace);
if(dirSize >= MAX_CHCHE_SPACE || sdcardAvailableSpace< MIN_SDCART_AVAILABLE_SPACE){
// 计算出需要删除的文件数量
int removeNum = (int)((files.length * CACHE_REMOVE_FACTOR) + 1);
Log.d(TAG, "需要清除"+removeNum+"个缓存文件");
// 根据文件的最后修改时间进行排序
Arrays.sort(files, new FileLastModifiedComparator());
for(int i = 0; i < removeNum; i++){
if(files[i].getName().endsWith(CACHE_FILE_SUFFIX)){
files[i].delete();
}
}
}else{
Log.d(TAG, "缓存情况还木有超出指定情况,无需清理...");
}
return true;
}
/**
* 根据文件的最后修改时间进行排序
* 把最后修改时间 越长的排到越前面
* @author ice
*
*/
private class FileLastModifiedComparator implements Comparator<File>{
@Override
public int compare(File arg0, File arg1) {
if(arg0.lastModified() > arg1.lastModified()){
return 1;
}else if(arg0.lastModified() == arg1.lastModified()){
return 0;
}
return -1;
}
}
/**
* 根据图片url生成文件缓存的名字
* @param url
* @return
*/
private String convertUrlToFileName(String url){
/**
* 将url进行MD5加密一下吧
*/
return MD5.getMD5Str(url)+CACHE_FILE_SUFFIX;
}
/**
* 修改 缓存文件的最后修改时间
* @param path 文件绝对路径
*/
private void updateFileTime(String path) {
File file = new File(path);
file.setLastModified(System.currentTimeMillis());
}
}
PS:针对上面的ImageFileCache.java 和 ImageMemCache.java可以再一步改造,抽出一个 ImageCache 的接口,然后ImageFileCache,java 和 ImageMemCache.java 都实现该接口,这样是不是更好呢?哈哈 动手吧... ...
小吕、初学Android,正在努力的学习阶段,各位针对上面的代码如果有更好的建议和想法的,可以留言给我哈! 多谢啦... ...