假如现在有很多的客户来找你做网站,有的客户要实现博客功能,有的空户要实现像电商那样的图片视频展示功能,有的实现像新闻那样展示图片+文字的效果,你应该怎么做?
如果我们实现一套代码,然后为每个客户端都使用复制+粘贴实现功能,然后每个客户都实现数据库。这样的做法的问题是什么?
如果说基础代码出现问题,那么每个客户的代码都要维护,其实每个客户都要申请独立空间和维护数据,这将会导致工程繁杂又庞大,会做很多无效功。
如果我们将这些客户,都抽象起来,比如现在大型的blog网站、电商网站,下面都有许多个小网站,我们学习以这样的方式去实现大网站,每个客户申请的只有你这个大网站的空间,而数据库大家都共用一套,代码也是如此,那么这样维护代价和耗费价格会大大降低,这就是享元模式的一种应用。
1. 享元模式的概念
享元模式(Flyweight),运用共享技术有效地支持大量细粒度的对象。
在纯代码中,享元模式是对象池的一种实现,用来尽可能的减少内存使用量,他适用于可能存在大量重复对象的场景,来缓存可共享的对象,达到对象共享、避免创建过多对象的效果。
享元对象中分为可共享和不可共享的状态:
- 部分状态是可以共享,可以共享的状态为内部状态,内部状态不会随着环境变化;
- 不可共享的状态则为外部状态,他们会随着环境的改变而改变。在享元模式中会建立一个对象容器,在经典的享元模式中该容器是一个
Map
,它的键是享元对象的内部状态,它的值就是享元对象本身。
客户端程序通过这个内部状态从享元工厂中获取享元对象,如果有缓存则使用缓存对象,否则创建一个享元对象并且存入容器中,这样一来就避免了创建过多对象的问题。
2. UML类图
看下享元模式的UML图:
Flyweight
享元对象抽象基类或接口ConcreteFlyweight
具体的需要享元的对象FlyweightFactory
享元工厂,负责管理享元对象池和创建享元对象- UnsharedConcreteFlyweight
不需要分享的享元对象,说明它是静态的、无需扩展的具体对象,所以它本身并不是享元模式的重点。
下面来看看实例的代码,首先是Flyweight
抽象类:
public abstract class Flyweight {
public abstract void operation(int extrinsicState);
}
ConcreteFlyweight
是继承Flyweight超类或实现其接口,并为内部状态增加存储空间:
public class ConcreteFlyweight extends Flyweight {
@Override
public void operation(int extrinsicState) {
System.out.println("具体Flyweight:" + extrinsicState);
}
}
UnsharedConcreteFlyweight
是指那些不需要共享的Flyweight子类,因为Flyweight接口共享成为可能,但它并不强制共享。
public class UnsharedConcreteFlyweight extends Flyweight {
@Override
public void operation(int extrinsicState) {
System.out.println("不共享具体Flyweight:" + extrinsicState);
}
}
FlyweightFactory
是一个享元工厂,用来创建并管理Flyweight对象。它主要是用来确保合理地共享Flyweight,当用户请求一个Flyweight时,FlyweightFactory对象提供一个已创建的实例或者创建一个(如果不存在的话)。
public class FlyweightFactory {
// 使用HashMap来存储对象
private HashMap<String, Flyweight> data = new HashMap<>();
/**
* 在获取对象的时候,如果对象不存在,则动态的创建它,然后返回
*/
public Flyweight getFlyweight(String key) {
if (!data.containsKey(key)) {
data.put(key, new ConcreteFlyweight());
}
return data.get(key);
}
}
最后在客户端的代码中使用这个对象池:
public class FlyweightMain {
public static void main(String[] args) {
// 代码外部状态,仅做记录
int extrinsicState = 30;
FlyweightFactory f = new FlyweightFactory();
Flyweight fx = f.getFlyweight("X");
fx.operation(--extrinsicState);
Flyweight fy = f.getFlyweight("Y");
fy.operation(--extrinsicState);
Flyweight fz = f.getFlyweight("Z");
fz.operation(--extrinsicState);
Flyweight uf = new UnsharedConcreteFlyweight();
uf.operation(--extrinsicState);
}
}
UnsharedConcreteFlyweight
存在的意义是因为尽管我们大部分时间都需要共享对象来降低内存的损耗,但是个别时候也有可能不需要共享的,那么此时的UnsharedConcreteFlyweight子类就有存在的必要了,它可以解决那些不需要共享对象的问题。
3. 享元模式的应用
享元模式的应用非常多,比如说 Executor
使用的线程池,RecyclerView
的缓存池。
这里讲一下 MessageQueue
中使用的 Message
把
我们知道在 MessageQueue中 Message是以链表的形式来存放的,每个 Message
都会有一个指向下一个Message的指针:
我们来看看 obtain()
:
private static Message sPool;
private static int sPoolSize = 0;
public static Message obtain() {
synchronized (sPoolSync) {
if (sPool != null) {
Message m = sPool;
sPool = m.next;
m.next = null;
m.flags = 0; // clear in-use flag
sPoolSize--;
return m;
}
}
return new Message();
}
可以看到 obtain()
中会去 sPool中获取一个Message,如果sPool为空,则返回一个new的Message,否则把sPool赋值给m,并将m返回,然后 sPool取其next。
那我们看到这里如果池子为空时,Message被new出来后并没有加入到池子,那这样池子不一直都是空的吗?
对象池的定义是:当我们需要一个对象时,将它从对象池中取出来,如果不再使用对象时,将它放到对象池中
所以很显然,除了获取的 obtain()
方法,还有 放入到对象池的 recycleUnchecked()
方法:
// Message.java
void recycleUnchecked() {
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) {
if (sPoolSize < MAX_POOL_SIZE) {
next = sPool;
sPool = this;
sPoolSize++;
}
}
}
这个方法清空了一个Message的内容,并将其放入到了链表中。
接下来我来用图总结一下Message如何使用链表和池的吧,假设现在 sPool
为空, 我们通过 obtain()
new出两个Message后,又通过 recycleUnchecked()
将其中一个放入到池子中,现在池子是这样的:
这个时候我们用完了第二个 Message,然后同样的也调用 recycleUnchecked()
,池子就是这样的:
这个时候我们再调用 obtain()
获取一个Message,由于sPool所指向的Message不为空,所以它能直接取出一个Message,池子就变成了这样了:
现在已经很明朗了,Message通过再内部构建一个链表来维护一个被回收的Message对象的对象池,当用户调用obtain函数时会优先从池中取,如果池中没有可以复用的对象则创建这个新的Message对象。这些新创建的Message对象在被使用完后会被回收到这个对象池中,当下次再调用obtain函数时,他们就会被复用。
这里的Message相当于承担了享元模式中3个元素的职责,即 它是 Flyweight
角色,又是 ConcreteFlyweight
角色,又承担了 FlyweightFactory
的角色。
虽然这里的享元模式的应用并不是经典的HashMap应用,而且这个地方它不适用于 单一职责原则
,Message
本身的职责过多了,但是关于设计模式的使用是见仁见智的,我们更应该灵活的使用设计模式来解决问题。
学习Handler可以看着一篇文章:写给Rikka自己的Handler源码说明书
4. 小结
享元模式的实现比较简单,但是它的作用在某些场景确实是极其重要的。它可以大大减少应用程序创建的对象,降低程序内存的占用,增强程序的性能,但是它同时也提高了系统的复杂性,需要分离出外部状态和内部状态,而且外部状态具有固化特性,不应该随内部状态改变而改变,否则导致系统的逻辑混乱。
享元模式的优点在于它能够大幅度的降低内存中对象的数量,但是,它做到这一点所付出的代价也是很高的。
- 享元模式使得系统更加复杂,为了使对象可以共享,需要将一些状态外部化,这使得程序的逻辑复杂化
- 享元模式将享元对象的状态外部化,而读取外部状态使得运行时间稍微变长。