图片缓存LruCache和DiskLruCache的使用
##前言
作为一个Android技术小白,感觉自己各方面知识与技能都存在欠缺、不足,知识体系不够系统。是时候整理一下学习知识和技术点,慢慢梳理,架构体系,如此才能有利于自身技术的更好发展。Android属于开源操作系统,若要快速开发App,最好是学会很好的使用各种优秀的开源框架,一方面完成工作项目,同行也是提升自己代码能力。
今天就先从内存缓存的优秀框架DiskLruCache和LruCache开始学习,毕竟Android新手开发都会经历一个OOM的处理问题,一个优秀的App开发中必须考虑资源优化的问题。闲言少续,言归正传。
这里声明学习了郭霖大神的几篇文章LruCache、DiskLruCache以及两者结合使用的Demo解析。
在此膜拜一下~~。
##LruCache
Android开发中我们都知道,大量的加载图片会导致内存泄露,也就是OOM,通常我们都会根据原始图片尺寸、设备屏幕尺寸、分辨率以及显示图片的view控件的大小,来使用图片压缩方式,减少资源占用。
然而尽管如此,大量的图片资源消耗的内存也是相当惊人的。不论是listView还是GridView中,我们都会做定期的资源缓存和无用资源清理。
若是从网络段加载图片,我们就要考虑到用户流量的问题,流量也是资源!除了使用图片压缩,就需要结合使用图片内存缓存技术了。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
/**
* 解析图片,生成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官方提供的内存缓存类,其内部源码详解请移步上面大牛的博客。此处仅作简单的使用说明:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
|
private LruCache<String, Bitmap> mMemoryCache; //LruCache对象
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) {
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> {
// 在后台加载图片。
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中使用到的工具类文件
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273//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的使用
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186public class MainActivity extends AppCompatActivity {DiskLruCache mDiskLruCache = null;//diskLruCache的对象String imgUrl = "//img-my.csdn.net/uploads/201309/01/1378037235_7476.jpg";//图片链接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() {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;//正在读取的bytewhile ((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);//类似写缓存时候,传入的是缓存的编号//可以使用bitmapFactoryDrawable 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,在其博客中也有简介。

浙公网安备 33010602011771号