Android 学习笔记之图片三级缓存

最近在学习的时候接触到Android对图片的获取途径,写一篇笔记记录一发。

什么是图片三级缓存

三级缓存指的是内存缓存,本地缓存和网络缓存,而App获取图片资源可以通过这三种途径来获取,也就是指图片的三级缓存。

为什么要使用图片三级缓存

如果一个App有很多图片,而每次获取图片都要通过网络来获取,则这个应用一定会很消耗流量,所有这时候有必要将获取的图片做缓存保存在本地或者内存中。而他们获取的优先级是首先从内存获取,其次是从本地获取,前面都获取不到图片才通过网络从服务器中获取图片。从内存获取图片的速度是最快的,如果是加载很多图片,有可能会导致内存溢出(OOM).

如何做图片三级缓存

可以从最外层做起,首先从网络获取图片

  • 网络获图片存工具类(NetCacheUtils),核心是使用Android提供的异步任务获取数据类AsyncTask来加载网络图片,他的底层其实就是对线程池和Handler的封装,所有其中的方法才有运行在主线程和子线程之分。
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.widget.ImageView;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import javax.net.ssl.HttpsURLConnection;
/** 
* Created by 毛麒添 on 2017/1/14 0014.
 * 图片三级缓存之网络缓存 
* 从网络中获取图片 
* 使用异步任务来获取图片 
*/
public class NetCacheUtils {   
 private ImageView imageView;    
private LocalCacheUtils localCacheUtils;   
 private MemoryCacheUtils memoryCacheUtils;    
private String url;    
public NetCacheUtils(LocalCacheUtils localCacheUtils, MemoryCacheUtils memoryCacheUtils) {
        this.localCacheUtils=localCacheUtils;        
        this.memoryCacheUtils=memoryCacheUtils;   
 }    

public void getBitmapFromNet(ImageView imageView, String url) {    
 //开启异步任务      
 new BitmapTack().execute(imageView,url);
    
}    
class BitmapTack extends AsyncTask<Object,Integer,Bitmap>{       
 //预备加载,运行在主线程        
@Override       
 protected void onPreExecute() 
{           
 super.onPreExecute();       
 }        
@Override        
protected Bitmap doInBackground(Object... params) {            
//获取外部需要设置的ImageView对象           
 imageView = (ImageView) params[0];            
url = (String) params[1];            //开始下载图片          
  //imageView.setTag(url);
给图片设置唯一标记,让其在设置bitmap时候判断url是否相同来进行设置,防止图片不一致           
 Bitmap bitmap=downLoadBitmap(url);           
 return bitmap;        }        
   
//执行完成,运行在主线程        
@Override        
protected void onPostExecute(Bitmap bitmap) 
{            
if(bitmap!=null){               
 imageView.setImageBitmap(bitmap);                
//设置本地缓存               
 localCacheUtils.setLocalBitmapCache(bitmap,url);               
 //设置内存缓存                
memoryCacheUtils.setMemoryCache(url,bitmap);           
 }           
 super.onPostExecute(bitmap);       
 }   
 }    
/**     
* 下载图片bitmap对象    
 * @param url 下载地址     
* @return 请求成功返回获取图片的bitmap对象,否则返回空     
*/    
private Bitmap downLoadBitmap(String url) {        
HttpsURLConnection conn=null;       
 try {                
conn= (HttpsURLConnection) new URL(url).openConnection();                
conn.setConnectTimeout(5000);//连接延时                
conn.setReadTimeout(5000);//读取延时                
int code = conn.getResponseCode();               
 if(code==200){                    
InputStream inputStream = conn.getInputStream();                   
 //根据网络获取的输入流生成Bitmap对象                   
 Bitmap bitmap = BitmapFactory.decodeStream(inputStream);                   
 return bitmap;            }       
 } catch (IOException e) 
{            e.printStackTrace();       
 }finally {           
 if(conn!=null){               
 conn.disconnect();           
     }        
   }       
 return  null;  
  }
}
  • 从内存中或图片缓存(LocalCacheUtils),要获取图片缓存,首先工具类中必定要有设置缓存的方法,然后获取网络数据到图片的Bitmap对象后就将其保存起来,在使用的时候调用获取的本地缓存的方法就可以的得到本地缓存的图片,而将图片保存一般是使用url地址作为图片的文件名称,所有为了服务器安全,有必要对url地址做MD5加密之后再作为缓存图片文件名称。
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Environment;
import java.io.File;import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
/** 
* Created by 毛麒添 on 2017/1/14 0014. 
* 图片三级缓存之本地缓存 
* 设置和获取本地图片缓存的方法(缓存放在Sdcard中) 
*/
public class LocalCacheUtils {     
//存放本地缓存的地址     
private static final  String CACHE_PATH=Environment.getExternalStorageDirectory().getAbsolutePath()+"bitmap_cache";     
//设置本地图片缓存   
 public void setLocalBitmapCache(Bitmap bitmap, String url)
{        
File dir=new File(CACHE_PATH);       
 if(!dir.exists()||!dir.isDirectory()){//如果不存在或者不是一个文件夹           
//创建文件夹            
dir.mkdirs();        
}        
String filename = Md5Util.encoder(url);//给文件名称使用MD5加密       
 //创建缓存文件       
 File bitmapcachefile=new File(dir,filename);        
try {            
//图片压缩格式,压缩比例            
bitmap.compress(Bitmap.CompressFormat.JPEG,100,new FileOutputStream(bitmapcachefile));        
} catch (FileNotFoundException e) {            
e.printStackTrace();        
}    
}   
 //获取本地图片缓存    
public Bitmap getLocalBitmapCache(String url){       
 //根据图片路径和名称获取图片       
 File bitmapcachefile=new File(CACHE_PATH,Md5Util.encoder(url));       
 if(bitmapcachefile.exists()){           
 try {               
 Bitmap bitmap = BitmapFactory.decodeStream(new FileInputStream(bitmapcachefile));               
 return bitmap;            
} catch (FileNotFoundException e) { 
       e.printStackTrace();           
 }        
}        
return null;    
}}
  • MD5加密工具类
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class Md5Util {   
/**    
* 给指定字符串按照md5算法去加密   
 * @param psd 需要加密的字符串    
* @return  返回字符串     
*/  
 public static String encoder(String psd) {     
 try {        
 //1,指定加密算法类型         
MessageDigest digest = MessageDigest.getInstance("MD5");        
 //2,将需要加密的字符串中转换成byte类型的数组,然后进行随机哈希过程        
 byte[] bs = digest.digest(psd.getBytes());
//       System.out.println(bs.length);         
//3,循环遍历bs,然后让其生成32位字符串,固定写法        
 //4,拼接字符串过程         
StringBuffer stringBuffer = new StringBuffer();        
 for (byte b : bs) {           
 int i = b & 0xff;            
//int类型的i需要转换成16进制字符            
String hexString = Integer.toHexString(i);     i
f(hexString.length()<2){              
 hexString = "0"+hexString;           
 }            
stringBuffer.append(hexString);         
}                 
return stringBuffer.toString();    
  } catch (NoSuchAlgorithmException e) {  
       e.printStackTrace();     
 }      return "";  
 }
}
  • 内存缓存(MemoryCacheUtils)要在内存中存放东西,而要存放东西,能想到的就是ArrayList和HashMap,当程序跑起来,他们运行在内存当中。而ArrayList是可以存放数据,但是要从其中取数据还需只要数据的角标,不好取出;而HashMap中key和values的结果存放数据,则正好我们只要传入图片的URL地址和图片的Bitmap对象,当从内存中获取图片缓存的时候只要用图片的key获取就可以了,好,开搞。
