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就是每张图片对应的url,value就是每个图片 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来计算变量的总大小。一旦超出大小,就开始删除对象,从而保证内存量在规定范围内!
呼~这篇文章总算写完了,拖了好多天,希望对你们有帮助 :)
相关文章推荐
- 深度学习/神经神经网络常用激活函数总结
- 菜鸟网络工程师的成长笔记——第15天(2016.09.01)
- HTTP协议之无连接与无状态
- http的无状态无连接到底是什么含义
- spoj spoj839 Optimal Marks 网络流 最小割 附数据生成器
- TCP-IP详解:TCP的连接与终止
- TCP协议的建立和断开过程
- IP地址分类
- HDU 4267 A Simple Problem with Integers(2012年长春网络赛A 多颗线段树+单点查询)
- (java题目第3讲)ACM(http://blog.csdn.net/kuhuaishuxia/article/details/52254209)
- 为apt-get设置http代理服务
- Android Http异步请求,Callback
- 网络请求框架(二):volley使用之自定义请求
- HTTP状态码->HTTP Status Code
- HttpURLConnection请求数据
- Retrofit2.0 OkHttp如何自动加载Cookie 持久化
- HTTP Status 500 - Unable to compile class for JSP: 报错
- 多线程和多进程的区别【转载网络】
- Gym - 100204G Network Wars 网络流相关定理 附一组数据
- POJ1459 Power Network(网络最大流)