美文网首页设计模式
设计模式-享元模式

设计模式-享元模式

作者: h2coder | 来源:发表于2020-10-14 00:03 被阅读0次

    什么是享元模式?什么时候用?

    享元模式也叫Flyweight(轻量级),享元模式可以有效地支持大量的细粒度的对象。其实就是对象缓存池的一种实现。大量频繁的回调调用或方法调用,如果这个方法不断的去new对象,内存肯定吃不消。而享元模式的做法是缓存这些对象,下次直接获取,实行复用。

    怎么实现享元模式?

    • 享元对象的基类或者抽象接口。

    • 享元对象,其实就是要缓存的对象。

    • 享元工厂,负责管理对象缓存池的管理和创建享元对象。(一般为一个Map)

    • 内部状态和外部状态,什么是内部状态呢,可以这么理解,每个对象中都有一个不变的字段,例如聊天室的Url,每次服用的都是同一种Url的对象,并且一般都是享元工厂的Map的key,外部状态就是Map的Value-缓存对象中的字段。

    享元模式实现消息对象缓存

    在聊天功能的实现中(WebSocket),使用RxJava封装WebSocket,需要在WebSocket的消息回调中发送Rx事件,而Rx事件需要发送一个实体(WebSocketInfo),这个实体代表了本次WebSocket消息的内容和一些其他字段,单聊量不大的时候还好,但是如果像微信、QQ这种比较大型的聊天软件的话,群聊消息是非常多的(尤其是红包、刷屏),所以消息回调的次数就会非常多,创建WebSocketInfo实体的次数也越多,内存消耗越大。很容易引起内存抖动和频繁的GC导致界面卡顿和线程挂起。

    所以这时候就可以使用享元模式,将WebSocketInfo缓存起来,每次消息回调时,检查对象缓存池里面是否有可用的缓存,有则使用,无则创建,复用WebSocketInfo对象,减少内存开销。

    实现步骤

    优先,为了日后的拓展,我们的对象缓存池,需要支持任意对象,并且支持自动创建对象和缓存对象。

    既然要实现,我们就按照上面的3步实现步骤来:

    • 享元对象或抽象接口。这里实现为CacheItem,内部包裹了要缓存的对象和最近使用的时间戳(用来进行排序)。新建一个CacheItem进行包裹的原因是将这个对象池支持其他缓存对象,并且最近使用的时间戳也不会侵入到具体缓存的对象中。
    public class CacheItem<T> implements Serializable {
        private static final long serialVersionUID = -401778630524300400L;
        
        /**
         * 缓存的对象
         */
        private T cacheTarget;
        /**
         * 最近使用时间
         */
        private long recentlyUsedTime;
        
        public CacheItem(T cacheTarget, long recentlyUsedTime) {
            this.cacheTarget = cacheTarget;
            this.recentlyUsedTime = recentlyUsedTime;
        }
        
        //...省略get、set方法
    }
    
    • 享元对象。这里是WebSocketInfoPool。为了复用,提供reset()方法重置所有字段为null。(其实享元模式还有一点)
    public class WebSocketInfo implements Serializable {
        private static final long serialVersionUID = -880481254453932113L;
    
        //内部状态
        private WebSocket mUrl;
        private WebSocket mWebSocket;
        private String mStringMsg;
        private ByteString mByteStringMsg;
        /**
         * 连接成功
         */
        private boolean isConnect;
        /**
         * 重连成功
         */
        private boolean isReconnect;
        /**
         * 准备重连
         */
        private boolean isPrepareReconnect;
    
        /**
         * 重置
         */
        public WebSocketInfo reset() {
            this.mUrl = null;
            this.mWebSocket = null;
            this.mStringMsg = null;
            this.mByteStringMsg = null;
            this.isConnect = false;
            this.isReconnect = false;
            this.isPrepareReconnect = false;
            return this;
        }
        
        //省略get、set方法
    }
    
    • 享元工厂。就是我们的对象缓存池,上面说过,我们的缓存池需要支持任意对象。所以每个对象的缓存池都是单独的一个类(需要缓存哪个对象,使用哪个,单一职责)。

    内部状态,Map的Key,我们每个聊天界面是一个WebSocket的连接地址Url,所以外部状态则是这个Url,内部状态则是WebSocketInfo中的字段。如果存在需要多个字段合并才能决定一组对象,就需要组合字段,就有了一定的成本。

    1. 第一步,面向接口编程,设计操作接口类。提供创建缓存、设置缓存对象的最大个数、获取缓存对象、获取缓存对象后的回调钩子方法。
    public interface ICachePool<T> {
        /**
         * 创建缓存时回调
         */
        T onCreateCache();
    
        /**
         * 设置缓存对象的最大个数
         */
        int onSetupMaxCacheCount();
    
        /**
         * 获取一个缓存对象
         *
         * @param cacheKey 缓存Key,为了应对多个观察者同时获取缓存使用
         */
        T obtain(String cacheKey);
    
        /**
         * 当获取一个缓存后回调,一般在该回调中重置对象的所有字段
         *
         * @param cacheTarget 缓存对象
         */
        void onObtainCacheAfter(T cacheTarget);
    }
    
    1. 因为每种缓存对象需要一个缓存池Java类,公共逻辑应该抽取出来,所以使用模板模式,建立基类BaseCachePool,并且泛型传入缓存对象类型。每次获取缓存对象时,先判断是否达到最大缓存对象数量,未达到直接创建并设置最近使用时间。按最近使用时间进行从小到大排序(所以最近使用的对象在队尾),如果达到,取出队头数据(肯定是最久没有使用的)。进行复用,并调用onObtainCacheAfter()回调钩子方法给子类。
    public abstract class BaseCachePool<T> implements ICachePool<T>, Comparator<CacheItem<T>> {
        /**
         * 缓存池
         */
        private ConcurrentHashMap<String, LinkedList<CacheItem<T>>> mPool;
    
        public BaseCachePool() {
            mPool = new ConcurrentHashMap<>(8);
        }
    
        @Override
        public T obtain(String cacheKey) {
            //缓存链
            LinkedList<CacheItem<T>> cacheChain;
            //没有缓存过,进行缓存
            if (!mPool.containsKey(cacheKey)) {
                cacheChain = new LinkedList<>();
            } else {
                cacheChain = mPool.get(cacheKey);
            }
            if (cacheChain == null) {
                throw new NullPointerException("cacheChain 缓存链创建失败");
            }
            //未满最大缓存数量,生成一个实例
            if (cacheChain.size() < onSetupMaxCacheCount()) {
                T cache = onCreateCache();
                CacheItem<T> cacheItem = new CacheItem<>(cache, System.currentTimeMillis());
                cacheChain.add(cacheItem);
                mPool.put(cacheKey, cacheChain);
                return cache;
            }
            //达到最大缓存数量。按最近的使用时间排序,最近使用的放后面,每次取只取最前面(最久没有使用的)
            Collections.sort(cacheChain, this);
            CacheItem<T> cacheItem = cacheChain.getFirst();
            cacheItem.setRecentlyUsedTime(System.currentTimeMillis());
            //重置所有属性
            T cacheTarget = cacheItem.getCacheTarget();
            onObtainCacheAfter(cacheTarget);
            return cacheTarget;
        }
    
        @Override
        public int compare(CacheItem<T> o1, CacheItem<T> o2) {
            return Long.compare(o1.getRecentlyUsedTime(), o2.getRecentlyUsedTime());
        }
    }
    
    1. 建立具体的缓存对象缓存池WebSocketInfoPool。继承基类BaseCachePool,复写3个方法。onCreateCache()中创建未满缓存数量时的对象。返回最大缓存对象数量。在回调钩子方法中重置对象。
    public class WebSocketInfoPool extends BaseCachePool<WebSocketInfo> {
    
        @Override
        public WebSocketInfo onCreateCache() {
            return new WebSocketInfo();
        }
    
        @Override
        public int onSetupMaxCacheCount() {
            return 8;
        }
    
        @Override
        public void onObtainCacheAfter(WebSocketInfo cacheTarget) {
            //重置所有字段
            cacheTarget.reset();
        }
    }
    
    1. 使用
    /**
     * WebSocketInfo缓存池
     */
    private final WebSocketInfoPool mWebSocketInfoPool;
    
    public WebSocketWorkerImpl(Context context...) {
        //...省略参数设置
        mWebSocketInfoPool = new WebSocketInfoPool();
    }
    
    /**
     * 初始化WebSocket
     */
    private synchronized void initWebSocket(ObservableEmitter<WebSocketInfo> emitter) {
        if (mWebSocket == null) {
            mWebSocket = mClient.newWebSocket(createRequest(mWebSocketUrl), new WebSocketListener() {
                @Override
                public void onOpen(WebSocket webSocket, Response response) {
                    super.onOpen(webSocket, response);
                    //发送连接成功消息
                    if (!emitter.isDisposed()) {
                        //..省略部分不重要代码
                        emitter.onNext(createConnect(mWebSocketUrl, webSocket));
                    }
                }
    
                @Override
                public void onMessage(WebSocket webSocket, String text) {
                    super.onMessage(webSocket, text);
                    //发送收到的WebSocket消息
                    if (!emitter.isDisposed()) {
                        emitter.onNext(createReceiveStringMsg(mWebSocketUrl, webSocket, text));
                    }
                }
    
                @Override
                public void onClosed(WebSocket webSocket, int code, String reason) {
                    super.onClosed(webSocket, code, reason);
                    //发送关闭消息
                    if (!emitter.isDisposed()) {
                        emitter.onNext(createClose(mWebSocketUrl));
                    }
                }
            });
        }
    }
    
    //------- 状态复用方法 --------
    private WebSocketInfo createConnect(String url, WebSocket webSocket) {
        return mWebSocketInfoPool.obtain(url)
                .setUrl(url)
                .setWebSocket(webSocket)
                .setConnect(true);
    }
    
    private WebSocketInfo createReceiveStringMsg(String url, WebSocket webSocket, String stringMsg) {
        return mWebSocketInfoPool.obtain(url)
                .setUrl(url)
                .setConnect(true)
                .setWebSocket(webSocket)
                .setStringMsg(stringMsg);
    }
    
    //...省略其他状态创建(都是一样的,就不贴了)
    

    Android中享元模式的应用

    Android开发中,Handler我们肯定熟悉,而Handler发送的Message对象复用就使用了享元模式,但是Message对象没用使用特定的缓存池,而是本身有一个next的字段保存下一个Message的引用,行成一个链表,所以Message对象既担任了缓存池的职责,也担任了享元对象,属于简化版的享元模式,虽然并不是最经典的享元模式的实现方法,但是分得太细,有些场景也会一定程度增加复杂度。所以至于是按经典方式实现还是简化版实现,得考虑项目来决定。

    总结

    • 享元模式的优点:在于能大幅度降低对象的创建开销。

    • 享元模式的缺点:因为需要决定不变的外部状态,需要多个字段组合成为Map的Key,就可能让系统的逻辑变得复杂。

    相关文章

      网友评论

        本文标题:设计模式-享元模式

        本文链接:https://www.haomeiwen.com/subject/oznmpktx.html