标签:
安卓应用的运行内存不像PC那样富有,虽然java本身也加入了GC垃圾回收机制,但是对内存的优化还是有一定必要的,尤其在一些低配手机上,内存小的问题更突出,OOM也更常见。
----------------------------------------------------------------------------------
一个安卓应用项目中,res目录下图片资源占了一大部分,虽然系统对Resource增加了缓存机制,但是仍然还是可以进行一些优化的,这篇文章主要说一下app中res下图片资源的加载过程中可能进行的一些简单优化。
----------------------------------------------------------------------------------
安卓控件设置背景,图片资源时一般会调用View#setBackgroundResource(int)和ImageView#setImageResource(int)等方法,
这些方法最终都会通过调用 Resources#getDrawable(int) ,Resources#loadDrawable(TypedValue, int)来加载Drawable资源,而该方法加载图片,颜色,xml资源时,是分开处理的,系统源码如下所示:
1.Drawable#loadDrawable
/*package*/ Drawable loadDrawable(TypedValue value, int id) throws NotFoundException { if (TRACE_FOR_PRELOAD) { // Log only framework resources if ((id >>> 24) == 0x1) { final String name = getResourceName(id); if (name != null) android.util.Log.d("PreloadDrawable", name); } } boolean isColorDrawable = false; if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT && value.type <= TypedValue.TYPE_LAST_COLOR_INT) { isColorDrawable = true; } final long key = isColorDrawable ? value.data : (((long) value.assetCookie) << 32) | value.data; Drawable dr = getCachedDrawable(isColorDrawable ? mColorDrawableCache : mDrawableCache, key); if (dr != null) { return dr; } Drawable.ConstantState cs; if (isColorDrawable) { cs = sPreloadedColorDrawables.get(key); } else { cs = sPreloadedDrawables[mConfiguration.getLayoutDirection()].get(key); } if (cs != null) { dr = cs.newDrawable(this); } else { if (isColorDrawable) { dr = new ColorDrawable(value.data); } if (dr == null) { if (value.string == null) { throw new NotFoundException( "Resource is not a Drawable (color or path): " + value); } String file = value.string.toString(); if (TRACE_FOR_MISS_PRELOAD) { // Log only framework resources if ((id >>> 24) == 0x1) { final String name = getResourceName(id); if (name != null) android.util.Log.d(TAG, "Loading framework drawable #" + Integer.toHexString(id) + ": " + name + " at " + file); } } if (DEBUG_LOAD) Log.v(TAG, "Loading drawable for cookie " + value.assetCookie + ": " + file); if (file.endsWith(".xml")) { Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, file); try { XmlResourceParser rp = loadXmlResourceParser( file, id, value.assetCookie, "drawable"); dr = Drawable.createFromXml(this, rp); rp.close(); } catch (Exception e) { Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); NotFoundException rnf = new NotFoundException( "File " + file + " from drawable resource ID #0x" + Integer.toHexString(id)); rnf.initCause(e); throw rnf; } Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); } else { Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, file); try { InputStream is = mAssets.openNonAsset( value.assetCookie, file, AssetManager.ACCESS_STREAMING); // System.out.println("Opened file " + file + ": " + is); dr = Drawable.createFromResourceStream(this, value, is, file, null); is.close(); // System.out.println("Created stream: " + dr); } catch (Exception e) { Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); NotFoundException rnf = new NotFoundException( "File " + file + " from drawable resource ID #0x" + Integer.toHexString(id)); rnf.initCause(e); throw rnf; } Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); } } } if (dr != null) { dr.setChangingConfigurations(value.changingConfigurations); cs = dr.getConstantState(); if (cs != null) { if (mPreloading) { final int changingConfigs = cs.getChangingConfigurations(); if (isColorDrawable) { if (verifyPreloadConfig(changingConfigs, 0, value.resourceId, "drawable")) { sPreloadedColorDrawables.put(key, cs); } } else { if (verifyPreloadConfig(changingConfigs, LAYOUT_DIR_CONFIG, value.resourceId, "drawable")) { if ((changingConfigs&LAYOUT_DIR_CONFIG) == 0) { // If this resource does not vary based on layout direction, // we can put it in all of the preload maps. sPreloadedDrawables[0].put(key, cs); sPreloadedDrawables[1].put(key, cs); } else { // Otherwise, only in the layout dir we loaded it for. final LongSparseArray<Drawable.ConstantState> preloads = sPreloadedDrawables[mConfiguration.getLayoutDirection()]; preloads.put(key, cs); } } } } else { synchronized (mAccessLock) { //Log.i(TAG, "Saving cached drawable @ #" + // Integer.toHexString(key.intValue()) // + " in " + this + ": " + cs); if (isColorDrawable) { mColorDrawableCache.put(key, new WeakReference<Drawable.ConstantState>(cs)); } else { mDrawableCache.put(key, new WeakReference<Drawable.ConstantState>(cs)); } } } } } return dr; }
其中,如果是色值,则
if (isColorDrawable) { dr = new ColorDrawable(value.data); }如果是xml资源,比如按钮背景selector,则最终调用Drawable#createFromXml(Resources, XmlResourceParser)来创建Drawable
if (file.endsWith(".xml")) { Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, file); try { XmlResourceParser rp = loadXmlResourceParser( file, id, value.assetCookie, "drawable"); dr = Drawable.createFromXml(this, rp); rp.close(); } catch (Exception e) { Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); NotFoundException rnf = new NotFoundException( "File " + file + " from drawable resource ID #0x" + Integer.toHexString(id)); rnf.initCause(e); throw rnf; } Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); }如果是图片资源(jpg,png),则最终调用Drawable#createFromResourceStream(Resources, TypedValue, InputStream, String, Option)来创建Drawable
if (file.endsWith(".xml")) { ... ... } else { Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, file); try { InputStream is = mAssets.openNonAsset( value.assetCookie, file, AssetManager.ACCESS_STREAMING); // System.out.println("Opened file " + file + ": " + is); dr = Drawable.createFromResourceStream(this, value, is, file, null); is.close(); // System.out.println("Created stream: " + dr); } catch (Exception e) { Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); NotFoundException rnf = new NotFoundException( "File " + file + " from drawable resource ID #0x" + Integer.toHexString(id)); rnf.initCause(e); throw rnf; } Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); }
到了这里,着重分析一下,我们也是主要从加载图片这一块来看一下优化思路。
接下来看一下Drawable#createFromResourceStream(Resources, TypedValue, InputStream, String, Option)方法
2.Drawable#createFromResourceStream(Resources, TypedValue, InputStream, String, Option)
/** * Create a drawable from an inputstream, using the given resources and * value to determine density information. */ public static Drawable createFromResourceStream(Resources res, TypedValue value, InputStream is, String srcName, BitmapFactory.Options opts) { if (is == null) { return null; } /* ugh. The decodeStream contract is that we have already allocated the pad rect, but if the bitmap does not had a ninepatch chunk, then the pad will be ignored. If we could change this to lazily alloc/assign the rect, we could avoid the GC churn of making new Rects only to drop them on the floor. */ Rect pad = new Rect(); // Special stuff for compatibility mode: if the target density is not // the same as the display density, but the resource -is- the same as // the display density, then don't scale it down to the target density. // This allows us to load the system's density-correct resources into // an application in compatibility mode, without scaling those down // to the compatibility density only to have them scaled back up when // drawn to the screen. if (opts == null) opts = new BitmapFactory.Options(); opts.inScreenDensity = res != null ? res.getDisplayMetrics().noncompatDensityDpi : DisplayMetrics.DENSITY_DEVICE; Bitmap bm = BitmapFactory.decodeResourceStream(res, value, is, pad, opts); if (bm != null) { byte[] np = bm.getNinePatchChunk(); if (np == null || !NinePatch.isNinePatchChunk(np)) { np = null; pad = null; } int[] layoutBounds = bm.getLayoutBounds(); Rect layoutBoundsRect = null; if (layoutBounds != null) { layoutBoundsRect = new Rect(layoutBounds[0], layoutBounds[1], layoutBounds[2], layoutBounds[3]); } return drawableFromBitmap(res, bm, np, pad, layoutBoundsRect, srcName); } return null; }
这里,参数Options opts传入的值为null,所以会创建一个初始配置对象,而创建对象的时候,
参数inPreferredConfig<Bitmap.Config > 默认值为Bitmap.Config.ARGB_8888,而该值则代表,图片每个像素会用8bit×4 = 32bit(4byte)来存储,即一张100x100的图片,会占用100×100×4 = 40000byte(大概4kb),然而如果我们把该参数设置为Bitmap.Config.RGB_565的话,图片每个像素会用5bit+6bit+5bit = 16bit(2byte)来存储,则会省掉一半内存。
public static class Options { /** * Create a default Options object, which if left unchanged will give * the same result from the decoder as if null were passed. */ public Options() { inDither = false; inScaled = true; inPremultiplied = true; } ... ... ... ... ... ... ... ... /** * If set to true, the decoder will return null (no bitmap), but * the out... fields will still be set, allowing the caller to query * the bitmap without having to allocate the memory for its pixels. */ public boolean inJustDecodeBounds; /** * If set to a value > 1, requests the decoder to subsample the original * image, returning a smaller image to save memory. The sample size is * the number of pixels in either dimension that correspond to a single * pixel in the decoded bitmap. For example, inSampleSize == 4 returns * an image that is 1/4 the width/height of the original, and 1/16 the * number of pixels. Any value <= 1 is treated the same as 1. Note: the * decoder uses a final value based on powers of 2, any other value will * be rounded down to the nearest power of 2. */ public int inSampleSize; /** * If this is non-null, the decoder will try to decode into this * internal configuration. If it is null, or the request cannot be met, * the decoder will try to pick the best matching config based on the * system's screen depth, and characteristics of the original image such * as if it has per-pixel alpha (requiring a config that also does). * * Image are loaded with the {@link Bitmap.Config#ARGB_8888} config by * default. */ public Bitmap.Config inPreferredConfig = Bitmap.Config.ARGB_8888; ... ... ... ... ... ... ... ... }
PS:Bitmap是一个极容易消耗内存的大胖子,减小创建出来的Bitmap的内存占用是很重要的,通常来说有下面2个措施:
- inSampleSize:缩放比例,在把图片载入内存之前,我们需要先计算出一个合适的缩放比例,避免不必要的大图载入。
- decode format:解码格式,选择ARGB_8888/RBG_565/ARGB_4444/ALPHA_8,存在很大差异。
在上面第二步中提到的Drawable#createFromResourceStream(Resources, TypedValue, InputStream, String, Option)加载Bitmap之后,Bitmap会通过Drawable#drawableFromBitmap(Resources, Bitmap, byte[], Rect, Rect, String)转化为Drawable
3.Drawable#drawableFromBitmap(Resources, Bitmap, byte[], Rect, Rect, String)
private static Drawable drawableFromBitmap(Resources res, Bitmap bm, byte[] np, Rect pad, Rect layoutBounds, String srcName) { if (np != null) { return new NinePatchDrawable(res, bm, np, pad, layoutBounds, srcName); } return new BitmapDrawable(res, bm); }public class BitmapDrawable extends Drawable { private BitmapState mBitmapState; ... ... ... ... /** * Create drawable from a bitmap, setting initial target density based on * the display metrics of the resources. */ public BitmapDrawable(Resources res, Bitmap bitmap) { this(new BitmapState(bitmap), res); mBitmapState.mTargetDensity = mTargetDensity; } ... ... ... ... private BitmapDrawable(BitmapState state, Resources res) { mBitmapState = state; if (res != null) { mTargetDensity = res.getDisplayMetrics().densityDpi; } else { mTargetDensity = state.mTargetDensity; } setBitmap(state != null ? state.mBitmap : null); } }
final static class BitmapState extends ConstantState { Bitmap mBitmap; BitmapState(Bitmap bitmap) { mBitmap = bitmap; } }
通过以上过程看到,最终加载到的Bitmap保存在了BitmapDrawable中mBitmapState<BitmapSate>中的mBitmap变量中。
到了这里,在回过头看之前的第一步的Resources#loadDrawable(TypedValue, int)中,最后几行代码,加载过Drawable之后,会通过弱引用来缓存该资源到LongSparseArray<WeakReference<Drawable.ConstantState> > mDrawableCache中
else { synchronized (mAccessLock) { //Log.i(TAG, "Saving cached drawable @ #" + // Integer.toHexString(key.intValue()) // + " in " + this + ": " + cs); if (isColorDrawable) { mColorDrawableCache.put(key, new WeakReference<Drawable.ConstantState>(cs)); } else { mDrawableCache.put(key, new WeakReference<Drawable.ConstantState>(cs)); } } }而这里的cs即为上面的mBitmapSate,其中保存着Bitmap的引用。
--------------------------------------------------------华丽的分割线---------------------------------------------------
通过以上分析,我们得出结论:
在加载图片的时候,虽然在Resources里面加入了弱引用缓存,但是好像是没有即时回收Bitmap的地方,虽然在3.0以上是不需要调用
Bitmap#recycle(),但是3.0之前的手机在创建Bitmap时,图片像素数据是在C层分配的内存,通过调用Bitmap#recycle()可以释放c层分配的内存,这里我们可以查看源代码:
Bitmap#recycle()
public void recycle() { if (!mRecycled) { if (nativeRecycle(mNativeBitmap)) { // return value indicates whether native pixel object was actually recycled. // false indicates that it is still in use at the native level and these // objects should not be collected now. They will be collected later when the // Bitmap itself is collected. mBuffer = null; mNinePatchChunk = null; } mRecycled = true; } }
private static native boolean nativeRecycle(int nativeBitmap);
1)统一管理图片加载,及时释放Bitmap内存
所以如果我们使用res目录下资源(尤其是图片)的时候,如果自己能统一管理所有的图片加载(主要是复用Bitmap,控制内存上限,记录引用方便以后释放内存),并且通过调用{@link Bitmap#recycle()}控制释放Bitmap内存的时机,是不是就可以减少内存使用。
2)降低Bitmap质量RGB
如上面分析的系统加载资源过程中,Resources#loadDrawable(TypedValue, int)最终调用
Drawable#createFromResourceStream(Resources, TypedValue, InputStream, String, Option)加载Bitmap的时候,没有传入Opition参数,所以使用的是新new出的默认配置对象,查看Bitmap.Opitions代码看到成员变量inPreferredConfig<Bitmap.Config > 默认值为Bitmap.Config.ARGB_8888。
所以如上文分析过的,我们可以指定参数Opitions#inPreferredConfig的值为BitmapFactory.Config.RGB_565来降低Bitmap质量,从而减少加载Bitmap需要的内存。(PS:当然前提是确定不会影响应用中图片背景的显示效果,不过通过该方法确实可以减少图片加载需要的内存,能减少大概一半的内存开销)
3)缩放图片尺寸
然而,在有些情况下,图片尺寸也是可以调整的,比如某张图片尺寸大于手机尺寸,那么大多数情况下我们是没有必要按照图片原尺寸加载的,这时我们就可以通过BitmapFactory.Opitions.inSampleSize等比例缩放原图,来控制每张图片加载的尺寸。当然我们也可以指定精确的长宽来加载,不过要确定不会使得原图片变形。
所以我们又可以通过Opitions参数中的BitmapFactory.Opitions#inSampleSize来指定缩放比例,
或者指定精确的长宽BitmapFactory.Opitions#outWidth和BitmapFactory.Opitions#outHeight,从而通过缩放图片来减少内存开销。
4)缓存Bitmap,复用Bitmap并控制内存上限
然而如果图片数量太大,还是会发生OOM,所以我们可以通过LruCache实现缓存,从而来控制内存上限,由于该类是API16以后新加的类,我们可以把源代码拷贝到自己工程中,并且可以自己加以修改,这里我们新增了onCreateMap方法,来自己创建Map,通过用ArrayMap替换LinkedHashMap,来节省一定的内存。如何使用ArrayMap
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); } } } } } }
所以我们可以使用单例缓存ResourceCache(代码见下面BitmapHelper.ResourceCache#getSingleInstance()),或者为每个Activity和Fragment设置单独缓存(BitmapHelper#getResourceCache(Object)),这样,在Activity或者Fragment退出时,可以及时针对性的回收掉当前使用的单独缓存ResourceCache。
结论)
根据以上阐述的所有优化点,我们编写BitmapHelper工具类,文章下面贴出代码BitmapHelper.java:
-> 我们可以使用BitmapHelper中的loadBackground(...)和loadImage(...)等一系列方法来替换代码中的
View#setBackgroundResource(int)和ImageView#setImageResource(int),从而统一管理图片资源设置和加载
-> 然后在合适的时机调用ResourceCache#recycleAll() 或者BitmapHelper#recycleAllResourceCache()来释放缓存中Bitmap内存
在Fragment和Activity销毁时候调用BitmapHelper#getResourceCache(Object)和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(); }
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.lorss.util.IBLog; import java.lang.ref.WeakReference; import java.util.Map; /** * Created by lorss 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 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:lorss 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:lorss 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()); } } }
标签:
原文地址:http://blog.csdn.net/u014725129/article/details/51262470