您的位置:首页 > 理论基础 > 计算机网络

Android 之 三级缓存(内存!!!、本地、网络)及内存LruCache扩展 及源码分析--- 学习和代码讲解

2016-09-01 21:24 429 查看

一. 三级缓存简介



如上图所示,目前App中UI界面经常会涉及到图片,特别是像“今日关注”新闻这类app中,图片运用的几率十分频繁。当手机上需要显示大量图片类似listView、gridView控件并且用户会上下滑动,即将浏览过的图片又加载一遍,若是不停的进行网络请求,很快就会OOM,这时三级缓存显得尤为重要,适时地利用资源,进行图片缓存,下面就用一个新闻组图demo进行图片缓存演示。

1.三级缓存的顺序

(1)内存缓存: 比如说需要加载图片时,系统第一步不会直接网络请求,而是首先找第一级缓存—内存缓存

(2)本地缓存: 如果内存缓存中没有,就会从第二级缓存—本地缓冲(即sd卡)

(3)网络缓存: 如果本地缓存中没有,就会从网络缓存中下载图片。



2. 三级缓存级别总结

(1)内存缓存: 速度快, 优先读取

(2)本地缓存: 速度其次, 内存没有,读本地

(3)网络缓存: 速度最慢, 本地也没有,才访问网络

二. 代码实现

关于这个三级缓存的实现,其实 Xutils开源项目中BitmapUtils已经替我们封装好了,下面新建一个MyBitmapUtils,自己实现三级缓存。

1.网络缓存(NetCacheUtils )

/**
* 三个泛型意义:
* 第一个泛型:doInBackground里的参数类型
* 第二个泛型: onProgressUpdate里的参数类型
* 第三个泛型:
* onPostExecute里的参数类型及doInBackground的返回类型
*/
private class BitmapTask extends AsyncTask<Object, Integer,Bitmap>{
//1.预加载,运行在主线程
@Override
protected void onPreExecute() {
super.onPreExecute();
}
//2.正在加载,运行在子线程(核心方法),可以直接异步请求
@Override
protected Bitmap doInBackground(Object[] objects) {
return null;
}
//3.更新进度的方法,运行在主线程
@Override
protected void onProgressUpdate(Integer... values) {
super.onProgressUpdate(values);
}
//4.加载结束,运行在主线程(核心方法),可以直接更新UI
@Override
protected void onPostExecute(Bitmap bitmap) {
super.onPostExecute(bitmap);
}
}


这里的逻辑就是 doInBackground方法 异步网络请求图片,onPostExecute方法将图片加载呈现出来。而 onPreExecute 的作用是预加载,使用不常。至于onProgressUpdate 可显示出请求图片过程中的进度,这两个并非核心方法。

(1)doInBackground : 核心方法,请求网络。大家都知道请求网络是一个耗时操作,需要在子线程中进行,这里也确实如此,不过不需要我们再new 一个Thread ,查看源码可知异步AsyncTask已经帮我们做到了。在这一步需要做的就是,获得方法参数中的url,进行网络请求,下载图片获得Bitmap.

(2)onPostExecute: 核心方法,图片加载完成后,显示在手机屏幕上。大家也了解子线程中无法做UI更新,需要使用消息机制,给handler发送消息,在主线程中更新UI。这里异步也都替我们做好了,查看源码可知UI更新是在异步中的hanler中进行。在这一步需要做的就是,将请求获得的Bitmap呈现到屏幕上。(更规范的是,还要将获得的Bitmap存储到内存和本地中,方便下次使用时可拿取缓存,不需重复请求网络!!!)

