1 /*
2  * Copyright (C) 2018 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 android.util.imagepool;
18 
19 import com.android.tools.layoutlib.annotations.Nullable;
20 import com.android.tools.layoutlib.annotations.VisibleForTesting;
21 
22 import android.util.imagepool.Bucket.BucketCreationMetaData;
23 import android.util.imagepool.ImagePool.Image.Orientation;
24 
25 import java.awt.image.BufferedImage;
26 import java.awt.image.DataBufferInt;
27 import java.lang.ref.Reference;
28 import java.util.Arrays;
29 import java.util.HashMap;
30 import java.util.HashSet;
31 import java.util.Map;
32 import java.util.Set;
33 import java.util.concurrent.locks.ReentrantReadWriteLock;
34 import java.util.function.Consumer;
35 
36 import com.google.common.base.FinalizablePhantomReference;
37 import com.google.common.base.FinalizableReferenceQueue;
38 
39 class ImagePoolImpl implements ImagePool {
40 
41     private final ReentrantReadWriteLock mReentrantLock = new ReentrantReadWriteLock();
42     private final ImagePoolPolicy mPolicy;
43     @VisibleForTesting final Map<String, Bucket> mPool = new HashMap<>();
44     @VisibleForTesting final ImagePoolStats mImagePoolStats = new ImagePoolStatsProdImpl();
45     private final FinalizableReferenceQueue mFinalizableReferenceQueue = new FinalizableReferenceQueue();
46     private final Set<Reference<?>> mReferences = new HashSet<>();
47 
ImagePoolImpl(ImagePoolPolicy policy)48     public ImagePoolImpl(ImagePoolPolicy policy) {
49         mPolicy = policy;
50         mImagePoolStats.start();
51     }
52 
53     @Override
acquire(int w, int h, int type)54     public Image acquire(int w, int h, int type) {
55         return acquire(w, h, type, null);
56     }
57 
acquire(int w, int h, int type, @Nullable Consumer<BufferedImage> freedCallback)58     /* package private */ Image acquire(int w, int h, int type,
59             @Nullable Consumer<BufferedImage> freedCallback) {
60         mReentrantLock.writeLock().lock();
61         try {
62             BucketCreationMetaData metaData =
63                     ImagePoolHelper.getBucketCreationMetaData(w, h, type, mPolicy, mImagePoolStats);
64             if (metaData == null) {
65                 return defaultImageImpl(w, h, type, freedCallback);
66             }
67 
68             final Bucket existingBucket = ImagePoolHelper.getBucket(mPool, metaData, mPolicy);
69             final BufferedImage img =
70                     ImagePoolHelper.getBufferedImage(existingBucket, metaData, mImagePoolStats);
71             if (img == null) {
72                 return defaultImageImpl(w, h, type, freedCallback);
73             }
74 
75             // Clear the image. - is this necessary?
76             if (img.getRaster().getDataBuffer().getDataType() == java.awt.image.DataBuffer.TYPE_INT) {
77                 Arrays.fill(((DataBufferInt)img.getRaster().getDataBuffer()).getData(), 0);
78             }
79 
80             return prepareImage(
81                     new ImageImpl(w, h, img, metaData.mOrientation),
82                     true,
83                     img,
84                     existingBucket,
85                     freedCallback);
86         } finally {
87             mReentrantLock.writeLock().unlock();
88         }
89     }
90 
91     /**
92      * Add statistics as well as dispose behaviour before returning image.
93      */
prepareImage( Image image, boolean offerBackToBucket, @Nullable BufferedImage img, @Nullable Bucket existingBucket, @Nullable Consumer<BufferedImage> freedCallback)94     private Image prepareImage(
95             Image image,
96             boolean offerBackToBucket,
97             @Nullable BufferedImage img,
98             @Nullable Bucket existingBucket,
99             @Nullable Consumer<BufferedImage> freedCallback) {
100         final Integer imageHash = image.hashCode();
101         mImagePoolStats.acquiredImage(imageHash);
102         FinalizablePhantomReference<Image> reference =
103                 new FinalizablePhantomReference<ImagePool.Image>(image, mFinalizableReferenceQueue) {
104                     @Override
105                     public void finalizeReferent() {
106                         // This method might be called twice if the user has manually called the free() method. The second call will have no effect.
107                         if (mReferences.remove(this)) {
108                             mImagePoolStats.disposeImage(imageHash);
109                             if (offerBackToBucket) {
110                                 if (!mImagePoolStats.fitsMaxCacheSize(img.getWidth(), img.getHeight(),
111                                         mPolicy.mBucketMaxCacheSize)) {
112                                     mImagePoolStats.tooBigForCache();
113                                     // Adding this back would go over the max cache size we set for ourselves. Release it.
114                                     return;
115                                 }
116 
117                                 // else stat does not change.
118                                 existingBucket.offer(img);
119                             }
120                             if (freedCallback != null) {
121                                 freedCallback.accept(img);
122                             }
123                         }
124                     }
125                 };
126         mReferences.add(reference);
127         return image;
128     }
129 
130     /**
131      * Default Image Impl to be used when the pool is not big enough.
132      */
defaultImageImpl(int w, int h, int type, @Nullable Consumer<BufferedImage> freedCallback)133     private Image defaultImageImpl(int w, int h, int type,
134             @Nullable Consumer<BufferedImage> freedCallback) {
135         BufferedImage bufferedImage = new BufferedImage(w, h, type);
136         mImagePoolStats.tooBigForCache();
137         mImagePoolStats.recordAllocOutsidePool(w, h);
138         return prepareImage(new ImageImpl(w, h, bufferedImage, Orientation.NONE),
139                 false,  null, null, freedCallback);
140     }
141 
142     @Override
dispose()143     public void dispose() {
144         mReentrantLock.writeLock().lock();
145         try {
146             for (Bucket bucket : mPool.values()) {
147                 bucket.clear();
148             }
149             mImagePoolStats.clear();
150         } finally {
151             mReentrantLock.writeLock().unlock();
152         }
153     }
154 
printStat()155     /* package private */ void printStat() {
156         System.out.println(mImagePoolStats.getStatistic());
157     }
158 }