设计模式-享元模式
什么是享元模式?什么时候用?
享元模式也叫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中的字段。如果存在需要多个字段合并才能决定一组对象,就需要组合字段,就有了一定的成本。
- 第一步,面向接口编程,设计操作接口类。提供创建缓存、设置缓存对象的最大个数、获取缓存对象、获取缓存对象后的回调钩子方法。
public interface ICachePool<T> {
/**
* 创建缓存时回调
*/
T onCreateCache();
/**
* 设置缓存对象的最大个数
*/
int onSetupMaxCacheCount();
/**
* 获取一个缓存对象
*
* @param cacheKey 缓存Key,为了应对多个观察者同时获取缓存使用
*/
T obtain(String cacheKey);
/**
* 当获取一个缓存后回调,一般在该回调中重置对象的所有字段
*
* @param cacheTarget 缓存对象
*/
void onObtainCacheAfter(T cacheTarget);
}
- 因为每种缓存对象需要一个缓存池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());
}
}
- 建立具体的缓存对象缓存池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();
}
}
- 使用
/**
* 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,就可能让系统的逻辑变得复杂。