《Android原始碼設計模式解析與實戰》讀書筆記(二) 《Android原始碼設計模式解析與實戰》PDF資料下載
一、單例模式介紹
單例模式是應用最廣的模式之一。在應用這個模式時,單例物件的類必須保證只有一個例項存在。很多時候整個系統只需要擁有一個全域性物件,這樣有利於我們協調系統整體的行為。
1.1、單例模式的定義
確保某一個類只有一個例項,而且自行例項化並向整個系統提供這個例項。
1.2、單例模式的使用場景
確保某個類有且只有一個物件的場景,避免產生多個物件消耗過多的資源,或者某種型別的物件只應該有且只有一個。
實現單例模式主要有如下幾個關鍵點:
- 建構函式不對外開放,一般為Private;
- 通過一個靜態方法或者列舉返回單例類物件;
- 確保單例類的物件有且只有一個,尤其是在多執行緒環境下;
- 確保單例類物件在反序列化時不會重新構建物件。
通過將單例類的建構函式私有化,使得不能通過new的形式手動構造單例類的物件。單例類會暴露一個公有靜態方法,需要呼叫這個靜態方法獲取到單例類的唯一物件,在獲取這個單例物件的和過程中需要確保執行緒安全,即在多執行緒環境下構造單例類的物件也是有且只有一個,這也是單例模式實現中比較困難的地方。
二、單例模式的簡單示例(餓漢式單例模式)
單例模式是設計模式中比較簡單的,只有一個單例類,沒有其他的層次結構與抽象。
該模式需要確保該類只能生成一個物件,通常是該類需要消耗較多的資源或者沒有多個例項的情況。
請看下面示例。 一個公司可以有幾個VP、無數個員工,但是CEO只有一個。
//普通員工
public class Staff {
public void work(){
//工作
}
}
複製程式碼
//副總裁
public class VP extends Staff{
@Override
public void work() {
//管理下面的經理
}
}
複製程式碼
//CEO,餓漢式單例模式
public class CEO extends Staff {
private static final CEO mCeo = new CEO();
//建構函式私有
public CEO() {
}
//公有的靜態函式,對外暴露獲取單例物件的介面
public static CEO getCeo() {
return mCeo;
}
@Override
public void work() {
//管理VP
}
}
複製程式碼
//公司類
public class Company {
private List<Staff> allStaffs=new ArrayList<Staff>();
public void addStaff(Staff per) {
allStaffs.add(per);
}
public void showAllStaffs(){
for (Staff per :
allStaffs) {
Log.e("TAG","Obj:" + per.toString());
}
}
}
複製程式碼
private void init() {
Company cp=new Company();
//CEO物件只能通過getCeo函式獲取
Staff ceo1 = CEO.getCeo();
Staff ceo2 = CEO.getCeo();
cp.addStaff(ceo1);
cp.addStaff(ceo2);
//通過new建立VP物件
Staff vp1 = new VP();
Staff vp2 = new VP();
//通過new建立Staff物件
Staff mStaff1 = new Staff();
Staff mStaff2 = new Staff();
Staff mStaff3 = new Staff();
cp.addStaff(vp1);
cp.addStaff(vp2);
cp.addStaff(mStaff1);
cp.addStaff(mStaff2);
cp.addStaff(mStaff3);
cp.showAllStaffs();
}
複製程式碼
輸出結果如下:
2019-01-07 23:13:26.925 5313-5313/? E/TAG: Obj:com.tengxin.mytest.CEO@4c4ca4b
2019-01-07 23:13:26.926 5313-5313/? E/TAG: Obj:com.tengxin.mytest.VP@e6f2c28
2019-01-07 23:13:26.926 5313-5313/? E/TAG: Obj:com.tengxin.mytest.VP@3ba2d41
2019-01-07 23:13:26.926 5313-5313/? E/TAG: Obj:com.tengxin.mytest.Staff@15542e6
2019-01-07 23:13:26.926 5313-5313/? E/TAG: Obj:com.tengxin.mytest.Staff@f93c027
2019-01-07 23:13:26.926 5313-5313/? E/TAG: Obj:com.tengxin.mytest.Staff@b9ee2d4
複製程式碼
從程式碼中可以看到,CEO類不能通過new的形式構造物件,只能通過CEO.getCEO()函式來獲取。 這個實現的核心在於量CEO類的構造方法進行了私有化,使得外部程式不能通過建構函式來構造CEO物件,而CEO類通過一個靜態方法返回一個靜態物件。
三、單例模式的其他實現方式
####3.1、懶漢式單例模式
懶漢式單例模式是宣告一個靜態物件,並且在使用者第一次呼叫getInstance時進行初始化;而餓漢式單例模式是在宣告靜態物件時就已經初始化。
懶漢式單例模式實現如下:
//懶漢式單例模式
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
複製程式碼
getInstance()方法中新增了synchronized關鍵字,使得在多執行緒情況下保證單例物件唯一。
但是懶漢式單例模式也存在一個問題,就是每次呼叫getInstance方法都會進行同步,這樣會消耗不必要的資源。
懶漢式單例模式總結
- 優點:單例只有在使用時才會被例項化,在一定程度上節約了資源;
- 缺點:第一次載入時需要及時進行例項化,反應稍慢,最大的問題是每次呼叫getInstance都進行同步,造成不必要的同步開銷。
3.2、Double Check Lock(DCL)實現單例
DCL方式實現單例模式的優點是既能夠在需要時初始化單例,又能夠保證執行緒安全,且單例物件初始化後呼叫getInstance不進行同步鎖。
程式碼如下:
//DCL方式實現單例模式
public class DCLSingleton {
private static DCLSingleton mSingleton = null;
public DCLSingleton() {}
public void doSomething(){
Log.e("TAG", "do sth.");
}
public static DCLSingleton getInstance(){
if (mSingleton == null) {
synchronized (DCLSingleton.class) {
if (mSingleton == null) {
mSingleton = new DCLSingleton();
}
}
}
return mSingleton;
}
}
複製程式碼
這種方式的亮點就在getInstance方法上,getInstance方法對mSingleton進行了兩次判空:
- 第一層判斷主要是為了避免不必要的同步;
- 第二層的判斷是為了在null的情況下建立例項。
DCL總結:
- 優點:資源利用率高,第一次執行getInstance時單例物件才會被例項化,效率高。
- 缺點:第一次載入時反應稍慢,也由於Java記憶體模型的原因偶爾會失敗。在高併發環境下也有一定的缺陷。
3.3、靜態內部類單例模式
DCL雖然在一定程度上解決了資源消耗、多餘的同步、執行緒安全等問題,但是,它還是在某些情況下出現失效的問題。這個問題被稱為雙重檢查鎖定(DCL)失效。
建議使用如下程式碼:
//靜態內部類單例模式
public class Singleton {
private Singleton() {}
public static Singleton getInstance(){
return SingletonHolder.sInstance;
}
/**
* 靜態內部類
*/
private static class SingletonHolder{
private static final Singleton sInstance =new Singleton();
}
}
複製程式碼
當第一次載入Singleton類時並不會初始化sInstance,只有在第一次呼叫Singleton的getInstance方法時才會導致sInstance被初始化。
這種方式不僅能夠確保執行緒安全,也能夠保證單例物件的唯一性,同時也延遲了單例的例項化,這是推薦使用的單例模式實現方式。
3.4、列舉單例
//列舉單例
public enum SingletonEnum {
INSTANCE;
public void doSomething(){
Log.e("TAG", "doSomething " );
}
}
複製程式碼
列舉單例最大的優點就是寫法簡單,列舉在Java中與普通的類是一樣的,不僅能夠有欄位,還能夠有自己的方法。最重要的是預設列舉例項的建立是執行緒安全的,並且字啊任何情況下它都是一個單例。
通過序列化可以將一個單例的例項物件寫到磁碟,然後再讀回來,從而有效地獲得一個例項。即使建構函式是私有的,反序列化時依然可以通過特殊的途徑去建立類的一個新的例項,相當於呼叫該類的建構函式。反序列化操作提供了一個很特別的鉤子函式,類中具有一個私有的、被例項化的方法readResolve(),這個方法可以控制物件的反序列化。上述幾個示例中要杜絕單例物件在被反序列化時重新生成物件,必須加入如下方法:
private Object readResolve() throws ObjectStreamException {
return mSingleton;
}
複製程式碼
而對於列舉,並不存在這個問題,因為即使反序列化也不會重新生成新的例項。
3.5、使用容器實現單例模式
下面是一種另類的實現:
//使用容器實現單例模式
public class SingletonManager {
private static Map<String, Object> objMap = new HashMap<>();
private SingletonManager() {}
public static void registerService(String key,Object instance){
if (!objMap.containsKey(key)) {
objMap.put(key, instance);
}
}
public static Object getService(String key) {
return objMap.get(key);
}
}
複製程式碼
這種方式可以管理多種型別的單例,並且在使用時可以通過統一的介面進行獲取操作,降低了使用者的使用成本,也對使用者隱藏了具體實現,降低了耦合度。
無論什麼形式實現單例模式,其核心原理都是將建構函式私有化,並且通過靜態方法獲取一個唯一的例項,在獲取的過程中要保證執行緒安全、防止反序列化導致重新生成例項物件等問題。
四、總結
單例模式是運用頻率很高的模式,在客戶端通常沒有高併發的情況下,選擇哪種實現方式並不會有太大的影響。出於效率考慮,推薦使用DCL形式單例模式、靜態內部類單例模式。
4.1、優點
- 由於單例模式在記憶體中只有一個例項,減少了記憶體開支,特別是一個物件需要頻繁地建立、銷燬時,而且建立或銷燬時效能又無法優化,單例模式的優勢就非常明顯;
- 由於單例模式只生產一個例項,所以,減少了系統的效能開銷,當一個物件的產生需要比較多的資源時,則可以通過在應用啟動時直接產生一個單例物件,然後用永久駐留記憶體的方式來解決;
- 單例模式可以避免對資源的多重佔用,例如一個寫檔案操作,由於只有一個例項存在記憶體中,避免對同一個資原始檔的同時寫操作;
- 單例模式可以在系統設定全域性的訪問點,優化和共享資源訪問,例如,可以設計一個單例類,負責所有資料表的對映處理。
4.2、缺點
- 單例模式一般沒有介面,擴充套件很困難,若要擴充套件,除了修改程式碼基本上沒有第二種途徑可以實現;
- 單例物件如果持有Context,那麼很容易引發記憶體洩漏,此時需要注意傳遞給單例物件的Context最好是Application Context。