rokevin
移动
前端
语言
  • 基础

    • Linux
    • 实施
    • 版本构建
  • 应用

    • WEB服务器
    • 数据库
  • 资讯

    • 工具
    • 部署
开放平台
产品设计
  • 人工智能
  • 云计算
计算机
其它
GitHub
移动
前端
语言
  • 基础

    • Linux
    • 实施
    • 版本构建
  • 应用

    • WEB服务器
    • 数据库
  • 资讯

    • 工具
    • 部署
开放平台
产品设计
  • 人工智能
  • 云计算
计算机
其它
GitHub
  • 享元模式(Flyweight Pattern)

  • 一、享元模式核心概念(通用)
    • 1. 定义
    • 2. 意图
    • 3. 核心状态划分
    • 4. 通用核心组件
  • 二、享元模式详细解析(以 “文字处理系统” 为例)
    • 1. 结构
    • 2. 类图(Mermaid)
    • 3. 时序图(Mermaid)
    • 4. 优点
    • 5. 缺点
  • 三、Java 代码实现(文字处理系统示例)
    • 1. 外部状态(Position)
    • 2. 抽象享元(Character)
    • 3. 具体享元(Letter)
    • 4. 享元工厂(CharacterFactory)
    • 5. 客户端(Client)
    • 6. 输出结果
  • 四、适用环境
  • 五、模式分析
    • 1. 核心本质
    • 2. 与其他模式的区别
    • 3. 关键设计原则
  • 六、模式扩展
    • 1. 复合享元模式(Composite Flyweight)
    • 2. 带缓存淘汰策略的享元
    • 3. 结合单例模式
  • 七、模式应用(通用 + Android)
    • 1. 通用领域应用
    • 2. Android 中的应用
      • (1)TextView 与字符缓存
      • (2)RecyclerView 缓存机制
      • (3)Drawable 资源复用
      • (4)系统图标与主题资源
  • 八、总结

享元模式(Flyweight Pattern)

享元模式是结构型设计模式的重要成员,其核心目标是通过共享技术有效支持大量细粒度对象的复用,从而减少内存占用和提高系统性能。它如同现实中的 “活字印刷术”—— 每个汉字(细粒度对象)被制成一个活字(共享对象),重复使用于不同的文章中,无需为每个出现的汉字重新制作活字。

一、享元模式核心概念(通用)

1. 定义

运用共享技术有效地支持大量细粒度对象的复用。系统只创建少量的同类对象,而这些对象可被多个场景共享,通过区分 “内部状态” 和 “外部状态” 实现共享。

2. 意图

  • 减少对象创建数量:针对大量相似的细粒度对象,通过共享避免重复创建,降低内存消耗。
  • 提高系统性能:减少对象实例化带来的资源开销(如内存分配、GC 压力)。
  • 平衡内存与复杂度:通过分离 “可共享状态” 和 “不可共享状态”,在共享对象的同时保持场景灵活性。

3. 核心状态划分

享元模式的关键是区分对象的两种状态,这是实现共享的基础:

  • 内部状态(Intrinsic State): 对象固有的、可共享的状态,不依赖于外部环境,一旦创建后不会改变(如字符的 “字形”)。
  • 外部状态(Extrinsic State): 依赖于外部环境的、不可共享的状态,随场景变化而变化,由客户端在使用时传入(如字符的 “位置坐标”)。

4. 通用核心组件

享元模式通过 “工厂管理共享对象”“分离内外状态” 实现,包含 4 个核心角色:

