最近在学习的时候接触到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;
}
经过这一番改造,垃圾回收器确实是可以起作用了,但是通过一些前辈们的研究,谷歌官方还是不推荐我们使用软引用,官方文档是这样说的:
抠脚的英语翻译:
在过去,我们经常会使用一种非常流行的内存缓存技术的实现,即软引用或弱引用 (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设置图片就可以使用这个图片三级缓存。
最后
其实做了这么多的东西,在加载很多的图片的时候还是会出现OOM,通过看Xutils3的源码结构,其实他的图片加载也是运用三级缓存结构,但是他做的事情的远远不至于我上面所说的这么简单,学无止境,所以我们可以先把核心的东西给先弄明白了解,再继续往里深入,菜鸟通过积累,总有一天会成为大鹏。