Android中缓存的必要性:
1、没有缓存的弊端: 流量开销:对于客户端——服务器端应用,从远程获取图片算是经常要用的一个功能,而图片资源往往会消耗比较大的流量。 加载速度:如果应用中图片加载速度很慢的话,那么用户体验会非常糟糕。 那么如何处理好图片资源的获取和管理呢?异步下载+本地缓存 2、缓存带来的好处: 1. 服务器的压力大大减小; 2. 客户端的响应速度大大变快(用户体验好); 3. 客户端的数据加载出错情况大大较少,大大提高了应有的稳定性(用户体验好); 4. 一定程度上可以支持离线浏览(或者说为离线浏览提供了技术支持)。 3、缓存管理的应用场景: 1. 提供网络服务的应用; 2. 数据更新不需要实时更新,即便是允许3-5分钟的延迟也建议采用缓存机制; 3. 缓存的过期时间是可以接受的(不会因为缓存带来的好处,导致某些数据因为更新不及时而影响产品的形象等) 4、大位图导致内存开销大的原因是什么? 1.下载或加载的过程中容易导致阻塞;大位图Bitmap对象是png格式的图片的30至100倍;2.大位图在加载到ImageView控件前的解码过程;BitmapFactory.decodeFile()会有内存消耗。(decodeByteArray()) 5、缓存设计的要点: 1.命中率;2.合理分配占用的空间;3.合理的缓存层级。
trimToSize()方法的作用就是为了保证当前数据的缓存大小不能超过我们指定的缓存总大小,如果超过了,就会开始移除最近最少使用的数据,直到size符合要求。trimToSize()方法在put()的时候一定会调用,在get()的时候有可能会调用。
(6)get方法获取缓存数据 get方法源码如下: /** * 根据key查询缓存,如果该key对应的value存在于缓存,直接返回value;* 访问到这个结点时,LinkHashMap会将它移动到双向循环链表的的尾部。* 如果如果没有缓存的值,则返回null。(如果开发者重写了create()的话,返回创建的value)*/public final V get(K key) { if (key == null) { throw new NullPointerException("key == null"); } V mapValue; synchronized (this) { // LinkHashMap 如果设置按照访问顺序的话,这里每次get都会重整数据顺序 mapValue = map.get(key); // 计算 命中次数 if (mapValue != null) { hitCount++; return mapValue; } // 计算 丢失次数 missCount++; } /* * 官方解释: * 尝试创建一个值,这可能需要很长时间,并且Map可能在create()返回的值时有所不同。如果在create()执行的时 * 候,用这个key执行了put方法,那么此时就发生了冲突,我们在Map中删除这个创建的值,释放被创建的值,保留put进去的值。 */ V createdValue = create(key); if (createdValue == null) { return null; } /*************************** * 不覆写create方法走不到下面 * ***************************/ /* * 正常情况走不到这里 * 走到这里的话 说明 实现了自定义的 create(K key) 逻辑 * 因为默认的 create(K key) 逻辑为null */ synchronized (this) { // 记录 create 的次数 createCount++; // 将自定义create创建的值,放入LinkedHashMap中,如果key已经存在,会返回 之前相同key 的值 mapValue = map.put(key, createdValue); // 如果之前存在相同key的value,即有冲突。 if (mapValue != null) { /* * 有冲突 * 所以 撤销 刚才的 操作 * 将 之前相同key 的值 重新放回去 */ map.put(key, mapValue); } else { // 拿到键值对,计算出在容量中的相对长度,然后加上 size += safeSizeOf(key, createdValue); } } // 如果上面 判断出了 将要放入的值发生冲突 if (mapValue != null) { /* * 刚才create的值被删除了,原来的 之前相同key 的值被重新添加回去了 * 告诉 自定义 的 entryRemoved 方法 */ entryRemoved(false, key, createdValue, mapValue); return mapValue; } else { // 上面 进行了 size += 操作 所以这里要重整长度 trimToSize(maxSize); return createdValue; }}get()方法的思路就是:
1)先尝试从map缓存中获取value,即mapVaule = map.get(key);如果mapVaule != null,说明缓存中存在该对象,直接返回即可; 2)如果mapVaule == null,说明缓存中不存在该对象,大多数情况下会直接返回null;但是如果我们重写了create()方法,在缓存没有该数据的时候自己去创建一个,则会继续往下走,中间可能会出现冲突,看注释; 3)注意:在我们通过LinkedHashMap进行get(key)或put(key,value)时都会对链表进行调整,即将刚刚访问get或加入put的结点放入到链表尾部。 (7)entryRemoved() entryRemoved的源码如下: /** * 1.当被回收或者删掉时调用。该方法当value被回收释放存储空间时被remove调用* 或者替换条目值时put调用,默认实现什么都没做。* 2.该方法没用同步调用,如果其他线程访问缓存时,该方法也会执行。* 3.evicted=true:如果该条目被删除空间 (表示 进行了trimToSize or remove) evicted=false:put冲突后 或 get里成功create后* 导致* 4.newValue!=null,那么则被put()或get()调用。*/protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {} 可以发现entryRemoved方法是一个空方法,说明这个也是让开发者自己根据需求去重写的。entryRemoved()主要作用就是在结点数据value需要被删除或回收的时候,给开发者的回调。开发者就可以在这个方法里面实现一些自己的逻辑: (1)可以进行资源的回收; (2)可以实现二级内存缓存,可以进一步提高性能,思路如下: 重写LruCache的entryRemoved()函数,把删除掉的item,再次存入另外一个LinkedHashMap<String, SoftWeakReference>中,这个数据结构当做二级缓存,每次获得图片的时候,先判断LruCache中是否缓存,没有的话,再判断这个二级缓存中是否有,如果都没有再从sdcard上获取。sdcard上也没有的话,就从网络服务器上拉取。 entryRemoved()在LruCache中有四个地方进行了调用:put()、get()、trimToSize()、remove()中进行了调用。 (8)LruCache的线程安全性 LruCache是线程安全的,因为在put、get、trimToSize、remove的方法中都加入synchronized进行同步控制。在LruCache中,由于数据是直接缓存中内存中,map中数据的建立是在使用LruCache缓存的过程中逐步建立的,而对于DiskLruCache,由于数据是缓存在本地文件,相当于是持久保存下来的一个文件,即使程序退出文件还在,因此,map中数据的建立,除了在使用DiskLruCache过程中建立外,map还应该包括之前已经存在的缓存文件,因此,在获取DiskLruCache的实例时,DiskLruCache会去读取journal这个日志文件,根据这个日志文件中的信息,建立map的初始数据,同时,会根据journal这个日志文件,维护本地的缓存文件。构造DiskLruCache的方法如下:
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize) throws IOException { if (maxSize <= 0) { throw new IllegalArgumentException("maxSize <= 0"); } if (valueCount <= 0) { throw new IllegalArgumentException("valueCount <= 0"); } // prefer to pick up where we left off DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize); if (cache.journalFile.exists()) { try { cache.readJournal(); cache.processJournal(); cache.journalWriter = new BufferedWriter(new FileWriter(cache.journalFile, true),IO_BUFFER_SIZE); return cache; } catch (IOException journalIsCorrupt) { // System.logW("DiskLruCache " + directory + " is corrupt: " // + journalIsCorrupt.getMessage() + ", removing"); cache.delete(); } } // create a new empty cache directory.mkdirs(); cache = new DiskLruCache(directory, appVersion, valueCount, maxSize); cache.rebuildJournal(); return cache;}其中,
cache.readJournal(); cache.processJournal(); 正是去读取journal日志文件,建立起map中的初始数据,同时维护缓存文件。 那journal日志文件到底保存了什么信息呢,一个标准的journal日志文件信息如下: libcore.io.DiskLruCache //第一行,固定内容,声明 1 //第二行,cache的版本号,恒为1 1 //第三行,APP的版本号 2 //第四行,一个key,可以存放多少条数据valueCount //第五行,空行分割行 DIRTY 335c4c6028171cfddfbaae1a9c313c52 CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 REMOVE 335c4c6028171cfddfbaae1a9c313c52 DIRTY 1ab96a171faeeee38496d8b330771a7a CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234 READ 335c4c6028171cfddfbaae1a9c313c52 READ 3400330d1dfc7f3f7f4b8d4d803dfcf6 前五行称为journal日志文件的头,下面部分的每一行会以四种前缀之一开始: DIRTY、CLEAN、REMOVE、READ。以一个DIRTY前缀开始的,后面紧跟着缓存图片的key。以DIRTY这个这个前缀开头,意味着这是一条脏数据。每当我们调用一次DiskLruCache的edit()方法时,都会向journal文件中写入一条DIRTY记录,表示我们正准备写入一条缓存数据,但不知结果如何。然后调用commit()方法表示写入缓存成功,这时会向journal中写入一条CLEAN记录,意味着这条“脏”数据被“洗干净了”,调用abort()方法表示写入缓存失败,这时会向journal中写入一条REMOVE记录。也就是说,每一行DIRTY的key,后面都应该有一行对应的CLEAN或者REMOVE的记录,否则这条数据就是“脏”的,会被自动删除掉。
在CLEAN前缀和key后面还有一个数值,代表的是该条缓存数据的大小。
因此,我们可以总结DiskLruCache中的工作流程:
1)初始化:通过open()方法,获取DiskLruCache的实例,在open方法中通过readJournal(); 方法读取journal日志文件,根据journal日志文件信息建立map中的初始数据;然后再调用processJournal();方法对刚刚建立起的map数据进行分析,分析的工作,一个是计算当前有效缓存文件(即被CLEAN的)的大小,一个是清理无用缓存文件;
2)数据缓存与获取缓存:上面的初始化工作完成后,我们就可以在程序中进行数据的缓存功能和获取缓存的功能了;
缓存数据的操作是借助DiskLruCache.Editor这个类完成的,这个类也是不能new的,需要调用DiskLruCache的edit()方法来获取实例,如下所示:
public Editor edit(String key) throws IOException在写入完成后,需要进行commit()。如下一个简单示例:
new Thread(new Runnable() { @Override public void run() { try { String imageUrl = "https://img-my.csdn.net/uploads/201309/01/1378037235_7476.jpg"; String key = hashKeyForDisk(imageUrl); //MD5对url进行加密,这个主要是为了获得统一的16位字符 DiskLruCache.Editor editor = mDiskLruCache.edit(key); //拿到Editor,往journal日志中写入DIRTY记录 if (editor != null) { OutputStream outputStream = editor.newOutputStream(0); if (downloadUrlToStream(imageUrl, outputStream)) { //downloadUrlToStream方法为下载图片的方法,并且将输出流放到outputStream editor.commit(); //完成后记得commit(),成功后,再往journal日志中写入CLEAN记录 } else { editor.abort(); //失败后,要remove缓存文件,往journal文件中写入REMOVE记录 } } mDiskLruCache.flush(); //将缓存操作同步到journal日志文件,不一定要在这里就调用 } catch (IOException e) { e.printStackTrace(); } } }).start(); 注意每次调用edit()时,会向journal日志文件写入DIRTY为前缀的一条记录;文件保存成功后,调用commit()时,也会向journal日志中写入一条CLEAN为前缀的一条记录,如果失败,需要调用abort(),abort()里面会向journal文件写入一条REMOVE为前缀的记录。 获取缓存数据是通过get()方法实现的,如下一个简单示例: try { String imageUrl = "https://img-my.csdn.net/uploads/201309/01/1378037235_7476.jpg"; String key = hashKeyForDisk(imageUrl); //MD5对url进行加密,这个主要是为了获得统一的16位字符 //通过get拿到value的Snapshot,里面封装了输入流、key等信息,调用get会向journal文件写入READ为前缀的记录 DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key); if (snapShot != null) { InputStream is = snapShot.getInputStream(0); Bitmap bitmap = BitmapFactory.decodeStream(is); mImage.setImageBitmap(bitmap); } } catch (IOException e) { e.printStackTrace(); } 3)合适的地方进行flush() 在上面进行数据缓存或获取缓存的时候,调用不同的方法会往journal中写入不同前缀的一行记录,记录写入是通过IO下的Writer写入的,要真正生效,还需要调用writer的flush()方法,而DiskLruCache中的flush()方法中封装了writer.flush()的操作,因此,我们只需要在合适地方调用DiskLruCache中的flush()方法即可。其作用也就是将操作记录同步到journal文件中,这是一个消耗效率的IO操作,我们不用每次一往journal中写数据后就调用flush,这样对效率影响较大,可以在Activity的onPause()中调用一下即可。 小结&注意: (1)我们可以在在UI线程中检测内存缓存,即主线程中可以直接使用LruCache; (2)使用DiskLruCache时,由于缓存或获取都需要对本地文件进行操作,因此需要另开一个线程,在子线程中检测磁盘缓存、保存缓存数据,磁盘操作从来不应该在UI线程中实现; (3)LruCache内存缓存的核心是LinkedHashMap,而DiskLruCache的核心是LinkedHashMap和journal日志文件,相当于把journal看作是一块“内存”,LinkedHashMap的value只保存文件的简要信息,对缓存文件的所有操作都会记录在journal日志文件中。 DiskLruCache可能的优化方案: DiskLruCache是基于日志文件journal的,这就决定了每次对缓存文件的操作都需要进行日志文件的记录,我们可以不用journal文件,在第一次构造DiskLruCache的时候, 直接从程序访问缓存目录下的缓存文件,并将每个缓存文件的访问时间作为初始值记录在map的value中,每次访问或保存缓存都更新相应key对应的缓存文件的访问时间,这样就避免了频繁的IO操作,这种情况下就需要使用单例模式对DiskLruCache进行构造了,上面的Acache轻量级的数据缓存类就是这种实现方式。http://www.binkery.com/archives/561.html
http://www.jianshu.com/p/bdbfdfd0641b http://blog.csdn.net/guolin_blog/article/details/28863651 LruCache源码解析 1、没有缓存的弊端: 流量开销:对于客户端——服务器端应用,从远程获取图片算是经常要用的一个功能,而图片资源往往会消耗比较大的流量。 加载速度:如果应用中图片加载速度很慢的话,那么用户体验会非常糟糕。 那么如何处理好图片资源的获取和管理呢?异步下载+本地缓存 2、缓存带来的好处: 1. 服务器的压力大大减小; 2. 客户端的响应速度大大变快(用户体验好); 3. 客户端的数据加载出错情况大大较少,大大提高了应有的稳定性(用户体验好); 4. 一定程度上可以支持离线浏览(或者说为离线浏览提供了技术支持)。 3、缓存管理的应用场景: 1. 提供网络服务的应用; 2. 数据更新不需要实时更新,即便是允许3-5分钟的延迟也建议采用缓存机制; 3. 缓存的过期时间是可以接受的(不会因为缓存带来的好处,导致某些数据因为更新不及时而影响产品的形象等) 4、大位图导致内存开销大的原因是什么? 1.下载或加载的过程中容易导致阻塞;大位图Bitmap对象是png格式的图片的30至100倍;2.大位图在加载到ImageView控件前的解码过程;BitmapFactory.decodeFile()会有内存消耗。(decodeByteArray()) 5、缓存设计的要点: 1.命中率;2.合理分配占用的空间;3.合理的缓存层级。