0%

Bitmap 的加载和 Cache(二):Android 中的缓存策略

1. 内存缓存概述

  • 内存缓存可以提供对位图的快速访问,但代价是会占用宝贵的应用内存

  • LruCache 是 Android 3.1 提供的一个线程安全的泛型类support-v4 库中也提供了该类,最低可支持 API 4,建议使用这个版本的 LruCache),非常适合用于:缓存位图

  • LruCache 缓存位图的原理是其内部采用一个 LinkedHashMap 以强引用的方式存储外界的缓存对象,并提供了 get()put() 方法来完成缓存的获取和添加操作,并且在缓存超出其指定大小之前移除最近最少使用的对象,然后再添加新的对象

    • 过去,最常用的内存缓存实现是 SoftReferenceWeakReference 位图缓存,但现在已不建议使用。从 Android 2.3(API 级别 9)开始,垃圾回收器会更积极地回收软引用/弱引用,导致它们效果不佳
    • 此外,在 Android 3.0(API 11)之前,位图的缓存数据存储在原生内存中,该内存不会以可预测的方式释放,因此可能导致引用短暂超出其内存限制并崩溃
  • 要为 LruCache 选择合适的缓存容量大小,需要考虑多种因素:

    • Activity 和/或应用的其余部分对内存的占用情况如何
    • 一次会在屏幕上显示多少张图片,有多少张图片需要准备好随时可以显示在屏幕上
    • 设备的屏幕尺寸和密度是多少,相比于 Nexus S(hdpi) 这样的设备,超高密度屏幕(xhdpi)设备(如 Galaxy Nexus)需要更大的缓存才能在内存中保存相同数量的图片
    • 位图的尺寸和配置如何,每个位图会占用多少内存
    • 图片的访问频率是多少,是否有一些图片的访问频率会高于其他图片,如果是这样,可能需要将某些项始终保留在内存中,甚至为不同的位图组创建多个 LruCache 对象
    • 能否在质量和数量之间取得平衡,有时存储更多低质量的位图会更有用,这样做可能需要在另一个后台任务中加载更高质量的位图
  • 没有适合所有应用的特定大小或公式,应该自行分析使用情况并找到适合的解决方案。缓存过小会产生额外的开销且没有任何好处,缓存过大又会造成 java.lang.OutOfMemory 异常并让应用的其余部分没有多少内存可用

  • 为位图设置 LruCache 的 Demo:

    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
    private LruCache<String, Bitmap> memoryCache;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    ...
    // Get max available VM memory, exceeding this amount will throw an
    // OutOfMemory exception. Stored in kilobytes as LruCache takes an
    // int in its constructor.
    final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);

    // Use 1/8th of the available memory for this memory cache.
    final int cacheSize = maxMemory / 8;

    memoryCache = new LruCache<String, Bitmap>(cacheSize) {
    @Override
    protected int sizeOf(String key, Bitmap bitmap) {
    // The cache size will be measured in kilobytes rather than
    // number of items.
    return bitmap.getByteCount() / 1024;
    }
    };
    ...
    }

    public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
    if (getBitmapFromMemCache(key) == null) {
    memoryCache.put(key, bitmap);
    }
    }

    public Bitmap getBitmapFromMemCache(String key) {
    return memoryCache.get(key);
    }
    • 在上面 Demo 中,将 1/8 的应用内存分配给了缓存。在普通 /hdpi 设备上,此内存最少为 4MB(32/8)左右。在分辨率为 800*480 的设备上,填充了图片的全屏 GridView 大约会占用 1.5MB(800*480*4 字节)的内存,这会在内存中缓存至少 2.5 页的图片
  • 将位图加载到 ImageView 时,首先会检查 LruCache。如果找到条目,则会立即使用该条目来更新 ImageView,否则会生成一个后台线程来处理图片:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public void loadBitmap(int resId, ImageView imageView) {
    final String imageKey = String.valueOf(resId);

    final Bitmap bitmap = getBitmapFromMemCache(imageKey);
    if (bitmap != null) {
    mImageView.setImageBitmap(bitmap);
    } else {
    mImageView.setImageResource(R.drawable.image_placeholder);
    BitmapWorkerTask task = new BitmapWorkerTask(mImageView);
    task.execute(resId);
    }
    }
  • 此外,需要更新 BitmapWorkerTask 才能将条目添加到内存缓存:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
    ...
    // Decode image in background.
    @Override
    protected Bitmap doInBackground(Integer... params) {
    final Bitmap bitmap = decodeSampledBitmapFromResource(getResource(), params[0], 100, 100);
    addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);
    return bitmap;
    }
    ...
    }
  • LruCache 源码

    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
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    /**
    * Copyright (C) 2011 The Android Open Source Project
    *
    * bla bla bla
    */

    package android.util;

    // import bla bla bla

    /**
    * A cache that holds strong references to a limited number of values. Each time
    * a value is accessed, it is moved to the head of a queue. When a value is
    * added to a full cache, the value at the end of that queue is evicted and may
    * become eligible for garbage collection.
    *
    * <p>If your cached values hold resources that need to be explicitly released,
    * override {@link #entryRemoved}.
    *
    * <p>If a cache miss should be computed on demand for the corresponding keys,
    * override {@link #create}. This simplifies the calling code, allowing it to
    * assume a value will always be returned, even when there's a cache miss.
    *
    * <p>By default, the cache size is measured in the number of entries. Override
    * {@link #sizeOf} to size the cache in different units. For example, this cache
    * is limited to 4MiB of bitmaps:
    * <pre> {@code
    * int cacheSize = 4 * 1024 * 1024; // 4MiB
    * LruCache<String, Bitmap> bitmapCache = new LruCache<String, Bitmap>(cacheSize) {
    * protected int sizeOf(String key, Bitmap value) {
    * return value.getByteCount();
    * }
    * }}</pre>
    *
    * <p>This class is thread-safe. Perform multiple cache operations atomically by
    * synchronizing on the cache: <pre> {@code
    * synchronized (cache) {
    * if (cache.get(key) == null) {
    * cache.put(key, value);
    * }
    * }
    * }</pre>
    *
    * <p>This class does not allow null to be used as a key or value. A return
    * value of null from {@link #get}, {@link #put} or {@link #remove} is
    * unambiguous: the key was not in the cache.
    *
    * <p>This class appeared in Android 3.1 (Honeycomb MR1); it's available as part
    * of <a href="http://developer.android.com/sdk/compatibility-library.html">Android's
    * Support Package</a> for earlier releases.
    */
    public class LruCache<K, V> {
    @UnsupportedAppUsage
    private final LinkedHashMap<K, V> map;

    /** Size of this cache in units. Not necessarily the number of elements. */
    private int size;
    private int maxSize;

    private int putCount;
    private int createCount;
    private int evictionCount;
    private int hitCount;
    private int missCount;

    /**
    * @param maxSize for caches that do not override {@link #sizeOf}, this is
    * the maximum number of entries in the cache. For all other caches,
    * this is the maximum sum of the sizes of the entries in this cache.
    */
    public LruCache(int maxSize) {
    if (maxSize <= 0) {
    throw new IllegalArgumentException("maxSize <= 0");
    }
    this.maxSize = maxSize;
    this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
    }

    /**
    * Sets the size of the cache.
    *
    * @param maxSize The new maximum size.
    */
    public void resize(int maxSize) {
    if (maxSize <= 0) {
    throw new IllegalArgumentException("maxSize <= 0);
    }

    synchronized (this) {
    this.maxSize = maxSize;
    }
    trimToSize(maxSize);
    }

    /**
    * Returns the value for {@code key} if it exists in the cache or can be
    * created by {@code #create}. If a value was returned, it is moved to the
    * head of the queue. This returns null if a value is not cached and cannot
    * be created.
    */
    public final V get(K key) {
    if (key == null) {
    throw new NullPointerException("key == null");
    }

    V mapValue;
    synchronized (this) {
    mapValue = map.get(key);
    if (mapValue != null) {
    hitCount++;
    return mapValue;
    }
    missCount++;
    }

    /**
    * Attempt to create a value. This may take a long time, and the map
    * may be different when create() returns. If a conflicting value was
    * added to the map while create() was working, we leave that value in
    * the map and release the created value.
    */

    V createdValue = create(key);
    if (createdValue == null) {
    return null;
    }

    synchronized (this) {
    createCount++;
    mapValue = map.put(key, createdValue);

    if (mapValue != null) {
    // There was a conflict so undo that last put
    map.put(key, mapValue);
    } else {
    size += safeSizeOf(key, createdValue);
    }
    }

    if (mapValue != null) {
    entryRemoved(false, key, createdValue, mapValue);
    return mapValue;
    } else {
    trimToSize(maxSize);
    return createdValue;
    }
    }

    /**
    * Caches {@code value} for {@code key}. The value is moved to the head of the queue.
    *
    * @return the previous value mapped by {@code key}.
    */
    public final V put(K key, V value) {
    if (key == null || value == null) {
    throw new NullPointerException("key == null || value == null");
    }

    V previous;
    synchronized (this) {
    putCount++;
    size += safeSizeOf(key, value);
    previous = map.put(key, value);
    if (previous != null) {
    size -= safeSizeOf(key, previous);
    }
    }

    if (previous != null) {
    entryRemoved(false, key, previous, value);
    }

    trimToSize(maxSzie);
    return previous;
    }

    /**
    * Remove the eldest entryies until the total of remaining entries is at or
    * below the requested size.
    *
    * @param maxSize the maximum size of the cache before returning. May be -1
    * to evict even 0-sized elements.
    */
    public void trimToSize(int maxSize) {
    while (true) {
    K key;
    V value;
    synchronized (this) {
    if (size < 0 || (map.isEmpty() && size != 0)) {
    throw new IllegalStateException(getClass().getName() + ".sizeOf() is reporting inconsistent results!");
    }

    if (size <= maxSize) {
    break;
    }

    Map.Entry<K, V> toEvict = map.eldest();
    if (toEvict == null) {
    break;
    }

    key = toEvict.getKey();
    value = toEvict.getValue();
    map.remove(key);
    size -= safeSizeOf(key, value);
    evictionCount++;
    }

    entryRemoved(true, key, value, null);
    }
    }

    /**
    * Removes the entry for {@code key} if it exists.
    *
    * @return the previous value mapped by {@code key}.
    */
    public final V remove(K key) {
    if (key == null) {
    throw new NullPointerException("key == null");
    }

    V previous;
    synchronized (this) {
    previous = map.remove(key);
    if (previous != null) {
    size -= safeSizeOf(key, previous);
    }
    }

    if (previous != null) {
    entryRemoved(false, key, previous, null);
    }

    return previous;
    }

    // bla bla bla
    }

2. 磁盘缓存概述

  • 内存缓存有助于加快对最近查看过的位图的访问,但开发中不能依赖此缓存中保留的图片

    • GridView 这样拥有较大数据集的组件很容易将内存缓存填满
    • 应用可能被其他任务(如电话)中断,而在后台时,应用可能会被终止,而内存缓存则会销毁
    • 用户恢复操作后,应用必须重新处理每张图片
  • 在上述情况下,可以使用磁盘缓存来保存经过处理的位图,并在图片已不在内存缓存中时帮助减少加载时间。当然,从磁盘获取图片比从内存中加载缓慢,而且应该在后台线程中完成,因为磁盘读取时间不可预测

    • 如果对缓存图片的访问频率较高(例如在图库应用中),则可能更适合将其存储在 ContentProvider
  • Demo: 使用了从 Android 源码中提取的 DiskLruCache 实现,在现有的内存缓存之外又添加了一个磁盘缓存

    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
    private DiskLruCache diskLruCache;
    private final Object diskCacheLock = new Object();
    private boolean diskCacheStarting = true;
    private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB
    private static final String DISK_CACHE_SUBDIR = "thumbnails";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    ...
    // Initialize memory cache
    ...
    // Initialize disk cache on background thread
    File cacheDir = getDiskCacheDir(this, DISK_CACHE_SUBDIR);
    new InitDiskCacheTask().execute(cacheDie);
    ...
    }

    class InitDiskCacheTask extends AsyncTask<File, Void, Void> {
    @Override
    protected Void doInBackground(File... params) {
    synchronized (diskCacheLock) {
    File cacheDie = params[0];
    diskLruCache = DiskLruCache.open(cacheDir, DISK_CACHE_SIZE);
    diskCacheStarting = false; // Finishing initialization
    diskCacheLock.notifyAll(); // Wake any waiting threads
    }
    return null;
    }
    }

    class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
    ...
    // Decode image in background.
    @Override
    protected Bitmap doInBackground(Integer... params) {
    final String imageKey = String.valueOf(params[0]);

    // Check disk cache in background thread
    Bitmap bitmap = getBitmapFromDiskCache(imageKey);

    if (bitmap == null) { // Not found in disk cache
    // Process as normal
    final Bitmap bitmap = decodeSampledBitmapFromResource(getResources(), params[0], 100, 100);
    }

    // Add final bitmap to caches
    addBitmapToCache(imageKey, bitmap);

    return bitmap;
    }
    ...
    }

    public void addBitmapToCache(String key, Bitmap bitmap) {
    // Add to memory cache as before
    if (getBitmapFromMemCache(key) == null) {
    memoryCache.put(key, bitmap);
    }

    // Also add to disk cache
    synchronized (diskCacheLock) {
    if (diskLruCache != null && diskLruCache.get(key) == null) {
    diskLruCache.put(key, bitmap);
    }
    }
    }

    public Bitmap getBitmapFromDiskCache(String key) {
    synchronized (diskCacheLock) {
    // Wait while disk cache is started from background thread
    while (diskCacheStarting) {
    try {
    diskCacheLock.wait();
    } catch (InterruptedException e) {
    }
    }
    if (diskLruCache != null) {
    return diskLruCache.get(key);
    }
    }
    return null;
    }

    // Creates a unique subdirectory of the designed app cache directory. Tries to use external
    // but if not mounted, falls back on internal storage.
    public static File getDiskCacheDir(Context context, String uniqueName) {
    // Check if media is mounted or storage is built-in, if so, try and use external cache dir
    // otherwise use internal cache dir
    final String cachePath =
    Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) ||
    !isExternalStorageRemovable() ? getExternalCacheDir(context).getPath() :
    context.getCacheDir().getPath();
    return new File(cachePath + File.separator + uniqueName);
    }
    • DiskLruCache 并不能通过构造方法来创建,它提供了 open() 方法来创建自身,内部实现上也用到了 LinkedHashMap
    • 需要注意的是,即使是初始化磁盘缓存也需要执行磁盘操作,因此不应在主线程上执行。不过,这也意味着可能会在初始化之前访问该缓存。为了解决这个问题,上述实现利用了一个 lock 对象来确保应用在磁盘缓存初始化之前不会从该缓存中读取数据
    • 虽然内存缓存是在 UI 线程中检查,但磁盘缓存会在后台线程中检查。UI 线程上不应执行磁盘操作。图片处理完毕后,系统会将最终的位图同时添加到内存缓存磁盘缓存中以供将来使用
  • DiskLruCache 源码

    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
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    /**
    * Copyright (C) 2011 The Android Open Source Project
    *
    * bla bla bla
    */

    package libcore.io;

    // import bla bla bla

    /**
    * A cache that uses a bounded amount of space on a filesystem. Each cache
    * entry has a string key and a fixed number of values. Values are byre
    * sequences, accessible as streams or files. Each value must be between {@code
    * 0} and {@code Integer.MAX_VALUE} bytes in length.
    *
    * <p>The cache stores its data in a directory on the filesystem. This
    * directory must be exclusive to the cache; the cache delete or overwrite
    * files from its directory. It is an error for multiple processes to use the
    * same cache directory at the same time.
    *
    * <p>This cache limits the number of bytes that will store on the
    * filesystem. When the number of stored bytes exceed the limit, the cache will
    * remove entries in the background until the limit is satisfied. The limit is
    * not strict: the cache may temporarily exceed it while waiting for files to be
    * deleted. The limit does not include filesystem overhead or the cache
    * journal so space-sensitive applications should set a conservative limit.
    *
    * <p>Clients call {@link #edit} to create or update the values of an entry. An
    * entry may have only one editor at one time; if a value is not available to be
    * edited then {@link #eitd} will return null.
    *
    * <ul>
    * <li>When an entry is being <strong>created</strong> it is necessary to
    * supply a full set of values; the empty value should be used as a
    * placeholder if necessary.
    * <li>When an entry is being <strong>edited</strong>, it is not necessary
    * to supply data for every value; values default to their previous
    * value.
    * </ul>
    * Every {@link #edit} call must be matched by a call to {@link Editor#commit}
    * or {@link Editor#abort}. Committing is atomic: a read observes the full set
    * of values as they were before or after the commit, but never a mix of values.
    *
    * <p>Clients call {@link #get} to read a snapshot of an entry. The read will
    * observe the value at the time that {@link #get} was called. Updates and
    * removals after the call do not impact ongoing reads.
    *
    * <p>This class is tolerant of some I/O errors. If files are missing from the filesystem,
    * the corresponding entries will be dropped from the cache. If
    * an error occurs while writing a cache value, the eidt will fail silently.
    * Callers should handle other problems by catching {@code IOException} and
    * responding appropriately.
    */
    public final class DiskLruCache implements Closeable {
    static final String JOURNAL_FILE = "journal";
    static final String JOURNAL_FILE_TMP = "journal.tmp";
    static final String MAGIC = "libcore.io.DiskLruCache";
    static final String VERSION_1 = "1";
    static final long ANY_SEQUENCE_NUMBER = -1;
    private static final String CLEAN = "CLEAN";
    private static final String DIRTY = "DIRTY";
    private static final String REMOVE = "REMOVE";
    private static final String READ = "READ";

    /**
    * This cache uses a journal file named "journal". A typical journal file
    * looks like this:
    * bla bla bla
    */

    private final File directory;
    private final File journalFile;
    private final File journalFileTmp;
    private final int appVersion;
    private final long maxSize;
    private final int valueCount;
    private long size = 0;
    private Write journalWriter;
    private final LinkedHashMap<String, Entry> lruEntries
    = new LinkedHashMap<String, Entry>(0, 0.75f, true);
    private int redundantOpCount;

    /**
    * To differentiate between old and current snapshots, each entry is given
    * a sequence number each time an edit is committed. A snapshot is stale if
    * its sequence number is not equal to its entry's sequence number.
    */
    private long nextSequenceNumber = 0;

    /** This cache uses a single background thread to evict entries. */
    private final ExecutorService executorService = new ThreadPoolExecutor (0, 1,
    60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
    private final Callable<Void> cleanupCallable = new Callable<Void>() {
    @Override public Void call() throws Exception {
    synchronized (DiskLruCache.this) {
    if (journalWriter == null) {
    return null; // closed
    }
    trimToSize();
    if (journalRebuildRequired()) {
    rebuildJournal();
    redundantOpCount = 0;
    }
    }
    return null;
    }
    };

    private DiskLruCache(File directory, int appVerison, int valueCount, long maxSize) {
    this.directory = directory;
    this.appVersion = appVersion;
    this.journalFile = new File(directory, JOURNAL_FILE);
    this.journalFileTmp = new File(directory, JOURNAL_FILE_TMP);
    this.valueCount = valueCount;
    this.maxSize = maxSize;
    }

    /**
    * Opens the cache in {@code directory}, creating a cache if none exists
    * there.
    * d
    * @param directory a writable directory
    * @param appVersion
    * @param valueCount the number of values per cache entry. Must be positive.
    * @param maxSize the maximum number of bytes this cache should use to store
    * @throws IOException if reading or writing the cache directory fails
    */
    public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize) throws IOException {
    if (maxSize <= 0) {
    throw new IllegalArgumentException("maxSize <= 0");
    }
    if (valueCount <=0 ) {
    throw new IllegalArgumentException("valueCount <= 0");
    }

    // prefer to pick up where we left off
    DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
    if (cache.journalFile.exists()) {
    try {
    cache.readJournal();
    cache.processJournal();
    cache.journalWriter = new BufferedWriter(new FileWriter(cache.journalFile, true));
    return cache;
    } catch (IOException journalIsCorrupt) {
    System.logw("DiskLruCache " + directory + " is corrupt: "
    + journalIsCorrupt.getMessage() + ", removing");
    cache.delete();
    }
    }

    // create a new empty cache
    directory.mkdirs();
    cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
    cache.rebuildJournal();
    return cache;
    }

    // bla bla bla

    private void trimToSize() throws IOException {
    while (size > maxSize) {
    Map.Entry<String, Entry> toEvict = lruEntries.eldest();
    remove(toEvict.getKey());
    }
    }

    // bla bla bla
    }

