Swift 單例的實現與解析

RickeyBoy發表於2017-10-15

單例 Singleton 是設計模式中非常重要的一種,在 iOS 中也非常常見。在之前的面試過程中也被問到過單例相關的問題,當時感覺自己答得不是很好,後來也是又深入研究了一下。本文主要是簡單分析一下單例,並且討論了一下 Swift 中單例的實現。

Singleton 基本介紹

單例是什麼?

單例模式(Singleton Pattern)是最簡單的設計模式之一。這種型別的設計模式屬於建立型模式,它提供了一種建立物件的最佳方式。這種模式涉及到一個單一的類,該類負責建立自己的物件,同時確保只有單個物件被建立。

Swift 單例的實現與解析

Swift 單例的實現與解析

基本要求:

  • 只能有一個例項。
  • 必須自己建立自己的唯一例項。
  • 必須給所有其他物件提供這一例項。

iOS 中的單例

  • UIApplication.shard :每個應用程式有且只有一個UIApplication例項,由UIApplicationMain函式在應用程式啟動時建立為單例物件。
  • NotificationCenter.defualt:管理 iOS 中的通知
  • FileManager.defualt:獲取沙盒主目錄的路徑
  • URLSession.shared:管理網路連線
  • UserDefaults.standard:儲存輕量級的本地資料
  • SKPaymentQueue.default():管理應用內購的佇列。系統會用 StoreKit framework 建立一個支付佇列,每次使用時通過類方法 default() 去獲取這個佇列。

單例的優點

  • 提供了對唯一例項的受控訪問:單例類封裝了它的唯一例項,防止其它物件對自己的例項化,確保所有的物件都訪問一個例項。
  • 節約系統資源:由於在系統記憶體中只存在一個物件,因此可以節約系統資源,對於一些需要頻繁建立和銷燬的物件,單例模式無疑可以提高系統的效能。
  • 伸縮性:單例模式的類自己來控制例項化程式,類就在改變例項化程式上有相應的伸縮性。
  • 避免對資源的多重佔用:比如寫檔案操作,由於只有一個例項存在記憶體中,避免對同一個資原始檔的同時寫操作

Singleton 在 Swift 中的實現

第一種方式:

也是最直接簡潔的方式:將例項定義為全域性變數。比如下面的程式碼,宣告瞭一個例項變數sharedManager

let sharedManager = MyManager(string: someString)
class MyManager {
    // Properties
    let string: String
    // Initialization
    init(string: String) {
        self.string = string
    }
}複製程式碼

而如果將上述例項變數在全域性命名區(global namespace)第一次呼叫,由於Swift中全域性變數是懶載入(lazy initialize)。所以,在application(_:didFinishLaunchingWithOptions:)中呼叫的時候之後,shardManager會在AppDelegate類中被初始化,之後程式中所有呼叫sharedManager例項的地方將都使用該例項。

另外,Swift 全域性變數初始化時預設使用dispatch_once,這保證了全域性變數的構造器(initializer)只會被呼叫一次,保證了shardManager原子性

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    // 初始化位置,以及使用方式
    print(sharedManager)
    return true
}複製程式碼
  • 關於 Swift 中全域性變數的懶載入

Initialize lazily, run the initializer for a global the first time it is referenced, similar to Java. It allows custom initializers, startup time in Swift scales cleanly with no global initializers to slow it down, and the order of execution is completely predictable.
Swift 採用與 Java 類似的方式,對全域性變數實行懶載入。這樣設計使得構造器可以自定義、啟動時間不會因為載入全域性變數而變慢、同時操作執行的順序也變得可控。

  • 關於 Swift 中的 dispatch_once 和 原子性

The lazy initializer for a global variable ... is launched as dispatch_once to make sure that the initialization is atomic. This enables a cool way to use dispatch_once in your code: just declare a global variable with an initializer and mark it private.
全域性變數的懶載入在初始化時會使用 dispatch_once 以確保初始化的原子性。所以這是一個很酷地使用 dispatch_once 的方式:僅在定義全域性變數時將其構造器標誌為 private 就行。

第二種方式:

Swift 2 開始增加了static關鍵字,用於限定變數的作用域。如果不使用static(比如let string),那麼每一個MyManager例項中均有一個string變數。而使用static之後,shared成為全域性變數,成為單例。

另外可以注意到,由於構造器使用了 private 關鍵字,所以也保證了單例的原子性。

class MyManager {
    // 全域性變數
    static let shared = MyManager(string: someString)

    // Properties
    let string: String
    // Initialization
    private init(string: String) {
        self.string = string
    }
}複製程式碼

第二種方式的使用如下。可以看出採用第二種方式實現單例,程式碼的可讀性增加了,能夠直觀的分辨出這是一個單例。

// 使用
print(MyManager.shared)複製程式碼

第三種方式:

第三種方式是第二種方式的變種,更加複雜。讓單例在閉包(Closure)中初始化,同時加入類方法來獲取單例。

class MyManager {
    // 全域性變數
    private static let sharedManager: MyManager = {
        let shared = MyManager(string: someString) 
        // 可以做一些其他的配置
        // ...
        return shared
    }()
    // Properties
    let string: String
    // Initialization
    private init(string: String) {
        self.string = string
    }
    // Accessors
    class func shared() -> MyManager {
        return sharedManager
    }
}複製程式碼

可以看出第三種方式雖然更加複雜,但是可以在閉包中作一些額外的配置。同時,呼叫單例的方式也不一樣,需要呼叫單例中的類方法shared()

print(MyManager.shared())複製程式碼

單例的缺陷

單例狀態的混亂

由於單例是共享的,所以當使用單例時,程式設計師無法清楚的知道單例當前的狀態。

當使用者登入,由一個例項負責當前使用者的各項操作。但是由於共享,當前使用者的狀態很可能已經被其他例項改變,而原來的例項仍然不知道這項改變。如果想要解決這個問題,例項就必須對單例的狀態進行監控。Notifications 是一種方式,但是這樣會使程式過於複雜,同時產生很多無謂的通知。

測試困難

測試困難主要是由於單例狀態的混亂而造成的。因為單例的狀態可以被其他共享的例項所修改,所以進行需要依賴單例的測試時,很難從一個乾淨、清晰的狀態開始每一個 test case

單例訪問的混亂

由於單例時全域性的,所以無法對訪問許可權作出限定。程式任何位置、任何例項都可以對單例進行訪問,這將容易造成管理上的混亂。

參考資料

相關文章