Android技術積累:圖片快取管理

yangxi_001發表於2013-11-28

如果每次載入同一張圖片都要從網路獲取,那代價實在太大了。所以同一張圖片只要從網路獲取一次就夠了,然後在本地快取起來,之後載入同一張圖片時就從快取中載入就可以了。從記憶體快取讀取圖片是最快的,但是因為記憶體容量有限,所以最好再加上檔案快取。檔案快取空間也不是無限大的,容量越大讀取效率越低,因此可以設定一個限定大小比如10M,或者限定儲存時間比如一天。

因此,載入圖片的流程應該是:

1、先從記憶體快取中獲取,取到則返回,取不到則進行下一步;

2、從檔案快取中獲取,取到則返回並更新到記憶體快取,取不到則進行下一步;

3、從網路下載圖片,並更新到記憶體快取和檔案快取。

 

接下來看記憶體快取類:ImageMemoryCache

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
public class ImageMemoryCache {
    /**
     * 從記憶體讀取資料速度是最快的,為了更大限度使用記憶體,這裡使用了兩層快取。
     * 硬引用快取不會輕易被回收,用來儲存常用資料,不常用的轉入軟引用快取。
     */
    private static final int SOFT_CACHE_SIZE = 15//軟引用快取容量
    private static LruCache<String, Bitmap> mLruCache;  //硬引用快取
    private static LinkedHashMap<String, SoftReference<Bitmap>> mSoftCache;  //軟引用快取
                                                                                        
    public ImageMemoryCache(Context context) {
        int memClass = ((ActivityManager)context.getSystemService(Context.ACTIVITY_SERVICE)).getMemoryClass();
        int cacheSize = 1024 * 1024 * memClass / 4//硬引用快取容量,為系統可用記憶體的1/4
        mLruCache = new LruCache<String, Bitmap>(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap value) {
                if (value != null)
                    return value.getRowBytes() * value.getHeight();
                else
                    return 0;
            }
                                                                                        
            @Override
            protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
                if (oldValue != null)
                    // 硬引用快取容量滿的時候,會根據LRU演算法把最近沒有被使用的圖片轉入此軟引用快取
                    mSoftCache.put(key, new SoftReference<Bitmap>(oldValue));
            }
        };
        mSoftCache = new LinkedHashMap<String, SoftReference<Bitmap>>(SOFT_CACHE_SIZE, 0.75f, true) {
            private static final long serialVersionUID = 6040103833179403725L;
            @Override
            protected boolean removeEldestEntry(Entry<String, SoftReference<Bitmap>> eldest) {
                if (size() > SOFT_CACHE_SIZE){    
                    return true;  
                }  
                return false
            }
        };
    }
                                                                                
    /**
     * 從快取中獲取圖片
     */
    public Bitmap getBitmapFromCache(String url) {
        Bitmap bitmap;
        //先從硬引用快取中獲取
        synchronized (mLruCache) {
            bitmap = mLruCache.get(url);
            if (bitmap != null) {
                //如果找到的話,把元素移到LinkedHashMap的最前面,從而保證在LRU演算法中是最後被刪除
                mLruCache.remove(url);
                mLruCache.put(url, bitmap);
                return bitmap;
            }
        }
        //如果硬引用快取中找不到,到軟引用快取中找
        synchronized (mSoftCache) { 
            SoftReference<Bitmap> bitmapReference = mSoftCache.get(url);
            if (bitmapReference != null) {
                bitmap = bitmapReference.get();
                if (bitmap != null) {
                    //將圖片移回硬快取
                    mLruCache.put(url, bitmap);
                    mSoftCache.remove(url);
                    return bitmap;
                } else {
                    mSoftCache.remove(url);
                }
            }
        }
        return null;
    
                                                                                
    /**
     * 新增圖片到快取
     */
    public void addBitmapToCache(String url, Bitmap bitmap) {
        if (bitmap != null) {
            synchronized (mLruCache) {
                mLruCache.put(url, bitmap);
            }
        }
    }
                                                                                
    public void clearCache() {
        mSoftCache.clear();
    }
}

 

