3. Flyweight
3. Flyweight:把大量实例的共享固有状态与逐实例外在状态分开,通过可 复位因果实验和反例证据验收。
3. Flyweight
学习目标
- 能区分固有状态与外在状态
- 能解释享元工厂如何共享重资源
- 能计算享元方案的内存节省
为什么"3. Flyweight"从问题证据开始
假设你的游戏是一片森林:一万棵橡树,每棵都有自己的位置、生长状态、渲染网格和材质贴图。最直觉的建模是给每棵树一个对象,各存各的网格和贴图——代码简单,但内存爆炸:一万份重复的网格顶点和贴图引用,把内存撑爆,卡顿随之而来。
本页把"无模式基线"定义为"每棵树一个完整对象"的代码,然后引入享元作为候选机制,验证它是否真的让"一万棵树的渲染内存"发生可观察的下降。通过条件是同一片森林在相同视觉质量下内存占用显著下降——而不是类的数量增加。关键洞察是:一万棵树里真正"不同"的部分只有位置和生长度,网格和贴图其实只有几种。
🔮 猜一猜:一万棵橡树共享一份网格,内存能省多少?
来源、版本与独立重写边界
本页用作者完整在线正文核对正式标题、设计分叉和时代语境,并以作者源码仓库交叉检查结构。仓库许可证明确正文、HTML与样式为 CC BY-NC-ND 4.0,示例程序等其他文件为 MIT;因此下列中文解释、图示、交互和代码均为独立教学重写,不翻译、拼接或改写受 ND 限制的原文表达。
本章机制与术语
理解 Flyweight 需要四个核心概念:
- ↡:所有实例共享、不可变的部分——树的网格、贴图、物种属性。享元里只存这一份。
- ↡:每个实例各自不同的部分——树的位置、生长度。由使用方持有,传入享元方法。
- ↡:按 key 管理享元对象池,保证相同固有状态的享元只创建一次。
- ↡:多个逻辑实例指向同一物理对象,用不同外在状态区分——内存换取的直接来源。
四个概念共同约束"把大量实例的共享固有状态与逐实例外在状态分开":任何结论都必须回到"相同视觉质量下的实例内存占用"这一可观察量。
第 1 / 5 步 · ① FlyweightFactory:按 key 管理共享对象池
一万棵橡树只占一份内部状态的内存;位置、缩放等外部状态由客户端持有。
💡 对着动画看:动画左侧是 FlyweightFactory 管理的共享池(Oak/Pine/Birch/Grass/Rock 各一份),右侧三个 Client 各自持有不同的位置/缩放参数,却共享同一份内部状态。注意"⬅ 共享"箭头——三个 Client 指向同一棵 Tree:Oak。这就是"一万棵树,一份网格"的直观呈现。读"森林案例"一节时回到这张图,体会内存是怎么省下来的。
官方结构逐项深读
3. Flyweight
Flyweight 的主张是:把对象拆成"共享的固有部分"和"各自的临在部分"。固有部分(如树的网格、贴图)在所有实例间完全相同,只存一份;临在部分(如位置、方向)每个实例都不同,由使用方保存。逻辑上你仍然有一万棵树,物理上共享的内存只有几种。核心代价是:固有状态必须不可变(共享的东西一旦被改,所有"树"同时变)。
Forest for the Trees
经典的森林案例:一万棵树,第一版实现每棵树一个对象,各自持有网格、纹理、颜色。作者数了一笔账——每棵树的网格和纹理占了绝大多数内存,而这些内容在一万棵橡树之间完全相同。省内存的思路不是压缩,而是去重:让所有橡树共享同一份网格和纹理对象。这正是 Flyweight 的起点:先观察数据里哪些部分在大量重复。
A Thousand Instances
一万个实例的问题本质是"重复的固有状态被复制了一万次"。如果每棵树的网格占 1MB,一万棵就是 10GB——不管机器内存多大都扛不住。而位置/生长度这类外在状态每棵只占几十字节,一万棵不过几百 KB。内存大头在重复部分,不在差异部分——这就是 Flyweight 的洞察:省内存要从"消灭重复"入手,而不是优化每个小对象。
The Flyweight Pattern
模式的核心是引入享元对象 + 享元工厂:享元对象只装固有状态(共享的一份),工厂按 key 返回共享实例(相同 key 永远返回同一个对象)。使用时,调用方把外在状态作为参数传入:tree.render(x, y, height)——同一棵享元树,用不同位置渲染一万次。实例的"身份"从对象变成"(共享对象, 外在状态)"的组合。
A Place To Put Down Roots
享元对象要放得进游戏世界里,需要一层"间接层":不是每个逻辑树都直接持有享元指针,而是把享元存在中心注册表(工厂),逻辑实体只持有一个索引或 key。游戏世界按需从工厂取用——要渲染时查表拿享元,传入自己的外在状态。这层间接让"新增树种"只需在工厂注册新 key,游戏实体完全无感。
What About Performance?
享元对性能的收益主要在内存带宽和缓存友好性:渲染一万棵树时,顶点和贴图数据都在同一批共享对象里,连续读取、缓存命中率高;而每棵树的网格若各自存储,渲染器要在一万块分散内存间跳转,缓存命中率惨不忍睹。代价是每次渲染多一层间接(查表取享元),但相比缓存命中的收益,这层间接微不足道——尤其在 GPU 上,纹理和网格的重复上载才是大开销。
See Also
- Data Locality:Flyweight 关注"共享同一份数据",Data Locality 关注"数据在内存里的排列"——两者常搭配:享元消除重复后,剩下的外在状态再按数组连续存放,性能更佳。
- Object Pool:Flyweight 省的是"重复内容",Object Pool 省的是"重复分配"——池化复用对象实例,两者互补但目标不同。
- Type Object:用数据描述类型(共享定义)与 Flyweight 的共享固有状态异曲同工,常结合使用。
可迁移实现或计算骨架
initial state -> 统计重复的固有状态 -> 享元工厂按 key 建池 -> 逻辑实体持 key -> 渲染时传外在状态
fault injection -> 每实例复制完整固有状态
pass condition -> 相同视觉质量下实例总内存显著下降
reset -> initial state对应实现要点:先 profile 找出内存大头(通常是重复的网格/贴图);建 FlyweightFactory 用 map 缓存 key→享元;逻辑实体只存 key 和外在状态;渲染/使用统一从工厂取享元并传参。该骨架只保存实验合同;真实项目还要固定场景规模、纹理预算与渲染路径,并保留基线实现以便回退。
本章练习与节点验证矩阵
练习
问题 1:重复统计。
操作:统计动画共享池中五种享元与三个客户端的固有/外在状态
问题 2:内存测量。
操作:实现"每树一对象"与"享元共享"两个版本,各建一万棵树并测内存
问题 3:不可变验证。
操作:尝试修改共享享元的固有状态(如换掉橡树网格)
术语复核
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 固有状态
- 外在状态
- 享元工厂
练习答案参考
练习判据即答案:每个练习的"验证判据"列给出了通过标准——先自己动手,再对照判据核验。
术语复核与本章回顾
掌握"3. Flyweight"意味着能从"一万棵树的网格被重复存储一万次"出发,解释固有状态、外在状态、享元工厂与共享四者的关系,再用"相同视觉质量下的实例内存占用"这一可观察量推翻或保留实现。若内存下降不能稳定复现,本章仍未通过。
一句话回顾:让一万棵树共享一份网格——固有状态放享元(一份),外在状态随实例(千份),森林内存从"万份"降到"一份加千份"。