Skip to content

Most visited

Recently visited

navigation
DisplayingBitmaps / src / com.example.android.displayingbitmaps / util /

ImageCache.java

1
/*
2
 * Copyright (C) 2012 The Android Open Source Project
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 *      http://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 */
16
 
17
package com.example.android.displayingbitmaps.util;
18
 
19
import android.annotation.TargetApi;
20
import android.content.Context;
21
import android.graphics.Bitmap;
22
import android.graphics.Bitmap.CompressFormat;
23
import android.graphics.Bitmap.Config;
24
import android.graphics.BitmapFactory;
25
import android.graphics.drawable.BitmapDrawable;
26
import android.os.Build.VERSION_CODES;
27
import android.os.Bundle;
28
import android.os.Environment;
29
import android.os.StatFs;
30
import android.support.v4.app.Fragment;
31
import android.support.v4.app.FragmentManager;
32
import android.support.v4.util.LruCache;
33
 
34
import com.example.android.common.logger.Log;
35
import com.example.android.displayingbitmaps.BuildConfig;
36
 
37
import java.io.File;
38
import java.io.FileDescriptor;
39
import java.io.FileInputStream;
40
import java.io.IOException;
41
import java.io.InputStream;
42
import java.io.OutputStream;
43
import java.lang.ref.SoftReference;
44
import java.security.MessageDigest;
45
import java.security.NoSuchAlgorithmException;
46
import java.util.Collections;
47
import java.util.HashSet;
48
import java.util.Iterator;
49
import java.util.Set;
50
 
51
/**
52
 * This class handles disk and memory caching of bitmaps in conjunction with the
53
 * {@link ImageWorker} class and its subclasses. Use
54
 * {@link ImageCache#getInstance(android.support.v4.app.FragmentManager, ImageCacheParams)} to get an instance of this
55
 * class, although usually a cache should be added directly to an {@link ImageWorker} by calling
56
 * {@link ImageWorker#addImageCache(android.support.v4.app.FragmentManager, ImageCacheParams)}.
57
 */
