---
title: 'Flyweight 享元模式'
disqus: kyleAlien
---
Flyweight 享元模式
===
## Overview of Content
使用共享對象有效的減少類的創建,增加物件的覆用率
如有引用參考請詳註出處,感謝 :cat:
:::success
* 如果喜歡讀更好看一點的網頁版本,可以到我新做的網站 [**DevTech Ascendancy Hub**](https://devtechascendancy.com/)
本篇文章對應的是 [**Flyweight 享元模式 | 實現與解說 | 物件導向設計**](https://devtechascendancy.com/object-oriented_design_flyweight/)
:::
[TOC]
## Flyweight 使用場景
1. **需要`緩衝池`的場景**、**存在大量相似對向**
2. 細粒度的物件都具備相近的外部狀態,而內部狀態與環境無關(物件沒有特定身份,可以相互替換,也就是這個物件更像是「值語意」物件)
### Flyweight 定義 & UML:內部、外部狀態
* **Flyweight 定義**:使用共享物件去支援,大量的細粒度物件
:::success
* 細粒度物件:物件的每個成員可以區分為 **內部狀態 (`intrinsic`)**、**外部狀態 (`extrinsic`)**
* **內部狀態 (`intrinsic`)**:代表它是一個物件中的 **可共享的成員**,是屬於「值物件」類型
> 儲存在 **享元對象** 內部並且不會隨著環境改變而改變
* **外部狀態 (`extrinsic`)**:代表它是一個物件中的 **不可共享的成員**,用來當作對象的唯一標誌(不可變更修改)
> 用來作為標示物件,重在「唯一性」的特質
:::
* **Flyweight UML 物件角色**
| 物件角色 | 說明 |
| - | - |
| Flyweight(抽象) | 定義產品的可共享(內部狀態)、不可共享(外部狀態)的實現或接口 |
| ConcreteFlyweight | 具體享元(產品)角色;可共享的成員處理須與環境無間,不應該出現一個操作改變可共享成員,又同時修改不可共享成員 |
| UnsharedConcreteFlyweight | 不可共享的享元(產品)角色;**不存在外部狀態、安全需求導致無法共享的角色** |
| FlyweightFactory | **內部創建一個池容器,用來除存 Flyweight 對象** |
> 
### Flyweight 設計 - 優缺點
* **Flyweight 設計優點**:
* 節省類的創建,也加快速度 ([**Java 創建類**](https://hackmd.io/kKOqFUXpRhyYTchh98xUUQ?view#虛擬機堆中對象的創建)十分耗資源)
* 降低內存消耗、減少 GC 回收
* **Flyweight 設計缺點**:
* 提高系統的複雜性!(要分離出外部狀態、內部狀態)
## Flyweight 實現
### Flyweight 標準
1. **`Flyweight` 抽象類**:持有不可修改的外部對象、可修改的內部對象
```kotlin=
// 建構函數有不可修改的外對象
abstract class Flyweight constructor(val extrinsic: String) {
// 可修改的內部對象
var intrinsic : Any? = null
abstract fun operation()
}
```
2. **`ConcreteFlyweight` 類**:實做 Flyweight 抽象類、其操作函數
```kotlin=
// 實做 Flyweight
class ConcreteFlyweight constructor(extrinsic: String):
Flyweight(extrinsic)
override fun operation() {
println("Show intrinsic date: $intrinsic")
}
}
```
3. **`FlyweightFactory` 類**:享元工廠,內部持有可覆用的 `Flyweight` 對象,透過外部特徵(不可修改的成員)儲存 Flyweight 對象
> 可以透過外部特徵(狀態)去判斷該物件是否共享
```kotlin=
object FlyweightFactory {
// 儲存共享物件
private val pool = HashMap<String, Flyweight>()
fun getFlyweightObject(extrinsic: String) : Flyweight {
if (!pool.containsKey(extrinsic)) {
pool[extrinsic] = ConcreteFlyweight(extrinsic)
}
return pool.getValue(extrinsic)
}
}
```
* 使用享元設計:
```kotlin=
fun main() {
FlyweightFactory.run {
// "Hello" 代表了外部狀態
val instance1 = getFlyweightObject("Hello")
val instance2 = getFlyweightObject("Hello")
println("instance1 == instance2? ${instance1 == instance2}")
}
}
```
> 
## Flyweight 模式的考量
### Thread 安全性
* 我們知道 Java、Kotlin 天生是多線程程式,由於多線程的問題,我們在使用多線程訪問同一個函數創建對象並設定成員總會造成 **不安全設定**
* 基於這個問題,我們在創建對象時要使用 **鎖** 才安全;以下提出幾個我們在做併發程式時會使用到的多線程(多執行序)鎖,它可以保證線程操作安全性
1. **使用 `@Synchronized` 註解同步方法**
```kotlin=
object FlyweightFactory {
private val pool = HashMap<String, Flyweight>()
@Synchronized
fun getFlyweightObject(extrinsic: String) : Flyweight {
if (!pool.containsKey(extrinsic)) {
pool[extrinsic] = ConcreteFlyweight(extrinsic)
}
return pool.getValue(extrinsic)
}
}
```
2. **使用 `ReentrantLock` 同步方法**
```kotlin=
object FlyweightFactory {
private val pool = HashMap<String, Flyweight>()
private val lock = ReentrantLock()
fun getFlyweightObject(extrinsic: String) : Flyweight {
lock.lock()
if (!pool.containsKey(extrinsic)) {
pool[extrinsic] = ConcreteFlyweight(extrinsic)
}
lock.unlock()
return pool.getValue(extrinsic)
}
}
```
### 外部狀態 - 數量 & 性能
* **物件數量**:在使用享元模式時,物件池 (`Pool`) 中的**享元物件不能太少**,要到足夠滿足業務為止!
:::info
* **外部狀態**
控制享元物件的創建重點是在 **外部狀態**,外部狀態過少過於簡單會導致物件的創建數量便少,從而更容易產生 Thread 問題
> 外部狀態控制了內部的物件數量
* **內部狀態**
內部狀態是被管理的物件的特徵;與物件本身較有關係
:::
* **性能**:外部狀態要使用 Java 內建還是自己創建?都可以~ **但依照性能來講建議還是使用 Java/Kotlin 內建物件**
* 如果要自己創建物件請務必複寫
1. `equals` 方法(影響 `Map#Contains(Object)` )
2. `hashCode` 方法(影響 `Map#get(Object)` )
```kotlin=
class SelfObject(private val extrinsic1: String, private val extrinsic2: String) {
var intrinsic : Any? = null
override fun hashCode(): Int {
return extrinsic1.hashCode() + extrinsic2.hashCode()
}
override fun equals(other: Any?): Boolean {
if (other == null || other !is SelfObject) return false
return other.extrinsic1.equals(extrinsic1) &&
other.extrinsic2.equals(extrinsic2)
}
}
```
## Android source Message 覆用
我們知道 Android 的 UI 事件是仰賴於「**事件驅動模型**」,它的設計概念是透過事件(可以是使用者發出,或是程式邏輯發出)來驅動 UI 給予不同的反應;
:::warning
而物件導向程式,在這種設計之下… 就會導致一個問題點的產生,就是「事件的這個物件過多」的問題… 如果沒有正視考慮到這個問題,那就很容易導致 OOM 的發生
:::
常見的用法如下…
```java=
// 範例
void handlerSendMsgToUpdateUI() {
Handler handler = new Handler(Looper.getMainLooper());
Message message = Message.obtain(handler, () -> {
// 更新 UI
});
handler.sendMessage(message);
}
```
:::success
* 以下分析的是 Android 10 的 [**Message.java**](https://cs.android.com/android/platform/superproject/+/android-10.0.0_r1:frameworks/base/core/java/android/os/Message.java)
:::
### [Message](https://cs.android.com/android/platform/superproject/+/android-10.0.0_r1:frameworks/base/core/java/android/os/Message.java) 覆用機制 obtain:在池中獲取 Message
* 當然,Android 框架的設計者在決定使用「驅動模型」時就有考慮到物件過多的問題,所以它也有設計置一個 **Flyweight 享元模式** 的設計,讓事件的物件可以循環被系統覆用
* Android 的「**事件**」就是由 Message 類來表達,它是一個值物件類型,**對應我們前面所說的,它具有 ++內部狀態++ 的特性**;在這個小節中,我們主要關注「**從池中取值**」的行為
Message 的覆用(從池中取值)入口函數為 `obtain`,接下來我們就從這裡開始分析
```java=
// Message.java
public static final Object sPoolSync = new Object();
private static Message sPool;
private static int sPoolSize = 0;
public static Message obtain() {
synchronized (sPoolSync) {
if (sPool != null) {
Message m = sPool;
sPool = m.next;
m.next = null;
m.flags = 0; // clear in-use flag
sPoolSize--;
return m;
}
}
return new Message();
}
```
從以上程式,我們來抓取 Message 設計時的幾個重點
1. **同步鎖 `sPoolSync` 物件**:Message 類中使用一個 **靜態的 `Object` 物件** 作為同步鎖的 Key,**它表達的是對於 Pool 的執行序安全操作**
```java=
// Message.java
public static final Object sPoolSync = new Object();
```
2. **物件池 `sPool` 物件**:在上面的享元模式範例中,我們使用 Map 作為享元模式的池(用來保存已經使用過得物件),而 Android Message 的設計則更為特別
我們可以看到 `sPool` 物件的類型是 `Message`,並且在操作時可以透過 `Message`#`next` 成員切換靜態的 `sPool` 物件,從這裡我們可以看出它是使用了 [**責任鏈 Chain 的設計**](https://devtechascendancy.com/object-oriented_design_chain_framework/) 來表達池,將一系列的 Message 串接起來作為「池」
```java=
// Message.java
public static final Object sPoolSync = new Object();
private static Message sPool;
private static int sPoolSize = 0;
public static Message obtain() {
synchronized (sPoolSync) {
if (sPool != null) {
// 將原先的指向暫存
Message m = sPool;
// 並將 sPool 賦予到原先 Message 的「前方」,也就是新 Message 會作為 Link Header
sPool = m.next;
m.next = null;
m.flags = 0; // clear in-use flag
sPoolSize--;
return m;
}
}
return new Message();
}
```
```mermaid
graph LR
subgraph chain as pool
m1(新 Message)
m2(原先 Message)
m3(Message)
end
m1 --> |next| m2 --> |next| m3
subgraph Message object
sPool -.-> |當前指向| m1
end
```
:::info
* `sPool` 靜態成員是在哪裡被賦予值的呢?
這個我們等等會看到,它是在 `recycleUnchecked` 方法中被賦予值的
:::
### [Message](https://cs.android.com/android/platform/superproject/+/android-10.0.0_r1:frameworks/base/core/java/android/os/Message.java) 覆用機制 recycle:把 Message 放入池中
* 在這個小節中,我們主要關注「**把物件放入池中**」的行為,目的是為了要保存值物件(`Message` 類)的實例,來避免 JVM 重複創建物件
* `Message` 類,把物件放入池中的方法是 `recycle`:
使用該函數時有個條件,就是當 Messsage 物件已經呼叫過 `recycle` 代表它已經被放入池中,處於一個可使用的狀態(`FLAG_IN_USE`),不可以重複呼叫 `recycle` 否則會拋出 `IllegalStateException` 異常
```java=
// Message.java
public void recycle() {
if (isInUse()) {
if (gCheckRecycle) {
throw new IllegalStateException("This message cannot be recycled because it "
+ "is still in use.");
}
return;
}
recycleUnchecked();
}
@UnsupportedAppUsage
void recycleUnchecked() {
flags = FLAG_IN_USE; // 設定該物件是可用狀態
// 清除物件內的共享資訊
what = 0;
arg1 = 0;
arg2 = 0;
obj = null;
replyTo = null;
sendingUid = UID_NONE;
workSourceUid = UID_NONE;
when = 0;
target = null;
callback = null;
data = null;
synchronized (sPoolSync) {
if (sPoolSize < MAX_POOL_SIZE) {
next = sPool;
sPool = this;
sPoolSize++;
}
}
}
```
我們同樣還看一下 `recycleUnchecked` 函數的重點:
1. **同步鎖 `sPoolSync` 物件**:與上個小節介紹的相同,**它表達的是對於 Pool 的執行序安全操作**
但這裡我們在特別強調它(鎖)的一個特性,它是「靜態」物件,代表了所有的 Message 實體共享這把鎖,也就是說在 把「Message 放入池中」的行為是安全,但也消耗一定效能的
```java=
// Message.java
public static final Object sPoolSync = new Object();
```
2. **池大小的控制** `MAX_POOL_SIZE`:
如果我們不對池中物件的數量做管控,那相當於無限制的物件(會有記憶體遺漏),仍會導致 OOM,所以 Android Message 有設定可回收的池的數量,而這個數量上限就是 50 個
```java=
// Message.java
private static final int MAX_POOL_SIZE = 50;
void recycleUnchecked() {
... 省略部份
synchronized (sPoolSync) {
if (sPoolSize < MAX_POOL_SIZE) {
...
}
}
}
```
3. **將物件放入池(鏈表)中**:
Message 類中有一個 `next` 成員,用來指向下一個 Message 物件(從這裡也可以很清楚的看到這是個鏈表設計),所以在回收 Message 物件時會有兩個行為,^1.^ 它會將當前 `sPool` 指向的物件作為下一個物件,^2.^ 再將自身(`this`)設定為當前物件 `sPool`
```java=
// Message.java
/*package*/ Message next;
void recycleUnchecked() {
... 省略部份
synchronized (sPoolSync) {
if (sPoolSize < MAX_POOL_SIZE) {
// 1. 它會將當前指向的物件作為下一個物件
next = sPool;
// 2. 將自身(`this`)設定為當前物件 `sPool`
sPool = this;
sPoolSize++;
}
}
}
```
### 總結 Android Message 如何規劃 Flyweight 角色
* **Flyweight UML 物件角色對應 Android Message 的設計** 如下表
可以看到 Android 將 Flyweight UML 物件角色幾乎完全融入到一個 Message 中,這是由於它用了「靜態成員」、「鏈表操作池」的原因!
| 物件角色 | 說明 | Android Message 哪個類所承擔 |
| - | - | - |
| Flyweight(抽象) | 定義產品的可共享(內部狀態)、不可共享(外部狀態)的實現或接口 | Message |
| ConcreteFlyweight | 具體享元(產品)角色;可共享的成員處理須與環境無間,不應該出現一個操作改變可共享成員,又同時修改不可共享成員 | Message |
| UnsharedConcreteFlyweight | 不可共享的享元(產品)角色;**不存在外部狀態、安全需求導致無法共享的角色** | 由於使用「鏈表」操作,所以不用外部狀態 |
| FlyweightFactory | **內部創建一個池容器,用來除存 Flyweight 對象** | Message |
## 更多的物件導向設計
物件導向的設計基礎如下,如果是初學者或是不熟悉的各位,建議可以從這些基礎開始認識,打好基底才能走個更穩(在學習的時候也需要不斷回頭看)!
:::info
* [**設計建模 2 大概念- UML 分類、使用**](https://devtechascendancy.com/introduction-to-uml-and-diagrams/)
* [**物件導向設計原則 – 6 大原則(一)**](https://devtechascendancy.com/object-oriented-design-principles_1/)
* [**物件導向設計原則 – 6 大原則(二)**](https://devtechascendancy.com/object-oriented-design-principles_2/)
:::
### 創建模式 - Creation Patterns
* [**創建模式 PK**](https://devtechascendancy.com/pk-design-patterns-factory-builder-best/)
* **創建模式 - `Creation Patterns`**:
創建模式用於「**物件的創建**」,它關注於如何更靈活、更有效地創建對象。這些模式可以隱藏創建對象的細節,並提供創建對象的機制,例如單例模式、工廠模式… 等等,詳細解說請點擊以下連結
:::success
* [**Singleton 單例模式 | 解說實現 | Android Framework Context Service**](https://devtechascendancy.com/object-oriented_design_singleton/)
* [**Abstract Factory 設計模式 | 實現解說 | Android MediaPlayer**](https://devtechascendancy.com/object-oriented_design_abstract-factory/)
* [**Factory 工廠方法模式 | 解說實現 | Java 集合設計**](https://devtechascendancy.com/object-oriented_design_factory_framework/)
* [**Builder 建構者模式 | 實現與解說 | Android Framwrok Dialog 視窗**](https://devtechascendancy.com/object-oriented_design_builder_dialog/)
* [**Clone 原型模式 | 解說實現 | Android Framework Intent**](https://devtechascendancy.com/object-oriented_design_clone_framework/)
* [**Object Pool 設計模式 | 實現與解說 | 利用 JVM**](https://devtechascendancy.com/object-oriented_design_object-pool/)
* [**Flyweight 享元模式 | 實現與解說 | 物件導向設計**](https://devtechascendancy.com/object-oriented_design_flyweight/)
:::
### 行為模式 - Behavioral Patterns
* [**行為模式 PK**](https://devtechascendancy.com/pk-design-patterns-cmd-strat-state-obs-chain/)
* **行為模式 - `Behavioral Patterns`**:
行為模式關注物件之間的「**通信**」和「**職責分配**」。它們描述了一系列對象如何協作,以完成特定任務。這些模式專注於改進物件之間的通信,從而提高系統的靈活性。例如,策略模式、觀察者模式… 等等,詳細解說請點擊以下連結
:::warning
* [**Stragety 策略模式 | 解說實現 | Android Framework 動畫**](https://devtechascendancy.com/object-oriented_design_stragety_framework/)
* [**Interpreter 解譯器模式 | 解說實現 | Android Framework PackageManagerService**](https://devtechascendancy.com/object-oriented_design_interpreter_framework/)
* [**Chain 責任鏈模式 | 解說實現 | Android Framework View 事件傳遞**](https://devtechascendancy.com/object-oriented_design_chain_framework/)
* [**State 狀態模式 | 實現解說 | 物件導向設計**](https://devtechascendancy.com/object-oriented_design_state/)
* [**Specification 規格模式 | 解說實現 | Query 語句實做**](https://devtechascendancy.com/object-oriented_design_specification-query/)
* [**Command 命令、Servant 雇工模式 | 實現與解說 | 物件導向設計**](https://devtechascendancy.com/object-oriented_design_command_servant/)
* [**Memo 備忘錄模式 | 實現與解說 | Android Framwrok Activity 保存**](https://devtechascendancy.com/object-oriented_design_memo_framework/)
* [**Visitor 設計模式 | 實現與解說 | 物件導向設計**](https://devtechascendancy.com/object-oriented_design_visitor_dispatch/)
* [**Template 設計模式 | 實現與解說 | 物件導向設計**](https://devtechascendancy.com/object-oriented_design_template/)
* [**Mediator 模式設計 | 實現與解說 | 物件導向設計**](https://devtechascendancy.com/object-oriented_programming_mediator/)
* [**Composite 組合模式 | 實現與解說 | 物件導向設計**](https://devtechascendancy.com/object-oriented_programming_composite/)
:::
### 結構模式 - Structural Patterns
* [**結構模式 PK**](https://devtechascendancy.com/pk-design-patterns-proxy-decorate-adapter/)
* **結構模式 - `Structural Patterns`**:
結構模式專注於「物件之間的組成」,以形成更大的結構。這些模式可以幫助你確保當系統進行擴展或修改時,不會破壞其整體結構。例如,外觀模式、代理模式… 等等,詳細解說請點擊以下連結
:::danger
* [**Bridge 橋接模式 | 解說實現 | 物件導向設計**](https://devtechascendancy.com/object-oriented_design_bridge/)
* [**Decorate 裝飾模式 | 解說實現 | 物件導向設計**](https://devtechascendancy.com/object-oriented_design_decorate/)
* [**Proxy 代理模式 | 解說實現 | 分析動態代理**](https://devtechascendancy.com/object-oriented_design_proxy_dynamic-proxy/)
* [**Iterator 迭代設計 | 解說實現 | 物件導向設計**](https://devtechascendancy.com/object-oriented_design_iterator/)
* [**Facade 外觀、門面模式 | 解說實現 | 物件導向設計**](https://devtechascendancy.com/object-oriented_design_facade/)
* [**Adapter 設計模式 | 解說實現 | 物件導向設計**](https://devtechascendancy.com/object-oriented_design_adapter/)
:::
## Appendix & FAQ
:::info
:::
###### tags: `Java 設計模式` `基礎進階`