享元模式
“享元”则是共享元件的意思。享元模式的英文flyweight是轻量级的意思,这就意味着享元模式能使程序变得更加轻量化。当系统存在大量的对象,并且这些对象又具有相同的内部状态时,我们就可以用享元模式共享相同的元件对象,以避免对象泛滥造成资源浪费。
游戏地图
在早期的RPG(角色扮演类)游戏中,为了营造出不同的环境氛围,游戏的地图系统可以绘制出各种各样的地貌特征,如河流、山川、草地、沙漠、荒原,以及人造的房屋、道路、围墙等。为了避免问题的复杂化,我们就以草原地图作为范例
如果我们加载一整张图片并显示在屏幕上,游戏场景的加载速度一定会比较慢,而且组装地图的灵活性也会大打折扣,后期主角的移动碰撞逻辑还要提前对碰撞点坐标进行标记,这种设计显然不够妥当。正如之前探讨过的马赛克,我们可以发现整张游戏地图都是由一个个小的单元图块组成的,其中除房屋比较大之外,其他图块的尺寸都一样,它们分别为河流、草地、道路,这些图块便是4个元图块
卡顿的加载过程
在开始代码实战之前,我们先思考怎样去建模。首先我们应该定义一个图块类来描述图块,具体属性应该包括“图片”和“位置”信息,并且具备按照这些信息去绘制图块的能力
package flyweight;
public class Tile {
private String image;//图块所用的材质图
private int x,y;//图块所在的坐标
public Tile(String image,int x,int y) {
this.image = image;
System.out.println("从磁盘中加载["+image+"]图片,耗时半秒");
this.x = x;
this.y = y;
}
public void draw(){
System.out.println("在位置["+x+":"+y+"]绘制图片:【"+image+"】");
}
}
图块类看起来非常简单直观,代码清单定义了图块的材质图对象的引用,此处我们用String来模拟。我们定义了图块所在游戏地图的横坐标与纵坐标:[插图]与[插图]。第7行开始在构造方法中进行图片与坐标的初始化。此时我们把图片加载到内存,如I/O操作要耗费半秒时间,我们在第9行模拟输出。最后是第14行的绘制方法,能够把图片按照坐标位置显示在游戏地图上。一切就绪,开始测试绘制一些图块.
如果我们用客户端将所有图块进行初始化并绘制出来,顺利完成地图拼接。然而,通过观察运行结果我们会发现一个问题,每次加载一张图片都要耗费半秒时间,10张图块就要耗费5秒,如果加载整张地图将会耗费多长时间?如此糟糕的游戏体验简直就是在挑战玩家的忍耐力,缓慢的地图加载过程会让玩家失去兴趣。
面对解决加载卡顿的问题,有些读者可能已经想到我们之前学过的原型模式了。对,我们完全可以把相同的图块对象共享,用克隆的方式来省去实例化的过程,从而加快初始化速度。然而,对这几个图块克隆貌似没什么问题,地图加载速度确实提高了,但是构建巨大的地图一定会在内存中产生庞大的图块对象群,从而导致大量的内存开销。如果没有内存回收机制,甚至会造成内存溢出,系统崩溃.
用原型模式一定是不合适的,地图中的图块并非像游戏中动态的人物角色一样可以实时移动,它们的图片与坐标状态初始化后就固定下来了,简单讲就是被绘制出来后就不必变动了,即使要变也是将拼好的地图作为一个大对象整体挪动。图块一旦被绘制出来就不需要保留任何坐标状态,内存中自然也就不需要保留大量的图块对象了
图件共享
要提高游戏性能,我们只能利用少量的对象拼接整张地图。继续分析地图,我们会发现每个图块的坐标是不同的,但有很大一部分图块的材质图(图片)是相同的,也就是说,同样的材质图会在不同的坐标位置上重复出现。于是我们可以得出结论,材质图是可以作为享元的,而坐标则不能。
既然要共享相同的图片,那么我们就得将图块类按图片拆分成更细的材质类,如河流类、草地类、道路类等。而坐标不能作为图块类的享元属性,所以我们就得设法把这个属性抽离出去由外部负责。不能纸上谈兵,我们继续代码实战,首先需要定义一个接口
package flyweight;
public interface Drawable {
void draw(int x,int y);
}
我们定义了绘图接口,使坐标作为参数传递进来并进行绘图。当然,除了接口方式,我们还可以用抽象类抽离出更多的属性和方法,使子类变得更加简单。接下来我们再定义一系列材质类并实现此绘图接口,首先是河流类
package flyweight;
public class River implements Drawable {
private String image;//河流图片材质
public River(){
this.image = "河流";
System.out.println("从磁盘加载【"+image+"】图片,耗时半秒");
}
@Override
public void draw(int x, int y) {
System.out.println("在位置【"+x+":"+y+"】上绘制图片:【"+image+"】");
}
}
河流类中只定义了图片作为内部属性。在第6行的类构造器中加载河流图片,这就是类内部即将共享的“元”数据了,我们通常称之为“内蕴状态”。而作为“外蕴状态”的坐标是无法作为享元的,所以将其作为参数实现的绘图方法中由外部传入。以此类推,接下来我们定义草地类、道路类、房屋类.
package flyweight;
public class Grass implements Drawable {
private String image;//草地图片材质
public Grass(){
this.image = "草地";
System.out.println("从磁盘加载【"+image+"】图片,耗时半秒");
}
@Override
public void draw(int x, int y) {
System.out.println("在位置【"+x+":"+y+"】上绘制图片:【"+image+"】");
}
}
package flyweight;
public class Road implements Drawable {
private String image;//道路图片材质
public Road(){
this.image = "道路";
System.out.println("从磁盘加载【"+image+"】图片,耗时半秒");
}
@Override
public void draw(int x, int y) {
System.out.println("在位置【"+x+":"+y+"】上绘制图片:【"+image+"】");
}
}
package flyweight;
public class House implements Drawable{
private String image;//房屋图片材质
public House(){
this.image = "房屋";
System.out.println("从磁盘加载【"+image+"】图片,耗时半秒");
}
@Override
public void draw(int x, int y) {
System.out.println("将图层切换到顶层");//房屋覆盖在地板上,所以切换到顶层图层
System.out.println("在位置【"+x+":"+y+"】上绘制图片:【"+image+"】");
}
}
房屋类与其他类有所区别,它拥有自己特定的绘图方法,调用后会在地板图层之上绘制房屋,覆盖下面的地板(房屋图片比其他图片要大一些),以使地图变得更加立体化。接下来就是实现“元之共享”的关键了,我们得定义一个图件工厂类,并将各种图件对象提前放入内存中共享,如此便可以避免每次从磁盘重新加载
package flyweight;
import java.util.HashMap;
import java.util.Map;
public class TileFactory {
private Map<String,Drawable> images;//图库
public TileFactory() {
this.images = new HashMap<>();
}
public Drawable getDrawable(String image){
//缓存池里面如果没有图件,则实例化并放入缓存池
if (!images.containsKey(image)){
switch (image){
case "河流":
images.put(image,new River());
break;
case "草地":
images.put(image,new Grass());
break;
case "道路":
images.put(image,new Road());
break;
case "房子":
images.put(image,new House());
break;
}
}
//至此,缓存池里必然哟图件,直接取得返回
return images.get(image);
}
}
图件工厂类类似于一个图库管理器,其中维护着所有的图件元对象。首先在构造方法中初始化一个散列图的“缓存池”,然后通过懒加载模式来维护它。当客户端调用获取图件方法getDrawable()时,程序首先会判断目标图件是否已经实例化并存在于缓存池中,如果没有则实例化并放入图库缓存池供下次使用,到这里目标图件必然存在于缓存池中了。最后直接从缓存池中获取目标图件并返回。如此,无论外部需要什么图件,也无论外部获取多少次图件,每类图件都只会在内存中被加载一次,这便是“元共享”的秘密所在。最后让我们来看客户端如何构建游戏地图.
package flyweight;
public class Client {
public static void main(String[] args) {
TileFactory factory = new TileFactory();
factory.getDrawable("河流").draw(10,10);
factory.getDrawable("河流").draw(10,20);
factory.getDrawable("道路").draw(10,30);
factory.getDrawable("草地").draw(10,40);
factory.getDrawable("草地").draw(10,60);
factory.getDrawable("草地").draw(10,70);
factory.getDrawable("道路").draw(10,80);
factory.getDrawable("道路").draw(10,100);
//绘制完地板后接着在顶层绘制房屋
factory.getDrawable("房子").draw(10,10);
factory.getDrawable("房子").draw(10,50);
}
}
我们抛弃了利用“new”关键字随意制造对象的方法,改用这个图件工厂类来构建并共享图件元,外部需要什么图件直接向图件工厂索要即可。此外,图件工厂类返回的图件实例也不再包含坐标信息这个属性了,而是将其作为绘图方法的参数即时传入。结果立竿见影,从开始的输出中可以看到,每个图件对象在初次实例化时会耗费半秒时间,而下次请求时就不会再出现加载图片的耗时操作了,也就是从图库缓存池直接拿到了.
万变不离其宗
至此,享元模式的运用让程序运行更加流畅,地图加载再也不会出现卡顿现象了,加载图片时的I/O流操作所导致的CPU效率及内存占用的问题同时得以解决,游戏体验得以提升和改善。享元模式让图件对象将可共享的内蕴状态“图片”维护起来,将外蕴状态“坐标”抽离出去并定义于接口参数中,基于此,享元工厂便可以顺利将图件对象共享,以供外部随时使用。我们来看享元模式的类结构
Flyweight(享元接口):所有元件的高层规范,声明与外蕴状态互动的接口标准。对应本章例程中的绘图接口Drawable。
ConcreteFlyweight(享元实现):享元接口的元件实现类,自身维护着内蕴状态,且能接受并响应外蕴状态,可以有多个实现,一个享元对象可以被称作一个“元”。对应本章例程中的河流类River、草地类Grass、道路类Road等。
FlyweightFactory(享元工厂):用来维护享元对象的工厂,负责对享元对象实例进行创建与管理,并对外提供获取享元对象的服务。
Client(客户端):享元的使用者,负责维护外蕴状态。对应本章例程中的图件工厂类TileFactory。
Go版本代码
package flyweight
import (
"fmt"
"strconv"
)
type Drawable interface {
Draw(x, y int)
}
type Grass struct {
image string
}
func NewGrass() *Grass {
img := &Grass{
image: "草地",
}
fmt.Println("从磁盘加载图片{" + img.image + "},耗时半秒")
return img
}
func (g *Grass) Draw(x, y int) {
fmt.Println("在位置【" + strconv.Itoa(x) + ":" + strconv.Itoa(y) + "],绘制图片:" + g.image)
}
type River struct {
image string
}
func NewRiver() *River {
img := &River{
image: "河流",
}
fmt.Println("从磁盘加载图片{" + img.image + "},耗时半秒")
return img
}
func (g *River) Draw(x, y int) {
fmt.Println("在位置【" + strconv.Itoa(x) + ":" + strconv.Itoa(y) + "],绘制图片:" + g.image)
}
type House struct {
image string
}
func NewHouse() *House {
img := &House{
image: "房屋",
}
fmt.Println("从磁盘加载图片{" + img.image + "},耗时半秒")
return img
}
func (h *House) Draw(x, y int) {
fmt.Println("在位置【" + strconv.Itoa(x) + ":" + strconv.Itoa(y) + "],绘制图片:" + h.image)
}
type Road struct {
image string
}
func NewRoad() *Road {
img := &Road{
image: "街道",
}
fmt.Println("从磁盘加载图片{" + img.image + "},耗时半秒")
return img
}
func (r *Road) Draw(x, y int) {
fmt.Println("在位置【" + strconv.Itoa(x) + ":" + strconv.Itoa(y) + "],绘制图片:" + r.image)
}
package flyweight
type TileFactory struct {
dic map[string]Drawable
}
func NewTileFactory() *TileFactory {
return &TileFactory{
dic: make(map[string]Drawable),
}
}
func (t *TileFactory) GetDrawable(image string) Drawable {
if _, ok := t.dic[image]; ok {
switch image {
case "草地":
t.dic[image] = NewGrass()
case "河流":
t.dic[image] = NewRiver()
case "街道":
t.dic[image] = NewRoad()
case "房屋":
t.dic[image] = NewHouse()
}
}
return t.dic[image]
}
package flyweight;
public class Client {
public static void main(String[] args) {
TileFactory factory = new TileFactory();
factory.getDrawable("河流").draw(10,10);
factory.getDrawable("河流").draw(10,20);
factory.getDrawable("道路").draw(10,30);
factory.getDrawable("草地").draw(10,40);
factory.getDrawable("草地").draw(10,60);
factory.getDrawable("草地").draw(10,70);
factory.getDrawable("道路").draw(10,80);
factory.getDrawable("道路").draw(10,100);
//绘制完地板后接着在顶层绘制房屋
factory.getDrawable("房子").draw(10,10);
factory.getDrawable("房子").draw(10,50);
}
}