Android 享元模式
Android 設計模式系列文章Android 23種設計模式
一、前言
享元模式即:Flyweight,它是物件池的一種實現。享元模式用來儘可能的減少記憶體的使用量。多用於存在大量重複物件的場景,或需要緩衝池的時候。用來快取共享的物件。這樣來避免記憶體移除等。
二、定義
運用共享技術有效的支援大量細粒度的物件。
三、例子
現在我們大概瞭解了什麼是享元模式,概念再多不如一個簡單例子來的痛快。下面我講解一個查詢巴士車票的例子來講解什麼是享元模式。
3.1、定義巴士車票
public interface Ticket { public void showTicketInfo(String type); } public class BusTicket implements Ticket{ private static final String TAG = BusTicket.class.getSimpleName(); String from; String to; int price; BusTicket(String from,String to) { this.from = from; this.to = to; } @Override public void showTicketInfo(String type) { price = new Random().nextInt(100); Log.d(TAG,from + "到" + to + "的" + type + "巴士票價位" + price); } }
程式碼很簡單根據from和to的地方來組成一張巴士車票。通過showTicketInfo可以列印車票價格資訊。
3.2、工廠類輔助查詢
我們先看這種寫法:
public class TicketFactory { public static Ticket getTicket(String from,String to) { return new BusTicket(from,to); } }
比如當我們查詢深圳到廣州的巴士票的時候呼叫
Ticket ticket = TicketFactory.getTicket("shenzhen","guangzhou"); ticket.showTicketInfo("硬座");
看似沒毛病,客戶端這樣呼叫查詢就可以了。普通情況下這樣確實沒有問題,但當有大量客戶同時查詢的時候就可能會OOM了。因為我們會建立大量的相同物件,十分耗記憶體。當java的GC對這些物件回收的時候同樣也屬於資源浪費。這時候,該享元模式登場的時候了。我們只需要修改一下Factory。
public class TicketFactory { static Map<String,Ticket> sTicketMap = new ConcurrentHashMap<String,Ticket>(); public static Ticket getTicket(String from,String to) { String key = from + "-" + to; if (sTicketMap.containsKey(key)) { return sTicketMap.get(key); } else { Ticket ticket = new BusTicket(from,to); sTicketMap.put(key,ticket); return ticket; } } }
這樣用map來儲存,以key查詢,有重複就複用,沒有就直接建立。避免了重複物件的建立。
四、JDK中的String
jdk中的String也是類似的訊息池。一個String被定義過後就存在於常量池中。當其他地方使用相同的字串時,實際使用的時快取。
String str1 = new String("abc"); String str2 = new String("abc"); String str3 = "abc"; String str4 = "ab" + "c";
string一般我們用兩個判斷,
一個是equals,這個是比較內容,他們四個都相等。
另一個就是“==”判斷。str1不等於str2,不等於str3。特別的,str3是等於str4。因為str1和str2是new的,str3是等號字面賦值。所以他們是不同的物件。而str4也是字面賦值。且str4使用了快取在快取池中的str3常量物件。所以用==判斷的時候,他們是相同物件。
五、Message中的享元模式
關於Handler、Mesaage和Looper之間的關係這裡不做詳細討論,有興趣的同學可以看我另外一篇文章Android訊息機制(Handler原理)-完全解析 。這裡我們只講Message是如何使用享元模式。我們直接從傳送message訊息講起,以下原始碼來自Android P。
5.1、handler傳送message
public final boolean sendEmptyMessageAtTime(int what, long uptimeMillis) { Message msg = Message.obtain(); msg.what = what; return sendMessageAtTime(msg, uptimeMillis); }
傳送訊息的時候最終呼叫sendEmptyMessageAtTime,通過Message.obtain();建立message併發送。享元模式就是從obtain這裡切入。
5.2、Message.obtain
private static int sPoolSize = 0; Message next; private static final Object sPoolSync = new Object(); private static Message sPool; 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要麼是從最後new Message返回一個新的物件,要麼返回sPool。當返回sPool時就是返回已建立的重複物件。再理解為啥sPool就是重複物件時,我們先看明白Message這個單鏈表物件。
5.3、Message是一個單鏈表物件
Message包含一個next的Message物件。sPoolSize表示個數,以此形成單鏈表結構。
單鏈表的取出
在obtain方法中
Message m = sPool---m等於單鏈表
sPool = m.next---sPool單鏈表捨棄表頭元素
m.next = null;---m捨棄除表頭之外的所有元素
m.flags = 0;---flag置0標記
sPoolSize--;---單鏈表大小減1
這樣就取出來了單鏈表的頭元素並返回,而我們的單鏈表sPool捨棄表頭。這樣就完成了元素的複用。當然這裡只是取出,接下來我們看插入
單鏈表的插入
插入的操作是在Message回收的時候
public void recycle() { if (isInUse()) { if (gCheckRecycle) { throw new IllegalStateException("This message cannot be recycled because it " + "is still in use."); } return; } recycleUnchecked(); } void recycleUnchecked() { // Mark the message as in use while it remains in the recycled object pool. // Clear out all other details. flags = FLAG_IN_USE; what = 0; arg1 = 0; arg2 = 0; obj = null; replyTo = null; sendingUid = -1; when = 0; target = null; callback = null; data = null; synchronized (sPoolSync) { if (sPoolSize < MAX_POOL_SIZE) { next = sPool; sPool = this; sPoolSize++; } } }
recycle判斷Message物件是否正在被使用,如果是則刨除異常,否則開始進行recycleUnchecked單鏈表插入操作。插入之前先清空了各個引數。重點在最後
synchronized (sPoolSync) { if (sPoolSize < MAX_POOL_SIZE) { next = sPool; sPool = this; sPoolSize++; } }
if (sPoolSize < MAX_POOL_SIZE)---單鏈表大小還沒超過MAX_POOL_SIZE則開始插入
next = sPool;---next直接等於自身
sPool = this;---sPool等於現在插入的元素
sPoolSize++;---大小+1
就是一個普通單鏈表插入表頭的操作。
5.4、Message享元模式小結
雖然Message並不是最標準的享元模式用法,但它通過單鏈表這種方式一樣實現了物件池的思想。也是另一種妙用。由此筆者想多說一句就是設計模式並不是一成不變的,理解它的核心,多思考你才能融會貫通。
六、寫在最後
享元模式實現起來還是比較簡單。在需要大量重複物件的時候起到重要作用。也用作快取池等場景。能夠降低記憶體中物件的數量。