角色名称职责描述
Flyweight(抽象享元角色)定义享元对象的接口,声明接收外部状态的方法(如 operation(extrinsicState))。
ConcreteFlyweight(具体享元角色)实现抽象享元接口,存储内部状态(可共享),在 operation() 中结合外部状态完成业务逻辑。
UnsharedConcreteFlyweight(非共享具体享元角色)部分场景中存在无法共享的细粒度对象,此类对象不参与共享,但其结构可与享元一致(可选角色)。
FlyweightFactory(享元工厂角色)核心管理角色,负责创建和缓存享元对象: - 当客户端请求享元时,先检查缓存中是否存在; - 存在则直接返回,不存在则创建新对象并缓存; - 通常使用哈希表(如 HashMap)存储 “内部状态标识” 与 “享元对象” 的映射。

二、享元模式详细解析(以 “文字处理系统” 为例)

以 “文字处理软件(如 Word)” 为场景:文档中可能包含大量重复字符(如字母 'A' 可能出现上千次),若每个字符都创建独立对象,会消耗大量内存。享元模式通过共享相同字符的对象(内部状态为 “字符值”),仅在使用时传入位置等外部状态,实现高效复用。

1. 结构

  1. Flyweight(抽象享元):Character,定义 display(position) 方法(position 为外部状态)。
  2. ConcreteFlyweight(具体享元):Letter,存储内部状态 charValue(如 'A'),在 display() 中结合位置参数显示字符。
  3. FlyweightFactory(享元工厂):CharacterFactory,用 HashMap 缓存 Letter 对象,键为字符值(如 'A'),值为对应的 Letter 实例。
  4. 客户端:通过工厂获取字符对象,传入位置参数调用 display()。

2. 类图(Mermaid)

- Flyweight: 享元对象

- IntrinsicState: 内部状态,享元对象共享内部状态

- ExtrinsicState: 外部状态,每个享元对象的外部状态不同

classDiagram
    %% 抽象享元角色:字符接口
    class Character {
        <<Interface>>
        +display(position: Position): void  // 接收外部状态(位置)并显示
    }

    %% 具体享元角色:字母(共享对象)
    class Letter {
        -charValue: char  // 内部状态(字符值,可共享)
        +Letter(charValue: char)
        +display(position: Position): void  // 结合外部状态(位置)显示
    }

    %% 外部状态:位置(客户端传入,不可共享)
    class Position {
        -x: int  // X坐标
        -y: int  // Y坐标
        +Position(x: int, y: int)
        +getX(): int
        +getY(): int
    }

    %% 享元工厂:管理字符享元对象
    class CharacterFactory {
        -flyweights: HashMap~char, Character~  // 缓存享元对象
        +getCharacter(charValue: char): Character  // 获取或创建享元
    }

    %% 客户端:使用享元的场景
    class Client {
        +useCharacters(): void
    }

    %% 关系
    %% 具体享元实现抽象享元
    Character <|-- Letter
    CharacterFactory o-- Character : 缓存(管理)
    Client --> CharacterFactory : 获取享元
    Client --> Position : 创建外部状态
    Client --> Character : 调用display()传入外部状态

3. 时序图(Mermaid)

以 “客户端显示两个 'A' 字符(位置不同)” 为例,展示享元模式的调用流程:

sequenceDiagram
    participant Client(客户端)
    participant CharacterFactory(享元工厂)
    participant Letter(具体享元:'A')
    participant Position(外部状态:位置1)
    participant Position2(外部状态:位置2)

    %% 1. 客户端首次请求字符'A'
    Client->>CharacterFactory: getCharacter('A')
    CharacterFactory->>CharacterFactory: 检查缓存(无'A')
    CharacterFactory->>Letter: new Letter('A')  // 创建新享元
    CharacterFactory->>CharacterFactory: 缓存'A' -> Letter对象
    CharacterFactory-->>Client: 返回Letter('A')

    %% 2. 客户端传入位置1显示'A'
    Client->>Position: new Position(10, 20)  // 创建外部状态
    Client->>Letter: display(position)
    Letter-->>Client: 显示 "A at (10,20)"

    %% 3. 客户端再次请求字符'A'
    Client->>CharacterFactory: getCharacter('A')
    CharacterFactory->>CharacterFactory: 检查缓存(有'A')
    CharacterFactory-->>Client: 返回缓存的Letter('A')

    %% 4. 客户端传入位置2显示'A'
    Client->>Position2: new Position(30, 40)  // 创建新外部状态
    Client->>Letter: display(position2)
    Letter-->>Client: 显示 "A at (30,40)"

