Bitmap之内存缓存和磁盘缓存详解
原文首发于微信公众号:躬行之(jzman-blog)
Android 中缓存的使用比较普遍,使用相应的缓存策略可以减少流量的消耗,也可以在一定程度上提高应用的性能,如加载网络图片的情况,不应该每次都从网络上加载图片,应该将其缓存到内存和磁盘中,下次直接从内存或磁盘中获取,缓存策略一般使用 LRU(Least Recently Used) 算法,即最近最少使用算法,下面将从内存缓存和磁盘缓存两个方面以图片为例 介绍 Android 中如何使用缓存,阅读本文之前,请先阅读上篇文章:
内存缓存
LruCache 是 Android 3.1 提供的一个缓存类,通过该类可以快速访问缓存的 Bitmap 对象,内部采用一个 LinkedHashMap 以强引用的方式存储需要缓存的 Bitmap 对象,当缓存超过指定的大小之前释放最近很少使用的对象所占用的内存。
注意:Android 3.1 之前,一个常用的内存缓存是一个 SoftReference 或 WeakReference 的位图缓存,现在已经不推荐使用了。Android 3.1 之后,垃圾回收器更加注重回收 SoftWeakference/WeakReference,这使得使用该种方式实现缓存很大程度上无效,使用 support-v4 兼容包中的 LruCache 可以兼容 Android 3.1 之前的版本。
LruCache 的使用
- 初始化 LruCache
首先计算需要的缓存大小,具体如下:
//第一种方式:
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();
}
};
- 添加 Bitmap 对象到 LruCache 缓存中
//参数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 ,则从网络上获取图片并添加到缓存中,使用过程中一旦退出应用,系统将会释放内存,关键方法如下:
//获取图片
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);
}
然后在相应的事件里调用上述获取图片、移出缓存的方法,具体如下:
@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 的构造方法是私有的,故不能用来创建 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 缓存的添加
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 中添加要缓存的东西咯,要加载一个网络图片到缓存中,显然就是的通过下载的方式将要缓存的东西写入文件系统中,那么就需要一个输出流往里面写东西,主要有两种处理方式:
- 创建 OutputStream 写入要缓存的数据,通过 DiskLruCache 的 edit(String key) 方法获得 Editor 对象,然后通过 OutputStream 转换为 Birmap,将该 Bitmap 写入由 Editor 对象创建的 OutputStream 中,最后调用 Editor 对象的 commit 方法提交;
- 先获得 Editor 对象,根据 Editor 对象创建出 OutputStream 直接写入要缓存的数据,最后调用 Editor 对象的 commit 方法提交;
这里以第一种方式为例,将根据 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 缓存的获取
在 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,一起交流学习。
Bitmap之内存缓存和磁盘缓存详解的更多相关文章
- Android内存优化之磁盘缓存
前言: 在上一篇文章中介绍了内存缓存,内存缓存的优点就是很快,但是它又有缺点: 空间小,内存缓存不可能很大: 内存紧张时可能被清除: 在应用退出时就会消失,做不到离线: 基于以上的缺点有时候又需要另外 ...
- 分布式缓存Memcached/memcached/memcache详解及区别
先来解释下标题中的三种写法:首字母大写的Memcached,指的是Memcached服务器,就是独立运行Memcached的后台服务器,用于存储缓存数据的“容器”.memcached和memcache ...
- PHP输出缓存ob系列函数详解
PHP输出缓存ob系列函数详解 ob,输出缓冲区,是output buffering的简称,而不是output cache.ob用对了,是能对速度有一定的帮助,但是盲目的加上ob函数,只会增加CPU额 ...
- [转载]windows任务管理器中的工作设置内存,内存专用工作集,提交大小详解
windows任务管理器中的工作设置内存,内存专用工作集,提交大小详解 http://shashanzhao.com/archives/832.html 虽然是中文字,但是理解起来还是很困难,什么叫工 ...
- c++中内存拷贝函数(C++ memcpy)详解
原型:void*memcpy(void*dest, const void*src,unsigned int count); 功能:由src所指内存区域复制count个字节到dest所指内存区域. 说明 ...
- PHP缓存机制Output Control详解
开启OB缓存的方式有如下两种: 1. php.ini中开启 output_buffering = 4096 启用了此指令,那么每个PHP脚本都相当于一开始就调用了ob_start()函数,PHP5.5 ...
- PHP APC缓存配置、使用详解
一.APC缓存简介 APC,全称是Alternative PHP Cache,官方翻译叫”可选PHP缓存”.它为我们提供了缓存和优化PHP的中间代码的框架. APC的缓存分两部分:系统缓存和用户数据缓 ...
- Mybatis 一级缓存和二级缓存原理区别 (图文详解)
Java面试经常问到Mybatis一级缓存和二级缓存,今天就给大家重点详解Mybatis一级缓存和二级缓存原理与区别@mikechen Mybatis缓存 缓存就是内存中的数据,常常来自对数据库查询结 ...
- Memcached集群/分布式/高可用 及 Magent缓存代理搭建过程 详解
当网站访问量达到一定时,如何做Memcached集群,又如何高可用,是接下来要讨论的问题. 有这么一段文字来描述“Memcached集群” Memcached如何处理容错的? 不处理!:) 在memc ...
随机推荐
- 查看chrome插件源码
简介 想查看chrome插件的源码,就需要找到chrome插件安装的位置,接着再文件夹下查找此插件的id. mac cd ~/Library/Application Support/Google/Ch ...
- C语言程序设计(八) 数组
第八章 数组 //L8-1 #include <stdio.h> int main() { int score1, score2, score3, score4, score5; int ...
- pyppeteer使用时常见的bug及基本使用(转)
pyppeteer使用时常见的bug及解决办法: https://blog.csdn.net/Mr__lqy/article/details/102626025 pyppeteer的基本使用: htt ...
- 洛谷P1000超级马里奥的神奇解法
话说上过洛谷的都知道,有一道经典例题P1000超级马里奥,这一题,可以说是非常简单非常经典,但是就算如此,还是可以人才辈出,我是个比较循规蹈矩的人(雾),所以我的代码就比较平常,也就是直接输出了所要求 ...
- js中字符串 stringObject 的 replace() 方法
一.定义 replace() 方法用于在字符串中用一些字符替换另一些字符,或替换一个与正则表达式匹配的字符串. 二.语法 stringObject.replace(regexp/substr,repl ...
- html5特性简要概括
1.html5主要的设计目的: 互联网语义化,以便更好地被人类和机器阅读 更好的在移动设备上支持web应用 https://www.w3.org/TR/html5 新增内容: 新的语义标签 <h ...
- java-字节流练习(新手)
参考手册: 关键字: FileInputStream() Input是从硬盘到内存 FileOutputStream() 而output是从内存到硬盘,所以实现了复制粘贴. read() 调用方法读取 ...
- 解决Requires: libc.so.6(GLIBC_2.14)(64bit)错误解决方法
glibc简介: glibc是GNU发布的libc库,即c运行库.glibc是linux系统中最底层的api,几乎其它任何运行库都会依赖于glibc.glibc除了封装linux操作系统所提供的系统服 ...
- Go语言之Go语言变量
GO 语言变量 Go语言是静态类型语言,因此变量(variable)是有明确类型的,编译器也会检查变量类型的正确性. 标识符 在编程语言中标识符就是程序员定义的具有特殊意义的词,比如变量名.常量名.函 ...
- MySQL基础知识_1
平时只会使用简单的增删改查,促使我学习这个来源于一道面试题,左思右想,依然想不出来,所以决定系统的学习一下. MySQL创建数据库 CREATE DATABASE <数据库名>; CREA ...