Android中写应用时,经常会遇到加载图片的事,由于很多图片是网络上下载获取的,当我们进页面时,便会去网络下载图片,一两次可能没啥问题,但如果同一张图片每次都去网络拉取,不仅速度慢,更影响用户体验,同时会浪费用户的流量。
基于此,很多人便想到了图片缓存的方法。
现在比较普遍的图片缓存主要有以下几个步骤:
一、从缓存中获取图片
二、如果缓存中未获取图片,则从存储卡中获取
三、如果存储卡中未获取图片,则从网络中获取
一、从缓存中获取图片
我们知道,Android中分配给每个应用的内存空间是有限的,不能无限使用,所以我们使用缓存存储的图片也是有限的,为了更有效的利用的这有限的存储空间,程序员们便想出了使用硬引用和软引用两种方式存储图片。
首先介绍下硬引用和软引用:
硬引用:表示持有当前对象的引用是强关系的,即使oom了,gc也不会回收改对象。
软引用:如果内存空间足够,垃圾回收器不会回收它,当内存不足时,,就会回收这些对象的内存。
基于以上两种引用特性,在缓存中存储图片时,一般是先将图片存储到硬引用,当硬引用空间不足时,则将最早存储到硬引用的图片存储到软引用空间,对应代码如下:
1 package meizu.imagecachemanager; 2 3 import android.app.ActivityManager; 4 import android.content.Context; 5 import android.graphics.Bitmap; 6 import android.support.v4.util.LruCache; 7 8 import java.lang.ref.SoftReference; 9 import java.util.LinkedHashMap; 10 11 /** 12 * Created by taomaogan on 15-3-30. 13 */ 14 public class ImageMemoryCache { 15 private static final String TAG = "Cache"; 16 17 //软引用缓存容量 18 private static final int SOFT_CACHE_SIZE = 15; 19 //硬引用缓存 20 private static LruCache<String, Bitmap> mLruCache; 21 //软引用缓存 22 private static LinkedHashMap<String, SoftReference> mSoftCache; 23 24 private int mLruCacheSize = -1; 25 26 public ImageMemoryCache(Context context) { 27 this(context, -1); 28 } 29 30 //硬引用缓存可配置 31 public ImageMemoryCache(Context context, int lruCacheSize) { 32 mLruCacheSize = lruCacheSize; 33 if (mLruCacheSize <= 0) { 34 //设置默认硬引用缓存大小 35 int memoryClass = ((ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE)).getMemoryClass(); 36 mLruCacheSize = 1024 * 1024 * memoryClass / 4; 37 } 38 mLruCache = new LruCache<String, Bitmap>(mLruCacheSize) { 39 40 @Override 41 protected int sizeOf(String key, Bitmap value) { 42 if (value != null) { 43 //计算每张图片的像素数量 44 return value.getRowBytes() * value.getHeight(); 45 } 46 return 0; 47 } 48 49 @Override 50 protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) { 51 if (oldValue != null) { 52 //当强引用空间不足时,将图片存入软引用 53 android.util.Log.d(TAG, "I am from SoftReference save!------"); 54 mSoftCache.put(key, new SoftReference(oldValue)); 55 } 56 } 57 }; 58 59 mSoftCache = new LinkedHashMap(SOFT_CACHE_SIZE, 0.75f, true) { 60 @Override 61 protected boolean removeEldestEntry(Entry eldest) { 62 if (size() > SOFT_CACHE_SIZE) { 63 return true; 64 } 65 return false; 66 } 67 }; 68 } 69 70 public Bitmap getBitmapFromCache(String url) { 71 Bitmap bitmap; 72 //硬引用中获取图片 73 synchronized (mLruCache) { 74 bitmap = mLruCache.get(url); 75 if (bitmap != null) { 76 android.util.Log.d(TAG, "I am from LruCache!------"); 77 mLruCache.remove(url); 78 mLruCache.put(url, bitmap); 79 return bitmap; 80 } 81 } 82 //当硬引用中未获取到图片时,从软引用中获取 83 synchronized (mSoftCache) { 84 SoftReference<Bitmap> bitmapReference = mSoftCache.get(url); 85 if (bitmapReference != null) { 86 bitmap = bitmapReference.get(); 87 if (bitmap != null) { 88 mLruCache.put(url, bitmap); 89 mSoftCache.remove(url); 90 android.util.Log.d(TAG, "I am from SoftCache!------"); 91 return bitmap; 92 } else { 93 mSoftCache.remove(url); 94 } 95 } 96 } 97 98 return null; 99 } 100 101 public void addBitmapToCache(String url, Bitmap bitmap) { 102 if (bitmap != null) { 103 synchronized (mLruCache) { 104 android.util.Log.d(TAG, "I am from LruCache save!------"); 105 mLruCache.put(url, bitmap); 106 } 107 } 108 } 109 110 111 }
上面代码getBitmapFromCache中可以看到程序首先去硬引用中寻找图片,当寻找不到时,则去软引用中寻找。
而在构造函数中mLruCache的初始化中可以看到,当硬引用空间不足时,图片会存储到软引用空间。
二、如果缓存中未获取图片,则从存储卡中获取
在缓存中查找不到图片时,我们会考虑从sd卡获取,代码如下:
1 package meizu.imagecachemanager; 2 3 import android.graphics.Bitmap; 4 import android.graphics.BitmapFactory; 5 import android.os.Environment; 6 import android.os.StatFs; 7 import android.widget.Filter; 8 9 import java.io.File; 10 import java.io.FileNotFoundException; 11 import java.io.FileOutputStream; 12 import java.io.IOException; 13 import java.io.OutputStream; 14 import java.lang.reflect.Array; 15 import java.util.Arrays; 16 import java.util.Comparator; 17 18 /** 19 * Created by taomaogan on 15-3-30. 20 */ 21 public class ImageFileCache { 22 private static final String TAG = "Cache"; 23 24 private static final String CACHE_DIR = "imageCache"; 25 private static final String WHOLESALE_CONV = ".cach"; 26 27 private static final int MB = 1024 * 1024; 28 private static final int CACHE_SIZE = 10; 29 private static final int FREE_SD_SPACE_NEEDED_TO_CACHE = 10; 30 31 public ImageFileCache() { 32 removeCache(getDirectory()); 33 } 34 35 public Bitmap getImageFromFile(String url) { 36 String path = getDirectory() + "/" + covertUrlToFileName(url); 37 File file = new File(path); 38 if (file.exists()) { 39 Bitmap bitmap = BitmapFactory.decodeFile(path); 40 if (bitmap == null) { 41 file.delete(); 42 } else { 43 updateFileTime(path); 44 android.util.Log.d(TAG, "I am from FileCache!------"); 45 return bitmap; 46 } 47 } 48 return null; 49 } 50 51 /** 52 * 将图片存储到sd卡 53 * @param url 54 * @param bitmap 55 */ 56 public void saveBitmap(String url, Bitmap bitmap) { 57 if (bitmap == null) { 58 return; 59 } 60 61 //保证存储控空间足够 62 if (FREE_SD_SPACE_NEEDED_TO_CACHE > freeSpaceOnSd()) { 63 return; 64 } 65 66 //文件名 67 String fileName = covertUrlToFileName(url); 68 //sd卡路径 69 String dir = getDirectory(); 70 File dirFile = new File(dir); 71 //判断路径是否存在 72 if (!dirFile.exists()) { 73 dirFile.mkdirs(); 74 } 75 76 File file = new File(dir + "/" + fileName); 77 try { 78 file.createNewFile(); 79 OutputStream outputStream = new FileOutputStream(file); 80 //存储png图片,图片质量为最高,意味着当存储大图时,效率低,有可能oom 81 bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream); 82 android.util.Log.d(TAG, "I am from FileCache save!------"); 83 outputStream.flush(); 84 outputStream.close(); 85 } catch (FileNotFoundException e) { 86 e.printStackTrace(); 87 } catch (IOException e) { 88 e.printStackTrace(); 89 } 90 } 91 92 private boolean removeCache(String dirPath) { 93 File dir = new File(dirPath); 94 File[] files = dir.listFiles(); 95 if (files == null) { 96 return true; 97 } 98 //sd卡是否有读取权限 99 if (!android.os.Environment.getExternalStorageState().equals( 100 Environment.MEDIA_MOUNTED)) { 101 return false; 102 } 103 104 int dirSize = 0; 105 for (int i = 0; i < files.length; i++) { 106 if (files[i].getName().contains(WHOLESALE_CONV)) { 107 dirSize += files[i].length(); 108 } 109 } 110 111 if (dirSize > CACHE_SIZE * MB || FREE_SD_SPACE_NEEDED_TO_CACHE > freeSpaceOnSd()) { 112 int removeFactor = (int) ((0.4 * files.length) + 1); 113 Arrays.sort(files, new FileLastModifSort()); 114 for (int i = 0; i < removeFactor; i++) { 115 if (files[i].getName().contains(WHOLESALE_CONV)) { 116 files[i].delete(); 117 } 118 } 119 } 120 //可用空间比需要的空间少 121 if (freeSpaceOnSd() <= CACHE_SIZE) { 122 return false; 123 } 124 125 return true; 126 } 127 128 public void updateFileTime(String path) { 129 File file = new File(path); 130 long newModifiedTime = System.currentTimeMillis(); 131 file.setLastModified(newModifiedTime); 132 } 133 134 private int freeSpaceOnSd() { 135 StatFs statFs = new StatFs(Environment.getExternalStorageDirectory().getPath()); 136 double sdFreeMB = ((double) statFs.getAvailableBlocksLong() * (double) statFs.getBlockSizeLong()) / MB; 137 return (int) sdFreeMB; 138 } 139 140 private String covertUrlToFileName(String url) { 141 String[] strs = url.split("/"); 142 return strs[strs.length - 1] + WHOLESALE_CONV; 143 } 144 145 private String getDirectory() { 146 String dir = getSDPath() + "/" + CACHE_DIR; 147 return dir; 148 } 149 150 private String getSDPath() { 151 File sdDir = null; 152 boolean sdCardExist = Environment.getExternalStorageState().equals( 153 Environment.MEDIA_MOUNTED); 154 if (sdCardExist) { 155 sdDir = Environment.getExternalStorageDirectory(); 156 } 157 if (sdDir != null) { 158 return sdDir.toString(); 159 } else { 160 return ""; 161 } 162 } 163 164 private class FileLastModifSort implements Comparator<File> { 165 166 @Override 167 public int compare(File lhs, File rhs) { 168 if (lhs.lastModified() > rhs.lastModified()) { 169 return 1; 170 } else if (lhs.lastModified() == rhs.lastModified()) { 171 return 0; 172 } else { 173 return -1; 174 } 175 } 176 } 177 }
上面的代码很简单,saveBitmap是存储图片,存储图片之前,检查一下sd卡权限,sd卡空间大小,并去从上面的注释中我们可以看到,其实本篇文章其实是不适合大图片存储的;getImageFromFile则是从存储卡中读取图片。
如果经过缓存,sd卡还是未获取到图片,最后只能通过网络获取了。
三、如果存储卡中未获取图片,则从网络中获取
网络下载图片,这里仅支持通过http获取,代码如下:
package meizu.imagecachemanager; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.DefaultHttpClient; import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; /** * Created by taomaogan on 15-3-30. */ public class ImageGetFromHttp { private static final String TAG = "Cache"; public static Bitmap downloadBitmap(String url) { final HttpClient httpClient = new DefaultHttpClient(); final HttpGet httpGet = new HttpGet(url); try { HttpResponse httpResponse = httpClient.execute(httpGet); int statusCode = httpResponse.getStatusLine().getStatusCode(); if (statusCode != HttpStatus.SC_OK) { return null; } final HttpEntity httpEntity = httpResponse.getEntity(); if (httpEntity != null) { InputStream inputStream = null; try { inputStream = httpEntity.getContent(); FilterInputStream fileInputStream = new FlushedInputStream(inputStream); android.util.Log.d(TAG, "I am from Http!------"); return BitmapFactory.decodeStream(fileInputStream); } finally { if (inputStream != null) { inputStream.close(); } httpEntity.consumeContent(); } } } catch (IOException e) { e.printStackTrace(); } catch (IllegalStateException e) { e.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } return null; } private static class FlushedInputStream extends FilterInputStream { /** * Constructs a new {@code FilterInputStream} with the specified input * stream as source. * <p/> * <p><strong>Warning:</strong> passing a null source creates an invalid * {@code FilterInputStream}, that fails on every method that is not * overridden. Subclasses should check for null in their constructors. * * @param in the input stream to filter reads on. */ protected FlushedInputStream(InputStream in) { super(in); } @Override public long skip(long byteCount) throws IOException { long totalBytesSkipped = 0l; while (totalBytesSkipped < byteCount) { long bytesSkipped = in.skip(byteCount - totalBytesSkipped); if (bytesSkipped == 0l) { int by = read(); if (by < 0) { break; } else { bytesSkipped = 1; } } totalBytesSkipped += bytesSkipped; } return totalBytesSkipped; } } }
以上便是3种获取图片的流程,汇总如下:
public Bitmap getBitmap(String url) { Bitmap bitmap = mImageMemoryCache.getBitmapFromCache(url); if (bitmap == null) { bitmap = mImageFileCache.getImageFromFile(url); if (bitmap == null) { bitmap = ImageGetFromHttp.downloadBitmap(url); if (bitmap != null) { mImageFileCache.saveBitmap(url, bitmap); mImageMemoryCache.addBitmapToCache(url, bitmap); } } else { mImageMemoryCache.addBitmapToCache(url, bitmap); } } return bitmap; }
以上代码是先从缓存读取图片,然后从文件读取图片,最后从网络下载图片,需要注意的是,从网络下载图片成功后,分别将其存储到sd卡和缓存中,不然以上实现的缓存便无意义了。
以上便是图片缓存的比较简单流程了,可以在demo中使用,为什么是仅仅在demo中使用呢?因为真正的图片下载缓存还需要用到线程池,像以上图片下载每次都需要手动新建线程,还是比较麻烦。
上面图片缓存的优点是:增强了用户体验,节省了用户流量
缺点是:加载大图片时容易产生oom问题
关于以上问题,以后会继续分析。
引用:
http://keegan-lee.diandian.com/post/2012-12-06/40047548955
源码: