您的位置:首页 > 移动开发 > Android开发

Android中图片缓存的一些总结

2015-08-24 17:26 309 查看
最近在做图片加载时遇到内存溢出问题,所以这里找到一些资料学习后,在这里做一个总结。

这里使用带了LruCache技术和DiskLruCache技术,简单地说,LruCache是做的内存缓存处理,只负责内存中图片的管理,也就是在内存中缓存被清除后还是需要重新从网络去加载,这就是导致效果很差,一个很直观的就是消耗流量,再一个就是网络加载就会很慢,大量图片时就会导致很卡顿不流畅,体验也不好。所以为了解决上面的问题还需要DiskLruCache做本地缓存。

Bitmap生成

在学习上缓存技术前,我先学习了一下BitmapFcatroy这个类的用法。BitmapFactory这个类提供了多个解析方法用于创建Bitmap对象,对不同的图片来源使用不一样的方法,具体如下:

SD卡图片选择decodeFile(),BitmapFactory.decodeFileDescriptor()方法

网络上的图片选择decodeStream()方法

资源文件选择使用decodeResource()方法

在默认情况下,使用上面的方法得到bitmap时会默认给bitmap分配内存,如果,图片过大就会导致内存溢出,所以,每一种解析方法都提供了一个可选的BitmapFactory.Options参数,将这个参数的inJustDecodeBounds属性设置为true就可以让解析方法禁止为bitmap分配内存,返回值也不再是一个Bitmap对象,而是null。虽然Bitmap是null了,但是BitmapFactory.Options的outWidth、outHeight和outMimeType属性都会被赋值。这个技巧让我们可以在加载图片之前就获取到图片的长宽值和MIME类型,从而根据情况对图片进行压缩。

BitmapFactory的具体用法如下demo:

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;
}


LruCache缓存

接下来就要用一下LruCache在内存中做缓存了,这个其实用法很简单,用到的也比较多,在使用Volley加载图片时也需要一个自己实现的缓存,也就是一个LruCache,一个简单的demo如下:

private LruCache<String, Bitmap> mMemoryCache;

@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);
}


在这个例子当中,使用了系统分配给应用程序的八分之一内存来作为缓存大小。在中高配置的手机当中,这大概会有4兆(32/8)的缓存空间。一个全屏幕的 GridView 使用4张 800x480分辨率的图片来填充,则大概会占用1.5兆的空间(800*480*4)。因此,这个缓存大小可以存储2.5页的图片。

当向 ImageView 中加载一张图片时,首先会在 LruCache 的缓存中进行检查。如果找到了相应的键值,则会立刻更新ImageView ,否则开启一个后台线程来加载这张图片。

DiskLruCache

那么接下来就是DiskLruCache,这是google官方认证的。使用是非常广泛的,我们看新闻时总是会发现有些看过的图片在我们断网后还可以打开查看,有些新闻网页也是可以在断网后查看的,他们都是做了本地缓存处理了。通常情况下多数应用程序都会将缓存的位置选择为 /sdcard/Android/data/<应用程序包名>/cache 这个路径。选择在这个位置有两点好处:第一,这是存储在SD卡上的,因此即使缓存再多的数据也不会对手机的内置存储空间有任何影响,只要SD卡空间足够就行。第二,这个路径被Android系统认定为应用程序的缓存路径,当程序被卸载的时候,这里的数据也会一起被清除掉,这样就不会出现删除程序之后手机上还有很多残留数据的问题。在使用了DiskLruCache缓存是可以在手机上查看的,里面有一些命名没有规则的文件,但是有一个文件可以看懂叫journal的文件,这个相当于一个日志文件,记录缓存文件的命名和大小还能用于统计缓存的大小。

关于DiskLruCache的使用

打开缓存

DiskLruCache是不能new出实例的,如果我们要创建一个DiskLruCache的实例,则需要调用它的open()方法,接口如下所示

public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)


open()方法接收四个参数,第一个参数指定的是数据的缓存地址,第二个参数指定当前应用程序的版本号,第三个参数指定同一个key可以对应多少个缓存文件,基本都是传1,第四个参数指定最多可以缓存多少字节的数据。

