--- 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 設計模式` `基礎進階`
×
Sign in
Email
Password
Forgot password
or
By clicking below, you agree to our
terms of service
.
Sign in via Facebook
Sign in via Twitter
Sign in via GitHub
Sign in via Dropbox
Sign in with Wallet
Wallet (
)
Connect another wallet
New to HackMD?
Sign up