对于一个应用来说,是否被用户接受、喜爱,除了其内容以外,对于流量的消耗也是一个重要原因。比如当一个页面需要展现很多图片,如果图片没有替换或者更新,当用户多次点击进入该界面,难道要一次一次从网络上加载图片吗?那样便会消耗很多流量,对于用户来说必定是一个糟糕的体验。这时候就需要缓存策略了:
当程序第一次运行,当程序从网络上加载图片后,便将它缓存到储存设备上,往往也会把图片缓存到内存中,当下次需要使用这些图片时,就先从内存中去获取,如果没有就从储存设备中去获取,如果还是没有才会从网络上加载。
缓存策略一般包括三个部分:缓存的添加、获取、删除。因为内存缓存和设备缓存的大小都是有限制的,当缓存满了的时候需要加入新的缓存就需要删除一些缓存了。怎么选择删除的缓存就需要使用缓存算法。
缓存算法中有一种比较常见的就是 LRU,即 近期最少使用算法,它会优先删除最近最少使用的缓存来为新的缓存提供空间。其中 LruCache 就是基于 LRU 实现内存缓存的,DiskLruCache 就是基于 LRU 实现存储设备缓存。
- LruCache
LruCache 是一个泛型类,内部使用了一个 LinkedHashMap 以强引用的方式存储缓存,通过 get() 和 put() 来进行获取和添加缓存。如下是 LruCache 的构造方法:
构造方法会要求传入一个最大值,一般是指你当前进程能够使用的最大内存,可以按照自己要求进行更改,它会先判断能够使用的内存是否大于0,如果不大于就会抛出一个一个 IllegalArgumentException 告诉你没有可用内存了。
一般来说如果要使用 LruCache 来进行缓存,就要重写它的 sizeOf() 方法,来计算缓存对象的大小。以图片为例:
int maxMemory = (int) ((Runtime.getRuntime().maxMemory())/1024); //取可用内存的1/8 int cacheSize = maxMemory/8; LruCache<String,Bitmap> cache = new LruCache<String, Bitmap>(cacheSize){ @Override protected int sizeOf(String key, Bitmap value) { return value.getRowBytes()*value.getHeight()/1024; } };
上述代码中,先是使用 Runtime.getRuntime().getmaxMemory() 来获取当前进程可以使用的最大内存,单位是字节。然后重写 sizeOf() 方法返回当前图片的缓存需要的大小。
下面是 LruCache 的 pet() 的源码:
下面是 LruCache 的 get() 的源码:
下面是 LruCache 的 remove() 的源码:
它的实现和使用相对来说都比较简单,都是基于 LinkedHashMap 的增、删、查。
- DiskLruCache
DiskLruCache 用于实现设备缓存,它将缓存写入文件系统实现缓存。和 Lrucache 还有一点不同就是 LruCache 从 Android 3.1 过后已经属于 Android 源码的一部分,但是 DiskLruCache 却是 JakeWharton 大神的作品。先附上 DiskLruCache 项目的 GitHub 地址:地址传送门
在使用 DiskLruCache 之前我们需要先导入它的 jar 包,可以导入项目:
compile 'com.jakewharton:disklrucache:2.0.2'
- DiskLruCache 的创建和缓存添加
DiskLruCache 的构造方法是私有的,所以不能通过构造方法来创建:
而是使用它的 open() 方法来创建:
该方法有四个参数:第一个表示缓存路径、第二个表示版本号,一般为设为1,当版本号发生变化 DiskLruCache 会清空之前的缓存、第三个表示单个节点对应的数据个数,一般也设为1、第四个表示缓存的总大小,当缓存大小超过该值时,DiskLruCache 就会清除一些缓存。下面是是创建 DiskLruCache 的简单方法:
private Context context; //定义缓存总大小,如 10M private static final long DISK_CACHE_SIZE = 1024 * 1024 * 10; private DiskLruCache cache; private void creatDiskLruCache() throws IOException { //定义缓存目录 File file = getDiskCacheDir(context,"bitmap"); //如果目录不存在则创建此抽象路径指定目录 if (!file.exists()){ file.mkdirs(); } cache = DiskLruCache.open(file,1,1,DISK_CACHE_SIZE); } //获取应用在 SD 卡上的缓存路径,该目录会随着应用删除而删除,也可以自定义缓存路径 public static File getDiskCacheDir(Context context, String uniqueName) { String cachePath; if (Environment.MEDIA_MOUNTED.equals(Environment .getExternalStorageState()) || !Environment.isExternalStorageRemovable()) { cachePath = context.getExternalCacheDir().getPath(); } else { cachePath = context.getCacheDir().getPath(); } return new File(cachePath + File.separator + uniqueName); }DiskLruCache 的 缓存添加是通过 Editor 来完成的,表示对一个缓存对象的编辑对象。首先获取图片的 rul 所对应的 key,根据 key 来通过 edit() 来获取 Editor 对象。DiskLruCache 重载了 edit() 方法,一般是调用第一个,不过底层其实第一个也是调用了第二个。
edit() 方法使用了 synchronized,不允许同时编辑同一个缓存对象,否则就返回 null。还有因为图片的 url 可能包含特殊字符,影响在 Android 中使用,所以一般采用 url 的 md5 值作为 key。有了 key 之后我们就可以获取 Editor 对象,如果不存在就会返回一个新的 Editor 对象,通过 newOutputStream() 方法来得到一个文件输出流。newOutputStream() 方法需要传入一个参数,表示这个节点的第几个数据,因为一般只储存一个数据,所以可以直接设为0:
private void creatDiskLruCache() throws IOException { ... cache = DiskLruCache.open(file,1,1,DISK_CACHE_SIZE); DiskLruCache.Editor editor = cache.edit(key); if (editor != null){ OutputStream stream = editor.newOutputStream(0); } }
有了输出流,我们需要将它写入文件系统:
public boolean downloadUrlToStream(String urlString,OutputStream stream) throws IOException { HttpURLConnection connection = null; BufferedOutputStream out = null; BufferedInputStream in = null; try { URL url = new URL(urlString); //获取图片url连接 connection = (HttpURLConnection) url.openConnection(); //获取图片的输入流 in = new BufferedInputStream(connection.getInputStream()); out = new BufferedOutputStream(out); int b; //将输入流写入输出流 while ((b=in.read())!=-1){ out.write(b); } return true; } catch (MalformedURLException e) { } finally { //下载完成或者失败都要关闭,避免资源浪费 if (connection!=null){ connection.disconnect(); } out.close(); in.close(); } return false; }
我们还需要调用 Editor 的 commit() 方法来提交写入操作才算完成。如果图片下载发生异常,我们也可以使用 Editor 的 abort() 方法来回退整个操作。
private void creatDiskLruCache() throws IOException { ... OutputStream stream = editor.newOutputStream(0); if (downloadUrlToStream(url,stream)){ editor.commit(); }else { editor.abort(); } cache.flush(); }
- DiskLruCache 的缓存查找
DiskLruCache 也是通过 key 来查找,得到一个 Snapshot 对象,然后获取输入流。
private void getDiskLruCache() throws IOException { DiskLruCache.Snapshot snapshot = cache.get(key); if (snapshot != null){ //这里传入的参数也是节点的第几个数据,所以也直接设置为0 FileInputStream in = (FileInputStream) snapshot.getInputStream(0); Bitmap bitmap = BitmapFactory.decodeStream(in); } }
但是大量加载图片可能会导致 OOM,这里就需要了解一下图片的高效加载,大家可以看一下我上一篇博客:Android 图片的高效加载
但是这里有一个问题,因为 FileInputStream 是一个有序输入流,而高效加载的两次 decode() 方法会影响文件流的位置属性,导致第二次 decode() 方法得到的是 null,我们可以使用 BitmapFactory 的 decodeFileDescriptor() 方法:
private void getDiskLruCache() throws IOException { DiskLruCache.Snapshot snapshot = cache.get(key); if (snapshot != null){ //这里传入的参数也是节点的第几个数据,所以也直接设置为0 FileInputStream in = (FileInputStream) snapshot.getInputStream(0); FileDescriptor descriptor = in.getFD(); Bitmap bitmap = BitmapFactory.decodeFileDescriptor(descriptor); } }
然后就可以使用进行高效加载。
学习自《Android 艺术开发探索》