其中缓存地址前面已经说过了,通常都会存放在 /sdcard/Android/data/<应用程序包名>/cache 这个路径下面,但同时我们又需要考虑如果这个手机没有SD卡,或者SD正好被移除了的情况,因此专门写一个方法来获取缓存地址,如下所示:

/**
* 获取DiskLruCache的缓存文件夹 注意第二个参数dataType
* DiskLruCache用一个String类型的唯一值对不同类型的数据进行区分.
* 比如bitmap,object等文件夹.其中在bitmap文件夹中缓存了图片. card/Android/data/<application
* package>/cache 如果 缓存数据的存放位置为: /sdSD卡不存在时缓存存放位置为: /data/data/<application
* package>/cache
*
*/
@SuppressLint("NewApi")
public static File getDiskLruCacheDir(Context context, String dataType) {
String dirPath;
File cacheFile = null;
if (Environment.MEDIA_MOUNTED.equals(Environment
.getExternalStorageState())
|| !Environment.isExternalStorageRemovable()) {
dirPath = context.getExternalCacheDir().getPath();
}else{
dirPath = context.getCacheDir().getPath();
}
cacheFile = new File(dirPath + File.separator + dataType);
return cacheFile;
}

/**
* 获取APP当前版本号
*
* @param context
* @return
*/
public static int getAppVersionCode(Context context) {
int versionCode = 1;
try {
String packageName = context.getPackageName();
PackageManager packageManager = context.getPackageManager();
PackageInfo packageInfo = packageManager.getPackageInfo(
packageName, 0);
versionCode = packageInfo.versionCode;
} catch (NameNotFoundException e) {
e.printStackTrace();
}
return versionCode;
}

/**
* 将字符串用MD5编码. 比如在该示例中将url进行MD5编码
*/
public static String getStringByMD5(String string) {
String md5String = null;
try {
// Create MD5 Hash
MessageDigest messageDigest = MessageDigest.getInstance("MD5");
messageDigest.update(string.getBytes());
byte messageDigestByteArray[] = messageDigest.digest();
if (messageDigestByteArray == null
|| messageDigestByteArray.length == 0) {
return md5String;
}

// Create hexadecimal String
StringBuffer hexadecimalStringBuffer = new StringBuffer();
int length = messageDigestByteArray.length;
for (int i = 0; i < length; i++) {
hexadecimalStringBuffer.append(Integer
.toHexString(0xFF & messageDigestByteArray[i]));
}
md5String = hexadecimalStringBuffer.toString();
return md5String;
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return md5String;
}


读取缓存

缓存已经写入成功之后,接下来我们就该学习一下如何读取了。读取的方法要比写入简单一些,主要是借助DiskLruCache的get()方法实现的,接口如下所示:

public synchronized Snapshot get(String key) throws IOException


很明显,get()方法要求传入一个key来获取到相应的缓存数据,而这个key毫无疑问就是将图片URL进行MD5编码后的值了,也就是getStringByMD5后的值。

获取到的是一个DiskLruCache.Snapshot对象,只需要调用它的getInputStream()方法就可以得到缓存文件的输入流了。同样地,getInputStream()方法也需要传一个index参数,这里传入0就好。有了文件的输入流之后,想要把缓存图片显示到界面上就轻而易举了。所以,一段完整的读取缓存,并将图片加载到界面上的代码如下所示:

try {
String imageUrl = "http://img.my.csdn.net/uploads/201309/01/1378037235_7476.jpg";
String key = getStringByMD5(imageUrl);
DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
if (snapShot != null) {
InputStream is = snapShot.getInputStream(0);
Bitmap bitmap = BitmapFactory.decodeStream(is);
mImage.setImageBitmap(bitmap);
}
} catch (IOException e) {
e.printStackTrace();
}


移出缓存

public synchronized boolean remove(String key) throws IOException


用法和get类似。

这里附上参考的一些文章地址

利用LruCache和DiskLruCache加载网络图片实现图片瀑布流效果

Android DiskLruCache缓存完全解析

Android高效加载大图、多图解决方案,有效避免程序OOM
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: