介绍
享元(Flyweight)模式:使用共享对象可有效地支持大量的细粒度的对象。
享元模式是对象池的一种实现,代表轻量级的意思。用来尽可能减少内存使用量,它适合用于大量重复对象的场景,来缓存可共享的对象,达到对象共享,避免创建过多对象的效果,这样一来就可以提升性能,避免内存移除等。
享元模式中存在以下两种状态:
- 内部状态,即不会随着环境的改变而改变的可共享部分;
- 外部状态,指随环境改变而改变的不可以共享的部分。
享元模式的实现要领就是区分应用中的这两种状态,并将外部状态外部化。
优点
相同对象只要保存一份,这降低了系统中对象的数量,从而降低了系统中细粒度对象给内存带来的压力。
缺点
- 为了使对象可以共享,需要将一些不能共享的状态外部化,这将增加程序的复杂性。
- 读取享元模式的外部状态会使得运行时间稍微变长。
使用场景
- 系统存在大量相似或相同的对象。
- 需要缓冲池时。
- 细粒度的对象都具备较接近的外部状态,而且内部状态与环境无关,也就是说对象没有特定身份。
结构与实现
模式包含以下主要角色。
- Flyweight(抽象享元角色):接口或抽象类,可以同时定义出对象的外部状态和内部状态的接口或实现。
- ConcreteFlyweight(具体享元角色):实现抽象享元角色中定义的业务。
- UnsharedConcreteFlyweight(不可共享的享元角色):并不是所有的抽象享元类的子类都需要被共享,不能被共享的子类可设计为非共享具体享元类;当需要一个非共享具体享元类的对象时可以直接通过实例化创建。该对象一般不会出现在享元工厂中。
- FlyweightFactory(享元工厂):管理对象池和创建享元对象。
其结构图如下图所示。
享元模式的实现代码如下:
测试代码:
程序运行结果如下:
示例
这个是一个买车票的例子,客户端通过输入起始地和目的地到服务器,服务器返回车票价格信息,一次请求总会产生一个车票价格信息对象,如果是成千上万的用户不停的请求势必会使得服务器产生大量的重复对象,为了避免不必要的内存开销,可以使用享元模式来优化这种情况。
这种方式把对象缓存到了sTicketMap, key为 “from + “-“ + to”,这样避免了重复的起始地和目的地产生重复对象的情况。
ANDROID 源码中的实现
Handler 消息机制中的 Message 消息池就是使用享元模式复用了 Message 对象。
使用 Message 时一般会用到 Message.obtain 来获取消息。如果使用 new Message() 会构造大量的 Message 对象。obtain 方法如下:
sPoolSync 和 sPool 定义如下:
sPoolSync 是一个对象锁,用于在获取 Message 对象时进行同步锁。
sPool 是一个静态的 Message 对象。
next 是一个 Message 对象,指向下一个 Message。
可以看出,Message 消息池没有使用 map 这样的容器,而是使用的链表。
那么这些 Message 是什么时候放入链表中的呢?我们在 obtain 函数中只看到了从链表中获取,并且看到存储。如果消息池链表中没有可用对象的时候,obtain 中则是直接返回一个通过 new 创建的 Message 对象,而且并没有存储到链表中。
Message 类有一个 recycle 方法,它用来回收消息,并且把回收掉的消息添加到对象池链表中。recycle 方法如下:
recycleUnchecked 方法如下:
recycle 会将一个 Message 回收到一个全局的池。如果消息在使用就抛出异常,否则调用 recycleUnchecked。
recycleUnchecked 先清空字段,然后回收消息,将 sPool 指向当前消息,同时 size 加一。
Message 通过在内部构建一个链表来维护一个被回收的 Message 对象的对象池,当用户调用 obtain 时会优先从池中取,如果池中没有可以复用的对象则创建这个新的 Message 对象。这些新创建的 Message 对象在被使用完之后会被回收到这个对象池中,当下次再调用 obtain 时,它们就会被复用。
因为 Android 应用是事件驱动的,因此,如果通过 new 创建 Message 会产生大量的重复的 Message 对象,导致内存占用率高、频繁 GC 等问题,通过享元模式创建一个大小为 50 的消息池,避免了上述问题。