public class MemoryCacheUtils {
//使用hashmap 存储图片
private HashMap<String,Bitmap> myMemoryCache=new HashMap<String,Bitmap>();

//设置内存缓存
public void setMemoryCache(String url,Bitmap bitmap){

myMemoryCache.put(url,bitmap);
}

//获取内存缓存public Bitmap getMemoryCache(String url){
 return myMemoryCache.get(url);
}
  • 这样一搞,好像是没有什么问题,而且代码少,感觉内存缓存是三级缓存中最简单的。其实不然,这样的方式在内存中存放数据,当图片加载很多的时候,程序就崩掉了,错误日志中显示内存溢出(OOM)。为什么会出现这种情况的,在大多数情况下,Android 系统给每个应用分配的内存为16M,如果图片加载太多超过系统分配的最大内存当然会造成内存溢出,而这时候java本身自带的垃圾回收器不起作用,那该如何解决呢?我们可以从java中对对象的引用来分析,java中对象的引用是放在栈中,指向的引用对象是放在堆中,而引用在默认的情况下是强引用,而我们使用的HashMap来存放数据就是默认的强引用,所有本身的垃圾回收器当然不会回收释放内存中图片的缓存。这时候java除了有强引用,也还提供了其他的引用方式:
  • 软引用(SoftReference):垃圾回收器会考虑回收
  • 弱引用(WeakReference):垃圾回收器优先考虑回收
  • 虚引用(PhantomReference):垃圾回收器最先考虑回收
    我们既要想在内存中缓存图片又想垃圾回收器能够起到回收作用,就可以选择软引用对Bitmap经常一次封装
