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         @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

源码:

https://github.com/taothreeyears/ImageCache

原文地址:https://www.cnblogs.com/tyrion/p/4379478.html