3. 处理配置变更概述

  • 运行时配置变更(例如屏幕方向更改)会导致 Android 销毁并使用新的配置重新启动正在运行的 Activity。需要避免重新处理所有图片,以便用户在配置发生变更时能够获得快速流畅的体验

  • 通过构建 一个实用的位图内存缓存,可以使用通过调用 setRetainInstance(true) 保留的 Fragment 将该缓存传递给新的 Activity 实例

  • 重新创建 Activity 后,系统会重新附加这个保留的 Fragment,并且将可以访问现有的缓存对象,从而能够快速获取图片并将其重新填充到 ImageView 对象中

  • Demo: 使用 Fragment 在配置变更是保留 LruCache 对象

    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
    private LruCache<String, Bitmap> memoryCache;

    @Override
    protected void onCreate(Bundle savedInstance) {
    ...
    RetainFragment retainFragment =
    RetainFragment.findOrCreateRetainFragment(getFragmentManager());
    memoryCache = retainFragment.retainedCache;
    if (memoryCache == null) {
    memoryCache = new LruCache<String, Bitmap>(cacheSzie) {
    ... // Initialize cache here as usual
    }
    retainFragment.retainedCode = memoryCache;
    }
    ...
    }

    class RetainFragment extends Fragment {
    private static final String TAG = "RetainFragment";
    public LruCache<String, Bitmap> retainedCache;

    public RetainFragment() {}

    public static RetainFragment findOrCreateRetainFragment(FragmentManager fm) {
    RetainFragment fragment = (RetainFragment) fm.findFragmentByTag(TAG);
    if (fragment == null) {
    fragment = new RetainFragment();
    fm.beginTransaction().add(fragment, TAG).commit();
    }
    return fragment;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setRetainInstance(true);
    }
    }
    • 对上面 Demo 进行测试的话,可以尝试在保留和不保留 Fragment 的情况下旋转设备。在保留缓存的情况下,几乎会看不到延迟,因为图片会立即从内存填充到 Activity 中
    • 在内存缓存中找不到的图片有可能在磁盘缓存中,如果不在,系统会照常处理它们