/**
* 网络缓存工具类
*
*
*/
public class NetCacheUtils {

LocalCacheUtils mLocalCacheUtils;
MemoryCacheUtils mMemoryCacheUtils;

public NetCacheUtils(LocalCacheUtils localCacheUtils,
MemoryCacheUtils memoryCacheUtils) {
mLocalCacheUtils = localCacheUtils;
mMemoryCacheUtils = memoryCacheUtils;
}

public void getBitmapFromNet(ImageView ivPic, String url) {
BitmapTask task = new BitmapTask();
task.execute(new Object[] { ivPic, url });
}

class BitmapTask extends AsyncTask<Object, Void, Bitmap> {

private ImageView imageView;
private String url;

/**
* 返回的对象会自动回传到onPostExecute里面
*/
@Override
protected Bitmap doInBackground(Object... params) {
imageView = (ImageView) params[0];
url = (String) params[1];
imageView.setTag(url);
Bitmap bitmap = downloadBitmap(url);
return bitmap;
}

@Override
protected void onPostExecute(Bitmap result) {
// 这里的result就是doInBackground返回回来的对象
if (result != null) {
String ivUrl = (String) imageView.getTag();
if (url.equals(ivUrl)) {// 确保imageview设置的是正确的图片(因为有时候listview有重用机制,多个item会公用一个imageview对象,从而导致图片错乱)
imageView.setImageBitmap(result);
System.out.println("从网络缓存读取图片");

// 向本地保存图片文件
mLocalCacheUtils.putBitmapToLocal(url, result);
// 向内存保存图片对象
mMemoryCacheUtils.putBitmapToMemory(url, result);
}
}
}
}

/**
* 下载图片
*
* @param url
* @return
*/
private Bitmap downloadBitmap(String url) {
HttpURLConnection conn = null;
try {
conn = (HttpURLConnection) new URL(url).openConnection();
conn.setConnectTimeout(5000);
conn.setReadTimeout(5000);
conn.setRequestMethod("GET");
conn.connect();

int responseCode = conn.getResponseCode();
if (responseCode == 200) {
InputStream inputStream = conn.getInputStream();

//图片压缩
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 2;//表示压缩比例,2表示宽高都压缩为原来的二分之一, 面积为四分之一
options.inPreferredConfig = Config.RGB_565;//设置bitmap的格式,565可以降低内存占用

Bitmap bitmap = BitmapFactory.decodeStream(inputStream, null, options);
return bitmap;
}

} catch (Exception e) {
e.printStackTrace();
} finally {
conn.disconnect();
}

return null;
}
}


2. 本地缓存(LocalCacheUtils )

/**
* 本地缓存工具类
*
*
*/
public class LocalCacheUtils {

private static final String LOCAL_PATH = Environment
.getExternalStorageDirectory().getAbsolutePath() + "/zhbj_cache";

/**
* 从本地读取图片
*
* @param url
* @return
*/
public Bitmap getBitmapFromLocal(String url) {
try {
String fileName = MD5Encoder.encode(url);
File file = new File(LOCAL_PATH, fileName);

if (file.exists()) {
// 图片压缩
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 2;// 表示压缩比例,2表示宽高都压缩为原来的二分之一, 面积为四分之一
options.inPreferredConfig = Config.RGB_565;// 设置bitmap的格式,565可以降低内存占用

Bitmap bitmap = BitmapFactory.decodeStream(new FileInputStream(
file), null, options);
return bitmap;
} else {
return null;
}

} catch (Exception e) {
e.printStackTrace();
}
return null;
}

/**
* 向本地存图片
*
* @param url
* @param bitmap
*/
public void putBitmapToLocal(String url, Bitmap bitmap) {
try {
String fileName = MD5Encoder.encode(url);
File file = new File(LOCAL_PATH, fileName);
File parent = file.getParentFile();

// 创建父文件夹
if (!parent.exists()) {
parent.mkdirs();
}

bitmap.compress(CompressFormat.JPEG, 100,
new FileOutputStream(file));
} catch (Exception e) {
e.printStackTrace();
}
}

}


如上所示,这里对于本地(sd卡)缓存的操作就两个方法,比较单一,一个存储缓存数据方法—putBitmapToLocal,一个拿取缓存数据方法—getBitmapToLocal

(1)putBitmapToLocal: 这里我们将每个存储图片的文件名设为 图片对应的url地址(MD5加密后的),判断父文件是否存在,不存在则新建,存在则直接存储进去。

(2)getBitmapToLocal: 先从方法参数中获取到图片对应的url,进行查找,若存在则将图片的Bitmap返回回去(最好返回前先压缩),不存在则返回null。

3. 内存缓存(LocalCacheUtils )重点!!!

3.1 HashMap版

/**
* 内存缓存工具类
*/
public class MemoryCacheUtils {

HashMap<String, Bitmap> mMemoryCache = new HashMap<String, Bitmap> ;
/**
* 从内存读取图片
*
* @param url
* @return
*/
public Bitmap getBitmapFromMemory(String url) {
Bitmap bitmap = mMemoryCache.get(url);
return bitmap;
}

/**
* 向内存存图片
*
* @param url
* @param bitmap
*/
public void putBitmapToMemory(String url, Bitmap bitmap) {
mMemoryCache.put(url, bitmap);
}
}


