你的位置:首页 > 操作系统

[操作系统]Android图片缓存分析(一)

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     @Override167     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 [email protected] FilterInputStream} with the specified input     * stream as source.     * <p/>     * <p><strong>Warning:</strong> passing a null source creates an invalid     * [email protected] 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

源码:

https://github.com/taothreeyears/ImageCache