4. 优点

  • 减少对象数量:相同对象只创建一次并共享,大幅降低内存消耗(如 1000 个 'A' 字符仅需 1 个对象)。
  • 提高性能:减少对象实例化和垃圾回收(GC)的开销,尤其适用于频繁创建大量相似对象的场景。
  • 分离内外状态:内部状态集中管理,外部状态由客户端控制,灵活适应不同场景。

5. 缺点

  • 系统复杂度增加:需分离内外状态,引入享元工厂管理缓存,理解和维护成本提高。
  • 外部状态管理成本:外部状态需由客户端传入,若外部状态复杂,可能导致客户端代码繁琐。
  • 线程安全风险:若共享的享元对象内部状态可被修改(违背不可变原则),多线程环境下可能出现状态混乱。

三、Java 代码实现(文字处理系统示例)

1. 外部状态(Position)

存储字符的位置信息,由客户端创建并传入:

// 外部状态:字符位置(不可共享,随场景变化)
public class Position {
    private final int x;  // X坐标
    private final int y;  // Y坐标

    public Position(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int getX() { return x; }
    public int getY() { return y; }
}

2. 抽象享元(Character)

定义字符的抽象接口,声明接收外部状态的方法:

// 抽象享元:字符接口
public interface Character {
    // 显示字符,接收外部状态(位置)
    void display(Position position);
}

3. 具体享元(Letter)

实现抽象接口,存储内部状态(字符值),结合外部状态完成显示:

// 具体享元:字母(共享对象,存储内部状态)
public class Letter implements Character {
    // 内部状态:字符值(如'A',可共享,创建后不可变)
    private final char charValue;

    // 构造器初始化内部状态
    public Letter(char charValue) {
        this.charValue = charValue;
    }

    // 结合外部状态(位置)显示字符
    @Override
    public void display(Position position) {
        System.out.printf("字符 '%c' 显示在位置 (%d, %d)%n",
                charValue, position.getX(), position.getY());
    }
}

4. 享元工厂(CharacterFactory)

管理享元对象的创建和缓存,确保相同内部状态的对象只存在一个:

import java.util.HashMap;
import java.util.Map;

// 享元工厂:管理字符享元的创建与缓存
public class CharacterFactory {
    // 缓存享元对象:键为内部状态(字符值),值为享元对象
    private final Map<Character, Character> flyweights = new HashMap<>();

    // 获取享元:存在则返回缓存,不存在则创建并缓存
    public Character getCharacter(char charValue) {
        // 检查缓存
        Character character = flyweights.get(charValue);
        // 若不存在,创建新对象并缓存
        if (character == null) {
            character = new Letter(charValue);
            flyweights.put(charValue, character);
            System.out.printf("创建新字符享元:'%c'(当前缓存大小:%d)%n",
                    charValue, flyweights.size());
        } else {
            System.out.printf("复用字符享元:'%c'(当前缓存大小:%d)%n",
                    charValue, flyweights.size());
        }
        return character;
    }
}

5. 客户端(Client)

通过工厂获取享元对象,传入外部状态使用:

// 客户端:使用字符享元
public class Client {
    public static void main(String[] args) {
        // 创建享元工厂
        CharacterFactory factory = new CharacterFactory();

        // 场景1:显示文档中的字符(多次出现相同字符)
        Character a1 = factory.getCharacter('A');
        a1.display(new Position(10, 20));  // A at (10,20)

        Character b = factory.getCharacter('B');
        b.display(new Position(15, 20));  // B at (15,20)

        Character a2 = factory.getCharacter('A');  // 复用已创建的'A'
        a2.display(new Position(30, 40));  // A at (30,40)

        Character a3 = factory.getCharacter('A');  // 再次复用
        a3.display(new Position(50, 60));  // A at (50,60)
    }
}

6. 输出结果

创建新字符享元:'A'(当前缓存大小:1)
字符 'A' 显示在位置 (10, 20)
创建新字符享元:'B'(当前缓存大小:2)
字符 'B' 显示在位置 (15, 20)
复用字符享元:'A'(当前缓存大小:2)
字符 'A' 显示在位置 (30, 40)
复用字符享元:'A'(当前缓存大小:2)
字符 'A' 显示在位置 (50, 60)

四、适用环境

享元模式适用于以下场景,核心判断标准是 “存在大量相似的细粒度对象,且这些对象大部分状态可共享”:

