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

 

你可能感兴趣的:(android)