一、简介
如图所示,我们的Android App经常需要通过网络获取图片。但是,如果每次启动App都需要从网络获取图片,或者是想重复浏览一些图片的时候,每次浏览都需要通过网络获取,那么势必就会消耗很多流量。在当前的状况下,对于非wifi用户来说,流量还是很贵的,一个很耗流量的App,其用户数量级肯定会受到影响。其次,每次启动App都需要从网络获取图片,在网络情况不佳的情况下会加载很慢,非常影响用户体验。另外,从开发角度来说,Bitmap的创建非常消耗时间和内存,可能导致频繁GC,进而可能导致卡顿。
因此,提出了三级缓存策略,通过网络、本地、内存三级缓存图片,来减少不必要的流量耗费,加快图片的加载速度,减少卡顿,从而增强用户体验,进而有助于提高App的用户数量级。
二、什么是三级缓存
- 内存缓存,优先加载,速度最快
- 本地缓存,次优先加载,速度快
- 网络缓存,最后加载,速度慢,浪费流量
三、三级缓存原理
- 首次启动App时,通过网络加载图片,在获取到图片之后将其保存到内存和本地SD卡中;
- 再次启动App时,优先加载内存中的图片缓存;
- 如果内存中没有图片缓存,则优先加载本地SD卡中的图片缓存;
缓存策略:内存作为一级缓存,本地SD卡作为二级缓存,最后是网络加载。加载图片的时候,优先加载内存中的图片缓存,如果内存中没有图片缓存,则优先加载本地SD卡中的图片,如果还没有就进行网络加载。
四、图片三级缓存代码实现
1、网络缓存
这里,我们使用了AsyncTask来进行网络图片的异步加载。简单来说,AsyncTask可以看作是对线程池和Handler的封装。其中,线程池用于线程调度、复用,以及执行任务,Handler则用于异步通信以及消息传递。可以看到,从网络获取图片后,将其分别保存至本地缓存和内存缓存。
public class NetworkCacheUtils {
private LocalCacheUtils mLocalCacheUtils;
private RamCacheUtils mRamCacheUtils;
public NetworkCacheUtils(LocalCacheUtils localCacheUtils, RamCacheUtils ramCacheUtils) {
this.mLocalCacheUtils = localCacheUtils;
this.mRamCacheUtils = ramCacheUtils;
}
/**
* 从网络加载图片
* @param url
* @return
*/
public Bitmap loadBitmapFromNetwork(String url) {
try {
return new LoadBitmapTask().execute(url).get();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
private class LoadBitmapTask extends AsyncTask {
private String loadUrl;
@Override
protected Bitmap doInBackground(String... strings) {
loadUrl = strings[0];
try {
URL url = new URL(loadUrl);
HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection();
httpURLConnection.setConnectTimeout(10000);
httpURLConnection.setReadTimeout(10000);
httpURLConnection.setRequestMethod("GET");
if (httpURLConnection.getResponseCode() == 200) {
return BitmapFactory.decodeStream(httpURLConnection.getInputStream());
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
@Override
protected void onPostExecute(Bitmap bitmap) {
if (bitmap != null) {
//从网络获取图片后,保存至本地缓存
mLocalCacheUtils.setBitmapToLocal(loadUrl, bitmap);
//从网络获取图片后,保存至内存
mRamCacheUtils.setBitmapToMemory(loadUrl, bitmap);
}
}
}
}
2、本地缓存
初次从网络获取图片后,将其保存至本地缓存。在保存的时候,我们一般会对图片的url进行MD5加密处理,以此处理后的字符串作为图片名称来进行保存。另外,在创建缓存地址的时候,我们需要考虑如果这个手机没有SD卡,或者SD正好被移除了的情况。
public class LocalCacheUtils {
private static final String LOCAL_CACHE_PATH = Environment.getExternalStorageDirectory().getAbsolutePath() + "/ImageCache";
private File cacheDir;
public LocalCacheUtils(Context context) {
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)
|| !Environment.isExternalStorageRemovable()) {
cacheDir = new File(LOCAL_CACHE_PATH);
} else {
cacheDir = context.getCacheDir();
}
if (!cacheDir.exists()) {
cacheDir.mkdirs();
}
}
public Bitmap loadBitmapFromLocal(String url) {
try {
//把图片的url当做文件名,并进行MD5加密
String fileName = MD5(url);
File file = new File(cacheDir, fileName);
return BitmapFactory.decodeStream(new FileInputStream(file));
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 从本地读取图片
*
* @param url
* @param bitmap
*/
public void setBitmapToLocal(String url, Bitmap bitmap) {
try {
//把图片的url当做文件名,并进行MD5加密
String fileName = MD5(url);
File file = new File(cacheDir, fileName);
//把图片保存至本地
bitmap.compress(Bitmap.CompressFormat.PNG, 100, new FileOutputStream(file));
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 往本地中保存图片
* @param content
* @return
*/
private String MD5(String content) {
byte[] hash;
try {
hash = MessageDigest.getInstance("MD5").digest(content.getBytes("UTF-8"));
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("NoSuchAlgorithmException", e);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("UnsupportedEncodingException", e);
}
StringBuilder hex = new StringBuilder(hash.length * 2);
for (byte b : hash) {
if ((b & 0xFF) < 0x10) {
hex.append("0");
}
hex.append(Integer.toHexString(b & 0xFF));
}
return hex.toString();
}
}
3、内存缓存
(1)LRU (Least Recently Used),最近最少使用算法
- 基本思想: 如果数据最近被访问过, 那么将来被访问的机率也很高, 那么这部分数据不应该被淘汰。
- 实现思路:按照正常的思路, 内存(缓存)的空间有限, 我们假定只能放 N 个数据, 那么当空间满了的时候, 根据 LRU 的定义, 我们就应该去淘汰最近最少使用的数据。同时这个 Cache 应该是高速的, 也就是说我们必须保证缓存读取的 get,set 的时间复杂度都是 O(1), 这里用 HashTable 很容易实现。那么淘汰数据呢? HashTable 删除数据的时间复杂度是 O(n), 显然是不可行的, 那么我们可以使用一个容量为 N 的双向链表, 每次 get 数据的时候, 把该条记录移动到 head, 淘汰数据只需要删除链表的 tail 即可。
- Java直接用 LinkedHashMap 实现即可, LinkedHashMap 底层用的正是双向链表 + HashMap
public class LRUCache {
private int capacity;
private Map cache;
public LRUCache(int capacity) {
this.capacity = capacity;
this.cache = new LinkedHashMap (capacity, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > capacity;
}
};
}
public int getCapacity() {
return capacity;
}
public int getCache(int key) {
if (cache.containsKey(key)) {
return cache.get(key);
} else {
return -1;
}
}
public void setCache(int key, int value) {
cache.put(key, value);
}
public void printLinkedHashMap() {
cache.forEach((k, v) -> System.out.println(String.format("key: %d, value: %d", k, v)));
System.out.println("-------");
}
public static void main(String[] args) {
LRUCache lruCache = new LRUCache(5);
lruCache.setCache(1, 100);
lruCache.setCache(2, 200);
lruCache.setCache(3, 300);
lruCache.setCache(4, 400);
lruCache.setCache(5, 500);
lruCache.printLinkedHashMap();
lruCache.getCache(1);
lruCache.printLinkedHashMap();
lruCache.getCache(3);
lruCache.printLinkedHashMap();
lruCache.setCache(6, 600);
lruCache.printLinkedHashMap();
}
}
(2)通过LruCache,实现内存缓存
public class RamCacheUtils {
private LruCache mRamCache;
public RamCacheUtils() {
//得到最大允许内存的1/8,即超过指定内存,则开始回收
long maxMemory = Runtime.getRuntime().maxMemory();
//指定mRamCache最大允许内存的1/8,即超过指定内存,则开始回收
mRamCache = new LruCache((int) (maxMemory / 8)) {
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getByteCount();
}
};
}
/**
* 从内存中读图片
* @param url
* @return
*/
public Bitmap loadBitmapFromRam(String url) {
return mRamCache.get(url);
}
/**
* 往内存中保存图片
* @param url
* @return
*/
public void setBitmapToMemory(String url, Bitmap bitmap) {
mRamCache.put(url, bitmap);
}
}
五、自定义图片缓存工具类
public class ImageLoaderUtils {
private static ImageLoaderUtils INSTANCE;
private RamCacheUtils mRamCacheUtils;
private LocalCacheUtils mLocalCacheUtils;
private NetworkCacheUtils mNetworkCacheUtils;
private Context mContext;
private ImageLoaderUtils(Context context) {
mContext = context.getApplicationContext();
mRamCacheUtils = new RamCacheUtils();
mLocalCacheUtils = new LocalCacheUtils(mContext);
mNetworkCacheUtils = new NetworkCacheUtils(mLocalCacheUtils, mRamCacheUtils);
}
public static ImageLoaderUtils getInstance(Context context) {
if (INSTANCE == null) {
INSTANCE = new ImageLoaderUtils(context);
}
return INSTANCE;
}
public Bitmap load(String url) {
//内存缓存
if(mRamCacheUtils.loadBitmapFromRam(url) != null){
Log.e("ImageLoaderUtils", "**** 从内存获取图片 ****");
return mRamCacheUtils.loadBitmapFromRam(url);
}
//本地缓存
if(mLocalCacheUtils.loadBitmapFromLocal(url) != null){
Log.e("ImageLoaderUtils", "**** 从本地获取图片 ****");
return mLocalCacheUtils.loadBitmapFromLocal(url);
}
//网络缓存
if(mNetworkCacheUtils.loadBitmapFromNetwork(url) != null){
Log.e("ImageLoaderUtils", "**** 从网络获取图片 ****");
return mNetworkCacheUtils.loadBitmapFromNetwork(url);
}
return null;
}
}
我们在加载图片的时候,直接调用工具类ImageLoaderUtils的load(String url)方法即可,大概可以直接像这样调用:
private void loadImage() {
String url = "http://up.boohee.cn/house/u/shop/cm25_yynd_200g/banner500_v1.jpg";
imageView.setImageBitmap(ImageLoaderUtils.getInstance(this).load(url));
}