  1. 大量细粒度对象场景:系统需要创建大量同类对象(如文档中的字符、游戏中的粒子、地图上的树木),导致内存占用过高。
  2. 对象大部分状态可共享:对象的状态可分为 “内部状态”(可共享)和 “外部状态”(不可共享),且内部状态占比高。
  3. 对象创建成本高:对象实例化消耗大量资源(如内存、CPU),且存在重复创建相同对象的情况。
  4. 需要缓存池管理:希望通过缓存机制复用对象,减少重复创建(如数据库连接池、线程池)。

五、模式分析

1. 核心本质

享元模式的本质是 “共享复用 + 状态分离”:

  • 共享复用:通过工厂缓存相同内部状态的对象,避免重复创建,降低内存消耗。
  • 状态分离:将对象状态拆分为 “内部(共享)” 和 “外部(非共享)”,使共享对象能适应不同场景。

2. 与其他模式的区别

模式核心差异典型场景
享元模式共享细粒度对象,分离内外状态,减少内存字符复用、粒子系统
单例模式确保全局只有一个对象实例全局配置、工具类
原型模式通过复制创建对象,避免重复初始化复杂对象的快速创建
工厂模式封装对象创建逻辑,不强调共享复杂对象的创建管理

3. 关键设计原则

  • 内部状态不可变:具体享元的内部状态应在创建后不可修改(如 Letter 的 charValue 用 final 修饰),确保多线程环境下的安全性。
  • 外部状态由客户端管理:外部状态不应存储在享元对象中,而应在使用时由客户端传入,避免共享对象被污染。
  • 工厂缓存策略:享元工厂的缓存实现(如 HashMap)应高效,可根据需求扩展为 LRU 等淘汰策略(适用于内存有限的场景)。

六、模式扩展

享元模式可根据场景需求扩展出以下变体:

1. 复合享元模式(Composite Flyweight)

当享元对象可组合成更复杂的结构(如 “单词” 由多个 “字符” 组成),且组合后的对象也可共享时,可引入复合享元:

  • CompositeCharacter(复合享元):实现 Character 接口,包含多个 Character 子享元(如字母);
  • 工厂同时管理简单享元和复合享元,客户端可直接获取 “单词” 等组合对象。

2. 带缓存淘汰策略的享元

当内存有限或对象数量极多时,享元工厂可采用 LRU(最近最少使用)、FIFO(先进先出)等策略淘汰不常用的享元,平衡内存占用:

// 示例:LRU缓存策略(简化版)
public class LRUCharacterFactory {
    private final LinkedHashMap<Character, Character> flyweights = 
        new LinkedHashMap<>(16, 0.75f, true) {
            @Override
            protected boolean removeEldestEntry(Map.Entry<Character, Character> eldest) {
                return size() > 10;  // 超过10个对象则淘汰最久未使用的
            }
        };
    // ... 其他代码与普通工厂类似
}

3. 结合单例模式

享元工厂本身可设计为单例,确保系统中只有一个享元管理器,避免重复缓存相同对象:

public class SingletonCharacterFactory {
    private static final SingletonCharacterFactory INSTANCE = new SingletonCharacterFactory();
    private SingletonCharacterFactory() {}  // 私有构造器
    public static SingletonCharacterFactory getInstance() { return INSTANCE; }
    // ... 缓存逻辑
}

七、模式应用(通用 + Android)

1. 通用领域应用