檔案快取類:ImageFileCache

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
public class ImageFileCache {
    private static final String CACHDIR = "ImgCach";
    private static final String WHOLESALE_CONV = ".cach";
                                                          
    private static final int MB = 1024*1024;
    private static final int CACHE_SIZE = 10;
    private static final int FREE_SD_SPACE_NEEDED_TO_CACHE = 10;
                                                              
    public ImageFileCache() {
        //清理檔案快取
        removeCache(getDirectory());
    }
                                                              
    /** 從快取中獲取圖片 **/
    public Bitmap getImage(final String url) {    
        final String path = getDirectory() + "/" + convertUrlToFileName(url);
        File file = new File(path);
        if (file.exists()) {
            Bitmap bmp = BitmapFactory.decodeFile(path);
            if (bmp == null) {
                file.delete();
            } else {
                updateFileTime(path);
                return bmp;
            }
        }
        return null;
    }
                                                              
    /** 將圖片存入檔案快取 **/
    public void saveBitmap(Bitmap bm, String url) {
        if (bm == null) {
            return;
        }
        //判斷sdcard上的空間
        if (FREE_SD_SPACE_NEEDED_TO_CACHE > freeSpaceOnSd()) {
            //SD空間不足
            return;
        }
        String filename = convertUrlToFileName(url);
        String dir = getDirectory();
        File dirFile = new File(dir);
        if (!dirFile.exists())
            dirFile.mkdirs();
        File file = new File(dir +"/" + filename);
        try {
            file.createNewFile();
            OutputStream outStream = new FileOutputStream(file);
            bm.compress(Bitmap.CompressFormat.JPEG, 100, outStream);
            outStream.flush();
            outStream.close();
        } catch (FileNotFoundException e) {
            Log.w("ImageFileCache", "FileNotFoundException");
        } catch (IOException e) {
            Log.w("ImageFileCache", "IOException");
        }
    
                                                              
    /**
     * 計算儲存目錄下的檔案大小,
     * 當檔案總大小大於規定的CACHE_SIZE或者sdcard剩餘空間小於FREE_SD_SPACE_NEEDED_TO_CACHE的規定
     * 那麼刪除40%最近沒有被使用的檔案
     */
    private boolean removeCache(String dirPath) {
        File dir = new File(dirPath);
        File[] files = dir.listFiles();
        if (files == null) {
            return true;
        }
        if (!android.os.Environment.getExternalStorageState().equals(
                android.os.Environment.MEDIA_MOUNTED)) {
            return false;
        }
                                                          
        int dirSize = 0;
        for (int i = 0; i < files.length; i++) {
            if (files[i].getName().contains(WHOLESALE_CONV)) {
                dirSize += files[i].length();
            }
        }
                                                          
        if (dirSize > CACHE_SIZE * MB || FREE_SD_SPACE_NEEDED_TO_CACHE > freeSpaceOnSd()) {
            int removeFactor = (int) ((0.4 * files.length) + 1);
            Arrays.sort(files, new FileLastModifSort());
            for (int i = 0; i < removeFactor; i++) {
                if (files[i].getName().contains(WHOLESALE_CONV)) {
                    files[i].delete();
                }
            }
        }
                                                          
        if (freeSpaceOnSd() <= CACHE_SIZE) {
            return false;
        }
                                                                  
        return true;
    }
                                                              
    /** 修改檔案的最後修改時間 **/
    public void updateFileTime(String path) {
        File file = new File(path);
        long newModifiedTime = System.currentTimeMillis();
        file.setLastModified(newModifiedTime);
    }
                                                              
    /** 計算sdcard上的剩餘空間 **/
    private int freeSpaceOnSd() {
        StatFs stat = new StatFs(Environment.getExternalStorageDirectory().getPath());
        double sdFreeMB = ((double)stat.getAvailableBlocks() * (double) stat.getBlockSize()) / MB;
        return (int) sdFreeMB;
    
                                                              
    /** 將url轉成檔名 **/
    private String convertUrlToFileName(String url) {
        String[] strs = url.split("/");
        return strs[strs.length - 1] + WHOLESALE_CONV;
    }
                                                              
    /** 獲得快取目錄 **/
    private String getDirectory() {
        String dir = getSDPath() + "/" + CACHDIR;
        return dir;
    }
                                                              
    /** 取SD卡路徑 **/
    private String getSDPath() {
        File sdDir = null;
        boolean sdCardExist = Environment.getExternalStorageState().equals(
                android.os.Environment.MEDIA_MOUNTED);  //判斷sd卡是否存在
        if (sdCardExist) {
            sdDir = Environment.getExternalStorageDirectory();  //獲取根目錄
        }
        if (sdDir != null) {
            return sdDir.toString();
        } else {
            return "";
        }
    
                                                          
    /**
     * 根據檔案的最後修改時間進行排序
     */
    private class FileLastModifSort implements Comparator<File> {
        public int compare(File arg0, File arg1) {
            if (arg0.lastModified() > arg1.lastModified()) {
                return 1;
            } else if (arg0.lastModified() == arg1.lastModified()) {
                return 0;
            } else {
                return -1;
            }
        }
    }
                                                          
}

 

從網路獲取圖片:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
public class ImageGetFormHttp {
    private static final String LOG_TAG = "ImageGetForHttp";
                                                         
    public static Bitmap downloadBitmap(String url) {
        final HttpClient client = new DefaultHttpClient();
        final HttpGet getRequest = new HttpGet(url);
                                                             
        try {
            HttpResponse response = client.execute(getRequest);
            final int statusCode = response.getStatusLine().getStatusCode();
            if (statusCode != HttpStatus.SC_OK) {
                Log.w("ImageDownloader", "Error " + statusCode + " while retrieving bitmap from " + url);
                return null;
            }
                                                                 
            final HttpEntity entity = response.getEntity();
            if (entity != null) {
                InputStream inputStream = null;
                try {
                    inputStream = entity.getContent();
                    FilterInputStream fit = new FlushedInputStream(inputStream);
                    return BitmapFactory.decodeStream(fit);
                } finally {
                    if (inputStream != null) {
                        inputStream.close();
                        inputStream = null;
                    }
                    entity.consumeContent();
                }
            }
        } catch (IOException e) {
            getRequest.abort();
            Log.w(LOG_TAG, "I/O error while retrieving bitmap from " + url, e);
        } catch (IllegalStateException e) {
            getRequest.abort();
            Log.w(LOG_TAG, "Incorrect URL: " + url);
        } catch (Exception e) {
            getRequest.abort();
            Log.w(LOG_TAG, "Error while retrieving bitmap from " + url, e);
        } finally {
            client.getConnectionManager().shutdown();
        }
        return null;
    }
                                                     
    /*
     * An InputStream that skips the exact number of bytes provided, unless it reaches EOF.
     */
    static class FlushedInputStream extends FilterInputStream {
        public FlushedInputStream(InputStream inputStream) {
            super(inputStream);
        }
                                                     
        @Override
        public long skip(long n) throws IOException {
            long totalBytesSkipped = 0L;
            while (totalBytesSkipped < n) {
                long bytesSkipped = in.skip(n - totalBytesSkipped);
                if (bytesSkipped == 0L) {
                    int b = read();
                    if (b < 0) {
                        break// we reached EOF
                    } else {
                        bytesSkipped = 1; // we read one byte
                    }
                }
                totalBytesSkipped += bytesSkipped;
            }
            return totalBytesSkipped;
        }
    }
}

 

最後,獲取一張圖片的流程就如下程式碼所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*** 獲得一張圖片,從三個地方獲取,首先是記憶體快取,然後是檔案快取,最後從網路獲取 ***/
public Bitmap getBitmap(String url) {
    // 從記憶體快取中獲取圖片
    Bitmap result;
    result = memoryCache.getBitmapFromCache(url);
    if (result == null) {
        // 檔案快取中獲取
        result = fileCache.getImage(url);
        if (result == null) {
            // 從網路獲取
            result = ImageGetFormHttp.downloadBitmap(url);
            if (result != null) {
                fileCache.saveBitmap(result, url);
                memoryCache.addBitmapToCache(url, result);
            }
        } else {
            // 新增到記憶體快取
            memoryCache.addBitmapToCache(url, result);
        }
    }
    return result;
}

 

相關文章