什么是享元模式(Flyweight Pattern)
享元模式旨在通过共享对象来有效地支持大量的细粒度对象,以最大限度地减少内存使用或计算开销。
想象一下你在 Word 文档中输入文字。文档里可能有成千上万个字符 'A'。如果每个 'A' 字符对象都存储自己的字体、大小、样式等信息,将会消耗大量内存。享元模式就像是说:所有相同字体、大小、样式的 'A' 字符,我们只创建一个共享的 'A' 对象(享元 Flyweight),这个共享对象包含了这些内在的、不变的属性(字体、大小、样式 - Intrinsic State)。而每个 'A' 字符在文档中的位置(外在的、变化的属性 - Extrinsic State)则在使用时由外部传入。
享元模式的核心在于区分对象的内部状态(Intrinsic)和外部状态(Extrinsic),并共享具有相同内部状态的对象实例。
核心思想
- 区分状态: 将对象的状态分为两部分:
- 内部状态 (Intrinsic State): 这是存储在享元对象内部的、不随上下文环境改变而改变的状态。它是可以被共享的部分。例如,字符 'A' 的字形数据、颜色 'Red' 的 RGB 值。
- 外部状态 (Extrinsic State): 这是随上下文环境改变而改变的、不可以共享的状态。它由客户端在每次调用享元对象的方法时传递给享元对象。例如,字符 'A' 在文档中的位置 (x, y)、绘制一个红色圆形的半径和圆心坐标。
- 定义享元接口 (Flyweight): 定义一个接口,所有具体享元类都实现这个接口。接口通常包含一个或多个方法,这些方法接受外部状态作为参数。例如,draw(x, y, context)。
- 创建具体享元 (ConcreteFlyweight): 实现 Flyweight 接口,并为内部状态增加存储空间。ConcreteFlyweight 对象必须是可共享的,其内部状态在创建后通常不应再改变(或至少对客户端透明地改变)。
- 创建享元工厂 (FlyweightFactory):
- 负责创建和管理享元对象。
- 它维护一个池 (Pool) 或缓存(通常是 Map 或 HashMap),存储已经创建的享元对象。
- 当客户端请求一个享元对象时,工厂首先检查池中是否已存在具有相同内部状态的实例。如果存在,则返回共享的实例;如果不存在,则创建一个新的实例,将其添加到池中,然后返回。
- 工厂通常实现为单例 (Singleton)。
- 客户端 (Client):
- 计算或存储对象的外部状态。
- 向享元工厂请求具有特定内部状态的享元对象。
- 在需要时,调用享元对象的方法,并将计算好的外部状态传递给它。
优点
- 显著减少内存占用: 这是享元模式最主要的优点。通过共享大量相似对象的内部状态,可以极大地减少系统中对象的数量,从而节省内存。
- 提高性能: 减少了对象的创建和销毁次数,降低了垃圾回收的压力,可能带来性能提升。
可能的缺点
- 增加了系统的复杂性: 需要区分内部状态和外部状态,并引入工厂类来管理享元对象,使得系统结构更复杂。
- 分离状态可能不自然: 将对象状态强制分为内部和外部,有时可能不符合自然的面向对象设计思路。
- 外部状态的管理: 客户端需要负责计算、存储和传递外部状态,增加了客户端的负担。
- 线程安全: 享元对象是共享的,如果其内部状态并非完全不可变,或者其操作方法不是线程安全的,需要特别注意多线程环境下的并发问题(通常要求享元对象是线程安全的)。
使用场景
- 当一个应用程序使用了大量的相似对象,导致内存占用过高或性能下降时。
- 对象的大部分状态可以外部化(即可以作为外部状态传递)。
- 可以用较少的共享对象取代大量的非共享对象。
- 对象ID(内部状态)很少,但每个ID可能有很多实例(需要不同的外部状态)。
- 文本编辑器: 字符对象的共享(如上所述)。
- 图形应用: 线条、形状、图标等图形元素,如果颜色、样式相同,可以共享,位置、大小等作为外部状态。
- 游戏开发: 大量的树木、岩石、粒子效果等,如果模型、纹理相同,可以共享,位置、旋转、缩放作为外部状态。
- 数据库连接池/线程池: 池中的连接/线程对象被复用,虽然不是严格的享元(状态分离不明显),但体现了共享资源的核心思想。
- Android 中的场景:
- RecyclerView 的 ViewHolder 复用机制: 这是 Android 中与享元模式思想最接近、最广为人知的例子。RecyclerView 创建有限数量的 ViewHolder 对象(共享池)。当列表滚动时,离开屏幕的 ViewHolder 被回收并重新绑定(onBindViewHolder)新的数据(外部状态),用于显示新的列表项。这里,ViewHolder 及其包含的 View 是被共享的“享元”(虽然状态分离不完全是教科书式的 Intrinsic/Extrinsic),列表项的数据是外部状态。这极大地减少了 View 的创建和内存消耗。
- 系统内部的资源缓存: Android 系统或框架内部可能对一些资源(如 Drawable 的 ConstantState、颜色 ColorStateList、字体 Typeface)进行缓存和共享,以节省内存,这体现了享元思想。
- 图片加载库的 BitmapPool: 像 Glide 这样的库使用 BitmapPool 来复用 Bitmap 对象,避免频繁创建和销毁大块内存,这也是资源共享思想的应用。
UML 图
下面是基于“绘制不同颜色的圆形”示例的享元模式 UML 图:
- Shape (Flyweight): 定义了绘制接口 draw,接受外部状态(画布、坐标、半径)。
- Circle (ConcreteFlyweight): 实现了 Shape。它存储内部状态 color。draw 方法使用 color 和传入的外部状态来绘制圆形。
- ShapeFactory (FlyweightFactory): 包含一个 circleCache (Map)。getCircle(color) 方法负责从缓存中获取或创建并缓存 Circle 实例。
- Client: 从 ShapeFactory 获取具有特定颜色(内部状态)的 Circle 对象,然后在调用 draw 时传入不同的坐标和半径(外部状态)。
代码示例
在这个例子中:
- Shape 是 Flyweight 接口,Circle 是 ConcreteFlyweight,存储内部状态 color。
- ShapeFactory 是 FlyweightFactory,使用 circleCache 来管理和复用 Circle 实例。
- 客户端代码多次请求不同颜色的 Circle。从工厂获取 Circle 时,如果该颜色的 Circle 已存在于缓存中,则返回共享实例(注意看输出中相同颜色的 Circle 具有相同的实例 ID),否则创建新的实例并缓存。
- 客户端在调用 draw 时传入坐标 x, y 和 radius 作为外部状态。
- 最终,虽然绘制了 7 个圆形,但只创建了 4 个 Circle 对象(对应 Red, Green, Blue, Yellow 四种颜色),有效节省了资源。
总结
- 享元模式是一种优化模式,主要目标是通过共享对象来减少内存消耗,尤其适用于需要创建大量相似对象的场景。
- 核心在于区分并分离对象的内部状态(共享)和外部状态(不共享,由客户端提供)。
- 需要一个工厂类来管理共享对象的池(缓存)。
- 在 Android 中,RecyclerView 的 ViewHolder 复用机制是其思想的最佳体现。
记住它的核心:对象太多内存爆?共享内部状态,享元来报到!外部状态?用时再给到!
- Author:CoderWdd
- URL:https://www.wuinsights.top//article/54c73865-eeef-4b6f-9c27-4a92164e443c
- Copyright:All articles in this blog, except for special statements, adopt BY-NC-SA agreement. Please indicate the source!
Relate Posts