4. 优化管理位图内存概述

  • Android 位图内存管理的演变过程

    • Android 2.2(API 8)及更低版本上,当发生垃圾回收时,应用的线程会停止,这会导致延迟,从而降低性能。Android 2.3 添加了并发垃圾回收功能,这意味着系统不再引用位图后,很快就会回收内存
    • Android 2.3.3(API 10)及更低版本上,位图的备份像素数据存储在本地内存中。它与存储在 Dalvik 堆中的位图本身是分开的。本地内存中的像素数据并不以可预测的方式释放,可能会导致应用短暂超出其内存限制并崩溃。从 Android 3.0(API 11)到 Android 7.1(API 25),像素数据会与关联的位图一起存储在 Dalvik 堆中。在 Android 8.0(API 26)及更高版本中,位图像素数据存储在原生堆中
  • Android 2.3.3 及更低版本上管理内存

    • 在 Android 2.3.3(API 10)及更低版本上,建议使用 recycle() 方法,应用可以尽快回收内存,因为当应用中显示大量位图数据时可能会遇到 OutOfMemoryError 异常

    • 只有当确定位图不再使用时才应该使用 recycle() 方法,如果在调用 recycle() 方法之后尝试绘制位图,则会收到错误:Canvas: trying to use a recycled bitmap

    • Demo: recycle() 方法使用示例

      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
      private int cacheRefCount = 0;
      private int displayRefCount = 0;
      ...
      // Notify the drawable that the displayed state has changed.
      // Keep a count to determine when the drawable is no longer displayed.
      public void setIsDisplayed(boolean isDisplayed) {
      synchronized (this) {
      if (isDisplayed) {
      displayRefCount++;
      hasBeenDisplayed = true;
      } else {
      displayRefCount--;
      }
      }
      // Check to see if recycle() can be called.
      checkState();
      }

      // Notify the drawable that the cache state has changed.
      // Keep a count to determine when the drawable is no longer being cached.
      public void setIsCached(boolean isCached) {
      synchronized (this) {
      if (isCached) {
      cacheRefCount++;
      } else {
      cacheRefCount--;
      }
      }
      // Check to see if recycle() can be called.
      checkState();
      }

      private synchronized void checkState() {
      // If the drawable cache and display ref counts = 0, and this drawable
      // has been displayed, then recycle.
      if (cacheRefCount <= 0 && displayRefCount <= 0 && hasBeenDisplayed
      && hasValidBitmap()) {
      getBitmap().recycle();
      }
      }

      private synchronized boolean hasValidBitmap() {
      Bitmap bitmap = getBitmap();
      return bitmap != null && !bitmap.isRecycled();
      }
      • 使用引用计数(在变量 mDisplayRefCountmCacheRefCount 中)来跟踪位图当前是显示状态还是缓存状态。当满足以下条件时,代码便会回收位图:
        • mDisplayRefCountmCacheRefCount 的引用计数均为 0
        • 位图不是 null,并且尚未被回收
  • Android 3.0 及更高版本上管理内存

    • BitmapFactory.Options.inBitmap 字段

      • Android 3.0(API 11)引入了 BitmapFactory.Options.inBitmap 字段。如果设置了此选项,那么采用 Options 对象的解码方法会在加载内容时尝试重复使用现有位图。这意味着位图的内存得到了重复使用,从而提高了性能,同时移除了内存分配和取消分配
      • 但是,inBitmap 的使用方式存在某些限制,特别是在 Android 4.4(API 19)之前,系统仅支持大小相同的位图,详情可参阅 inBitmap 文档
    • 保存位图供稍后使用

      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
      Set<SoftReference<Bitmap> reusableBitmaps;
      private LruCache<String, BitmapDrawable> memoryCache;

      // If you're running on Honeycomb or newer, create a
      // synchronized HashSet of references to reusable bitmaps.
      if (Utils.hashHoneycomb) {
      reusableBitmaps =
      Collections.synchronizedSet(new HashSet<SoftReference<Bitmap>>());
      }

      memoryCache = new LruCache<String, BitmapDrawable>(cacheParams.memCacheSize) {

      // Notify the removed entry that is no longer being cached.
      @Override
      protected void entryRemoved(boolean evicted, String key, BitmapDrawable oldValue, BitmapDrawable newValue) {
      if (RecyclingBitmapDrawable.class.isInstance(oldValue)) {
      // The removed entry is a recycling drawable, so notify it
      // that it has been removed from the memory cache.
      ((RecyclingBitmapDrawable) oldValue).setIsCached(false);
      } else {
      // The removed entry is a standard BitmapDrawable.
      if (Utils.hasHoneycomb()) {
      // We're running on Honeycomb or later, so add the bitmap
      // to a SoftReference set for possible use with inBitmap later.
      reusableBitmaps.add(new SoftReference<Bitmap>(oldValue.getBitmap()));
      }
      }
      }
      }
      • 当应用在 Android 3.0 或更高版本上运行并且位图从 LruCache 删除时,对位图的软引用会放置在 HashSet 中,以供稍后通过 inBitmap 重复使用
    • 使用现有位图

      • 在正在运行的应用中,解码器方法会检查是否存在可以使用的现有位图

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        public static Bitmap decodeSampledBitmapFromFile(String filename,
        int reqWidth, int reqHeight, ImageCache cache) {

        final BitmapFactory.Options options = new BitmapFactory.Options();
        ...
        BitmapFactory.decodeFile(filename, options);
        ...

        // If we're running on Honeycomb or newer, try to use inBitmap.
        if (Utils.hasHoneycomb()) {
        addInBitmapOptions(options, cache);
        }
        ...
        return BitmapFactory.decodeFile(filename, options);
        }
      • 下面代码是 addInBitmapOptions() 方法的实现,它会查找现有的位图以设为 inBitmap 的值。需要注意的是,只有找到合适的匹配项,此方法才会为 inBitmap 设置一个值(不应假定会找到匹配项)

        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
        private static void addInBitmapOptions(BitmapFactory.Options options, ImageCache cache) {
        // inBitmap only works with mutable bitmaps, so force the decoder to
        // return mutable bitmaps.
        options.inMutable = true;

        if (cache != null) {
        // Try to find a bitmap to use for inBitmap.
        Bitmap inBitmap = cache.getBitmapFromReusableSet(options);

        if (inBitmap != null) {
        // If a suitable bitmap has been found, set it as the value of
        // inBitmap.
        options.inBitmap = inBitmap;
        }
        }
        }

        // This method interates through the reusable bitmaps, looking for one
        // to use for inBitmap.
        protected Bitmap getBitmapFromReusableSet(BitmaoFactory.Options options) {
        Bitmap bitmap = null;

        if (reusableBitmaps != null && !reusableBitmaps.isEmpty()) {
        synchronized (reusableBitmaps) {
        final Iterator<SoftReference<Bitmap>> iterator = reusableBitmaps.iterator();
        Bitmap item;

        while (iterator.hasNext()) {
        item = iterator.next().get();

        if (null != item && item.isMutable()) {
        // Check to see it the item can be used for inBitmap.
        if (canUseForInBitmap(item, options)) {
        bitmap = item;

        // Remove from reusable set so it can't be used again.
        iterator.remove();
        break;
        }
        } else {
        // Remove from the set if the reference has been cleared.
        iterator.remove();
        }
        }
        }
        }
        return bitmap;
        }
      • 最后,此方法会确定用于 inBitmap 的候选位图是否满足相应的大小条件

        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
        static boolean canUseForInBitmap(Bitmap candidate, BitmapFactory.Options targetOptions) {

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODE.KITKAT) {
        // From Android 4.4 (KitKat) onward we can re-use if the byte size of
        // the new bitmap is smaller than the reusable bitmap candidate
        // allocation byte count.
        int width = targetOptions.outWidth / targetOptions.inSampleSize;
        int height = targetOptions.outHeight / targetOptions.inSampleSize;
        int byteCount = width * height * getBytesPerxel(candidate.getConfig());
        return byteCount <= candidate.getAllocationByteCount();
        }

        // On earlier versions, the dimensions must match exactly and the inSampleSize must be 1
        return candidate.getWidth() == targetOptions.outWidth
        && candidate.getHeight() == targetOptions.outHeight
        && targetOptions.inSampleSize == 1;
        }

        /**
        * A helper function to return the byte usage per pixel of a bitmap based on its configuration.
        */
        static int getBytesPerPixel(Config cofig) {
        if (config == Config.ARGB_8888) {
        return 4;
        } else if (config == Config.RGB_565) {
        return 2;
        } else if (config == Config.ARGB_4444) {
        return 2;
        } else if (config == Config.ALPHA_8) {
        return 1;
        }
        return 1;
        }
-------------------- 本文结束感谢您的阅读 --------------------