设计模式与内存管理:享元模式共享对象、代理模式延迟加载、备忘录模式保存状态
内存管理,说白了就是怎么用好你手头那点内存。我做了十几年项目,见过太多因为内存爆炸导致线上事故的案例。今天聊的三个模式——享元、代理、备忘录,恰好从三个不同角度帮你管好内存。
享元模式:共享对象,减少重复
先说说享元模式。这个模式的核心思想很简单:能共享的就别重复创建。
我记得有一次做游戏开发,场景里有成千上万个子弹对象。每个子弹都有自己的位置、速度、颜色。但仔细一想,其实很多子弹的纹理、形状、碰撞逻辑是完全一样的。如果每个子弹都独立保存这些数据,内存很快就爆了。
享元模式的做法是:把对象的状态分成两类——
- 内部状态:可以共享的,比如纹理、颜色、字体
- 外部状态:不能共享的,比如位置、速度、方向
内部状态存在享元对象里,外部状态由客户端传入。这样,一千个子弹只需要一个纹理对象。
核心要点:享元模式的关键是区分内部状态和外部状态。内部状态越少,共享效果越好。
来看个代码示例:
// Java 示例:享元模式实现字符缓存
public class CharacterFlyweight {
private final char symbol; // 内部状态
private final String font; // 内部状态
public CharacterFlyweight(char symbol, String font) {
this.symbol = symbol;
this.font = font;
}
public void display(int x, int y) { // 外部状态传入
System.out.println("绘制字符 " + symbol +
" 字体 " + font +
" 位置 (" + x + "," + y + ")");
}
}
// 享元工厂
public class CharacterFactory {
private Map<String, CharacterFlyweight> pool = new HashMap<>();
public CharacterFlyweight getCharacter(char symbol, String font) {
String key = symbol + ":" + font;
CharacterFlyweight flyweight = pool.get(key);
if (flyweight == null) {
flyweight = new CharacterFlyweight(symbol, font);
pool.put(key, flyweight);
}
return flyweight;
}
}
我的经验:享元模式特别适合用在文本编辑器、游戏粒子系统、数据库连接池这些场景。我曾经在一个报表系统里用享元模式缓存了上万个单元格样式,内存占用从 200MB 降到了 30MB。
代理模式:延迟加载,按需创建
代理模式解决的是另一个问题:别急着创建,等真正需要的时候再说。
你想想看,一个大型系统里,有些对象创建成本很高,比如加载一张高清图片、建立数据库连接、初始化一个重量级服务。如果一启动就全部加载,启动时间会变得不可接受。
代理模式的做法是:用一个轻量级的代理对象代替真实对象。客户端操作代理对象,代理对象在真正需要时才去创建真实对象。
我遇到过这样一个场景:一个文档查看器,打开文档时不需要加载所有图片,只需要加载第一屏的图片。剩下的图片等用户滚动到那里再加载。这就是典型的代理模式——虚拟代理。
// C++ 示例:代理模式实现图片延迟加载
class Image {
public:
virtual void display() = 0;
};
class RealImage : public Image {
private:
string filename;
void loadFromDisk() {
cout << "正在从磁盘加载图片: " << filename << endl;
// 模拟耗时操作
this_thread::sleep_for(chrono::seconds(2));
}
public:
RealImage(const string& filename) : filename(filename) {
loadFromDisk();
}
void display() override {
cout << "显示图片: " << filename << endl;
}
};
class ProxyImage : public Image {
private:
RealImage* realImage = nullptr;
string filename;
public:
ProxyImage(const string& filename) : filename(filename) {}
void display() override {
if (realImage == nullptr) {
realImage = new RealImage(filename);
}
realImage->display();
}
~ProxyImage() {
delete realImage;
}
};
注意:代理模式不是万能的。如果对象总是会被用到,那延迟加载反而增加了开销。我曾经见过有人对每个对象都套一层代理,结果性能反而下降了。用之前先问问自己:这个对象真的可能用不到吗?
备忘录模式:保存状态,随时回滚
备忘录模式,说白了就是给对象拍快照。你玩游戏时存档、写代码时撤销、做设计时保存历史版本——都是备忘录模式的应用。
这个模式由三部分组成:
- 发起人:需要保存状态的对象
- 备忘录:保存状态的对象
- 管理者:管理备忘录的存储和恢复
我记得在一个编辑器项目里,用户每输入一个字符都要保存一次状态,以便支持撤销功能。如果每次都保存完整状态,内存很快就撑不住了。后来我们只保存了变化的部分——增量备忘录。
// Java 示例:备忘录模式实现文本编辑器撤销
class TextEditor {
private StringBuilder content = new StringBuilder();
public void write(String text) {
content.append(text);
}
public Memento save() {
return new Memento(content.toString());
}
public void restore(Memento memento) {
content = new StringBuilder(memento.getState());
}
public String getContent() {
return content.toString();
}
// 备忘录类
static class Memento {
private final String state;
public Memento(String state) {
this.state = state;
}
private String getState() {
return state;
}
}
}
// 管理者
class History {
private Stack<TextEditor.Memento> history = new Stack<>();
public void push(TextEditor.Memento memento) {
history.push(memento);
}
public TextEditor.Memento pop() {
return history.pop();
}
}
避坑指南:我曾经犯过一个错误——备忘录里保存了对象的引用而不是副本。结果修改了对象后,备忘录里的状态也跟着变了。记住,备忘录一定要保存状态的深拷贝,否则你恢复的永远是当前状态。
三个模式的对比
| 模式 | 核心思想 | 内存管理策略 | 典型场景 |
|---|---|---|---|
| 享元模式 | 共享内部状态 | 减少对象数量 | 文本编辑器、游戏粒子 |
| 代理模式 | 延迟对象创建 | 按需分配内存 | 图片加载、远程服务 |
| 备忘录模式 | 保存状态快照 | 管理历史版本 | 撤销操作、存档系统 |
这三个模式其实可以组合使用。比如,享元模式共享对象,代理模式延迟加载,备忘录模式保存状态——它们从不同维度帮你管好内存。
总结一下:内存管理不是靠一个模式就能搞定的。享元模式帮你减少重复,代理模式帮你按需创建,备忘录模式帮你管理历史。三个模式配合使用,才能写出既高效又健壮的系统。