  • 字符串常量池:Java 中字符串常量(如 "abc")存储在常量池,相同字符串复用同一对象("abc" == "abc" 为 true)。
  • 数据库连接池:复用数据库连接对象,避免频繁创建和关闭连接(连接信息为内部状态,SQL 语句为外部状态)。
  • 游戏粒子系统:游戏中大量相同类型的粒子(如火焰、雨滴)共享纹理、大小等内部状态,位置、速度为外部状态。
  • 图标库:UI 框架中相同图标(如 “关闭按钮”)复用同一图标对象,位置为外部状态。

2. Android 中的应用

Android 框架大量使用享元模式优化内存和性能,尤其是在 UI 渲染和资源管理场景:

(1)TextView 与字符缓存

  • 原理:TextView 渲染文字时,相同字符(如 'A')复用同一 Glyph(字形)对象,字形数据为内部状态,位置为外部状态。
  • 优化:避免为每个字符重复加载字形数据,减少内存占用和绘制开销。

(2)RecyclerView 缓存机制

  • 原理:RecyclerView 的 Recycler 缓存池复用 ViewHolder 对象(享元),ViewHolder 的布局结构为内部状态,绑定的数据为外部状态(通过 onBindViewHolder 传入)。
  • 优化:减少 ViewHolder 的频繁创建和销毁,提升列表滑动性能。
// RecyclerView缓存复用核心逻辑(简化)
public class Recycler {
    // 缓存ViewHolder(享元对象)
    private final SparseArray<ViewHolder> cache = new SparseArray<>();

    // 获取缓存的ViewHolder,若无则创建
    public ViewHolder getViewHolder(int viewType) {
        ViewHolder holder = cache.get(viewType);
        if (holder == null) {
            holder = createViewHolder(viewType);  // 创建新对象
            cache.put(viewType, holder);
        }
        return holder;
    }
}

(3)Drawable 资源复用

  • 原理:Android 对相同资源 ID 的 Drawable(如 R.drawable.ic_launcher)进行缓存,多个 ImageView 可共享同一 Drawable 对象,ImageView 的尺寸、位置为外部状态。
  • 优化:避免重复加载图片资源,减少内存消耗(尤其是大图片)。

(4)系统图标与主题资源

  • 原理:系统主题中的图标(如菜单图标、按钮背景)被多个应用共享,图标资源为内部状态,应用中的显示位置为外部状态。
  • 优化:减少系统资源的重复存储,提升整体运行效率。

八、总结

享元模式是优化 “大量细粒度对象” 场景的核心方案,其核心价值在于 “通过共享复用减少对象数量,平衡内存与性能”:

  1. 核心优势:大幅降低内存消耗,减少对象创建和 GC 开销,尤其适用于字符、粒子、UI 组件等场景。
  2. 适用场景:存在大量相似对象,且可分离出可共享的内部状态和场景相关的外部状态。
  3. 实践建议:
    • 严格区分内部状态(不可变、可共享)和外部状态(可变、客户端传入);
    • 享元工厂采用高效缓存(如 HashMap),必要时添加淘汰策略;
    • 结合单例模式确保工厂唯一,避免重复缓存;
    • 避免过度设计:若对象数量少或状态难以分离,使用享元模式可能增加复杂度。

享元模式不仅是一种设计技巧,更是一种 “资源复用” 的思维 —— 通过识别可共享的共性,减少重复消耗,在有限资源下实现系统高效运行。

最近更新:: 2025/10/22 15:36
Contributors: 罗凯文, luokaiwen