大多数情况下,安卓应用的运行内存,res目录下图片资源占了一大部分,虽然系统对Resource增加了缓存机制,但是仍然还是有优化空间的。
1.Resources#loadDrawable存在优化空间。
2.在Activity或者Fragment销毁的时候,部分Bitmap是可以及时回收的。
先贴出代码,后续慢慢完善
BitmapHelper.java
import android.app.Activity; import android.content.Context; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.os.Build; import android.support.annotation.NonNull; import android.support.v4.app.Fragment; import android.support.v4.util.ArrayMap; import android.support.v4.util.LongSparseArray; import android.text.TextUtils; import android.util.TypedValue; import android.view.View; import android.widget.ImageView; import com.<span style="font-family:FangSong_GB2312;">lorss</span>.util.IBLog; import java.lang.ref.WeakReference; import java.util.Map; /** * Created by <span style="font-family:FangSong_GB2312;">lorss</span> on 16-4-20. */ public class BitmapHelper { public static final String TAG = "Mem#BitmapHelper"; private static final boolean CACHE_RESOURCES = true; private static int sReqWidth; private static int sReqHeight; private static LongSparseArray<ResourceCache> sResourceCaches; /** * return the {@link cn.qihoo.reader.util.BitmapHelper.ResourceCache} related to target, every target holds single one */ public static ResourceCache getResourceCache(Object target) { if (target == null) { return ResourceCache.getSingleInstance(); } else { if (sResourceCaches == null) sResourceCaches = new LongSparseArray<>(); ResourceCache cache; int key = target.hashCode(); if ((cache = sResourceCaches.get(key)) == null) { cache = ResourceCache.newInstance(); sResourceCaches.put(key, cache); IBLog.v(ResourceCache.TAG, "# : sResourceCaches put " + target.getClass().getSimpleName()); } return cache; } } /** * recycle all the Resource Cache.<br/> * Please ensure that all the Bitmap cached not used again after recycled, otherwise it will throw Exception. * <br/>See : {@link ResourceCache#recycleAll()} */ public static void recycleAllResourceCache() { IBLog.v(ResourceCache.TAG, "#recycleAllResourceCache : "); if (sResourceCaches != null) { for (int i = 0; i < sResourceCaches.size(); i++) { ResourceCache cache = sResourceCaches.valueAt(i); if (cache != null) { cache.recycleAll(); } } sResourceCaches.clear(); } } public static void loadBackground(Fragment fragment, int viewId, int resId) { if (fragment == null || viewId < 0 || resId < 0) return; Activity activity = fragment.getActivity(); if (activity != null) { loadBackground(fragment, activity.findViewById(viewId), resId); } } public static void loadBackground(Activity activity, int viewId, int resId) { if (activity == null || viewId < 0 || resId < 0) return; View view = activity.findViewById(viewId); loadBackground(activity, view, resId); } public static void loadBackground(Context context, View view, int resId) { if (context == null || view == null || resId < 0) return; loadBackground(view, context.getResources(), resId, CACHE_RESOURCES ? getResourceCache(context) : null, false); } public static void loadBackground(Fragment fragment, View view, int resId) { if (fragment == null || view == null || resId < 0) return; loadBackground(view, fragment.getResources(), resId, CACHE_RESOURCES ? getResourceCache(fragment) : null, false); } /** * @param exactSize see {@link #decodeSampledBitmapFromResource(Resources, int, int, int, boolean, ResourceCache)} */ private static void loadBackground(View view, Resources res, int resId, ResourceCache cache, boolean exactSize) { IBLog.v(TAG, "#loadBackground : resId = " + resId); if (view == null || res == null || resId < 0) return; final Resources r = res; loadDrawable(view, res, resId, cache, exactSize, new Callback<View, Drawable>() { @Override public void callback(View view, Drawable drawable) { if (view != null && drawable != null) { setBackground(view, drawable); } } }); } public static void loadImage(Fragment fragment, int viewId, int resId) { if (fragment == null || viewId < 0 || resId < 0) return; Activity activity = fragment.getActivity(); View view; if (activity != null && (view = activity.findViewById(viewId)) instanceof ImageView) { loadImage(fragment, (ImageView) view, resId); } } public static void loadImage(Activity activity, int viewId, int resId) { if (activity == null || viewId < 0 || resId < 0) return; View view = activity.findViewById(viewId); if (view instanceof ImageView) loadImage(activity, (ImageView) view, resId); } public static void loadImage(Context context, ImageView view, int resId) { if (context == null || view == null || resId < 0) return; loadImage(view, context.getResources(), resId, CACHE_RESOURCES ? getResourceCache(context) : null, false); } public static void loadImage(Fragment fragment, ImageView view, int resId) { if (fragment == null || view == null || resId < 0) return; loadImage(view, fragment.getResources(), resId, CACHE_RESOURCES ? getResourceCache(fragment) : null, false); } /** * @param exactSize see {@link #decodeSampledBitmapFromResource(Resources, int, int, int, boolean, ResourceCache)} */ private static void loadImage(ImageView view, Resources res, int resId, ResourceCache cache, boolean exactSize) { IBLog.v(TAG, "#loadImage : resId = " + resId); if (view == null || res == null || resId < 0) return; final Resources r = res; loadDrawable(view, res, resId, cache, exactSize, new Callback<View, Drawable>() { @Override public void callback(View view, Drawable drawable) { if (view != null && drawable != null) { ((ImageView) view).setImageDrawable(drawable); } } }); } /** * @param exactSize see {@link #decodeSampledBitmapFromResource(Resources, int, int, int, boolean, ResourceCache)} */ private static void loadDrawable(View view, Resources res, int resId, ResourceCache cache, boolean exactSize, Callback<View, Drawable> callback) { IBLog.v(TAG, "#loadDrawable : resId = " + resId); if (view == null || res == null || resId < 0) return; Drawable drawable = null; if (isResourcePicture(res, resId)) { int reqWidth; int reqHeight; if (view.getWidth() != 0 && view.getHeight() != 0) { reqWidth = view.getWidth(); reqHeight = view.getHeight(); IBLog.v(TAG, "#loadDrawable : reqWidth = " + reqWidth + ", reqHeight = " + reqHeight); } else { initScreenSize(); reqWidth = sReqWidth; reqHeight = sReqHeight; } IBLog.v(TAG, "#loadDrawable : [isResourcePicture]"); Bitmap bitmap = decodeSampledBitmapFromResource(res, resId, reqWidth, reqHeight, exactSize, cache); drawable = new BitmapDrawable(res, bitmap); } else { IBLog.v(TAG, "#loadDrawable : [is NOT ResourcePicture]"); try { drawable = res.getDrawable(resId); } catch (Exception ignored) { } catch (Error ignored) { } } if (callback != null) callback.callback(view, drawable); } public static void setBackground(View view, Drawable drawable) { if (view == null) return; if (Build.VERSION.SDK_INT >= 16) { view.setBackground(drawable); } else { view.setBackgroundDrawable(drawable); } } private static void initScreenSize() { sReqWidth = ResolutionUtil.getScreenWidth(); sReqHeight = ResolutionUtil.getScreenHeight(); IBLog.v(TAG, "#initScreenSize : sReqWidth = " + sReqWidth); IBLog.v(TAG, "#initScreenSize : sReqHeight = " + sReqHeight); } /** * calculate the InSampleSize which indicates the rate to scale the loading Bitmap. */ public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) { //default inSampleSize int inSampleSize = 1; if (options == null) return inSampleSize; //size of original picture final int height = options.outHeight; final int width = options.outWidth; if (height > reqHeight || width > reqWidth) { // calculate the ratio for scaling final int heightRatio = Math.round((float) height / (float) reqHeight); final int widthRatio = Math.round((float) width / (float) reqWidth); inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio; } return inSampleSize; } /** * @param exactSize if true, to decode Bitmap by exact reqWidth and reqHeight; otherwise to scale by {@link #calculateInSampleSize(BitmapFactory.Options, int, int)} */ public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight, boolean exactSize, ResourceCache cache) { IBLog.v(TAG, "#decodeSampledBitmapFromResource : resId = " + resId); Bitmap bitmap = null; if (cache != null && (bitmap = cache.get(resId)) != null) { IBLog.v(TAG, "#decodeSampledBitmapFromResource : ResourceCache -- Hit !!!"); if (!bitmap.isRecycled()) { return bitmap; } else { IBLog.v(TAG, "#decodeSampledBitmapFromResource : ResourceCache -- Hit But Recycled !!!"); cache.remove(resId); //TODO m:<span style="font-family:FangSong_GB2312;">lorss</span> how to deal with the views which has set background with this Bitmap } } if (!isResourcePicture(res, resId)) { IBLog.v(TAG, "#decodeSampledBitmapFromResource : [is not ResourcePicture]"); } else if (exactSize) { IBLog.v(TAG, "#decodeSampledBitmapFromResource : [exactSize]"); final BitmapFactory.Options op = new BitmapFactory.Options(); op.outWidth = reqWidth; op.outHeight = reqHeight; op.inPreferredConfig = Bitmap.Config.RGB_565; try { bitmap = BitmapFactory.decodeResource(res, resId, op); } catch (Exception ignored) { } catch (Error ignored) { } } else { IBLog.v(TAG, "#decodeSampledBitmapFromResource : [InSample]"); try { if (res == null || resId < 0 || reqWidth == 0 || reqHeight == 0) return null; // get the size of original picture final BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeResource(res, resId, options); options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); options.inPreferredConfig = Bitmap.Config.RGB_565; IBLog.v(TAG, "#decodeSampledBitmapFromResource : resId = " + resId + " , inSampleSize = " + options.inSampleSize); options.inJustDecodeBounds = false; bitmap = BitmapFactory.decodeResource(res, resId, options); } catch (Exception ignored) { } catch (Error ignored) { } } if (bitmap != null && cache != null) { cache.addBitmapToMemoryCache(resId, bitmap); } return bitmap; } /** * whether the Resource represented by resId is picture(e.g. jpg or png). */ public static boolean isResourcePicture(Resources res, int resId) { IBLog.v(TAG, "#isResourcePicture : resId = " + resId); if (res == null || resId < 0) return false; TypedValue value = new TypedValue(); res.getValue(resId, value, true); IBLog.v(TAG, "#isResourcePicture : resId = " + resId + ", file = " + value.string); if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT && value.type <= TypedValue.TYPE_LAST_COLOR_INT) { //isColorDrawable IBLog.v(TAG, "#isResourcePicture : resId = " + resId + " -- isColorDrawable!!!"); return false; } if (!TextUtils.isEmpty(value.string)) { final String file = value.string.toString(); if (file.endsWith(".xml")) { //isDrawable but xml return false; } } return true; } public static int bytesOf(Bitmap bitmap) { if (bitmap == null) return 0; if (Build.VERSION.SDK_INT >= 19) { return bitmap.getAllocationByteCount(); } else if (Build.VERSION.SDK_INT >= 12) { return bitmap.getByteCount(); } else { return bitmap.getRowBytes(); } } public static interface Callback<T, R> { void callback(T t, R r); } public static class ResourceCache extends LruCache<Integer, Bitmap> { private static ResourceCache sResourceCache; private LongSparseArray<WeakReference<Bitmap>> mLeakBitmaps; private static final String TAG = "Mem#ResourceCache"; /** * @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 ResourceCache(int maxSize) { super(maxSize); } /** * get the Single Instance. */ public static ResourceCache getSingleInstance() { if (sResourceCache == null) { synchronized (BitmapHelper.class) { if (sResourceCache == null) { sResourceCache = ResourceCache.newInstance(); } } } return sResourceCache; } public static ResourceCache newInstance() { long maxMemory/*KB*/ = (Runtime.getRuntime().maxMemory()/*byte*/ / 1024); long cacheSize = maxMemory / 8; if (cacheSize > Integer.MAX_VALUE) { IBLog.v(TAG, "#ResourceCache : cacheSize larger than Integer.MAX_VALUE(" + Integer.MAX_VALUE + ")"); } IBLog.v(TAG, "#ResourceCache : maxMemory = " + maxMemory + "KB, cacheSize = " + cacheSize + "KB"); return new ResourceCache((int) cacheSize); } @NonNull @Override protected Map<Integer, Bitmap> onCreateMap() { IBLog.v(TAG, "#onCreateMap : "); return new ArrayMap<Integer, Bitmap>(); } @Override protected int sizeOf(Integer key, Bitmap bitmap) { return bytesOf(bitmap) / 1024; } @Override protected void entryRemoved(boolean evicted, Integer key, Bitmap oldValue, Bitmap newValue) { IBLog.v(TAG, "#entryRemoved : "); if (evicted) {/*caused by evictAll (methods which called trimToSize) */ if (oldValue != null) { //m:<span style="font-family:FangSong_GB2312;">lorss</span> just record the leaked Bitmap which has not been recycled if (mLeakBitmaps == null) mLeakBitmaps = new LongSparseArray<>(); mLeakBitmaps.put(key, new WeakReference<Bitmap>(oldValue)); IBLog.v(TAG, "#entryRemoved : leakBitmaps add " + key); } } else /*caused by remove or put*/ { if (oldValue != null && !oldValue.isRecycled()) { try { oldValue.recycle(); IBLog.v(TAG, "#entryRemoved : @oldValue#recycle!!!"); System.gc(); } catch (Exception ignored) { } catch (Error ignored) { } } logStatus(); } } public void addBitmapToMemoryCache(Integer key, Bitmap bitmap) { IBLog.v(TAG, "#addBitmapToMemoryCache : "); if (get(key) == null) { put(key, bitmap); } logStatus(); } public LongSparseArray<WeakReference<Bitmap>> getLeakBitmaps() { return mLeakBitmaps; } /** * recycle all the Cached Resource, including the leaking Bitmap that has not been recycled which maybe is still being used. * <br/>Please ensure that all the Bitmap cached not used again after recycled, otherwise it will throw Exception. * <br/>See : {@link Bitmap#recycle()} */ public void recycleAll() { removeAll(); if (mLeakBitmaps != null) { for (int i = 0; i < mLeakBitmaps.size(); i++) { WeakReference<Bitmap> ref = mLeakBitmaps.valueAt(i); if (ref != null && ref.get() != null) { ref.get().recycle(); } } } } private void logStatus() { IBLog.v(TAG, "#logStatus : Size : " + size() + "/" + maxSize() + ", hitCount : " + hitCount() + ", missCount : " + missCount()); IBLog.v(TAG, toString()); } } }
LruCache.java
/* * Copyright (C) 2011 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. */ import android.support.annotation.NonNull; import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; /** * BEGIN LAYOUTLIB CHANGE * This is a custom version that doesn't use the non standard LinkedHashMap#eldest. * END LAYOUTLIB CHANGE * <p/> * 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/> * <p>If your cached values hold resources that need to be explicitly released, * override {@link #entryRemoved}. * <p/> * <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/> * <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/> * <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/> * <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/> * <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> { private final Map<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 = onCreateMap(); } @NonNull protected Map<K, V> onCreateMap() { return new LinkedHashMap<K, V>(0, 0.75f, true); } /** * Sets the size of the cache. * * @param maxSize The new maximum size. * @hide */ 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(maxSize); return previous; } /** * @param maxSize the maximum size of the cache before returning. May be -1 * to evict even 0-sized elements. */ private 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; } // BEGIN LAYOUTLIB CHANGE // get the last item in the linked list. // This is not efficient, the goal here is to minimize the changes // compared to the platform version. Map.Entry<K, V> toEvict = null; for (Map.Entry<K, V> entry : map.entrySet()) { toEvict = entry; } // END LAYOUTLIB CHANGE 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; } /** * Called for entries that have been evicted or removed. This method is * invoked when a value is evicted to make space, removed by a call to * {@link #remove}, or replaced by a call to {@link #put}. The default * implementation does nothing. * <p/> * <p>The method is called without synchronization: other threads may * access the cache while this method is executing. * * @param evicted true if the entry is being removed to make space, false * if the removal was caused by a {@link #put} or {@link #remove}. * @param newValue the new value for {@code key}, if it exists. If non-null, * this removal was caused by a {@link #put}. Otherwise it was caused by * an eviction or a {@link #remove}. */ protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) { } /** * Called after a cache miss to compute a value for the corresponding key. * Returns the computed value or null if no value can be computed. The * default implementation returns null. * <p/> * <p>The method is called without synchronization: other threads may * access the cache while this method is executing. * <p/> * <p>If a value for {@code key} exists in the cache when this method * returns, the created value will be released with {@link #entryRemoved} * and discarded. This can occur when multiple threads request the same key * at the same time (causing multiple values to be created), or when one * thread calls {@link #put} while another is creating a value for the same * key. */ protected V create(K key) { return null; } private int safeSizeOf(K key, V value) { int result = sizeOf(key, value); if (result < 0) { throw new IllegalStateException("Negative size: " + key + "=" + value); } return result; } /** * Returns the size of the entry for {@code key} and {@code value} in * user-defined units. The default implementation returns 1 so that size * is the number of entries and max size is the maximum number of entries. * <p/> * <p>An entry's size must not change while it is in the cache. */ protected int sizeOf(K key, V value) { return 1; } /** * Clear the cache, calling {@link #entryRemoved} on each removed entry. */ public final void evictAll() { trimToSize(-1); // -1 will evict 0-sized elements } /** * For caches that do not override {@link #sizeOf}, this returns the number * of entries in the cache. For all other caches, this returns the sum of * the sizes of the entries in this cache. */ public synchronized final int size() { return size; } /** * For caches that do not override {@link #sizeOf}, this returns the maximum * number of entries in the cache. For all other caches, this returns the * maximum sum of the sizes of the entries in this cache. */ public synchronized final int maxSize() { return maxSize; } /** * Returns the number of times {@link #get} returned a value that was * already present in the cache. */ public synchronized final int hitCount() { return hitCount; } /** * Returns the number of times {@link #get} returned null or required a new * value to be created. */ public synchronized final int missCount() { return missCount; } /** * Returns the number of times {@link #create(Object)} returned a value. */ public synchronized final int createCount() { return createCount; } /** * Returns the number of times {@link #put} was called. */ public synchronized final int putCount() { return putCount; } /** * Returns the number of values that have been evicted. */ public synchronized final int evictionCount() { return evictionCount; } /** * Returns a copy of the current contents of the cache, ordered from least * recently accessed to most recently accessed. */ public synchronized final Map<K, V> snapshot() { return new LinkedHashMap<K, V>(map); } @Override public synchronized final String toString() { int accesses = hitCount + missCount; int hitPercent = accesses != 0 ? (100 * hitCount / accesses) : 0; return String.format("LruCache[maxSize=%d,hits=%d,misses=%d,hitRate=%d%%]", maxSize, hitCount, missCount, hitPercent); } /** * Return whether the Cache contains value to the key. */ public boolean contains(K key) { if (map != null) { Set<K> set; if ((set = map.keySet()) != null) { return set.contains(key); } } return false; } /** * Clear the cache, calling {@link #remove(Object)} on each removed entry. */ public void removeAll() { if (map != null) { Set<K> set; if ((set = map.keySet()) != null) { for (K key : set) { if (key != null) { remove(key); } } } } } }
在Fragment和Activity销毁时候调用{@link BitmapHelper#getResourceCache(Object)}和{@link ResourceCache#recycleAll()}
//Activity @Override protected void onDestroy() { super.onDestroy(); BitmapHelper.getResourceCache(this).recycleAll(); }
//Fragment @Override public void onDestroyView() { super.onDestroyView(); BitmapHelper.getResourceCache(this).recycleAll(); }在应用退出时,或者所有界面销毁时,调用{@link BitmapHelper#recycleAllResourceCache()}
//Application exit or all foreground item exit. @Override public void onApplicationDestroy() { BitmapHelper.recycleAllResourceCache(); }