Flyweight 設計使用場景
● 需要 緩衝池
的場景、存在大量相似 物件
● 細粒度的物件都具備相近的外部狀態,而內部狀態與環境無關(物件沒有特定身份,可以相互替換,也就是這個物件更像是「值語意」物件)
● 使用共享對象有效的減少類的創建,增加物件的覆用率
Flyweight 定義 & Flyweight UML
● Flyweight 定義:使用共享物件去支援,大量的細粒度物件
● 細粒度物件:物件的每個成員可以區分為 內部狀態 (
intrinsic
)、外部狀態 (extrinsic
)● 內部狀態 (
intrinsic
):代表它是一個物件中的 可共享的成員儲存在 享元對象 內部並且不會隨著環境改變而改變
● 外部狀態 (
extrinsic
):代表它是一個物件中的 不可共享的成員,用來當作對象的唯一標誌(不可變更修改)
● Flyweight UML 物件角色
物件角色 | 說明 |
---|---|
Flyweight (抽象) | 定義產品的可共享(內部狀態)、不可共享(外部狀態)的實現或接口 |
ConcreteFlyweight | 具體享元(產品)角色;可共享的成員處理須與環境無間,不應該出現一個操作改變可共享成員,又同時修改不可共享成員 |
UnsharedConcreteFlyweight | 不可共享的享元(產品)角色;不存在外部狀態、安全需求導致無法共享的角色 |
FlyweightFactory | 內部創建一個池容器(Pool ),用來儲存 Flyweight 對象 |
Flyweight 設計優缺點
● Flyweight 設計優點:
● 節省類的創建,也加快速度 (Java 創建類十分耗資源)
● 降低內存消耗、減少 JVM GC 回收導致的 Stop the world
造成的卡頓感
● Flyweight 設計缺點:
● 提高系統的複雜性(要分離出外部狀態、內部狀態),請判斷在必要的時候再使用,否則可能造成設計過度的問題!
Flyweight 實現
Flyweight 標準
A. Flyweight
抽象類:持有不可修改的外部對象、可修改的內部對象
// 建構函數有不可修改的外對象
abstract class Flyweight constructor(val extrinsic: String) {
// 可修改的內部對象
var intrinsic : Any? = null
abstract fun operation()
}
B. ConcreteFlyweight
類:實做 Flyweight 抽象類、其操作函數
// 實做 Flyweight
class ConcreteFlyweight constructor(extrinsic: String):
Flyweight(extrinsic)
override fun operation() {
println("Show intrinsic date: $intrinsic")
}
}
C. FlyweightFactory
類:享元工廠,內部持有可覆用的 Flyweight
對象,透過外部特徵(不可修改的成員)儲存 Flyweight 對象
可以透過外部特徵(狀態)去判斷該物件是否共享
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)
}
}
● 使用享元設計:
fun main() {
FlyweightFactory.run {
// "Hello" 代表了外部狀態
val instance1 = getFlyweightObject("Hello")
val instance2 = getFlyweightObject("Hello")
println("instance1 == instance2? ${instance1 == instance2}")
}
}
Flyweight 模式的考量
Thread 安全性
● 我們知道 Java、Kotlin 天生是多線程(多執行序)程式,由於多線程的問題,我們在使用多線程訪問同一個函數創建對象並設定成員總會造成 不安全設定
● 基於這個問題,我們在創建對象時要使用 鎖(Lock
) 才安全;以下提出幾個我們在做併發程式時會使用到的多線程(多執行序)鎖,它可以保證線程操作安全性
A. 使用 @Synchronized
註解同步方法
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)
}
}
B. 使用 ReentrantLock
同步方法
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
) 中的享元物件不能太少,要到足夠滿足業務為止!
● 外部狀態
控制享元物件的創建重點是在 外部狀態,外部狀態過少過於簡單會導致物件的創建數量便少,從而更容易產生 Thread 問題
外部狀態控制了內部的物件數量
● 內部狀態
內部狀態是被管理的物件的特徵;與物件本身較有關係
● 性能:外部狀態要使用 Java 內建還是自己創建?都可以~ 但依照性能來講建議還是使用 Java
/Kotlin
內建物件(像是一些基礎類別)
● 如果要自己創建物件請務必複寫以下函數,這些函數主要是會影響到外部狀態的「比對」
A. equals
方法(影響 Map#Contains(Object)
)
B. hashCode
方法(影響 Map#get(Object)
)
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 給予不同的反應;
而物件導向程式,在這種設計之下… 就會導致一個問題點的產生,就是「事件的這個物件過多」的問題… 如果沒有正視考慮到這個問題,那就很容易導致 OOM 的發生
常見的用法如下…
// 範例
void handlerSendMsgToUpdateUI() {
Handler handler = new Handler(Looper.getMainLooper());
Message message = Message.obtain(handler, () -> {
// 更新 UI
});
handler.sendMessage(message);
}
● 以下分析的是 Android 10 的 Message.java
Message 覆用機制 obtain:在池中獲取 Message
● 當然,Android 框架的設計者在決定使用「驅動模型」時就有考慮到物件過多的問題,所以它也有設計置一個 Flyweight 享元模式 的設計,讓事件的物件可以循環被系統覆用
● Android 的「事件」就是由 Message 類來表達,它是一個值物件類型,對應我們前面所說的,它具有 內部狀態 的特性;在這個小節中,我們主要關注「從池中取值」的行為
Message 的覆用(從池中取值)入口函數為 obtain
,接下來我們就從這裡開始分析
// 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 設計時的幾個重點
A. 同步鎖 sPoolSync
物件:Message 類中使用一個 靜態的 Object
物件 作為同步鎖的 Key,它表達的是對於 Pool 的執行序安全操作
// Message.java
public static final Object sPoolSync = new Object();
B. 物件池 sPool
物件:在上面的享元模式範例中,我們使用 Map 作為享元模式的池(用來保存已經使用過得物件),而 Android Message 的設計則更為特別
我們可以看到 sPool
物件的類型是 Message
,並且在操作時可以透過 Message
#next
成員切換靜態的 sPool
物件,從這裡我們可以看出它是使用了 責任鏈 Chain 的設計 來表達池,將一系列的 Message 串接起來作為「池」
// 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();
}
graph LR
subgraph chain as pool
m1(新 Message)
m2(原先 Message)
m3(Message)
end
m1 --> |next| m2 --> |next| m3
subgraph Message object
sPool -.-> |當前指向| m1
end
●
sPool
靜態成員是在哪裡被賦予值的呢?這個我們等等會看到,它是在
recycleUnchecked
方法中被賦予值的
Message 覆用機制 recycle:把 Message 放入池中
● 在這個小節中,我們主要關注「把物件放入池中」的行為,目的是為了要保存值物件(Message
類)的實例,來避免 JVM 重複創建物件
● Message
類,把物件放入池中的方法是 recycle
:
使用該函數時有個條件,就是當 Messsage 物件已經呼叫過 recycle
代表它已經被放入池中,處於一個可使用的狀態(FLAG_IN_USE
),不可以重複呼叫 recycle
否則會拋出 IllegalStateException
異常
// 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
函數的重點:
A. 同步鎖 sPoolSync
物件:與上個小節介紹的相同,它表達的是對於 Pool 的執行序安全操作
但這裡我們在特別強調它(鎖)的一個特性,它是「靜態」物件,代表了所有的 Message 實體共享這把鎖,也就是說在 把「Message 放入池中」的行為是安全,但也消耗一定效能的
// Message.java
public static final Object sPoolSync = new Object();
B. 池大小的控制 MAX_POOL_SIZE
:
如果我們不對池中物件的數量做管控,那相當於無限制的物件(會有記憶體遺漏),仍會導致 OOM,所以 Android Message 有設定可回收的池的數量,而這個數量上限就是 50 個
// Message.java
private static final int MAX_POOL_SIZE = 50;
void recycleUnchecked() {
... 省略部份
synchronized (sPoolSync) {
if (sPoolSize < MAX_POOL_SIZE) {
...
}
}
}
C. 將物件放入池(鏈表)中:
Message 類中有一個 next
成員,用來指向下一個 Message 物件(從這裡也可以很清楚的看到這是個鏈表設計),所以在回收 Message 物件時會有兩個行為,1. 它會將當前 sPool
指向的物件作為下一個物件,2. 再將自身(this
)設定為當前物件 sPool
// 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 |
更多的物件導向設計
物件導向的設計基礎如下,如果是初學者或是不熟悉的各位,建議可以從這些基礎開始認識,打好基底才能走個更穩(在學習的時候也需要不斷回頭看)!
創建模式 Creation Patterns
● 創建模式 PK
● 創建模式 - Creation Patterns
:
創建模式用於「物件的創建」,它關注於如何更靈活、更有效地創建物件。這些模式可以隱藏創建物件的細節,並提供創建物件的機制,例如單例模式、工廠模式… 等等,詳細解說請點擊以下連結
● Singleton 單例模式 | 解說實現 | Android Framework Context Service
● Abstract Factory 設計模式 | 實現解說 | Android MediaPlayer
● Factory 工廠方法模式 | 解說實現 | Java 集合設計
● Builder 建構者模式 | 實現與解說 | Android Framwrok Dialog 視窗
● Clone 原型模式 | 解說實現 | Android Framework Intent
行為模式 Behavioral Patterns
● 行為模式 PK
● 行為模式 - Behavioral Patterns
:
行為模式關注物件之間的「通信」和「職責分配」。它們描述了一系列物件如何協作,以完成特定任務。這些模式專注於改進物件之間的通信,從而提高系統的靈活性。例如,策略模式、觀察者模式… 等等,詳細解說請點擊以下連結
● Stragety 策略模式 | 解說實現 | Android Framework 動畫
● Interpreter 解譯器模式 | 解說實現 | Android Framework PackageManagerService
● Chain 責任鏈模式 | 解說實現 | Android Framework View 事件傳遞
● Specification 規格模式 | 解說實現 | Query 語句實做
● Command 命令、Servant 雇工模式 | 實現與解說 | 物件導向設計
● Memo 備忘錄模式 | 實現與解說 | Android Framwrok Activity 保存
● Visitor 設計模式 | 實現與解說 | 物件導向設計
● Template 設計模式 | 實現與解說 | 物件導向設計
● Mediator 模式設計 | 實現與解說 | 物件導向設計
● Composite 組合模式 | 實現與解說 | 物件導向設計
● Observer 觀察者模式 | JDK Observer | Android Framework Listview
結構模式 Structural Patterns
● 結構模式 PK
● 結構模式 - Structural Patterns
:
結構模式專注於「物件之間的組成」,以形成更大的結構。這些模式可以幫助你確保當系統進行擴展或修改時,不會破壞其整體結構。例如,外觀模式、代理模式… 等等,詳細解說請點擊以下連結
● Decorate 裝飾模式 | 解說實現 | 物件導向設計
● Iterator 迭代設計 | 解說實現 | 物件導向設計