如上所示,这里对于内存缓存的操作也是两个方法,一个是设置内存缓存方法—putBitmapToLocal,一个是取内存缓存方法—getBitmapToLocal。用对象来存储图片,集合来存储对象,集合都在内存里面,所以决定用集合。

关于Android,集合就涉及到两个,ArrayList用的多,但是取数据时必须要传递数组位置;但是Hashmap用的是键值对结构,只要有了key,就可以找到对应的value。(而我们这里的key就是每张图片对应的urlvalue就是每个图片 Bitmap对象

3.2 软引用版

你说以上就是内存缓存的重点?绝对不可能,Bitmap对象虽存在于集合中,但我们每次都 new 一个新的Bitmap,如果有大量的图片,集合内存根本不够,很快就会OOM,也就是内存溢出。也许你的手机内存很大,但是不管安卓设备总内存有多大,它只给每个APP分配一定内存大小(16M),所以内存是非常有限的,而且在这里 垃圾回收机制是不起作用的!



3.2.1 栈、堆、垃圾回收器

如上图所示,内存缓存这里涉及到栈和堆。java里的一般存的是成员变量、方法声明、引用之类的。里存储的是一个又一个的对象。(例如,new了一个 p,p存在栈里,但是 person对象存储在 堆中,p引用,指向一个person对象)。垃圾回收器会定时地从堆里回收圾释放内存。(例如上图,只要栈与堆中的连接断掉,堆中的对象就是垃圾,回收站可进行回收。所以说,垃圾回收器有个特点:只回收没有引用的对象!

再回到内存溢出上,我们

HashMap<String,Bitmap> mMemoryCache = new HashMap<String, Bitmap> ;


集合中有许多个对象,都被集合引用!这个引用一直在!垃圾回收器并不会回收,所以会导致内存溢出。以上只是一方面,而且即使它会回收这些引用的集合,可它是隔一段时间才会回收,无法及时清理内存!

现在我们需要解决的是:能否在引用的情况下,垃圾回收器可以照样回收?

3.2.2 内存缓存中的 引用级别

(1) 强引用 默认引用, 即使内存溢出,也不会回收

(2) 软引用 SoftReference, 内存不够时, 会考虑回收

(3) 弱引用 WeakReference 内存不够时, 更会考虑回收

(4)虚引用 PhantomReference 内存不够时, 最优先考虑回收!

Person p = new Person();
就属于强引用。回收器断然不会回收!而虚引用则太容易被回收,所以最常用的是软引用弱引用,在需求不强烈或内存实在是不够的情况下,垃圾回收器才会回收引用的对象。我们主要回收的是Bitmap对象,对集合进行包装,使用软引用

//用法举例
Bitmap bitmap = new Bitmap();
SoftReference<Bitmap> sBitmap = new SoftReference<Bitmap>(bitmap);
Bitmap bitmap2 = sBitmap.get();


( 软引用版):

/**
* 内存缓存工具类
*/
public class MemoryCacheUtils {

HashMap<String, SoftReference<Bitmap>> mMemoryCache = new HashMap<String, SoftReference<Bitmap>> ;
/**
* 从内存读取图片
*
* @param url
* @return
*/
public Bitmap getBitmapFromMemory(String url) {
SoftReference<Bitmap> softBitmap = mMemoryCache.get(url);

if(softReference != null){
Bitmap bitmap = softReference.get();
return bitmap;
}
return null;
}

/**
* 向内存存图片
*
* @param url
* @param bitmap
*/
public void putBitmapToMemory(String url, Bitmap bitmap) {
SoftReference<Bitmap> softBitmap = new SoftReference<Bitmap>(bitmap);
mMemoryCache.put(url, bitmap);
}
}


3.3 LruCache 版(重点!!!)

(一位大神关于这一点的讲解,很好,下面有引用部分

http://blog.csdn.net/fancylovejava/article/details/25705169

可是自从 Android 2.3 (API Level 9)开始,垃圾回收器会更倾向于回收持有软引用或弱引用的对象,这让软引用和弱引用变得不再可靠。这么说来即使内存很充分的情况下,也有优先回收弱引用和软引用。

官方文档的截图:



https://developer.android.com/training/displaying-bitmaps/cache-bitmap.html官方链接

翻译:
在过去,我们经常会使用一种非常流行的内存缓存技术的实现,即软引用或弱引用 (SoftReference or WeakReference)。
但是现在已经不再推荐使用这种方式了,因为从 Android 2.3 (API Level 9)开始,垃圾回收器会更倾向于回收持有软引用或弱引用的对象,
这让软引用和弱引用变得不再可靠。另外,Android 3.0 (API Level 11)中,图片的数据会存储在本地的内存当中,因而无法用一种可预见的方式将其释放,
这就有潜在的风险造成应用程序的内存溢出并崩溃。所以看到还有很多相关文章还在推荐用软引用或弱引用 (SoftReference or WeakReference),就有点out了。


所以为了解决这个问题,google为我们推荐了LruCache类,这个类在 V4包 下,非常适合用来缓存图片。

3.3.1 LruCache

Lru定义 :least recentlly used 最近最少使用的算法。(比如说,先后使用A、B、C、A、C、D对象,这时会回收的则是B对象。)

LruCache : 可以将最近最少使用的对象回收掉, 从而保证内存不会超出范围!

3.3.2 分配空间



获得分配给App最大的内存大小 —— 16M(16777216/1024)

long maxMemory = Runtime.getRuntime().maxMemory();
mMemoryCache = new LruCache<String, Bitmap>((int) (maxMemory / 8))


但是在分配内存的过程中,切不可一次分配全部内存出去,毕竟这只是App的一部分模块,其余部分还需要空间。(分配1/8 —— 2M)

3.3.2 重写LruCache 的 sizeOf方法

这个方法要返回每个对象的大小。Lru要控制内存的总大小,所以它需要知道每个Bitmap有多大。所以需要重写这个方法,让开发者自己计算,返回大小。

protected int sizeOf(String key, Bitmap value) {
// int byteCount = value.getByteCount();
int byteCount = value.getRowBytes() * value.getHeight();// 计算图片大小:每行字节数*高度
return byteCount;
}


( LruCache版):

private LruCache<String, Bitmap> mMemoryCache;

public MemoryCacheUtils() {

long maxMemory = Runtime.getRuntime().maxMemory();// 获取分配给app的内存大小
System.out.println("maxMemory:" + maxMemory);

mMemoryCache = new LruCache<String, Bitmap>((int) (maxMemory / 8)) {

// 返回每个对象的大小
@Override
protected int sizeOf(String key, Bitmap value) {
// int byteCount = value.getByteCount();//有版本兼容问题
int byteCount = value.getRowBytes() * value.getHeight();// 计算图片大小:每行字节数*高度
return byteCount;
}
};
}

/**
* 写缓存
*/
public void setMemoryCache(String url, Bitmap bitmap) {
mMemoryCache.put(url, bitmap);
}

/**
* 读缓存
*/
public Bitmap getMemoryCache(String url) {
return mMemoryCache.get(url);
}


4. 工具类,将以上三级缓存封装起来

/**
* 自定义三级缓存图片加载工具
*/
public class MyBitmapUtils {

private NetCacheUtils mNetCacheUtils;
private LocalCacheUtils mLocalCacheUtils;
private MemoryCacheUtils mMemoryCacheUtils;

public MyBitmapUtils() {
mMemoryCacheUtils = new MemoryCacheUtils();
mLocalCacheUtils = new LocalCacheUtils();
mNetCacheUtils = new NetCacheUtils(mLocalCacheUtils, mMemoryCacheUtils);
}

public void display(ImageView imageView, String url) {
// 设置默认图片
imageView.setImageResource(R.drawable.pic_item_list_default);

// 优先从内存中加载图片, 速度最快, 不浪费流量
Bitmap bitmap = mMemoryCacheUtils.getMemoryCache(url);
if (bitmap != null) {
imageView.setImageBitmap(bitmap);
System.out.println("从内存加载图片啦");
return;
}

// 其次从本地(sdcard)加载图片, 速度快, 不浪费流量
bitmap = mLocalCacheUtils.getLocalCache(url);
if (bitmap != null) {
imageView.setImageBitmap(bitmap);
System.out.println("从本地加载图片啦");

// 写内存缓存
mMemoryCacheUtils.setMemoryCache(url, bitmap);
return;
}

// 最后从网络下载图片, 速度慢, 浪费流量
mNetCacheUtils.getBitmapFromNet(imageView, url);
}

}


以上,将工具类封装号之后,我们可以不使用 Xutils里的方法,使用我们自定义的MyBitmapUtils,以下代码为调用过程。

class PhotoAdapter extends BaseAdapter {

//private BitmapUtils mBitmapUtils;
private MyBitmapUtils mBitmapUtils;

public PhotoAdapter() {
mBitmapUtils = new MyBitmapUtils();
//mBitmapUtils = new BitmapUtils(mActivity);
//          mBitmapUtils
//                  .configDefaultLoadingImage(R.drawable.pic_item_list_default);
}


三. 结果呈现

呈现出来的顺序就是:



这是我测试之后的,如果是第一次打开这个模块,最先使用的只能是网络缓存,一旦第一次进行网络缓存后,本地缓存和内存缓存就会有相应的数据。下一次再打开此模块时,首先加载的是本地缓存,得到Bitmap**对象后,之后进行的都是 内存缓存**了。

四. LruCache扩展及源码分析(重点)

Lru 就像我们家用的洗漱池里小开口,水龙头流出来的水就像是内存,所以我们的洗漱池会堵吗?不会!如果你把口子给堵起来防水,很快水就会满出来,就像是 内存溢出。这里,我们来看下 V4 包下的 LruCache 源码

public class LruCache<K, V> {
private final LinkedHashMap<K, V> map;


点进去一看,Lrucache是一个泛型,它维护了一个 LinkedHashMap,将来在存图片的时候,底层也是存在一个HashMap里。

1. LruCache 的 put 方法

public final V put(K key, V value) {
if (key == null || value == null) {
throw new NullPointerException("key == null || value == null");
}

V previous;
synchronized (this) {
putCount++;
//!!!!!!!  size += safeSizeOf(key, value);
//!!!!!!!   previous = map.put(key, value);
if (previous != null) {
size -= safeSizeOf(key, previous);
}
}

if (previous != null) {
entryRemoved(false, key, previous, value);
}

//!!!!!!! trimToSize(maxSize);
return previous;
}


我们去找它的一个put 方法。

previous = map.put(key, value);
标记感叹号地方 的 map 就是 一开始的 LinkedHashMap,底层就是对HashMap的封装。

size += safeSizeOf(key, value);


全局维护了一个变量size,时时在统计集合目前对象大小。它走的就是sizeOf方法。但是源码中方法返回的是1,

protected int sizeOf(K key, V value) {
return 1;
}


所以我们需要去重写它的sizeOf方法。所以LruCache 在put 的时候都会把总大小计算出来,然后调用trimToSize(maxSize);方法,来看下此方法源码

public void trimToSize(int maxSize) {
while (true) {
K key;
V value;
synchronized (this) {
if (size < 0 || (map.isEmpty() && size != 0)) {
throw new IllegalStateException(getClass().getName()
+ ".sizeOf() is reporting inconsistent results!");
}

//!!!!!!! if (size <= maxSize || map.isEmpty()) {
break;
}

//!!!!!!!  Map.Entry<K, V> toEvict = map.entrySet().iterator().next();
key = toEvict.getKey();
value = toEvict.getValue();
map.remove(key);
size -= safeSizeOf(key, value);
evictionCount++;
}

//!!!!!!! entryRemoved(true, key, value, null);
}
}


一上来就是一个While循环,先不看抛出异常,直接看if判断
if (size <= maxSize || map.isEmpty()) { break; }
,如果内存正常,则break出去,否则

Map.Entry<K, V> toEvict = map.entrySet().iterator().next();
key = toEvict.getKey();
value = toEvict.getValue();
map.remove(key);
size -= safeSizeOf(key, value);
evictionCount++;


通过map 拿到迭代器的第一个对象,再直接拿到key,再remove出去,所以总内存大小就减少了。这时继续While循环,因为减少一个不一定符合大小,所以一直减少直到内存大小少于规定值为止!

所以LruCache所谓的算法:
可以将最近最少使用的对象回收掉, 从而保证内存不会超出范围


其中的核心原理就在这里,不停的删掉开头的key,这就是最近最少用的对象。

2. LruCache 的 get 方法

public final V get(K key) {
if (key == null) {
throw new NullPointerException("key == null");
}

V mapValue;
synchronized (this) {
mapValue = map.get(key);
if (mapValue != null) {
hitCount++;
return mapValue;
}
missCount++;
}


这里的get方法则更简单,参数将 key 传过来,直接从map中get出对象,再return出来就行了。

3. LruCache 的 核心

最核心的地方其实就是维护一个 HashMap,再设置了一个全局变量 size来计算变量的总大小。一旦超出大小,就开始删除对象,从而保证内存量在规定范围内!

呼~这篇文章总算写完了,拖了好多天,希望对你们有帮助 :)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: