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
    }