前言
实际项目开发中, 一般不需要自己去写图片缓存框架, 直接用glide之类的成熟sdk是明智之举, 但如果自己开发SDK库给别人用, 例如我之前做的hola广告sdk, 是让别人集成到他们的app中使用的, 在这种情况下, 就需要自己来完成图片缓存框架的编写, 一方面是为了减少sdk的size, 另一方便, 如果我用glide的话, 客户的app也引用glide, 就可能会造成包冲突, 所以对于sdk库的开发来说, 应该尽量少引用第三方库.
总体的设计目标
首先要搞明白, 对图片缓存的实现目标. 对任何图片来说, 从网上下载后, 都要保存到磁盘上.
而对于内存来说, 从网上下载的图片文件, 会根据文件生成Bitmap对象, 这个Bitmap对象是保存在内存上的. 但内存空间有限, 这就需要给内存缓存设置一个最大值, 当达到这个值后, 就根据LRU策略, 把最少使用的Bitmap对象从内存缓存空间中删除掉, 以释放出一些内存空间, 用来存储新得到的Bitmap对象.
对于加载图片的实现思路是: 首先从内存中查看是否存在这个url对应的bitmap对象, 没有的话, 再检查磁盘中是否存在这个url所对应的图片文件, 还是没有的话, 从网上下载文件, 并且在下载文件后, 把所生成的bitmap对象放入内存缓存集合中 (LinkedHashMap), 并且还需要把这个文件写入到磁盘中.
一些特别要注意的开发要点:
(1) 往SD卡上存储图片文件时, 对于文件名的命名, 要特别注意, 一般来说, 网上的图片地址格式是: http://www.baidu/image/abc.png, 如果要以abc.png作为文件名保存的话, 就会有这样的问题, 由于Android手机上有一个媒体库, 它会定期扫描SD卡上的图片和视频文件, 并以ContentProvider的形式对外提供数据, 如果文件名是.png, 那么就会被媒体库收录进来, 在其他的图片查看器App中, 就会看到这些"私密"图片.
解决办法是, 设置文件名时, 对原地址(http://www.baidu/image/abc.png)取MD5值, 以这个计算后的值作为文件名进行保存. 这样就避免了这些文件被媒体库收录的问题了.
(2) 既然要用到LRU思想, 那么采用什么数据结构在内存中存储这些Bitmap对象?
要使用LinkedHashMap来实现, 这是因为LinkedHashMap的特性决定的, 它有一个成员变量accessOrder指定了对象的存放顺序, false为按插入顺序存放, true为按访问顺序存放. 在LRU思想下, 设置accessOrder为true即可.
这就正好满足了LRU的设计思想.
具体的可以看我之前的文章:
http://www.jianshu.com/p/e5c3b522e8c5
代码实现
MyImageLoader.java
/**
* Created by wangxin on 17-8-29.
*/
/* 使用方法
ImageView iv_type;
MyImageLoader imageLoader = MyImageLoader.getInstance(this);
ArrayList<String> urls = new ArrayList<>();
urls.add("http://img5.imgtn.bdimg.com/it/u=3206533887,2931873948&fm=27&gp=0.jpg");
urls.add("http://img0.imgtn.bdimg.com/it/u=2138081325,2407756075&fm=27&gp=0.jpg");
urls.add("http://img4.imgtn.bdimg.com/it/u=196278633,1521334363&fm=27&gp=0.jpg");
urls.add("http://img3.imgtn.bdimg.com/it/u=3487766690,1195649711&fm=27&gp=0.jpg");
urls.add("http://img1.imgtn.bdimg.com/it/u=698943482,1934898431&fm=27&gp=0.jpg");
urls.add("http://img1.imgtn.bdimg.com/it/u=3187985879,2007978564&fm=27&gp=0.jpg");
urls.add("http://img5.imgtn.bdimg.com/it/u=760482064,3255510007&fm=27&gp=0.jpg");
for (String u : urls) {
imageLoader.loadImage(u, iv_type);
}
*/
public class MyImageLoader {
private static MyImageLoader mInstance;
private static final int MAX_CAPACITY = 20;
private static Context mContext;
private MyImageLoader(Context context) {
mContext = context;
}
public static MyImageLoader getInstance(Context context) {
if(mInstance == null) {
mInstance = new MyImageLoader(context);
}
return mInstance;
}
private static LinkedHashMap<String, SoftReference<Bitmap>> firstCacheMap = new LinkedHashMap<String, SoftReference<Bitmap>>(MAX_CAPACITY) //这里设置一个初始大小值.
{
//LinkedHashMap每次添加一个新元素进来, 都会回调这个接口, 询问是否要移除掉最老的那个元素.
@Override
protected boolean removeEldestEntry(Map.Entry<String, SoftReference<Bitmap>> eldest) {
JLog.i();
if(this.size() > MAX_CAPACITY) {
//返回true, 表示移除最老的那个元素
return true;
}
// 把最老的那个元素保存到磁盘中
diskCache(eldest.getKey(), eldest.getValue());
return false;
}
};
//对外提供的API.
public void loadImage(String key, ImageView view) {
synchronized (view) {
//检查缓存里是否有
Bitmap bitmap = getFromCache(key);
if(bitmap != null) {
//缓存存在, 直接显示
view.setBackgroundDrawable(new ColorDrawable(Color.GRAY));
view.setImageBitmap(bitmap);
} else {
// 下载
view.setBackgroundDrawable(new ColorDrawable(Color.GRAY));
AsyncImageLoaderTask task = new AsyncImageLoaderTask(view);
task.execute(key);
}
}
}
private Bitmap getFromCache(String key) {
//检查内存缓存
synchronized (firstCacheMap) {
if(firstCacheMap.get(key) != null) {
Bitmap bitmap = firstCacheMap.get(key).get();
if(bitmap != null) {
//把这个元素再放进去一次的目的是, 增加它的计数值, 表示它又被访问了一次.
firstCacheMap.put(key,new SoftReference<Bitmap>(bitmap));
return bitmap;
}
}
//检查磁盘
Bitmap bitmap = getFromLocalKey(key);
if (bitmap != null) {
firstCacheMap.put(key,new SoftReference<Bitmap>(bitmap));
return bitmap;
}
return null;
}
}
// 检查SD卡中是否有该图片
private Bitmap getFromLocalKey(String key) {
String filename = MD5Utils.digest(key);
if (filename == null) {
return null;
} else {
String path = mContext.getCacheDir().getAbsolutePath() + filename;
//如果这个path指定的文件不存在的话, 后面也会return null.
InputStream is = null;
try {
is = new FileInputStream(new File(path));
Bitmap bitmap = BitmapFactory.decodeStream(is);
return bitmap;
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
return null;
}
//把图片保存到本地磁盘
//缓存文件的路径: /data/data/com.hola.weather/cache/539911d002dd6b9e0f23c851e6eb40ce
private static void diskCache(String key, SoftReference<Bitmap> value) {
//消息摘要算法
String fileName = MD5Utils.digest(key);
String path = mContext.getCacheDir().getAbsolutePath()+"/"+fileName;
JLog.i("path: " + path);
FileOutputStream os = null;
try {
os = new FileOutputStream(new File(path));
if(value.get() != null) {
value.get().compress(Bitmap.CompressFormat.JPEG, 100, os);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
if(os != null) {
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
class AsyncImageLoaderTask extends AsyncTask<String, Void, Bitmap> {
private ImageView imageView;
private String key;
public AsyncImageLoaderTask(ImageView imageView) {
this.imageView = imageView;
}
@Override
protected Bitmap doInBackground(String... params) {
this.key = params[0]; //图片的路径
Bitmap bitmap = download(key);
return bitmap;
}
private Bitmap download(String key) {
InputStream is = null;
try {
is = HttpUtil.download(key);
return BitmapFactory.decodeStream(is);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return null;
}
@Override
protected void onPostExecute(Bitmap bitmap) {
super.onPostExecute(bitmap);
if (bitmap != null) {
addFirstCache(key, bitmap);
imageView.setImageBitmap(bitmap);
}
}
}
private void addFirstCache(String key, Bitmap bitmap) {
if (bitmap != null) {
synchronized (firstCacheMap) {
firstCacheMap.put(key, new SoftReference<Bitmap>(bitmap));
}
}
}
}
github:
https://github.com/AandK/MyImageLoader/blob/master/MyImageLoader.java
dong nao video note.
--- DONE. ---