Swift 單例的實現與解析

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

Singleton 基本介紹

單例是什麼?

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

 

 

基本要求:

  • 只能有一個實例。
  • 必須自己創建自己的唯一實例。
  • 必須給所有其他對象提供這一實例。

iOS 中的單例

  • UIApplication.shard:每個應用程序有且只有一個UIApplication實例,由UIApplicationMain函數在應用程序啓動時創建爲單例對象。
  • NotificationCenter.defualt:管理 iOS 中的通知
  • FileManager.defualt:獲取沙盒主目錄的路徑
  • URLSession.shared:管理網絡連接
  • UserDefaults.standard:存儲輕量級的本地數據
  • SKPaymentQueue.default():管理應用內購的隊列。系統會用 StoreKitframework 創建一個支付隊列,每次使用時通過類方法 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_onceto make sure that the initialization is atomic. This enables a cool way to use dispatch_oncein 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

單例訪問的混亂

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

參考資料

轉載自:https://juejin.im/post/59e30701f265da432f3026ad

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章