原文首发于微信公众号:jzman-blog,欢迎关注交流!
Android 中缓存的使用比较广泛,使用相应的缓存策略能够减小流量的消耗,也能够在必定程度上提升应用的性能,如加载网络图片的状况,不该该每次都从网络上加载图片,应该将其缓存到内存和磁盘中,下次直接从内存或磁盘中获取,缓存策略通常使用 LRU(Least Recently Used) 算法,即最近最少使用算法,下面将从内存缓存和磁盘缓存两个方面以图片为例 介绍 Android 中如何使用缓存,阅读本文以前,请先阅读上篇文章:java
LruCache 是 Android 3.1 提供的一个缓存类,经过该类能够快速访问缓存的 Bitmap 对象,内部采用一个 LinkedHashMap 以强引用的方式存储须要缓存的 Bitmap 对象,当缓存超过指定的大小以前释放最近不多使用的对象所占用的内存。android
注意:Android 3.1 以前,一个经常使用的内存缓存是一个 SoftReference 或 WeakReference 的位图缓存,如今已经不推荐使用了。Android 3.1 以后,垃圾回收器更加注重回收 SoftWeakference/WeakReference,这使得使用该种方式实现缓存很大程度上无效,使用 support-v4 兼容包中的 LruCache 能够兼容 Android 3.1 以前的版本。git
首先计算须要的缓存大小,具体以下:github
//第一种方式: ActivityManager manager = (ActivityManager) getSystemService(ACTIVITY_SERVICE); //获取当前硬件条件下应用所占的大体内存大小,单位为M int memorySize = manager.getMemoryClass();//M int cacheSize = memorySize/ 8; //第二种方式(比较经常使用) int memorySize = (int) Runtime.getRuntime().maxMemory();//bytes int cacheSize = memorySize / 8;
而后,初始化 LruCache ,具体以下:算法
//初始化 LruCache 且设置了缓存大小 LruCache<String, Bitmap> lruCache = new LruCache<String, Bitmap>(cacheSize){ @Override protected int sizeOf(String key, Bitmap value) { //计算每个缓存Bitmap的所占内存的大小,内存单位应该和 cacheSize 的单位保持一致 return value.getByteCount(); } };
//参数put(String key,Bitmap bitmap) lruCache.put(key,bitmap)
//参数get(String key) Bitmap bitmap = lruCache.get(key); imageView.setImageBitmap(bitmap);
下面使用 LruCache 加载一张网络图片来演示 LruCache 的简单使用。缓存
建立一个简单的 ImageLoader,里面封装获取缓存 Bitmap 、添加 Bitmap 到缓存中以及从缓存中移出 Bitmap 的方法,具体以下:微信
//ImageLoader public class ImageLoader { private LruCache<String , Bitmap> lruCache; public ImageLoader() { int memorySize = (int) Runtime.getRuntime().maxMemory() / 1024; int cacheSize = memorySize / 8; lruCache = new LruCache<String, Bitmap>(cacheSize){ @Override protected int sizeOf(String key, Bitmap value) { //计算每个缓存Bitmap的所占内存的大小 return value.getByteCount()/1024; } }; } /** * 添加Bitmapd到LruCache中 * @param key * @param bitmap */ public void addBitmapToLruCache(String key, Bitmap bitmap){ if (getBitmapFromLruCache(key)==null){ lruCache.put(key,bitmap); } } /** * 获取缓存的Bitmap * @param key */ public Bitmap getBitmapFromLruCache(String key){ if (key!=null){ return lruCache.get(key); } return null; } /** * 移出缓存 * @param key */ public void removeBitmapFromLruCache(String key){ if (key!=null){ lruCache.remove(key); } } }
而后建立一个线程类用于加载图片,具体以下:网络
//加载图片的线程 public class LoadImageThread extends Thread { private Activity mActivity; private String mImageUrl; private ImageLoader mImageLoader; private ImageView mImageView; public LoadImageThread(Activity activity,ImageLoader imageLoader, ImageView imageView,String imageUrl) { this.mActivity = activity; this.mImageLoader = imageLoader; this.mImageView = imageView; this.mImageUrl = imageUrl; } @Override public void run() { HttpURLConnection connection = null; InputStream is = null; try { URL url = new URL(mImageUrl); connection = (HttpURLConnection) url.openConnection(); is = connection.getInputStream(); if (connection.getResponseCode() == HttpURLConnection.HTTP_OK){ final Bitmap bitmap = BitmapFactory.decodeStream(is); mImageLoader.addBitmapToLruCache("bitmap",bitmap); mActivity.runOnUiThread(new Runnable() { @Override public void run() { mImageView.setImageBitmap(bitmap); } }); } } catch (IOException e) { e.printStackTrace(); } finally { if (connection!=null){ connection.disconnect(); } if (is!=null){ try { is.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
而后,在 MainActivity 中使用 ImageLoader 加载并缓存网络图片到内存中, 先从内存中获取,若是缓存中没有须要的 Bitmap ,则从网络上获取图片并添加到缓存中,使用过程当中一旦退出应用,系统将会释放内存,关键方法以下:app
//获取图片 private void loadImage(){ Bitmap bitmap = imageLoader.getBitmapFromLruCache("bitmap"); if (bitmap==null){ Log.i(TAG,"从网络获取图片"); new LoadImageThread(this,imageLoader,imageView,url).start(); }else{ Log.i(TAG,"从缓存中获取图片"); imageView.setImageBitmap(bitmap); } } // 移出缓存 private void removeBitmapFromL(String key){ imageLoader.removeBitmapFromLruCache(key); }
而后在相应的事件里调用上述获取图片、移出缓存的方法,具体以下:ide
@Override public void onClick(View v) { switch (v.getId()){ case R.id.btnLoadLruCache: loadImage(); break; case R.id.btnRemoveBitmapL: removeBitmapFromL("bitmap"); break; } }
下面来一张日志截图说明执行状况:
磁盘缓存就是指将缓存对象写入文件系统,使用磁盘缓存可有助于在内存缓存不可用时缩短加载时间,从磁盘缓存中获取图片相较从缓存中获取较慢,若是能够应该在后台线程中处理;磁盘缓存使用到一个 DiskLruCache 类来实现磁盘缓存,DiskLruCache 收到了 Google 官方的推荐使用,DiskLruCache 不属于 Android SDK 中的一部分,首先贴一个 DiskLruCache 的源码连接
DiskLruCache 源码地址 。
DiskLruCache 的构造方法是私有的,故不能用来建立 DiskLruCache,它提供一个 open 方法用于建立自身,方法以下:
/** * 返回相应目录中的缓存,若是不存在则建立 * @param directory 缓存目录 * @param appVersion 表示应用的版本号,通常设为1 * @param valueCount 每一个Key所对应的Value的数量,通常设为1 * @param maxSize 缓存大小 * @throws IOException if reading or writing the cache directory fails */ public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize) throws IOException { ... // 建立DiskLruCache DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize); if (cache.journalFile.exists()) { ... return cache; } //若是缓存目录不存在,建立缓存目录以及DiskLruCache directory.mkdirs(); cache = new DiskLruCache(directory, appVersion, valueCount, maxSize); ... return cache; }
注意:缓存目录能够选择 SD 卡上的缓存目录,及 /sdcard/Android/data/应用包名/cache 目录,也能够选择当前应用程序 data 下的缓存目录,固然能够指定其余目录,若是应用卸载后但愿删除缓存文件,就选择 SD 卡上的缓存目录,若是但愿保留数据请选择其余目录,还有一点,若是是内存缓存,退出应用以后缓存将会被清除。
DiskLruCache 缓存的添加是经过 Editor 完成的,Editor 表示一个缓存对象的编辑对象,能够经过其 edit(String key) 方法来获取对应的 Editor 对象,若是 Editor 正在使用 edit(String key) 方法将会返回 null,即 DiskLruCache 不容许同时操做同一个缓存对象。固然缓存的添加都是经过惟一的 key 来进行添加操做的,那么什么做为 key 比较方便吗,以图片为例,通常讲 url 的 MD5 值做为 key ,计算方式以下:
//计算url的MD5值做为key private String hashKeyForDisk(String url) { String cacheKey; try { final MessageDigest mDigest = MessageDigest.getInstance("MD5"); mDigest.update(url.getBytes()); cacheKey = bytesToHexString(mDigest.digest()); } catch (NoSuchAlgorithmException e) { cacheKey = String.valueOf(url.hashCode()); } return cacheKey; } private String bytesToHexString(byte[] bytes) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < bytes.length; i++) { String hex = Integer.toHexString(0xFF & bytes[i]); if (hex.length() == 1) { sb.append('0'); } sb.append(hex); } return sb.toString(); }
经过 url 的 MD5 的值获取到 key 以后,就能够经过 DiskLruCache 对象的 edit(String key) 方法获取 Editor 对象,而后经过 Editor 对象的 commit 方法,大概意思就是释放 Editir 对象,以后就能够经过 key 进行其余操做咯。
固然,获取到 key 以后就能够向 DiskLruCache 中添加要缓存的东西咯,要加载一个网络图片到缓存中,显然就是的经过下载的方式将要缓存的东西写入文件系统中,那么就须要一个输出流往里面写东西,主要有两种处理方式:
这里以第一种方式为例,将根据 url 将网络图片添加到磁盘缓存中,同时也添加到内存缓存中,具体以下:
//添加网络图片到内存缓存和磁盘缓存 public void putCache(final String url, final CallBack callBack){ Log.i(TAG,"putCache..."); new AsyncTask<String,Void,Bitmap>(){ @Override protected Bitmap doInBackground(String... params) { String key = hashKeyForDisk(params[0]); DiskLruCache.Editor editor = null; Bitmap bitmap = null; try { URL url = new URL(params[0]); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setReadTimeout(1000 * 30); conn.setConnectTimeout(1000 * 30); ByteArrayOutputStream baos = null; if(conn.getResponseCode()==HttpURLConnection.HTTP_OK){ BufferedInputStream bis = new BufferedInputStream(conn.getInputStream()); baos = new ByteArrayOutputStream(); byte[] bytes = new byte[1024]; int len = -1; while((len=bis.read(bytes))!=-1){ baos.write(bytes,0,len); } bis.close(); baos.close(); conn.disconnect(); } if (baos!=null){ bitmap = decodeSampledBitmapFromStream(baos.toByteArray(),300,200); addBitmapToCache(params[0],bitmap);//添加到内存缓存 editor = diskLruCache.edit(key); //关键 bitmap.compress(Bitmap.CompressFormat.JPEG, 100, editor.newOutputStream(0)); editor.commit();//提交 } } catch (IOException e) { try { editor.abort();//放弃写入 } catch (IOException e1) { e1.printStackTrace(); } } return bitmap; } @Override protected void onPostExecute(Bitmap bitmap) { super.onPostExecute(bitmap); callBack.response(bitmap); } }.execute(url); }
在 DiskLruCache 缓存的添加中了解了如何获取 key,获取到 key 以后,经过 DiskLruCache 对象的 get 方法得到 Snapshot 对象,而后根据 Snapshot 对象得到 InputStream,最后经过 InputStream 就能够得到 Bitmap ,固然能够利用 上篇文章 中的对 Bitmap 采样的方式进行适当的调整,也能够在缓存以前先压缩再缓存,获取 InputStream 的方法具体以下:
//获取磁盘缓存 public InputStream getDiskCache(String url) { Log.i(TAG,"getDiskCache..."); String key = hashKeyForDisk(url); try { DiskLruCache.Snapshot snapshot = diskLruCache.get(key); if (snapshot!=null){ return snapshot.getInputStream(0); } } catch (IOException e) { e.printStackTrace(); } return null; }
DiskLruCache 的主要部分大体如上,下面实现一个简单的三级缓存来讲明 LruCache 和 DiskLruCache 的具体使用,MainActivity 代码以下:
//MainActivity.java public class MainActivity extends AppCompatActivity { private static final String TAG = "cache_test"; public static String CACHE_DIR = "diskCache"; //缓存目录 public static int CACHE_SIZE = 1024 * 1024 * 10; //缓存大小 private ImageView imageView; private LruCache<String, String> lruCache; private LruCacheUtils cacheUtils; private String url = "http://img06.tooopen.com/images/20161012/tooopen_sy_181713275376.jpg"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); imageView = (ImageView) findViewById(R.id.imageView); } @Override protected void onResume() { super.onResume(); cacheUtils = LruCacheUtils.getInstance(); //建立内存缓存和磁盘缓存 cacheUtils.createCache(this,CACHE_DIR,CACHE_SIZE); } @Override protected void onPause() { super.onPause(); cacheUtils.flush(); } @Override protected void onStop() { super.onStop(); cacheUtils.close(); } public void loadImage(View view){ load(url,imageView); } public void removeLruCache(View view){ Log.i(TAG, "移出内存缓存..."); cacheUtils.removeLruCache(url); } public void removeDiskLruCache(View view){ Log.i(TAG, "移出磁盘缓存..."); cacheUtils.removeDiskLruCache(url); } private void load(String url, final ImageView imageView){ //从内存中获取图片 Bitmap bitmap = cacheUtils.getBitmapFromCache(url); if (bitmap == null){ //从磁盘中获取图片 InputStream is = cacheUtils.getDiskCache(url); if (is == null){ //从网络上获取图片 cacheUtils.putCache(url, new LruCacheUtils.CallBack<Bitmap>() { @Override public void response(Bitmap bitmap1) { Log.i(TAG, "从网络中获取图片..."); Log.i(TAG, "正在从网络中下载图片..."); imageView.setImageBitmap(bitmap1); Log.i(TAG, "从网络中获取图片成功..."); } }); }else{ Log.i(TAG, "从磁盘中获取图片..."); bitmap = BitmapFactory.decodeStream(is); imageView.setImageBitmap(bitmap); } }else{ Log.i(TAG, "从内存中获取图片..."); imageView.setImageBitmap(bitmap); } } }
布局文件比较简单就不贴代码了,下面是日志运行截图说明执行状况,以下图所示:
这篇文章记录了 LruCache 和 DiskLruCache 的基本使用方式,至少应该对这两个缓存辅助类有了必定的了解,它的具体实现请参考源码。
【文中代码】:传送门
能够关注公众号:jzman-blog,一块儿交流学习。