享元模式
享元模式是对象池的一种实现,主打轻量级。它一般用来尽可能减少内存使用量,适用于可能存在大量重复对象的场景,缓存可共享的对象,达到对象共享、避免创建过多对象的效果,从而提升性能,减少内存占用,提高内存利用效率。
使用享元模式可有效地支持大量细粒度对象的复用。
看上去很厉害的样子,其实全是废话,说白了就是对象的复用。
经典实现
假设某个对象个别重要属性是不变的,但是有几个细微属性会不停地发生变化,如果每次都新建对象会浪费内存,这种情况下一般就可以考虑使用享元模式。
在享元模式中,会建立一个对象容器,经典的享元模式,该容器为一个 Map,它的键就是之前说到的不变属性,它的值则是对象本身。
举例来说,多个人同时抢购北京到上海的火车票,起始站和终点站都是固定的,但是即使是同一辆列车上也有不同的席别,有可能是硬座,有可能是软座,有可能是硬卧,还有可能是软卧,相对应的价钱也都是不同的。
public class TickeyFactory{
static Map<String, Ticket> sTicketMap = new ConcurrentHashMap<String, Ticket>();
public static Ticket getTickey(String fromStation, String toStation){
String key = fromStation + "-" + toStation;
if(sTicketMap.contains(key)){
return sTicketMap.get(key);
}else{
return new Ticket(fromStation, toStation);
}
}
}
代码很简单,就是使用 ConcurrentHashMap 做了一个对象的缓存。如我们之前说的,重要属性是不变的(起始站和到达站),但是细微属性是变化的(席别和价格)。在这种情况下,我们使用 Map 集合,以不变的属性为 key,以对象为 value,从而实现对象的复用而不用每次都新创建对象,这就是经典享元模式。
Android 源码应用
其实这个都用不到在源码中应用,日常开发中偶尔也会用到,是比较基础的一个对象复用的形式。不过模式也是为了帮助我们解决问题的,源码中应用享元模式或者说享元思路的方式还挺多变的,可以挨个看看学习一下。
-
LayoutInflater#createView
如果我们在一个 LinearLayout 中包裹了五个 ImageView,那么在系统渲染布局的时候,并不是粗暴的直接 new ImageView() x 5,而是会应用享元模式,使用 Map 集合对 View 对象进行复用。
private static final HashMap<String, Constructor<? extends View>> sConstructorMap = new HashMap<String, Constructor<? extends View>>(); public final View createView(String name, String prefix, AttributeSet attrs){ // 根据 name 从构造器 Map 中取数据 Constructor<? extends View> constructor = sConstructorMap.get(name); // 如果不为空,证明之前有缓存;校验一下 ClassLoader,如果不能通过就从 map 中移除 if (constructor != null && !verifyClassLoader(constructor)) { constructor = null; sConstructorMap.remove(name); } Class<? extends View> clazz = null; // 如果未能从缓存中拿到数据,或者没能通过 classLoader 校验 // 就重新初始化,使用反射,拿到对应类的构造方法 if (constructor == null) { clazz = mContext.getClassLoader().loadClass( prefix != null ? (prefix + name) : name).asSubclass(View.class); constructor = clazz.getConstructor(mConstructorSignature); constructor.setAccessible(true); sConstructorMap.put(name, constructor); } // 利用反射初始化 View 对象并返回 final View view = constructor.newInstance(args); return view; }
-
Message.obtain()
这次就是享元思路了,而不是严格的享元模式。
我们知道整个 Android 系统都是基于消息机制,如果不停地新建 Message 对象,那对虚拟机无疑是个沉重的负担。Google 在设计 Message 对象池的时候,利用 Message 链表的特性,维护了一个可缓存 50 条消息的缓存池。
private static Message sPool; private static int sPoolSize = 0; private static final int MAX_POOL_SIZE = 50;
所以在 Message 默认构造器的注释里更建议调用者使用 Message.obtain() 方法:
// the preferred way to get a Message is to call {@link #obtain() Message.obtain() public Message() {} // obtain 静态工厂,第一次会返回一个初始化的对象,之后从缓存池中获取 public static Message obtain() { synchronized (sPoolSync) { // 第一次进来 sPool 肯定是空 if (sPool != null) { // 取出 sPool 并赋值给局部变量,最终返回给调用者 Message m = sPool; // 最前面的消息对象已经取出,将 sPool 指向链表的下一条数据 sPool = m.next; // 给要返回的消息进行重置操作,next 无指向,也没有 in-use 标记 m.next = null; m.flags = 0; // clear in-use flag // 更新消息池数量 sPoolSize--; return m; } } // 返回一个新创建的对象 return new Message(); }
当我们的消息完成处理以后,会在 Looper#loop 方法中调用 Message#recycle 方法,对当前对象进行回收:
public static void loop() { for (;;) { msg.target.dispatchMessage(msg); msg.recycleUnchecked(); } } // 回收消息 void recycleUnchecked() { // 将除 next 以外的所有属性重置 // 同时标记消息为可用状态,不可操作,obtain 时才予以重置 flags = FLAG_IN_USE; what = 0; arg1 = 0; arg2 = 0; obj = null; replyTo = null; sendingUid = -1; when = 0; target = null; callback = null; data = null; synchronized (sPoolSync) { // 如果当前消息数还没有达到 50 条 if (sPoolSize < MAX_POOL_SIZE) { // 就将当前消息指向之前的 sPool 对象,当前消息变为消息池的首个对象 next = sPool; // sPool 指向的应该是消息池的首个对象,即当前对象 sPool = this; // 更新消息池数量 sPoolSize++; } } } // 之前的 sPool,MessageA 是链表的表头 MessageA( next = MessageB( next = MessageC( next = null ) ) ); // 新回收一条消息 // next = sPool MessageNew( next = MessageA( next = MessageB( next = MessageC( next = null ) ) ) ); // sPool = this; // 现在 sPool 指向的就是新回收的消息,也就是链表表头了
-
EventBus#FindState
上面两个主要是系统源码级别的应用,很多第三方库也会有类似的应用。今天我们以 EventBus 为例,看一下它是怎么用另一种形式应用享元思想的。
EventBus 的源码分析之前已经写过,具体细节就不展开了,直接上主菜。
我们知道 EventBus 的原理是筛选订阅类中所有 @Subscribe 方法,然后将其构造成一个 @Subscribe 方法参数类型为键,订阅类以及订阅方法组成的新对象为值的 Map 集合,然后根据反射机制在 post 方法调用的时候进行调用对应的订阅方法。
那么在遍历订阅类方法时,因为有太多类似的数据,EventBus 选择的实现思路正是享元模式。
private List<SubscriberMethod> findUsingInfo(Class<?> subscriberClass) { // 构建 FindState 类对象,存储系列相关的属性 FindState findState = prepareFindState(); findState.initForSubscriber(subscriberClass); while (findState.clazz != null) { ... ... } return getMethodsAndRelease(findState); }
我们先来看一下 prepareFindState() :
private static final int POOL_SIZE = 4; private static final FindState[] FIND_STATE_POOL = new FindState[POOL_SIZE]; private FindState prepareFindState() { synchronized (FIND_STATE_POOL) { for (int i = 0; i < POOL_SIZE; i++) { FindState state = FIND_STATE_POOL[i]; if (state != null) { FIND_STATE_POOL[i] = null; return state; } } } return new FindState(); }
经典的享元对象池的应用,这次的实现方式是数组。
prepareFindState()
方法中,遍历获取 FIND_STATE_POOL 缓存池中的数组,返回第一个不为空的对象使用;如果全部为空,则初始化一个新对象使用。然后代码走到最后,返回数据时,需要将 FindState 类中存储的数据取出加工并返回给调用者了,也就是 FindState 对象该回收的时候了:
private List<SubscriberMethod> getMethodsAndRelease(FindState findState) { List<SubscriberMethod> methods = new ArrayList<>(findState.subscriberMethods); findState.recycle(); synchronized (FIND_STATE_POOL) { for (int i = 0; i < POOL_SIZE; i++) { if (FIND_STATE_POOL[i] == null) { FIND_STATE_POOL[i] = findState; break; } } } return methods; }
很明显,代码取出 FindState 中存储的集合后,之后的工作都是在操作缓存池。
我们来看一下 recycle 方法:
void recycle() {
subscriberMethods.clear();
anyMethodByEventType.clear();
subscriberClassByMethodKey.clear();
methodKeyBuilder.setLength(0);
subscriberClass = null;
clazz = null;
skipSuperClasses = false;
subscriberInfo = null;
}
很简单,就是清空所有数据,方便下一次使用。
synchronized (FIND_STATE_POOL) {
for (int i = 0; i < POOL_SIZE; i++) {
if (FIND_STATE_POOL[i] == null) {
FIND_STATE_POOL[i] = findState;
break;
}
}
}
清空之前使用的 FindState 对象后,再次遍历缓存池,如果发现数组哪个位置没有缓存数据,就把最新的对象缓存到该位置上,等下次调用 prepareFindState
方法时,遍历到某个位置有缓存对象,就会直接使用,而不是再创建新对象了:
private FindState prepareFindState() {
synchronized (FIND_STATE_POOL) {
for (int i = 0; i < POOL_SIZE; i++) {
FindState state = FIND_STATE_POOL[i];
// 如果哪个位置不为空,就说明是之前缓存下来的数据
if (state != null) {
// 把数组对应位置清空,给下一个对象腾出空间
FIND_STATE_POOL[i] = null;
// 然后把刚取出的缓存对象返回给调用者
return state;
}
}
}
return new FindState();
}
好吧,享元模式,对象的复用,差不多就是这些吧。