58
public class ImageCache {
59
    private static final String TAG = "ImageCache";
60
 
61
    // Default memory cache size in kilobytes
62
    private static final int DEFAULT_MEM_CACHE_SIZE = 1024 * 5; // 5MB
63
 
64
    // Default disk cache size in bytes
65
    private static final int DEFAULT_DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB
66
 
67
    // Compression settings when writing images to disk cache
68
    private static final CompressFormat DEFAULT_COMPRESS_FORMAT = CompressFormat.JPEG;
69
    private static final int DEFAULT_COMPRESS_QUALITY = 70;
70
    private static final int DISK_CACHE_INDEX = 0;
71
 
72
    // Constants to easily toggle various caches
73
    private static final boolean DEFAULT_MEM_CACHE_ENABLED = true;
74
    private static final boolean DEFAULT_DISK_CACHE_ENABLED = true;
75
    private static final boolean DEFAULT_INIT_DISK_CACHE_ON_CREATE = false;
76
 
77
    private DiskLruCache mDiskLruCache;
78
    private LruCache<String, BitmapDrawable> mMemoryCache;
79
    private ImageCacheParams mCacheParams;
80
    private final Object mDiskCacheLock = new Object();
81
    private boolean mDiskCacheStarting = true;
82
 
83
    private Set<SoftReference<Bitmap>> mReusableBitmaps;
84
 
85
    /**
86
     * Create a new ImageCache object using the specified parameters. This should not be
87
     * called directly by other classes, instead use
88
     * {@link ImageCache#getInstance(android.support.v4.app.FragmentManager, ImageCacheParams)} to fetch an ImageCache
89
     * instance.
90
     *
91
     * @param cacheParams The cache parameters to use to initialize the cache
92
     */
93
    private ImageCache(ImageCacheParams cacheParams) {
94
        init(cacheParams);
95
    }
96
 
97
    /**
98
     * Return an {@link ImageCache} instance. A {@link RetainFragment} is used to retain the
99
     * ImageCache object across configuration changes such as a change in device orientation.
100
     *
101
     * @param fragmentManager The fragment manager to use when dealing with the retained fragment.
102
     * @param cacheParams The cache parameters to use if the ImageCache needs instantiation.
103
     * @return An existing retained ImageCache object or a new one if one did not exist
104
     */
105
    public static ImageCache getInstance(
106
            FragmentManager fragmentManager, ImageCacheParams cacheParams) {
107
 
108
        // Search for, or create an instance of the non-UI RetainFragment
109
        final RetainFragment mRetainFragment = findOrCreateRetainFragment(fragmentManager);
110
 
111
        // See if we already have an ImageCache stored in RetainFragment
112
        ImageCache imageCache = (ImageCache) mRetainFragment.getObject();
113
 
114
        // No existing ImageCache, create one and store it in RetainFragment
115
        if (imageCache == null) {
116
            imageCache = new ImageCache(cacheParams);
117
            mRetainFragment.setObject(imageCache);
118
        }
119
 
120
        return imageCache;
121
    }
122
 
123
    /**
124
     * Initialize the cache, providing all parameters.
125
     *
126
     * @param cacheParams The cache parameters to initialize the cache
127
     */
128
    private void init(ImageCacheParams cacheParams) {
129
        mCacheParams = cacheParams;
130
 
132
        // Set up memory cache
133
        if (mCacheParams.memoryCacheEnabled) {
134
            if (BuildConfig.DEBUG) {
135
                Log.d(TAG, "Memory cache created (size = " + mCacheParams.memCacheSize + ")");
136
            }
137
 
138
            // If we're running on Honeycomb or newer, create a set of reusable bitmaps that can be
139
            // populated into the inBitmap field of BitmapFactory.Options. Note that the set is
140
            // of SoftReferences which will actually not be very effective due to the garbage
141
            // collector being aggressive clearing Soft/WeakReferences. A better approach
142
            // would be to use a strongly references bitmaps, however this would require some
143
            // balancing of memory usage between this set and the bitmap LruCache. It would also
144
            // require knowledge of the expected size of the bitmaps. From Honeycomb to JellyBean
145
            // the size would need to be precise, from KitKat onward the size would just need to
146
            // be the upper bound (due to changes in how inBitmap can re-use bitmaps).
147
            if (Utils.hasHoneycomb()) {
148
                mReusableBitmaps =
149
                        Collections.synchronizedSet(new HashSet<SoftReference<Bitmap>>());
150
            }
151
 
152
            mMemoryCache = new LruCache<String, BitmapDrawable>(mCacheParams.memCacheSize) {
153
 
154
                /**
155
                 * Notify the removed entry that is no longer being cached
156
                 */
157
                @Override
158
                protected void entryRemoved(boolean evicted, String key,
159
                        BitmapDrawable oldValue, BitmapDrawable newValue) {
160
                    if (RecyclingBitmapDrawable.class.isInstance(oldValue)) {
161
                        // The removed entry is a recycling drawable, so notify it
162
                        // that it has been removed from the memory cache
163
                        ((RecyclingBitmapDrawable) oldValue).setIsCached(false);
164
                    } else {
165
                        // The removed entry is a standard BitmapDrawable
166
 
167
                        if (Utils.hasHoneycomb()) {
168
                            // We're running on Honeycomb or later, so add the bitmap
169
                            // to a SoftReference set for possible use with inBitmap later
170
                            mReusableBitmaps.add(new SoftReference<Bitmap>(oldValue.getBitmap()));
171
                        }
172
                    }
173
                }
174
 
175
                /**
176
                 * Measure item size in kilobytes rather than units which is more practical
177
                 * for a bitmap cache
178
                 */
179
                @Override
180
                protected int sizeOf(String key, BitmapDrawable value) {
181
                    final int bitmapSize = getBitmapSize(value) / 1024;
182
                    return bitmapSize == 0 ? 1 : bitmapSize;
183
                }
184
            };
185
        }
187
 
188
        // By default the disk cache is not initialized here as it should be initialized
189
        // on a separate thread due to disk access.
190
        if (cacheParams.initDiskCacheOnCreate) {
191
            // Set up disk cache
192
            initDiskCache();
193
        }
194
    }
195
 
196
    /**
197
     * Initializes the disk cache.  Note that this includes disk access so this should not be
198
     * executed on the main/UI thread. By default an ImageCache does not initialize the disk
199
     * cache when it is created, instead you should call initDiskCache() to initialize it on a
200
     * background thread.
201
     */
202
    public void initDiskCache() {
203
        // Set up disk cache
204
        synchronized (mDiskCacheLock) {
205
            if (mDiskLruCache == null || mDiskLruCache.isClosed()) {
206
                File diskCacheDir = mCacheParams.diskCacheDir;
207
                if (mCacheParams.diskCacheEnabled && diskCacheDir != null) {
208
                    if (!diskCacheDir.exists()) {
209
                        diskCacheDir.mkdirs();
210
                    }
211
                    if (getUsableSpace(diskCacheDir) > mCacheParams.diskCacheSize) {
212
                        try {
213
                            mDiskLruCache = DiskLruCache.open(
214
                                    diskCacheDir, 1, 1, mCacheParams.diskCacheSize);
215
                            if (BuildConfig.DEBUG) {
216
                                Log.d(TAG, "Disk cache initialized");
217
                            }
218
                        } catch (final IOException e) {
219
                            mCacheParams.diskCacheDir = null;
220
                            Log.e(TAG, "initDiskCache - " + e);
221
                        }
222
                    }
223
                }
224
            }
225
            mDiskCacheStarting = false;
226
            mDiskCacheLock.notifyAll();
227
        }
228
    }
229
 
230
    /**
231
     * Adds a bitmap to both memory and disk cache.
232
     * @param data Unique identifier for the bitmap to store
233
     * @param value The bitmap drawable to store
234
     */
235
    public void addBitmapToCache(String data, BitmapDrawable value) {
237
        if (data == null || value == null) {
238
            return;
239
        }
240
 
241
        // Add to memory cache
242
        if (mMemoryCache != null) {
243
            if (RecyclingBitmapDrawable.class.isInstance(value)) {
244
                // The removed entry is a recycling drawable, so notify it
245
                // that it has been added into the memory cache
246
                ((RecyclingBitmapDrawable) value).setIsCached(true);
247
            }
248
            mMemoryCache.put(data, value);
249
        }
250
 
251
        synchronized (mDiskCacheLock) {
252
            // Add to disk cache
253
            if (mDiskLruCache != null) {
254
                final String key = hashKeyForDisk(data);
255
                OutputStream out = null;
256
                try {
257
                    DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
258
                    if (snapshot == null) {
259
                        final DiskLruCache.Editor editor = mDiskLruCache.edit(key);
260
                        if (editor != null) {
261
                            out = editor.newOutputStream(DISK_CACHE_INDEX);
262
                            value.getBitmap().compress(
263
                                    mCacheParams.compressFormat, mCacheParams.compressQuality, out);
264
                            editor.commit();
265
                            out.close();
266
                        }
267
                    } else {
268
                        snapshot.getInputStream(DISK_CACHE_INDEX).close();
269
                    }
270
                } catch (final IOException e) {
271
                    Log.e(TAG, "addBitmapToCache - " + e);
272
                } catch (Exception e) {
273
                    Log.e(TAG, "addBitmapToCache - " + e);
274
                } finally {
275
                    try {
276
                        if (out != null) {
277
                            out.close();
278
                        }
279
                    } catch (IOException e) {}
280
                }
281
            }
282
        }
284
    }
285
 
286
    /**
287
     * Get from memory cache.
288
     *
289
     * @param data Unique identifier for which item to get
290
     * @return The bitmap drawable if found in cache, null otherwise
291
     */
292
    public BitmapDrawable getBitmapFromMemCache(String data) {
294
        BitmapDrawable memValue = null;
295
 
296
        if (mMemoryCache != null) {
297
            memValue = mMemoryCache.get(data);
298
        }
299
 
300
        if (BuildConfig.DEBUG && memValue != null) {
301
            Log.d(TAG, "Memory cache hit");
302
        }
303
 
304
        return memValue;
306
    }
307
 
308
    /**
309
     * Get from disk cache.
310
     *
311
     * @param data Unique identifier for which item to get
312
     * @return The bitmap if found in cache, null otherwise
313
     */
314
    public Bitmap getBitmapFromDiskCache(String data) {
316
        final String key = hashKeyForDisk(data);
317
        Bitmap bitmap = null;
318
 
319
        synchronized (mDiskCacheLock) {
320
            while (mDiskCacheStarting) {
321
                try {
322
                    mDiskCacheLock.wait();
323
                } catch (InterruptedException e) {}
324
            }
325
            if (mDiskLruCache != null) {
326
                InputStream inputStream = null;
327
                try {
328
                    final DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
329
                    if (snapshot != null) {
330
                        if (BuildConfig.DEBUG) {
331
                            Log.d(TAG, "Disk cache hit");
332
                        }
333
                        inputStream = snapshot.getInputStream(DISK_CACHE_INDEX);
334
                        if (inputStream != null) {
335
                            FileDescriptor fd = ((FileInputStream) inputStream).getFD();
336
 
337
                            // Decode bitmap, but we don't want to sample so give
338
                            // MAX_VALUE as the target dimensions
339
                            bitmap = ImageResizer.decodeSampledBitmapFromDescriptor(
340
                                    fd, Integer.MAX_VALUE, Integer.MAX_VALUE, this);
341
                        }
342
                    }
343
                } catch (final IOException e) {
344
                    Log.e(TAG, "getBitmapFromDiskCache - " + e);
345
                } finally {
346
                    try {
347
                        if (inputStream != null) {
348
                            inputStream.close();
349
                        }
350
                    } catch (IOException e) {}
351
                }
352
            }
353
            return bitmap;
354
        }
356
    }
357
 
358
    /**
359
     * @param options - BitmapFactory.Options with out* options populated
360
     * @return Bitmap that case be used for inBitmap
361
     */
362
    protected Bitmap getBitmapFromReusableSet(BitmapFactory.Options options) {
364
        Bitmap bitmap = null;
365
 
366
        if (mReusableBitmaps != null && !mReusableBitmaps.isEmpty()) {
367
            synchronized (mReusableBitmaps) {
368
                final Iterator<SoftReference<Bitmap>> iterator = mReusableBitmaps.iterator();
369
                Bitmap item;
370
 
371
                while (iterator.hasNext()) {
372
                    item = iterator.next().get();
373
 
374
                    if (null != item && item.isMutable()) {
375
                        // Check to see it the item can be used for inBitmap
376
                        if (canUseForInBitmap(item, options)) {
377
                            bitmap = item;
378
 
379
                            // Remove from reusable set so it can't be used again
380
                            iterator.remove();
381
                            break;
382
                        }
383
                    } else {
384
                        // Remove from the set if the reference has been cleared.
385
                        iterator.remove();
386
                    }
387
                }
388
            }
389
        }
390
 
391
        return bitmap;
393
    }
394
 
395
    /**
396
     * Clears both the memory and disk cache associated with this ImageCache object. Note that
397
     * this includes disk access so this should not be executed on the main/UI thread.
398
     */
399
    public void clearCache() {
400
        if (mMemoryCache != null) {
401
            mMemoryCache.evictAll();
402
            if (BuildConfig.DEBUG) {
403
                Log.d(TAG, "Memory cache cleared");
404
            }
405
        }
406
 
407
        synchronized (mDiskCacheLock) {
408
            mDiskCacheStarting = true;
409
            if (mDiskLruCache != null && !mDiskLruCache.isClosed()) {
410
                try {
411
                    mDiskLruCache.delete();
412
                    if (BuildConfig.DEBUG) {
413
                        Log.d(TAG, "Disk cache cleared");
414
                    }
415
                } catch (IOException e) {
416
                    Log.e(TAG, "clearCache - " + e);
417
                }
418
                mDiskLruCache = null;
419
                initDiskCache();
420
            }
421
        }
422
    }
423
 
424
    /**
425
     * Flushes the disk cache associated with this ImageCache object. Note that this includes
426
     * disk access so this should not be executed on the main/UI thread.
427
     */
428
    public void flush() {
429
        synchronized (mDiskCacheLock) {
430
            if (mDiskLruCache != null) {
431
                try {
432
                    mDiskLruCache.flush();
433
                    if (BuildConfig.DEBUG) {
434
                        Log.d(TAG, "Disk cache flushed");
435
                    }
436
                } catch (IOException e) {
437
                    Log.e(TAG, "flush - " + e);
438
                }
439
            }
440
        }
441
    }
442
 
443
    /**
444
     * Closes the disk cache associated with this ImageCache object. Note that this includes
445
     * disk access so this should not be executed on the main/UI thread.
446
     */
447
    public void close() {
448
        synchronized (mDiskCacheLock) {
449
            if (mDiskLruCache != null) {
450
                try {
451
                    if (!mDiskLruCache.isClosed()) {
452
                        mDiskLruCache.close();
453
                        mDiskLruCache = null;
454
                        if (BuildConfig.DEBUG) {
455
                            Log.d(TAG, "Disk cache closed");
456
                        }
457
                    }
458
                } catch (IOException e) {
459
                    Log.e(TAG, "close - " + e);
460
                }
461
            }
462
        }
463
    }
464
 
465
    /**
466
     * A holder class that contains cache parameters.
467
     */
468
    public static class ImageCacheParams {
469
        public int memCacheSize = DEFAULT_MEM_CACHE_SIZE;
470
        public int diskCacheSize = DEFAULT_DISK_CACHE_SIZE;
471
        public File diskCacheDir;
472
        public CompressFormat compressFormat = DEFAULT_COMPRESS_FORMAT;
473
        public int compressQuality = DEFAULT_COMPRESS_QUALITY;
474
        public boolean memoryCacheEnabled = DEFAULT_MEM_CACHE_ENABLED;
475
        public boolean diskCacheEnabled = DEFAULT_DISK_CACHE_ENABLED;
476
        public boolean initDiskCacheOnCreate = DEFAULT_INIT_DISK_CACHE_ON_CREATE;
477
 
478
        /**
479