Android简单内存缓存

一. 问题

前段时间在公司项目中遇到一个问题,在集成百度地图时想要在地图上动态添加 ICON,这些 ICON 都是随时可变的,起初设计时,每次添加的 ICON 都是动态 new 出一个新的Bitmap,这样可以满足 ICON 随时可变的需求,但是需求完成之后,通过 AS 观察到 APP 内存飙升,APP 内存吃紧带来的坏处我就不再赘述了,为了解决这个问题,我就设计了一个简单的内存缓存框架来解决这个问题,有效的减少了 APP 的内存消耗。

二. 解决

解决问题时的问题思路我就不再说了,直接看下源码~

/**
 * Created by daiyiming on 2016/12/10.
 * 本地内存缓存
 */
public final class MemoryCache<KO, VO> {

    public static final int CAPACITY_DEFAULT = 10; // 默认缓存
    public static final int CAPACITY_INFINITY = -1; // 无限缓存
    public static final int MAX_CAPACITY_DEFAULT = 20; // 默认最大缓存
    public static final int MAX_CAPACITY_INFINITY = -1; // 无限最大缓存

    private final LinkedList<ValueHolder<KO, VO>> mCacheList; // 缓存
    private final HashSet<KO> mKeySet; // 键缓存
    private volatile int mCapacity; // 容量
    private volatile int mMaxCapacity; // 最大容量
    private volatile boolean mAllowUpdate; // 键值冲突时是否准许更新

    /**
     * 键值对组合类
     *
     * @param <KI> 键类型
     * @param <VI> 值类型
     */
    private static final class ValueHolder<KI, VI> {
        private final KI mKey; // 键不准许修改,若修改则用添加新的对象
        private VI mValue; // 值可以修改,用于更新

        private ValueHolder(KI key, VI value) {
            mKey = key;
            mValue = value;
        }

        /**
         * 更新值对象
         * @param value 新的值对象
         */
        private void update(VI value) {
            recycleValue();
            mValue = value;
        }

        /**
         * 回收Holder
         */
        private void recycle() {
            recycleKey();
            recycleValue();
        }

        private void recycleKey() {
            if (mKey instanceof IRecycleInterface) {
                ((IRecycleInterface) mKey).recycle();
            }
        }

        private void recycleValue() {
            if (mValue instanceof IRecycleInterface) {
                ((IRecycleInterface) mValue).recycle();
            }
        }

    }

    /**
     * 值接口,实现帮助对象键值内存回收
     */
    public interface IRecycleInterface {
        void recycle();
    }

    public MemoryCache() {
        this(CAPACITY_DEFAULT, MAX_CAPACITY_DEFAULT);
    }

    public MemoryCache(int capacity, int maxCapacity) {
        if (capacity < CAPACITY_INFINITY
                || maxCapacity < MAX_CAPACITY_INFINITY) {
            throw new IllegalArgumentException("MemoryCache:构造函数参数错误");
        }
        mCacheList = new LinkedList<>();
        mKeySet = new HashSet<>();
        mCapacity = capacity;
        mMaxCapacity = maxCapacity;
        mAllowUpdate = false;
    }

    public synchronized void put(KO key, VO value) {
        if (key == null || value == null) {
            return;
        }
        if (mKeySet.contains(key)) { // 如果已经存在则复用对象
            ListIterator<ValueHolder<KO, VO>> iterator = mCacheList.listIterator();
            while (iterator.hasNext()) {
                ValueHolder<KO, VO> holder = iterator.next();
                if (holder.mKey.equals(key)) {
                    holder.update(value);
                    // 如果不准许更新则删除重新添加
                    if (!mAllowUpdate && iterator.previousIndex() != 0) {
                        iterator.remove();
                        mCacheList.addFirst(holder);
                    }
                    break;
                }
            }
        } else { // 不存在则添加
            mKeySet.add(key);
            mCacheList.addFirst(new ValueHolder<>(key, value));
        }
        // 如果大于最大容量且不是无限容量则清除末尾一个
        if (mMaxCapacity != MAX_CAPACITY_INFINITY
                && mCacheList.size() > mMaxCapacity) {
            remove(mCacheList.size() - 1);
        }
    }

    public synchronized VO get(KO key) {
        if (mKeySet.contains(key)) {
            ListIterator<ValueHolder<KO, VO>> iterator = mCacheList.listIterator();
            while (iterator.hasNext()) {
                ValueHolder<KO, VO> holder = iterator.next();
                if (holder.mKey.equals(key)) { // 找到
                    if (iterator.previousIndex() != 0) { // 如果不是在头部就移动到头部
                        iterator.remove();
                        mCacheList.addFirst(holder);
                    }
                    // 删除一个超出容量的数据
                    if (mCapacity != CAPACITY_INFINITY
                            && mCacheList.size() > mCapacity) {
                        remove(mCacheList.size() - 1);
                    }
                    return holder.mValue;
                }
            }
        }
        return null;
    }

    public synchronized boolean contains(KO k) {
        return mKeySet.contains(k);
    }

    public synchronized void clear() {
        mKeySet.clear();
        for (ValueHolder<KO, VO> holder : mCacheList) {
            holder.recycle();
        }
        mCacheList.clear();
    }

    public synchronized int size() {
        return mCacheList.size();
    }

