图片缓存LruCache和DiskLruCache的使用
2017-03-27 16:40
423 查看
前言
作为一个Android技术小白,感觉自己各方面知识与技能都存在欠缺、不足,知识体系不够系统。是时候整理一下学习知识和技术点,慢慢梳理,架构体系,如此才能有利于自身技术的更好发展。Android属于开源操作系统,若要快速开发App,最好是学会很好的使用各种优秀的开源框架,一方面完成工作项目,同行也是提升自己代码能力。今天就先从内存缓存的优秀框架DiskLruCache和LruCache开始学习,毕竟Android新手开发都会经历一个OOM的处理问题,一个优秀的App开发中必须考虑资源优化的问题。闲言少续,言归正传。
这里声明学习了郭霖大神的几篇文章LruCache、DiskLruCache以及两者结合使用的Demo解析。
在此膜拜一下~~。
LruCache
Android开发中我们都知道,大量的加载图片会导致内存泄露,也就是OOM,通常我们都会根据原始图片尺寸、设备屏幕尺寸、分辨率以及显示图片的view控件的大小,来使用图片压缩方式,减少资源占用。然而尽管如此,大量的图片资源消耗的内存也是相当惊人的。不论是listView还是GridView中,我们都会做定期的资源缓存和无用资源清理。
若是从网络段加载图片,我们就要考虑到用户流量的问题,流量也是资源!除了使用图片压缩,就需要结合使用图片内存缓存技术了。
/** * 解析图片,生成Bitmap对象。 */ public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) { // 第一次解析将inJustDecodeBounds设置为true,来获取图片大小 final BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeResource(res, resId, options); // 调用下面定义的方法计算inSampleSize值 options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); // 使用获取到的inSampleSize值再次解析图片 options.inJustDecodeBounds = false; return BitmapFactory.decodeResource(res, resId, options); } /** * 计算图片缩放比例 */ public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) { // 源图片的高度和宽度 final int height = options.outHeight; final int width = options.outWidth; int inSampleSize = 1; if (height > reqHeight || width > reqWidth) { // 计算出实际宽高和目标宽高的比率 final int heightRatio = Math.round((float) height / (float) reqHeight); final int widthRatio = Math.round((float) width / (float) reqWidth); // 选择宽和高中最小的比率作为inSampleSize的值,这样可以保证最终图片的宽和高 // 一定都会大于等于目标的宽和高。 inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio; } return inSampleSize; }
注:此处找到一个关于图片压缩的比较不错的博客一叶飘舟–Bitmap压缩。
LruCache是Google官方提供的内存缓存类,其内部源码详解请移步上面大牛的博客。此处仅作简单的使用说明:
private LruCache<String, Bitmap> mMemoryCache; //LruCache对象 @Override protected void onCreate(Bundle savedInstanceState) { // 获取到可用内存的最大值,使用内存超出这个值会引起OutOfMemory异常。 // LruCache通过构造函数传入缓存值,以KB为单位。 int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); // 使用最大可用内存值的1/8作为缓存的大小。 int cacheSize = maxMemory / 8; mMemoryCache = new LruCache<String, Bitmap>(cacheSize) { @Override protected int sizeOf(String key, Bitmap bitmap) { // 重写此方法来衡量每张图片的大小,默认返回图片数量。 return bitmap.getByteCount() / 1024; } }; } //首次使用,将图片资源添加到内存中 public void addBitmapToMemoryCache(String key, Bitmap bitmap) { if (getBitmapFromMemCache(key) == null) { mMemoryCache.put(key, bitmap); } } //从内存缓存获取图片资源 public Bitmap getBitmapFromMemCache(String key) { return mMemoryCache.get(key); } //加载图片时候,会看看有没有缓存,没有的话,在重新构建,有则使用缓存 public void loadBitmap(int resId, ImageView imageView) { final String imageKey = String.valueOf(resId); final Bitmap bitmap = getBitmapFromMemCache(imageKey); if (bitmap != null) { imageView.setImageBitmap(bitmap); } else { imageView.setImageResource(R.drawable.image_placeholder); BitmapWorkerTask task = new BitmapWorkerTask(imageView); task.execute(resId); } } //后台Task加载图片资源 class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> { // 在后台加载图片。 @Override protected Bitmap doInBackground(Integer... params) { final Bitmap bitmap = decodeSampledBitmapFromResource( getResources(), params[0], 100, 100); addBitmapToMemoryCache(String.valueOf(params[0]), bitmap); return bitmap; } }
在此说明一下,以前认为的使用什么软引用、弱引用来保存资源引用的方式,不再提倡,而且也不能保证优化效果了,因为高版本的Android中Java回收机制,会偏向于回收这些引用,从而并不能很好的起到缓存的作用。
DiskLruCache
DiskLruCache类似于LruCache,其是将资源缓存在外部存储磁盘上,而不是内存,这样就可以有相对更为充足的资源空间,缓存更多的数据。具体的原理介绍,同样烦请移步上面大神的博客。此处仅作简要使用说明:先贴出DiskLruCache中使用到的工具类文件
//DiskLruCache中使用到的工具方法 public class Utils { /** * 获取缓存文件夹,这里优先选择SD卡下面的android/data/packageName/cache/路径,若没有SD卡,就选择data/data/packageName/cache * * @param context 上下文环境 * @param uniqueName 缓存文件夹名称 * @return 返回缓存文件 */ 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); } /** * 获取本App的版本号 * * @param context context上下文 * @return 返回版本号 */ public static int getAppVersion(Context context) { try { PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0); return info.versionCode; } catch (PackageManager.NameNotFoundException e) { e.printStackTrace(); } return 1; } /** * 给字符串来个md5加密, * @param key 需要加密的string * @return 返回加密后的string ,或者加密失败,就返回string的哈希值 */ public static String hashKeyForDisk(String key) { String cacheKey; try { //md5加密 MessageDigest mDigest = MessageDigest.getInstance("MD5"); mDigest.update(key.getBytes()); cacheKey = bytesToHexString(mDigest.digest()); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); //若md5加密失败,就用哈希值 cacheKey = String.valueOf(key.hashCode()); } return cacheKey; } /** * 字节数组转为十六进制字符串 * @param bytes 字节数组 * @return 返回十六进制字符串 */ private static String bytesToHexString(byte[] bytes) { StringBuilder sb = new StringBuilder(); for (byte b : bytes) { String hex = Integer.toHexString(0xFF & b); if (hex.length()==1){ sb.append('0'); } sb.append(hex); } return sb.toString(); } }
DiskLruCache的使用
public class MainActivity extends AppCompatActivity { DiskLruCache mDiskLruCache = null;//diskLruCache的对象 String imgUrl = "http://img.my.csdn.net/uploads/201309/01/1378037235_7476.jpg";//图片链接 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //初始化缓存配置 openCache(); } /** * 初始化缓存配置 */ private void openCache() { try { //缓存图片数据的文件夹 File cacheDir = Utils.getDiskCacheDir(this, "bitmap"); if (!cacheDir.exists()) { //使用mkdirs可以连同上级文件夹一同创建,否则mkdir可能会报错 cacheDir.mkdirs(); } //参数,1、缓存目录;2、app版本号,因为它认为版本升级,缓存就没必要保存。3、一个key值对应多少个缓存文件,一般1个。4、单个缓存多大,10M就够了。 //超过最大缓存限制的,就会被自动清除了,所以一般不用程序中调用removeCache。 mDiskLruCache = DiskLruCache.open(cacheDir, Utils.getAppVersion(this), 1, 10 * 1024 * 1024); } catch (IOException e) { e.printStackTrace(); } } /** * 写入缓存 */ public void writeCache(View view) { new Thread(new Runnable() { @Override public void run() { //将图片的url地址md5 加密后,生成的key来作为缓存的唯一标志key,那么就可以实现图片和缓存对应起来。 String key = Utils.hashKeyForDisk(imgUrl); try { DiskLruCache.Editor editor = mDiskLruCache.edit(key); if (editor != null) { //此处传入0参数的含义是,缓存的编号,因为DiskLruCache.open时候,传入了最大缓存个数为1,所以次数就是0就好。 OutputStream outputStream = editor.newOutputStream(0); //根据现在成功与否,来决定是否提交缓存 if (downloadImage(imgUrl, outputStream)) { editor.commit(); } else { editor.abort(); } } //刷新,写入 mDiskLruCache.flush(); } catch (IOException e) { e.printStackTrace(); } } }).start(); } /** * 下载图片 * * @param imgUrl 图片网址链接 * @param outputStream 输出流对象 * @return 返回时候完成下载成功 */ private boolean downloadImage(String imgUrl, OutputStream outputStream) { HttpURLConnection urlConnection = null; BufferedOutputStream out = null; BufferedInputStream in = null; try { URL url = new URL(imgUrl); urlConnection = (HttpURLConnection) url.openConnection(); in = new BufferedInputStream(urlConnection.getInputStream(), 8 * 1024);//Buffer输入流,8M大小的缓存 out = new BufferedOutputStream(outputStream, 8 * 1024); int b;//正在读取的byte while ((b = in.read()) != -1) { out.write(b); } return true; } catch (MalformedURLException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } //关闭资源 finally { if (urlConnection != null) { urlConnection.disconnect(); } if (out != null) { try { out.close(); } catch (IOException e) { e.printStackTrace(); } } if (in != null) { try { in.close(); } catch (IOException e) { e.printStackTrace(); } } } return false; } /** * 读取缓存 * * @param view */ public void readCache(View view) { //使用DiskLruCache获取缓存,需要传入key,而key是imageUrl加密后的字符串, String key = Utils.hashKeyForDisk(imgUrl); try { //通过key获取的只是一个快照,需要从快照获取输入流,转化为数据对象 DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key); if (snapshot != null) { InputStream inputStream = snapshot.getInputStream(0);//类似写缓存时候,传入的是缓存的编号 //可以使用bitmapFactory Drawable drawable = Drawable.createFromStream(inputStream, "drawable"); ImageView imageView = (ImageView) findViewById(R.id.iv_cache); imageView.setImageDrawable(drawable); } } catch (IOException e) { e.printStackTrace(); } } /** * 清除缓存 * * @param view */ public void removeCache(View view) { String key = Utils.hashKeyForDisk(imgUrl); try { //清除指定key的缓存 mDiskLruCache.remove(key); } catch (IOException e) { e.printStackTrace(); } } /** * 演示DiskLruCache的其他api */ public void otherAPI() { //缓存目录大小 mDiskLruCache.size(); //将内存中的操作记录,同步到日志文件(journal),一般不要频繁操作,在Activity的onPause中调用于此就好。 try { mDiskLruCache.flush(); } catch (IOException e) { e.printStackTrace(); } //关闭缓存,一般在Activity的onDestroy中调用就好 try { mDiskLruCache.close(); } catch (IOException e) { e.printStackTrace(); } //清空缓存,不同于remove,这是清空全部缓存 try { mDiskLruCache.delete(); } catch (IOException e) { e.printStackTrace(); } /* * journal文件分析 * 前五行基本就是open相关的参数的配置信息 * 第六行开始,DIRTY开头的,表示脏数据记录,每次调用DiskLruCache.edit都会有一个记录, * 调用commit时候,会写入CLEAN记录,而调用abort,则写入REMOVE记录。 * 日志里item还会记录缓存的大小。READ就是调用get时候写入的记录。journal的记录不知无止境的,2000条左右计数,就会重构。 */ }
单独的使用内存缓存或者外部缓存,都未必是是最好的,一个优秀的开发者,必然会考虑到两者的结合使用。上面大神也有提供一个演示用的demo,在其博客中也有简介。
相关文章推荐
- 使用LruCache和DiskLruCache实现内存磁盘二级图片缓存
- Android 异步加载图片,使用LruCache和SD卡或手机缓存,效果非常的流畅
- Android 应用开发 之使用LruCache和DiskLruCache来在内存和SD卡中缓存图片
- Android 异步加载图片,使用LruCache和SD卡或手机缓存,效果非常的流畅
- [转载]Android使用 LruCache 缓存图片
- Android 异步加载图片,使用LruCache和SD卡或手机缓存,效果非常的流畅
- [置顶] 异步加载图片,使用LruCache和SD卡或手机缓存,效果非常的流畅
- Android使用 LruCache 缓存图片
- Android使用 LruCache 缓存图片
- Android使用 LruCache 缓存图片
- Android 异步加载图片,使用LruCache和SD卡或手机缓存,效果非常的流畅
- 异步加载图片,使用LruCache和SD卡或手机缓存,效果非常的流畅
- Android 应用开发 之使用LruCache和DiskLruCache来在内存和SD卡中缓存图片
- Android 异步加载图片,使用LruCache和SD卡或手机缓存,效果非常的流畅
- Android使用 LruCache 缓存图片
- 异步加载图片,使用LruCache和SD卡或手机缓存,效果非常的流畅
- Android使用 LruCache 缓存图片
- Android使用 LruCache 缓存图片
- Android使用 LruCache 缓存图片
- Android使用 LruCache 缓存图片