public class MemoryCacheUtils 
{
private HashMap<String,SoftReference<Bitmap>> myMemoryCache=new HashMap<String,SoftReference<Bitmap>>();
//设置内存缓存
public void setMemoryCache(String url,Bitmap bitmap)
{
  SoftReference<Bitmap> bitmapSoftReference=new SoftReference<Bitmap>(bitmap);
    myMemoryCache.put(url,bitmapSoftReference);
}
/获取内存缓存
public Bitmap getMemoryCache(String url){ 
SoftReference<Bitmap> bitmapSoftReference=myMemoryCache.get(url);
if(bitmapSoftReference!=null){
Bitmap bitmap = bitmapSoftReference.get();    
  return bitmap;
}
return null;
}

经过这一番改造,垃圾回收器确实是可以起作用了,但是通过一些前辈们的研究,谷歌官方还是不推荐我们使用软引用,官方文档是这样说的:

官方文档截图.png

抠脚的英语翻译:
在过去,我们经常会使用一种非常流行的内存缓存技术的实现,即软引用或弱引用 (SoftReference or WeakReference)。但是现在已经不再推荐使用这种方式了,因为从 Android 2.3 (API Level 9)开始,垃圾回收器会更倾向于回收持有软引用或弱引用的对象,这让软引用和弱引用变得不再可靠。另外,Android 3.0 (API Level 11)中,图片的数据会存储在本地的内存当中,因而无法用一种可预见的方式将其释放,这就有潜在的风险造成应用程序的内存溢出并崩溃。
官方文档链接地址:
http://developer.android.com/training/displaying-bitmaps/cache-bitmap.html

而在文档中则是推荐我们使用LruCache(使用v4包中的)这个类来做内存缓存,其实他的本质也是对HashMap的封装。好吧,继续改造

import android.graphics.Bitmap;
import android.support.v4.util.LruCache;

public class MemoryCacheUtils 
{
private LruCache<String,Bitmap> myMemoryCache;

public MemoryCacheUtils(){    
long maxMemory = Runtime.getRuntime().maxMemory();//获取分配给每个App的内存大小    
myMemoryCache=new LruCache<String,Bitmap>((int) (maxMemory/8)){        
//返回每个对象的大小        
@Override        
protected int sizeOf(String key, Bitmap value) {           
 int byteCount = value.getByteCount();           
 return byteCount;        
       }    
   };
}
//设置内存缓存
public void setMemoryCache(String url,Bitmap bitmap)
{
  myMemoryCache.put(url,bitmap);
}
/获取内存缓存
public Bitmap getMemoryCache(String url){ 
   return myMemoryCache.get(url);
}
  • 到此图片三级缓存的三个层次基本上算是完成了,然在获取图片的工具方法中调用即可
import android.graphics.Bitmap;
import android.widget.ImageView;
/** 
* Created by 毛麒添 on 2017/1/14 0014. 
* 自己创建三级缓存加载图片 
*/
public class BitmapUtils {    
//网络获取图片工具类对象    
private NetCacheUtils netCacheUtils;   
private MemoryCacheUtils memoryCacheUtils;    
private LocalCacheUtils localCacheUtils;    
private Bitmap bitmap;    

public BitmapUtils(){       
 localCacheUtils=new LocalCacheUtils();        
memoryCacheUtils=new MemoryCacheUtils();       
 netCacheUtils=new NetCacheUtils(localCacheUtils,memoryCacheUtils);    
}    
/**     
* 显示图片的方法     
* @param imageView 需要设置图片的图片对象    
* @param url 请求地址    
* 优先从内存中加载图片    
* 其次从本地中(Sdcard)加载图片     
* 最后从网络中获取图片     
*/    
public void displayBitmapImage(ImageView imageView, String url) {       
 //从缓存缓存中获取图片        
bitmap = memoryCacheUtils.getMemoryCache(url);       
 if(bitmap!=null){
//如果内存中图片缓存不为空,直接将其设置给ImageView           
 imageView.setImageBitmap(bitmap);           
 return;        
}        
//从本地缓存中获取图片       
bitmap = localCacheUtils.getLocalBitmapCache(url);        
if(bitmap !=null){//如果本地图片缓存不为空,直接将其设置给ImageView            
imageView.setImageBitmap(bitmap);           
 return;        
}       
 //从网络获取图片        
netCacheUtils.getBitmapFromNet(imageView,url);   
 }
}

终于,图片三级缓存的小框架已经搭建好,我们使用ListView或者ViewPager的控件加载图片的时候在适配器getView()方法中给ImageView设置图片就可以使用这个图片三级缓存。

最后

xutils3 源码结构.png

其实做了这么多的东西,在加载很多的图片的时候还是会出现OOM,通过看Xutils3的源码结构,其实他的图片加载也是运用三级缓存结构,但是他做的事情的远远不至于我上面所说的这么简单,学无止境,所以我们可以先把核心的东西给先弄明白了解,再继续往里深入,菜鸟通过积累,总有一天会成为大鹏。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容