    public synchronized void remove(KO key) {
        if (mKeySet.contains(key)) {
            ListIterator<ValueHolder<KO, VO>> iterator = mCacheList.listIterator();
            while (iterator.hasNext()) {
                ValueHolder<KO, VO> holder = iterator.next();
                if (holder.mKey.equals(key)) {
                    iterator.remove();
                    mKeySet.remove(holder.mKey);
                    holder.recycle();
                    break;
                }
            }
        }
    }

    public synchronized void remove(int position) {
        if (position >= 0 && position < size()) {
            ValueHolder<KO, VO> removedHolder = mCacheList.remove(position);
            mKeySet.remove(removedHolder.mKey);
            removedHolder.recycle();
        }
    }

    public void setCapacity(int capacity) {
        mCapacity = capacity;
    }

    public void setMaxCapacity(int maxCapacity) {
        mMaxCapacity = maxCapacity;
    }

    public int getCapacity() {
        return mCapacity;
    }

    public int getMaxCapacity() {
        return mMaxCapacity;
    }

    /**
     * 准许更新
     * 决定添加过程中键值重复时直接原位置更新还是删除重新添加
     *
     * @param allowUpdate 是否准许更新
     */
    public void allowUpdate(boolean allowUpdate) {
        mAllowUpdate = allowUpdate;
    }

}

通过一个链表保存需要缓存的元素,每次添加的新元素放到链表的首部,添加时检测如果超出最大容量,则从链表的尾部删除一个元素,为什么这么做呢?主要是为了防止疯狂 put 元素导致 OOM。在每次get元素的时候,通过传入的 key 值遍历链表,当命中想要获取的元素的时候,将这个元素移到链表的首部,这样可以保证频繁获取的元素在链表靠前的位置,减少获取时间。在 get 成功的时候,如果链表长度大于容量(注意与最大容量的区别),我们就从列表末尾删除一个元素,因为末尾的元素表明这个元素很大的可能是长时间没人使用(get操作),即可视为过期元素。

为什么要设置一个容量和最大容量的区别呢?因为考虑到可能存在疯狂 put 的操作,即用户一直在 put,如果最大容量大于容量,就给了最先 put 的元素有被重新拿到链表首部的可能性,即一定程度的加强了这个元素被再次利用的可能性,在之后的 get 的操作中,链表尾部元素逐渐被清除,链表长度逐渐回归正常。又因为设置了最大容量,给 put 操作设置了上限,所以基本不会有 OOM。

当然防止有些时候需要缓存的时候 IO 连接,SOCKET 连接,Bitmap 等需要手动销毁的东西,我还实现了一个 Recycle 接口,如果缓存的键或者值需要用户自定义销毁操作,则实现这个接口即可。

具体见下图

put.png
get.png

三. 使用

/**
 * Created by daiyiming on 2016/12/11.
 * 缓存执行类
 */
public final class MarkerMemoryCache {

    private static MemoryCache<Key, Value> sLocalCache = new MemoryCache<>();

    private static void confirmEnable() {
        if (sLocalCache == null) {
            sLocalCache = new MemoryCache<>();
        }
    }

    public static void put(Key key, Value value) {
        confirmEnable();
        sLocalCache.put(key, value);
    }

    public static BitmapDescriptor get(Key key) {
        confirmEnable();
        Value value = sLocalCache.get(key);
        if (value != null) {
            return value.mBitmapDescriptor;
        }
        return null;
    }

    public static Key generateKey(int kind, String styleDetail, String styleId) {
        return new Key(kind, styleDetail == null ? "" : styleDetail, styleId == null ? "" : styleId);
    }

    public static Value generateValue(BitmapDescriptor bitmapDescriptor) {
        return new Value(bitmapDescriptor);
    }

    public static void clear() {
        sLocalCache.clear();
    }

    public static final class Key {

        private final int mKind;
        private final String mStyleDetail;
        private final String mStyleId;
        private final int mHashCode;

        private Key(int kind, String styleDetail, String styleId) {
            mKind = kind;
            mStyleDetail = styleDetail;
            mStyleId = styleId;
            mHashCode = generateHashCode();
        }

        private int generateHashCode() {
            int result = 17;
            result = 31 * result + mKind;
            result = 31 * result + mStyleDetail.hashCode();
            result = 31 * result + mStyleId.hashCode();
            return result;
        }

        @Override
        public boolean equals(Object object) {
            if (object instanceof Key) {
                if (object == this) {
                    return true;
                }
                Key key = (Key) object;
                return mKind == key.mKind
                        && mStyleDetail.equals(key.mStyleDetail)
                        && mStyleId.equals(key.mStyleId);
            }
            return false;
        }

        @Override
        public int hashCode() {
            return mHashCode;
        }

    }

    public static final class Value implements MemoryCache.IRecycleInterface {

        private BitmapDescriptor mBitmapDescriptor = null;

        private Value(BitmapDescriptor bitmapDescriptor) {
            mBitmapDescriptor = bitmapDescriptor;
        }

        @Override
        public void recycle() {
            if (mBitmapDescriptor != null) {
                Bitmap bitmap = mBitmapDescriptor.getBitmap();
                if (bitmap != null && !bitmap.isRecycled()) {
                    bitmap.recycle();
                }
            }
        }
    }

}

这个实现是复杂实现,��基本实现了所有功能,当然�也可以简单使用~
第一次在简书写记录东西哈哈以后继续~�

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,651评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,468评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,931评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,218评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,234评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,198评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,084评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,926评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,341评论 1 311
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,563评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,731评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,430评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,036评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,676评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,829评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,743评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,629评论 2 354

推荐阅读更多精彩内容