Android图片三级缓存策略(网络、本地、内存缓存)
一、简介
现在的android应用程序中,不可避免的都会使用到图片,如果每次加载图片的时候都要从网络重新拉取,这样不但很耗费用户的流量,而且图片加载的也会很慢,用户体验很不好。所以一个应用的图片缓存策略是很重要的。通常情况下,android应用程序中图片的缓存策略采用“内存-本地-网络”三级缓存策略,首先应用程序访问网络拉取图片,分别将加载的图片保存在本地sd卡中和内存中,当程序再一次需要加载图片的时候,先判断内存中是否有缓存,有则直接从内存中拉取,否则查看本地sd卡中是否有缓存,sd卡中如果存在缓存,则图片从sd卡中拉取,否则从网络加载图片。依据这三级缓存机制,可以让我们的应用程序在加载图片的时候做到游刃有余,有效的避免内存溢出。
ps:当然现在处理网络图片的时候,一般人都会选择xutils中的bitmaputil,它已经将网络缓存处理的相当好了,使用起来非常方便--本人就一直在用。仿照bitmaputil的实现思路,定制一个自己的图片加载工具,来理解一下三级缓存的策略,希望对自己会有一个提升。
二、网络缓存
网络拉取图片严格来讲不能称之为缓存,实质上就是下载url对应的图片,我们这里姑且把它看作是缓存的一种。仿照bitmaputil中的display方法,我自己定制的custombitmaputils也定义这个方法,根据传入的url,将图片设置到ivpic控件上。
public void display(imageview ivpic, string url) { }
定义网络缓存的工具类,在访问网络的时候,我使用了asynctask来实现,在asynctask的doinbackground方法里下载图片,然后将 图片设置给ivpic控件,asynctask有三个泛型,其中第一个泛型是执行异步任务的时候,通过execute传过来的参数,第二个泛型是更新的进度,第三个泛型是异步任务执行完成之后,返回来的结果,我们这里返回一个bitmap。具体的下载实现代码如下:
/** * 网络缓存的工具类 * * @author zhy * */ public class netcacheutils { private localcacheutils localcacheutils; private memorycacheutils memorycacheutils; public netcacheutils() { localcacheutils = new localcacheutils(); memorycacheutils = new memorycacheutils(); } /** * 从网络下载图片 * * @param ivpic * @param url */ public void getbitmapfromnet(imageview ivpic, string url) { // 访问网络的操作一定要在子线程中进行,采用异步任务实现 myasynctask task = new myasynctask(); task.execute(ivpic, url); } /** * 第一个泛型--异步任务执行的时候,通过execute传过来的参数; 第二个泛型--更新进度; 第三个泛型--异步任务执行以后返回的结果 * * @author zhy * */ private class myasynctask extends asynctask { private imageview ivpic; private string url; // 耗时任务执行之前 --主线程 @override protected void onpreexecute() { super.onpreexecute(); } // 后台执行的任务 @override protected bitmap doinbackground(object... params) { // 执行异步任务的时候,将url传过来 ivpic = (imageview) params[0]; url = (string) params[1]; bitmap bitmap = downloadbitmap(url); // 为了保证imageview控件和url一一对应,给imageview设定一个标记 ivpic.settag(url); // 关联ivpic和url return bitmap; } // 更新进度 --主线程 @override protected void onprogressupdate(void... values) { super.onprogressupdate(values); } // 耗时任务执行之后--主线程 @override protected void onpostexecute(bitmap result) { string mcurrenturl = (string) ivpic.gettag(); if (url.equals(mcurrenturl)) { ivpic.setimagebitmap(result); system.out.println("从网络获取图片"); // 从网络加载完之后,将图片保存到本地sd卡一份,保存到内存中一份 localcacheutils.setbitmap2local(url, result); // 从网络加载完之后,将图片保存到本地sd卡一份,保存到内存中一份 memorycacheutils.setbitmap2memory(url, result); } } } /** * 下载网络图片 * * @param url * @return */ private bitmap downloadbitmap(string url) { httpurlconnection conn = null; try { url murl = new url(url); // 打开httpurlconnection连接 conn = (httpurlconnection) murl.openconnection(); // 设置参数 conn.setconnecttimeout(5000); conn.setreadtimeout(5000); conn.setrequestmethod("get"); // 开启连接 conn.connect(); // 获得响应码 int code = conn.getresponsecode(); if (code == 200) { // 相应成功,获得网络返回来的输入流 inputstream is = conn.getinputstream(); // 图片的输入流获取成功之后,设置图片的压缩参数,将图片进行压缩 bitmapfactory.options options = new bitmapfactory.options(); options.insamplesize = 2; // 将图片的宽高都压缩为原来的一半,在开发中此参数需要根据图片展示的大小来确定,否则可能展示的不正常 options.inpreferredconfig = bitmap.config.rgb_565; // 这个压缩的最小 // bitmap bitmap = bitmapfactory.decodestream(is); bitmap bitmap = bitmapfactory.decodestream(is, null, options) ;// 经过压缩的图片 return bitmap; } } catch (exception e) { e.printstacktrace(); } finally { // 断开连接 conn.disconnect(); } return null; } }
三、本地缓存
从网络加载完图片之后,将图片保存到本地sd卡中。在加载图片的时候,判断一下sd卡中是否有图片缓存,如果有,就直接从sd卡加载图片。本地缓存的工具类中有两个公共的方法,分别是向本地sd卡设置网络图片,获取sd卡中的图片。设置图片的时候采用键值对的形式进行存储,将图片的url作为键,作为文件的名字,图片的bitmap作位值来保存。由于url含有特殊字符,不能直接作为图片的名字来存储,故采用url的md5值作为文件的名字。
/** * 本地缓存 * * @author zhy * */ public class localcacheutils { /** * 文件保存的路径 */ public static final string file_path = environment .getexternalstoragedirectory().getabsolutepath() + "/cache/pics"; /** * 从本地sd卡获取网络图片,key是url的md5值 * * @param url * @return */ public bitmap getbitmapfromlocal(string url) { try { string filename = md5encoder.encode(url); file file = new file(file_path, filename); if (file.exists()) { bitmap bitmap = bitmapfactory.decodestream(new fileinputstream( file)); return bitmap; } } catch (exception e) { e.printstacktrace(); } return null; } /** * 向本地sd卡写网络图片 * * @param url * @param bitmap */ public void setbitmap2local(string url, bitmap bitmap) { try { // 文件的名字 string filename = md5encoder.encode(url); // 创建文件流,指向该路径,文件名叫做filename file file = new file(file_path, filename); // file其实是图片,它的父级file是文件夹,判断一下文件夹是否存在,如果不存在,创建文件夹 file fileparent = file.getparentfile(); if (!fileparent.exists()) { // 文件夹不存在 fileparent.mkdirs();// 创建文件夹 } // 将图片保存到本地 bitmap.compress(compressformat.jpeg, 100, new fileoutputstream(file)); } catch (exception e) { e.printstacktrace(); } } }
四、内存缓存
内存缓存说白了就是在内存中保存一份图片集合,首先会想到hashmap这种键值对的形式来进行保存,以url作为key,bitmap作为value。但是在java中这种默认的new对象的方式是强引用,jvm在进行垃圾回收的时候是不会回收强引用的,所以如果加载的图片过多的话,map会越来越大,很容易出现oom异常。在android2.3之前,还可以通过软引用或者弱引用来解决,但是android2.3之后,google官方便不再推荐软引用了,google推荐我们使用lrucache。
在过去,我们经常会使用一种非常流行的内存缓存技术的实现,即软引用或弱引用 (softreference or weakreference)。但是现在已经不再推荐使用这种方式了,因为从 android 2.3 (api level 9)开始,垃圾回收器会更倾向于回收持有软引用或弱引用的对象,这让软引用和弱引用变得不再可靠。另外,android 3.0 (api level 11)中,图片的数据会存储在本地的内存当中,因而无法用一种可预见的方式将其释放,这就有潜在的风险造成应用程序的内存溢出并崩溃。
为了能够选择一个合适的缓存大小给lrucache, 有以下多个因素应该放入考虑范围内,例如:
你的设备可以为每个应用程序分配多大的内存?android默认是16m。 设备屏幕上一次最多能显示多少张图片?有多少图片需要进行预加载,因为有可能很快也会显示在屏幕上? 你的设备的屏幕大小和分辨率分别是多少?一个超高分辨率的设备(例如 galaxy nexus) 比起一个较低分辨率的设备(例如 nexus s),在持有相同数量图片的时候,需要更大的缓存空间。 图片的尺寸和大小,还有每张图片会占据多少内存空间。 图片被访问的频率有多高?会不会有一些图片的访问频率比其它图片要高?如果有的话,你也许应该让一些图片常驻在内存当中,或者使用多个lrucache 对象来区分不同组的图片。 你能维持好数量和质量之间的平衡吗?有些时候,存储多个低像素的图片,而在后台去开线程加载高像素的图片会更加的有效。 以上是google对lrucache的描述,其实lrucache的使用非常简单,跟map非常相近,只是在创建lrucache对象的时候需要指定它的最大允许内存,一般设置为当前应用程序的最大运行内存的八分之一即可。
/** * 内存缓存 * * @author zhy * */ public class memorycacheutils { /* * 由于map默认是强引用,所有在jvm进行垃圾回收的时候不会回收map的引用 */ // private hashmap<string, bitmap=""> map = new hashmap<string, bitmap="">(); // 软引用的实例,在内存不够时,垃圾回收器会优先考虑回收 // private hashmap<string, bitmap="">> msoftreferencemap = new // hashmap<string, bitmap="">>(); // lrucache private lrucache<string, bitmap=""> lrucache; public memorycacheutils() { // lrucache最大允许内存一般为android系统分给每个应用程序内存大小(默认android系统给每个应用程序分配16兆内存)的八分之一(推荐) // 获得当前应用程序运行的内存大小 long mcurrentmemory = runtime.getruntime().maxmemory(); int maxsize = (int) (mcurrentmemory / 8); // 给lrucache设置最大的内存 lrucache = new lrucache<string, bitmap="">(maxsize) { @override protected int sizeof(string key, bitmap value) { // 获取每张图片所占内存的大小 // 计算方法是:图片显示的宽度的像素点乘以高度的像素点 int bytecount = value.getrowbytes() * value.getheight();// 获取图片占用内存大小 return bytecount; } }; } /** * 从内存中读取bitmap * * @param url * @return */ public bitmap getbitmapfrommemory(string url) { // bitmap bitmap = map.get(url); // softreference<bitmap> softreference = msoftreferencemap.get(url); // bitmap bitmap = softreference.get(); // 软引用在android2.3以后就不推荐使用了,google推荐使用lrucache // lru--least recently use // 最近最少使用,将内存控制在一定的大小内,超过这个内存大小,就会优先释放最近最少使用的那些东东 bitmap bitmap = lrucache.get(url); return bitmap; } /** * 将图片保存到内存中 * * @param url * @param bitmap */ public void setbitmap2memory(string url, bitmap bitmap) { // 向内存中设置,key,value的形式,首先想到hashmap // map.put(url, bitmap); // 保存软引用到map中 // softreference<bitmap> msoftreference = new // softreference<bitmap>(bitmap); // msoftreferencemap.put(url, msoftreference); lrucache.put(url, bitmap); } }</bitmap></bitmap></bitmap></string,></string,></string,></string,></string,></string,>
好了。现在三级缓存策略封装完毕,接下来定制我们自己的bitmaputils
/** * 自定义的加载图片的工具类,类似于xutils中的bitmaputil,在实际使用中,一般使用bitmaputil,为了理解三级缓存, * 这里模拟bitmaputil自定义了custombitmaputil * * @author zhy * */ public class custombitmaputils { private bitmap bitmap; private netcacheutils netcacheutils; private localcacheutils localcacheutils; private memorycacheutils memorycacheutils; public custombitmaputils() { netcacheutils = new netcacheutils(); localcacheutils = new localcacheutils(); memorycacheutils = new memorycacheutils(); } /** * 加载图片,将当前url对应的图片显示到ivpic的控件上 * * @param ivpic * imageview控件 * @param url * 图片的地址 */ public void display(imageview ivpic, string url) { // 设置默认显示的图片 ivpic.setimageresource(r.drawable.ic_launcher); // 1、内存缓存 bitmap = memorycacheutils.getbitmapfrommemory(url); if (bitmap != null) { ivpic.setimagebitmap(bitmap); system.out.println("从内存缓存中加载图片"); return; } // 2、本地磁盘缓存 bitmap = localcacheutils.getbitmapfromlocal(url); if (bitmap != null) { ivpic.setimagebitmap(bitmap); system.out.println("从本地sd卡加载的图片"); memorycacheutils.setbitmap2memory(url, bitmap);// 将图片保存到内存 return; } // 3、网络缓存 netcacheutils.getbitmapfromnet(ivpic, url); /* * 从网络获取图片之后,将图片保存到手机sd卡中,在进行图片展示的时候,优先从sd卡中读取缓存,key是图片的url的md5值, * value是保存的图片bitmap */ } }
在mainactivity中使用listview加载网络图片
/** * android中三级缓存--网络缓存-本地缓存-内存缓存 * * @author zhy * */ public class mainactivity extends activity { private listview list; private button btn; private custombitmaputils utils; private static final string base_url = "http://192.168.0.148:8080/pics"; // 初始化一些网络图片 string[] urls = { base_url + "/1.jpg", base_url + "/2.jpg", base_url + "/3.jpg", base_url + "/4.jpg", base_url + "/5.jpg", base_url + "/6.jpg", base_url + "/7.jpg", base_url + "/8.jpg", base_url + "/9.jpg", base_url + "/10.jpg", base_url + "/11.jpg", base_url + "/12.jpg", base_url + "/13.jpg", base_url + "/14.jpg", base_url + "/15.jpg", base_url + "/16.jpg", base_url + "/17.jpg", base_url + "/18.jpg", base_url + "/19.jpg", base_url + "/20.jpg", base_url + "/21.jpg", base_url + "/22.jpg", base_url + "/23.jpg", base_url + "/24.jpg", base_url + "/25.jpg", base_url + "/26.jpg", base_url + "/27.jpg", base_url + "/28.jpg", base_url + "/29.jpg", base_url + "/30.jpg" }; @override protected void oncreate(bundle savedinstancestate) { super.oncreate(savedinstancestate); setcontentview(r.layout.activity_main); list = (listview) findviewbyid(r.id.list); btn = (button) findviewbyid(r.id.btn_load); utils = new custombitmaputils(); // 加载网络图片 btn.setonclicklistener(new onclicklistener() { @override public void onclick(view v) { myadapter adapter = new myadapter(); list.setadapter(adapter); } }); } class myadapter extends baseadapter { @override public int getcount() { return urls.length; } @override public string getitem(int position) { return urls[position]; } @override public long getitemid(int position) { return position; } @override public view getview(int position, view convertview, viewgroup parent) { viewholder holder; if (convertview == null) { convertview = view.inflate(mainactivity.this, r.layout.item_list, null); holder = new viewholder(); holder.ivpic = (imageview) convertview.findviewbyid(r.id.iv); convertview.settag(holder); } else { holder = (viewholder) convertview.gettag(); } utils.display(holder.ivpic, urls[position]); return convertview; } class viewholder { imageview ivpic; } } }
运行的结果如下:
程序第一次运行,日志打印如下
之后将图片缓存在sd卡中,从本地加载图片
然后将图片缓存到内存,从内存加载图片
ok,到目前为止,android中图片的三级缓存原理就都介绍完了,我自己本人受益匪浅,希望能够帮助到需要的朋友。需要源码的请点击如下链接进行下载。