转自:http://blog.csdn.net/androidzhaoxiaogang/article/details/8152348
如果你还在因为大量不同size的图片缓存产生的OOM而烦恼,如果你还在因为用软引用(SoftReference)快速回收的蛋疼用户体验而不知所措,那么我建议无论你是高手还是菜鸟,真的很有必要看一下这篇文章,希望能从中给你一些启发,给你的产品用户带去一些好的体验。
思维的火花
既然我们要提供用户的体验,既然我们摒弃了软应用,那么我这里才用的是使用LRU的缓存机制来达到我们的目的。在android 3.1以上我们可以使用LruCache类,但如果在低一些的版本我们则只要把源代码copy出来放进工程就ok了。但是,仅仅把LruCache的代码copy出来只是完成了我们实现这里图片缓存方案的准备工作。
精心的构建
1.LruCache- package XXXl;
- import java.util.LinkedHashMap;
- import java.util.Map;
- /**
- * 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.
- */
- /**
- * Static library version of {@code android.util.LruCache}. Used to write apps
- * that run on API levels prior to 12. When running on API level 12 or above,
- * this implementation is still used; it does not try to switch to the
- * framework's implementation. See the framework SDK documentation for a class
- * overview.
- */
- public class LruCache<K, V> {
- private LogUtils mLog = LogUtils.getLog(LruCache.class);
- 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);
- }
- /**
- * 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);
- }
- mLog.debug("maxSize :" + maxSize);
- mLog.debug("total size :" + size);
- trimToSize(maxSize);
- return previous;
- }
- /**
- * @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 || map.isEmpty()) {
- break;
- }
- Map.Entry<K, V> toEvict = map.entrySet().iterator().next();
- 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>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>The method is called without synchronization: other threads may
- * access the cache while this method is executing.
- *
- * <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);
- }
- mLog.debug("size :" + result);
- 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>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.
- */
- 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);
- }
- }
2.自定义ImageView
- package XXX.view;
- import java.io.FilterInputStream;
- import java.io.IOException;
- import java.io.InputStream;
- import java.lang.ref.SoftReference;
- import java.util.HashMap;
- import java.util.Map;
- import java.util.WeakHashMap;
- import java.util.concurrent.RejectedExecutionException;
- import android.app.ActivityManager;
- import android.content.Context;
- import android.content.pm.PackageInfo;
- import android.content.pm.PackageManager;
- import android.content.pm.PackageManager.NameNotFoundException;
- import android.graphics.Bitmap;
- import android.graphics.BitmapFactory;
- import android.graphics.BitmapFactory.Options;
- import android.graphics.Canvas;
- import android.graphics.drawable.BitmapDrawable;
- import android.graphics.drawable.Drawable;
- import android.os.AsyncTask;
- import android.text.TextUtils;
- import android.util.AttributeSet;
- import android.widget.ImageView;
- import ch.boye.httpclientandroidlib.HttpEntity;
- import ch.boye.httpclientandroidlib.HttpResponse;
- import ch.boye.httpclientandroidlib.HttpStatus;
- import ch.boye.httpclientandroidlib.client.methods.HttpGet;
- public class CacheImageView extends ImageView {
- private static int mCacheSize;
- private int mDefaultImage = 0;
- private static Map<ImageView, String> mImageViews;
- private static LruCache<String, Bitmap> mLruCache;
- private static HashMap<Integer, SoftReference<Bitmap>> mResImage;
- private Context mContext;
- public CacheImageView (Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- init(context);
- }
- public CacheImageView (Context context, AttributeSet attrs) {
- super(context, attrs);
- init(context);
- }
- public CacheImageView (Context context) {
- super(context);
- init(context);
- }
- private void init(Context context) {
- if (mImageViews == null) {
- mImageViews = new WeakHashMap<ImageView, String>();
- }
- if (mLruCache == null) {
- final int memClass = ((ActivityManager)context
- .getSystemService(Context.ACTIVITY_SERVICE)).getMemoryClass();
- // Use 1/8th of the available memory for this memory cache.
- mCacheSize = 1024 * 1024 * memClass / 8;
- mLruCache = new LruCache<String, Bitmap>(mCacheSize) {
- @Override
- protected int sizeOf(String key, Bitmap bitmap) {
- // The cache size will be measured in bytes rather than
- // number of items.
- return bitmap.getRowBytes() * bitmap.getHeight();
- }
- @Override
- protected void entryRemoved(boolean evicted, String key, Bitmap oldValue,
- Bitmap newValue) {
- if (evicted && oldValue !=null && !oldValue.isRecycled()) {
- oldValue.recycle();
- oldValue = null;
- }
- }
- };
- }
- if (mResImage == null) {
- mResImage = new HashMap<Integer, SoftReference<Bitmap>>();
- }
- mContext = context;
- }
- @Override
- protected void onDraw(Canvas canvas) {
- BitmapDrawable drawable = (BitmapDrawable)getDrawable();
- if (drawable == null ){
- setImageBitmap(getLoadingBitmap(mContext));
- } else {
- if( drawable.getBitmap() == null || drawable.getBitmap().isRecycled()) {
- setImageBitmap(getLoadingBitmap(mContext));
- }
- }
- super.onDraw(canvas);
- }
- public void setImageUrl(String url, int resId) {
- mDefaultImage = resId;
- mImageViews.put(this, url);
- Bitmap bitmap = getBitmapFromCache(url);
- if (bitmap == null || bitmap.isRecycled()) {
- setImageBitmap(getLoadingBitmap(mContext));
- try {
- new DownloadTask().execute(url);
- } catch (RejectedExecutionException e) {
- // do nothing, just keep not crash
- }
- } else {
- setImageBitmap(bitmap);
- }
- }
- private Bitmap getLoadingBitmap(Context context) {
- SoftReference<Bitmap> loading = mResImage.get(mDefaultImage);
- if (loading == null || loading.get() == null || loading.get().isRecycled()) {
- loading = new SoftReference<Bitmap>(BitmapFactory.decodeResource(
- context.getResources(), mDefaultImage));
- mResImage.put(mDefaultImage, loading);
- }
- return loading.get();
- }
- private class DownloadTask extends AsyncTask<String, Void, Bitmap> {
- private String mParams;
- @Override
- public Bitmap doInBackground(String... params) {
- mParams = params[0];
- Bitmap bm = null;
- if (mParams.startsWith("http:") || mParams.startsWith("https:")) {// 网络列表icon
- bm = download(mParams);
- } else {
- // other types of icons
- }
- addBitmapToCache(mParams, bm);
- return bm;
- }
- @Override
- public void onPostExecute(Bitmap bitmap) {
- String tag = mImageViews.get(RemoteImageView.this);
- if (!TextUtils.isEmpty(tag) && tag.equals(mParams)) {
- if (bitmap != null) {
- setImageBitmap(bitmap);
- }
- }
- }
- };
- /*
- * An InputStream that skips the exact number of bytes provided, unless it
- * reaches EOF.
- */
- static class FlushedInputStream extends FilterInputStream {
- public FlushedInputStream(InputStream inputStream) {
- super(inputStream);
- }
- @Override
- public long skip(long n) throws IOException {
- long totalBytesSkipped = 0L;
- while (totalBytesSkipped < n) {
- long bytesSkipped = in.skip(n - totalBytesSkipped);
- if (bytesSkipped == 0L) {
- int b = read();
- if (b < 0) {
- break; // we reached EOF
- } else {
- bytesSkipped = 1; // we read one byte
- }
- }
- totalBytesSkipped += bytesSkipped;
- }
- return totalBytesSkipped;
- }
- }
- private Bitmap download(String url) {
- InputStream in = null;
- HttpEntity entity = null;
- Bitmap bmp = null;
- try {
- final HttpGet get = new HttpGet(url);
- final HttpResponse response = HttpManager.execute(mContext, get);
- if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
- entity = response.getEntity();
- in = entity.getContent();
- try {
- bmp = getDecodeBitmap(in, url);
- } catch (OutOfMemoryError err) {
- Runtime.getRuntime().gc();
- bmp = getDecodeBitmap(in, url);
- }
- } else {
- get.abort();
- return bmp;
- }
- } catch (Exception e) {
- return bmp;
- } finally {
- IOUtils.closeStream(in);
- }
- return bmp;
- }
- private Bitmap getDecodeBitmap(InputStream in, String url) {
- Options options = new Options();
- options.inPurgeable = true;
- options.inInputShareable = true;
- return BitmapFactory.decodeStream(new FlushedInputStream(in), null, options);
- }
- public void addBitmapToCache(String url, Bitmap bitmap) {
- if (bitmap != null) {
- mLruCache.put(url, bitmap);
- Runtime.getRuntime().gc();
- }
- }
- /**
- * @param url The URL of the image that will be retrieved from the cache.
- * @return The cached bitmap or null if it was not found.
- */
- public static Bitmap getBitmapFromCache(String url) {
- return mLruCache.get(url);
- }
- public static void recycle() {
- if (mImageViews != null && !mImageViews.isEmpty()) {
- mImageViews.clear();
- mImageViews = null;
- }
- if (mLruCache != null) {
- mLruCache.evictAll();
- mLruCache = null;
- }
- if (mResImage != null) {
- for (SoftReference<Bitmap> reference : mResImage.values()) {
- Bitmap bitmap = reference.get();
- if (bitmap != null && !bitmap.isRecycled()) {
- bitmap.recycle();
- bitmap = null;
- }
- }
- mResImage = null;
- }
- }
- }
这一步是实现LRU缓存方案的最关键一步,里面需要对几个地方做详细和认真的解释。
在初始化LruCache的时候我们有用到:
- protected void entryRemoved(boolean evicted, String key, Bitmap oldValue,
- Bitmap newValue) {
- if (evicted && oldValue !=null && !oldValue.isRecycled()) {
- oldValue.recycle();
- oldValue = null;
- }
- }
注意if里面的条件一共由3个(evicted && oldValue !=null && !oldValue.isRecycled())组成一个都不能少,至于原因希望你们去思考。
另外调用addBitmapToCache方法我是在后台调用的,没有在主线程里面操作,原因是里面调用了Runtime.getRuntime().gc(),基本上每次GC的执行都要花去20~50ms如果是在列表里面的话,对ui应该有一定的影响。在此强调一下Runtime.getRuntime().gc()在每次加载图片之后最好调用他。这是一个小兄弟测试的结果,在android调用GC有助于虚拟机减少内存碎片和加速内存碎片的重整理。
下载图片建立连接我用了httpclient的连接池方式,如果你觉的麻烦你可以使用URLconnection,这里暂时不给出httpclient连接池框架的部分,如果你随时关注我的话,你可以从我后面的博客中看到关于它的话题。
3.如何使用
可能你的项目中有多个地方要用到图片,那么只要在你的xml中需要用到imageview的这样去定义(以listview的row举例):
- <XXX.view.CacheImageView
- android:id="@+id/icon"
- android:layout_width="40dip"
- android:layout_height="40dip"
- android:layout_marginLeft="10dip" />
然后再你的adapter代码中只需要简单的两句:
- holder.icon = (CacheImageView)convertView.findViewById(R.id.icon);
- holder.icon.setImageUrl(url, resId);
完美的总结
该方案是尽量减少图片被回收的时间,但是并不是不被回收,所以需要一直展示给用户的情况不适合本方案。对于某些国产机内存特小的那种,即使使用软引用都很容易挂的那种,建议不要再设置为内存的8分之一大小,而是获取到手机的UA(model),去硬编码一个大小吧。
本方案在这里只展示了基于内存的缓存方式,基于disk的部分代码,朋友们可以去实现,这里不再赘述。
可能本方案还有很多不足,欢迎大家提意见,我好不断完善