summaryrefslogtreecommitdiffstats
path: root/src/com/android/photos/data/SparseArrayBitmapPool.java
blob: 95e10267b28130fa45285973cfe31091f2e42352 (plain)
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
/*
 * Copyright (C) 2013 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.photos.data;

import android.graphics.Bitmap;
import android.util.SparseArray;

import android.util.Pools.Pool;
import android.util.Pools.SimplePool;

/**
 * Bitmap pool backed by a sparse array indexing linked lists of bitmaps
 * sharing the same width. Performance will degrade if using this to store
 * many bitmaps with the same width but many different heights.
 */
public class SparseArrayBitmapPool {

    private int mCapacityBytes;
    private SparseArray<Node> mStore = new SparseArray<Node>();
    private int mSizeBytes = 0;

    private Pool<Node> mNodePool;
    private Node mPoolNodesHead = null;
    private Node mPoolNodesTail = null;

    protected static class Node {
        Bitmap bitmap;

        // Each node is part of two doubly linked lists:
        // - A pool-level list (accessed by mPoolNodesHead and mPoolNodesTail)
        //   that is used for FIFO eviction of nodes when the pool gets full.
        // - A bucket-level list for each index of the sparse array, so that
        //   each index can store more than one item.
        Node prevInBucket;
        Node nextInBucket;
        Node nextInPool;
        Node prevInPool;
    }

    /**
     * @param capacityBytes Maximum capacity of the pool in bytes.
     * @param nodePool Shared pool to use for recycling linked list nodes, or null.
     */
    public SparseArrayBitmapPool(int capacityBytes, Pool<Node> nodePool) {
        mCapacityBytes = capacityBytes;
        if (nodePool == null) {
            mNodePool = new SimplePool<Node>(32);
        } else {
            mNodePool = nodePool;
        }
    }

    /**
     * Set the maximum capacity of the pool, and if necessary trim it down to size.
     */
    public synchronized void setCapacity(int capacityBytes) {
        mCapacityBytes = capacityBytes;

        // No-op unless current size exceeds the new capacity.
        freeUpCapacity(0);
    }

    private void freeUpCapacity(int bytesNeeded) {
        int targetSize = mCapacityBytes - bytesNeeded;
        // Repeatedly remove the oldest node until we have freed up at least bytesNeeded.
        while (mPoolNodesTail != null && mSizeBytes > targetSize) {
            unlinkAndRecycleNode(mPoolNodesTail, true);
        }
    }

    private void unlinkAndRecycleNode(Node n, boolean recycleBitmap) {
        // Unlink the node from its sparse array bucket list.
        if (n.prevInBucket != null) {
            // This wasn't the head, update the previous node.
            n.prevInBucket.nextInBucket = n.nextInBucket;
        } else {
            // This was the head of the bucket, replace it with the next node.
            mStore.put(n.bitmap.getWidth(), n.nextInBucket);
        }
        if (n.nextInBucket != null) {
            // This wasn't the tail, update the next node.
            n.nextInBucket.prevInBucket = n.prevInBucket;
        }

        // Unlink the node from the pool-wide list.
        if (n.prevInPool != null) {
            // This wasn't the head, update the previous node.
            n.prevInPool.nextInPool = n.nextInPool;
        } else {
            // This was the head of the pool-wide list, update the head pointer.
            mPoolNodesHead = n.nextInPool;
        }
        if (n.nextInPool != null) {
            // This wasn't the tail, update the next node.
            n.nextInPool.prevInPool = n.prevInPool;
        } else {
            // This was the tail, update the tail pointer.
            mPoolNodesTail = n.prevInPool;
        }

        // Recycle the node.
        n.nextInBucket = null;
        n.nextInPool = null;
        n.prevInBucket = null;
        n.prevInPool = null;
        mSizeBytes -= n.bitmap.getByteCount();
        if (recycleBitmap) n.bitmap.recycle();
        n.bitmap = null;
        mNodePool.release(n);
    }

    /**
     * @return Capacity of the pool in bytes.
     */
    public synchronized int getCapacity() {
        return mCapacityBytes;
    }

    /**
     * @return Total size in bytes of the bitmaps stored in the pool.
     */
    public synchronized int getSize() {
        return mSizeBytes;
    }

    /**
     * @return Bitmap from the pool with the desired height/width or null if none available.
     */
    public synchronized Bitmap get(int width, int height) {
        Node cur = mStore.get(width);

        // Traverse the list corresponding to the width bucket in the
        // sparse array, and unlink and return the first bitmap that
        // also has the correct height.
        while (cur != null) {
            if (cur.bitmap.getHeight() == height) {
                Bitmap b = cur.bitmap;
                unlinkAndRecycleNode(cur, false);
                return b;
            }
            cur = cur.nextInBucket;
        }
        return null;
    }

    /**
     * Adds the given bitmap to the pool.
     * @return Whether the bitmap was added to the pool.
     */
    public synchronized boolean put(Bitmap b) {
        if (b == null) {
            return false;
        }

        // Ensure there is enough room to contain the new bitmap.
        int bytes = b.getByteCount();
        freeUpCapacity(bytes);

        Node newNode = mNodePool.acquire();
        if (newNode == null) {
            newNode = new Node();
        }
        newNode.bitmap = b;

        // We append to the head, and freeUpCapacity clears from the tail,
        // resulting in FIFO eviction.
        newNode.prevInBucket = null;
        newNode.prevInPool = null;
        newNode.nextInPool = mPoolNodesHead;
        mPoolNodesHead = newNode;

        // Insert the node into its appropriate bucket based on width.
        int key = b.getWidth();
        newNode.nextInBucket = mStore.get(key);
        if (newNode.nextInBucket != null) {
            // The bucket already had nodes, update the old head.
            newNode.nextInBucket.prevInBucket = newNode;
        }
        mStore.put(key, newNode);

        if (newNode.nextInPool == null) {
            // This is the only node in the list, update the tail pointer.
            mPoolNodesTail = newNode;
        } else {
            newNode.nextInPool.prevInPool = newNode;
        }
        mSizeBytes += bytes;
        return true;
    }

    /**
     * Empty the pool, recycling all the bitmaps currently in it.
     */
    public synchronized void clear() {
        // Clearing is equivalent to ensuring all the capacity is available.
        freeUpCapacity(mCapacityBytes);
    }
}