打造增强用户体验的图片缓存方案(包含OOM处理技术)

如果你还在因为大量不同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的部分代码,朋友们可以去实现,这里不再赘述。

可能本方案还有很多不足,欢迎大家提意见,我好不断完善。

注意结合另一外一篇文章来看:  android上的一个网络接口和图片缓存框架enif


你可能感兴趣的:(打造增强用户体验的图片缓存方案(包含OOM处理技术))