上次讲了使用内存缓存LruCache去加载很多图片而不造成OOM,而这种缓存的特点是在应用程序运行时管理内存中的资源(图片)的存储和释放,如果LruCache中有一张图片被释放了,再次加载该图片时需要重新从网络上下载下来,这就显得废流量不说,而且费时,网络不好的状况下用户需要等待,而且在没有网络的情况下不会显示任何数据。
那么怎样才能解决这种情况呢?答案就是加入硬盘缓存DiskLruCache。
1、什么是硬盘缓存呢?
顾名思义,就是把从网络上加载的数据存储在本地硬盘上,当再次加载这些数据时候,通过一系列判断本地是否有该数据,就不会从先网络上加载,而是从本地硬盘缓存中拿取数据,这样即使在没有网络情况下,也可以把数据显示出来。举个例子:比如网易新闻app,我们打开客户端后开始浏览新闻,之后发现在手机没有联网的情况下,之前浏览的界面还是能正常的显示出来,这显然就是用到了硬盘缓存DiskLruCache技术,其实硬盘缓存技术在诸多app中运用了,比如一些视频类app、小说类app。。。然而,DiskLruCache并不是Google官方编写的,不过获得了Google的认可,我们要使用它需要在Google官网上去下载这个类:android.googlesource.com/platform/libcore/+/jb-mr2-release/luni/src/main/java/libcore/io/DiskLruCache.java。
2、硬盘缓存中缓存的数据存储在哪里呢?
/** * @param cacheDirName - 缓存的最终目录文件夹名称 * @return - 获取硬盘缓存的目录 */ private File getDiskLruCacheDir(Context context, String cacheDirName) { String cachePath; if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) || !Environment.isExternalStorageRemovable()) { cachePath = context.getExternalCacheDir().getPath(); } else { cachePath = context.getCacheDir().getPath(); } File file = new File(cachePath + "/" + cacheDirName); if (!file.exists()) { file.mkdirs(); } return file; }
3、硬盘缓存中缓存的数据格式是什么样子的呢?
1、怎么创建DiskLruCache对象呢?
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
需要传入四个参数,意思分别为:
- directory - 我们设置的缓存目录,最好用刚刚我在第二点上写的那个方法得到的缓存目录。
- appVersion - 应用程序的版本号,可以这样得到:
PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0); int versionID = info.versionCode;
- valueCount - 就是一个key可以对应几个缓存文件,一般传1。
- maxSize - 就是缓存空间的大小,传入的是字节,当缓存空间存储的缓存内容超过该大小后,DiskLruCache会自动清除一些缓存文件,来腾出空间进行缓存。一般我们可以根据需要缓存的内容来定,大多情况下可以设为10*1024*1024,也就是10M。
DiskLruCache mBitmapDiskLruCache = null; File file = getDiskLruCacheDir(this, "bitmap"); int versionID = getAppVersionNum(this); try { mBitmapDiskLruCache = DiskLruCache.open(file, versionID, 1, 10 * 1024 * 1024); } catch (IOException e) { e.printStackTrace(); }
getDiskLruCacheDir():
private File getDiskLruCacheDir(Context context, String cacheDirName) { String cachePath; if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) || !Environment.isExternalStorageRemovable()) { cachePath = context.getExternalCacheDir().getPath(); } else { cachePath = context.getCacheDir().getPath(); } File file = new File(cachePath + "/" + cacheDirName); if (!file.exists()) { file.mkdirs(); } return file; }
getAppVersionNum():
private int getAppVersionNum(Context context) { try { PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0); return info.versionCode; } catch (PackageManager.NameNotFoundException e) { e.printStackTrace(); } return 1; }
好了,DiskLruCache对象就创建好了,接下来就是根据我们创建的DiskLruCache对象来对进行数据的一系列操作了。
2、将数据写入硬盘缓存中
将数据写入硬盘缓存中,我们需要用到的一个方法就是edit()方法:
public Editor edit(String key)
可以看到我们需要传入一个key值,这就是缓存文件的名称了,这个key值一般情况下我们会采用md5(url)的形式,就是把数据(图片等)对应的url进行md5进行编码加密后即可。
public class String2MD5Tools { /** * @param key * @return * 对key进行MD5加密并返回加密过的散列值 */ public static String hashKeyForDisk(String key) { String cacheKey; try { final MessageDigest mDigest = MessageDigest.getInstance("MD5"); mDigest.update(key.getBytes()); cacheKey = bytesToHexString(mDigest.digest()); } catch (NoSuchAlgorithmException e) { cacheKey = String.valueOf(key.hashCode()); } return cacheKey; } private static 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(); } }
然后通过调用hashKeyForDisk就可以对url进行编码了:
String key = String2MD5Tools.hashKeyForDisk(url);
好了,回到edit()方法上,该方法返回的是一个DiskLruCache.Editor类型的对象,Editor是DiskLruCache的一个成员内部类:
DiskLruCache.Editor editor = mBitmapDiskLruCache.edit(String2MD5Tools.hashKeyForDisk(url)); if (editor != null) { OutputStream outputStream = editor.newOutputStream(0);//得到输出流,往里面写数据 bitmap = downloadBitmap(url, outputStream);//把刚刚下载的图片存入硬盘缓存中,对应的key值为md5(url) if (bitmap != null) { editor.commit();//提交表示写入缓存成功 }else{ editor.abort();//表示放弃此次写入 } }
其中downloadBitmap()方法就是一个普通的从网络上下载图片的方法:
/** * 从网络上下载图片,下载时候把下载的图片写入OutputStream中 * @param urlStr * @return */ private Bitmap downloadBitmap(String urlStr, OutputStream outputStream) { HttpURLConnection connection = null; BufferedOutputStream bufferedOutputStream; Bitmap bitmap = null; try { URL url = new URL(urlStr); connection = (HttpURLConnection) url.openConnection(); connection.setConnectTimeout(5000); connection.setReadTimeout(5000); connection.setDoInput(true); connection.setDoOutput(true); connection.connect(); if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) { InputStream mInputStream = connection.getInputStream(); bitmap = BitmapFactory.decodeStream(mInputStream); bufferedOutputStream = new BufferedOutputStream(outputStream); int len=-1; byte[] by = new byte[1024]; while ((len = mInputStream.read(by)) != -1) { bufferedOutputStream.write(by, 0, len); } bufferedOutputStream.flush(); bufferedOutputStream.close(); mInputStream.close(); } } catch (MalformedURLException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { if (connection != null) { connection.disconnect(); } } return bitmap; }
当然,上述写入数据到缓存的过程需要放到另外一个线程中去执行,原因肯定不用我多说,因为有请求网络的操作。
3、从硬盘缓存中读取数据
public synchronized Snapshot get(String key)
可知它需要传入一个key值,这里我想都知道怎么传入这个key值吧,就是我们通过md5(url)编码后的key值,该方法是根据我们传入的key值来查找对应了缓存文件名称,如果找到了key和缓存文件名一样那就返回一个Snapshot类型的对象,如果没找到就返回null。
DiskLruCache.Snapshot snapshot = mBitmapDiskLruCache.get(String2MD5Tools.hashKeyForDisk(url)); if (snapshot != null) {//如果硬盘缓存中存在该缓存文件 InputStream inputStream = snapshot.getInputStream(0);//获得该缓存文件的输入流 Bitmap bitmap = BitmapFactory.decodeStream(inputStream);//把输入流解析成Bitmap if (bitmap != null) { mImageView.setImageBitmap(bitmap); }else{ //硬盘缓存中不存在图片缓存则开启一个线程去下载图片 //do sthing }
4、从硬盘缓存中删除指定key值的缓存文件
public synchronized boolean remove(String key)
代码很简单,传入指定的key即可删除对应名称的缓存文件:
boolean flag = mBitmapDiskCache.remove(String2MD5Tools.hashKeyForDisk(url)); if(flag){ //删除成功 }else{ //删除失败 }
5、获取当前缓存目录下所使用的内存大小和清空缓存数据
获取缓存目录下总缓存文件的大小是通过size()方法返回,返回的是long类型的字节数,我们可以这样用:
long totalCacheSize = mBitmapDiskLruCache.size();
像很多应用都会在设置界面下显示该app的缓存大小,其实就是通过它得到的,然后我们可以手动清除它,清除全部的缓存数据是使用:
mBitmapDiskLruCache.delete();
来清空缓存数据。
6、同步缓存文件的操作记录至journal和关闭缓存
我们在上面讲了写入缓存数据、读取缓存数据、移除缓存数据等,那么上面的这些方法是通过实际操作来达到效果的,而记录同步缓存文件的操作信息则由journal文件来完成的,DiskLruCache能够正常的工作完全是依赖journal文件,所以在上述操作做完后同步操作记录尤为重要,同步操作记录是通过:
mBitmapDiskLruCache.flush();
完成的,通常我们会在OnPause()方法中调用它,表示操作全部结束后进行同步记录。
mBitmapDiskLruCache.close();
通常我们会在onDestroy()方法中调用它,那么完整的代码为:
@Override protected void onPause() { super.onPause(); try { if (mBitmapDiskLruCache != null) { mBitmapDiskLruCache.flush(); } } catch (IOException e) { e.printStackTrace(); } } @Override protected void onDestroy() { super.onDestroy(); try { if (mBitmapDiskLruCache != null) { mBitmapDiskLruCache.close(); } } catch (IOException e) { e.printStackTrace(); } }
好了,DiskLruCache的原理就讲完了!接下来给个demo,该demo是同时缓存了图片和文本,然后在断网情况下打开数据正常显示,我们来看看效果:
好了,贴下主要代码:
public class MainActivity extends ActionBarActivity { private ImageView mImageView; private TextView mTextView; private String url = "http://img.my.csdn.net/uploads/201507/21/1437459521_5133.jpg"; private DiskLruCache mBitmapDiskLruCache ; private DiskLruCache mTextDiskLruCache; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mImageView = (ImageView) findViewById(R.id.imageView); mTextView = (TextView) findViewById(R.id.textView); initBitmapDiskLruCache(); initTextDiskLruCache(); loadBitmap(url, mBitmapDiskLruCache,mTextDiskLruCache); } //初始化硬盘缓存,得到缓存对象 public void initBitmapDiskLruCache() { File file = getDiskLruCacheDir(this, "bitmap"); int versionID = getAppVersionNum(this); try { mBitmapDiskLruCache = DiskLruCache.open(file, versionID, 1, 10 * 1024 * 1024); } catch (IOException e) { e.printStackTrace(); } } public void initTextDiskLruCache() { File file = getDiskLruCacheDir(this, "text"); int versionID = getAppVersionNum(this); try { mTextDiskLruCache = DiskLruCache.open(file, versionID, 1, 10 * 1024 * 1024); } catch (IOException e) { e.printStackTrace(); } } /** * 加载图片和文本数据,先判断硬盘缓存中有木有,有则从硬盘缓存中取出设置,没有则开启线程下载数据 * @param url * @param mBitmapDiskLruCache * @param mTextDiskLruCache */ public void loadBitmap(String url, DiskLruCache mBitmapDiskLruCache,DiskLruCache mTextDiskLruCache) { try { //从硬盘缓存中加载图片 DiskLruCache.Snapshot snapshot = mBitmapDiskLruCache.get(String2MD5Tools.hashKeyForDisk(url)); if (snapshot != null) {//如果硬盘缓存中存在该缓存文件 InputStream inputStream = snapshot.getInputStream(0); Bitmap bitmap = BitmapFactory.decodeStream(inputStream); if (bitmap != null) { mImageView.setImageBitmap(bitmap); } }else{ //硬盘缓存中不存在图片缓存则开启一个线程去下载图片 DownloadTask task = new DownloadTask(mImageView, mTextView,mBitmapDiskLruCache,mTextDiskLruCache); task.execute(url); } DiskLruCache.Snapshot snapshot1 = mTextDiskLruCache.get(String2MD5Tools.hashKeyForDisk(url)); if(snapshot1!=null){ InputStream inputStream = snapshot1.getInputStream(0); if(inputStream!=null){ int len=-1; byte[] by = new byte[1024]; StringBuilder builder = new StringBuilder(); while ((len=inputStream.read(by))!=-1){ builder.append(new String(by,0,len)); } mTextView.setText(builder.toString()); } }else{ //硬盘缓存中不存在文本缓存则开启一个线程去下载 DownloadTask task = new DownloadTask(mImageView, mTextView,mBitmapDiskLruCache,mTextDiskLruCache); task.execute(url); } } catch (IOException e) { e.printStackTrace(); } } /** * @param context * @return - 得到应用的版本号 */ private int getAppVersionNum(Context context) { try { PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0); return info.versionCode; } catch (PackageManager.NameNotFoundException e) { e.printStackTrace(); } return 1; } /** * @param cacheDirName - 缓存的最终目录文件夹名称 * @return - 获取硬盘缓存的目录 */ private File getDiskLruCacheDir(Context context, String cacheDirName) { String cachePath; if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) || !Environment.isExternalStorageRemovable()) { cachePath = context.getExternalCacheDir().getPath(); } else { cachePath = context.getCacheDir().getPath(); } File file = new File(cachePath + "/" + cacheDirName); if (!file.exists()) { file.mkdirs(); } return file; } @Override protected void onPause() { super.onPause(); try { if (mBitmapDiskLruCache != null) { mBitmapDiskLruCache.flush(); } if(mTextDiskLruCache!=null){ mTextDiskLruCache.flush(); } } catch (IOException e) { e.printStackTrace(); } } @Override protected void onDestroy() { super.onDestroy(); try { if (mBitmapDiskLruCache != null) { mBitmapDiskLruCache.close(); } if(mTextDiskLruCache!=null){ mTextDiskLruCache.close(); } } catch (IOException e) { e.printStackTrace(); } } }
异步下载类:
public class DownloadTask extends AsyncTask<String, Void, Bitmap> { private ImageView imageView; private TextView mTextView; private String url; private DiskLruCache mBitmapDiskLruCache; private DiskLruCache mTextDiskLruCache; public DownloadTask(ImageView imageView,TextView mTextView, DiskLruCache mBitmapDiskLruCache,DiskLruCache mTextDiskLruCache) { this.imageView = imageView; this.mTextView = mTextView; this.mBitmapDiskLruCache = mBitmapDiskLruCache; this.mTextDiskLruCache = mTextDiskLruCache; } @Override protected Bitmap doInBackground(String... params) { url = params[0]; Bitmap bitmap=null; boolean flag = false; try { //将下载的图片写入硬盘缓存中,bitmap目录下 DiskLruCache.Editor editor = mBitmapDiskLruCache.edit(String2MD5Tools.hashKeyForDisk(url)); if (editor != null) { OutputStream outputStream = editor.newOutputStream(0); flag = downloadBitmap(url,outputStream); if(flag){ editor.commit();//提交表示写入缓存成功 }else{ editor.abort();//表示放弃此次写入 } } //从硬盘缓存中获取图片 DiskLruCache.Snapshot snapshot = mBitmapDiskLruCache.get(String2MD5Tools.hashKeyForDisk(url)); if(snapshot!=null){ InputStream inputStream = snapshot.getInputStream(0); if(inputStream!=null){ bitmap = BitmapFactory.decodeStream(inputStream);// } } //将图片的文本数据写入硬盘缓存,text目录下 DiskLruCache.Editor editor1 = mTextDiskLruCache.edit(String2MD5Tools.hashKeyForDisk(url)); if(editor1!=null){ OutputStream outputStream = editor1.newOutputStream(0); if(bitmap!=null){ outputStream.write(("height=" + bitmap.getHeight() + ",width=" + bitmap.getWidth()).toString().getBytes()); editor1.commit(); }else{ editor1.abort(); } } } catch (IOException e) { e.printStackTrace(); } return bitmap; } @Override protected void onPostExecute(Bitmap bitmap) { super.onPostExecute(bitmap); if(imageView!=null&&bitmap!=null){ imageView.setImageBitmap(bitmap); } if(mTextView!=null&&bitmap!=null){ mTextView.setText(("height="+bitmap.getHeight()+",width="+bitmap.getWidth()).toString()); } } /** * 从网络上下载图片 * @param urlStr * @return */ private boolean downloadBitmap(String urlStr,OutputStream outputStream) { HttpURLConnection connection = null; Bitmap bitmap = null; BufferedOutputStream bos; try { URL url = new URL(urlStr); connection = (HttpURLConnection) url.openConnection(); connection.setConnectTimeout(5000); connection.setReadTimeout(5000); connection.setDoInput(true); connection.setDoOutput(true); connection.connect(); if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) { InputStream mInputStream = connection.getInputStream(); bos = new BufferedOutputStream(outputStream); int len=-1; byte[] by = new byte[1024]; while((len=mInputStream.read(by))!=-1){ bos.write(by,0,len); bos.flush(); } mInputStream.close(); return true; } } catch (MalformedURLException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { if (connection != null) { connection.disconnect(); } } return false; } }