iOS開發之進階篇(5)—— 單例

singleton.png

1. 最終推薦寫法

SingleObject.h

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface SingleObject : NSObject

+ (instancetype)sharedInstance;

@end

NS_ASSUME_NONNULL_END

SingleObject.m

#import "SingleObject.h"

static SingleObject *_singleInstance = nil;

@implementation SingleObject

+ (instancetype)sharedInstance
{
    return [[self alloc] init];
}

+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _singleInstance = [super allocWithZone:zone];
    });
    return _singleInstance;
}

- (instancetype)init
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _singleInstance = [super init];
        if (_singleInstance) {
            // 在這裏初始化self的屬性和方法
        }
    });
    return _singleInstance;
}

/*
#pragma mark - 如果遵循了 NSCopying / NSMutableCopying 協議

- (id)copyWithZone:(nullable NSZone *)zone
{
    return _singleInstance;
}

- (id)mutableCopyWithZone:(nullable NSZone *)zone
{
    return _singleInstance;
}
 */

@end

2. 何爲單例?

2.1 單例概念

蘋果文檔
A singleton class returns the same instance no matter how many times an application requests it. A typical class permits callers to create as many instances of the class as they want, whereas with a singleton class, there can be only one instance of the class per process. A singleton object provides a global point of access to the resources of its class.

翻譯過來就是:

  1. 在App運行期間, 單例類有且僅有一個實例對象;
  2. 這個單例對象是可以全局訪問的.

2.2 幾個官方單例

  1. NSFileManager
  2. NSWorkspace
  3. UIApplication
  4. UIAccelerometer (Deprecated)

按照慣例, 返回單例類實例對象的方法是一種工廠方法, 約定方法名稱這樣命名:sharedClassType. 比如sharedFileManager, sharedWorkspace, sharedApplication等等.

官方樣例: 無
😂😂😂

2.3 單例原理

蘋果文檔
The class lazily creates its sole instance the first time it is requested and thereafter ensures that no other instance can be created.
五星翻譯: 在單例類第一次創建的時候稍微做些處理, 使這個類無法再創建其他實例對象.

簡單來說, 就是防止一個單例類被多次創建, 或者說這個單例類的創建方法僅執行一次.
結論是:
讓單例類的創建實例方法只執行一次.
讓單例類的創建實例方法只執行一次.
讓單例類的創建實例方法只執行一次.

接下來我們從對象的創建入手. 請看下小節.

3. 對象的創建

object_creation.png

對象的創建分兩步: 分配內存和初始化.
常規操作如下:

TheClass *newObject = [[TheClass alloc] init];

分配內存
爲對象分配內存有兩種方法: allocallocWithZone:
其實, 使用alloc最終還是會調用allocWithZone:方法.
allocWithZone:這個方法蘋果不建議我們直接使用, 但是這個方法在OC中沒有被遺棄, 它的存在是歷史遺留問題:

This method exists for historical reasons; memory zones are no longer used by Objective-C.

所以, 我們分配內存時應該使用alloc而不是allocWithZone:方法.

但是, 不排除個別人使用allocWithZone:去分配內存的情況. 所以, 後面我們討論單例的寫法時, 還是要考慮這種情況. 這裏先埋下伏筆.

初始化
初始化處在創建對象階段, 該階段通過將對象的實例變量設置爲合理的初始值, 還可以分配和準備對象所需的其他全局資源, 才使得該對象可用.
按照約定, 初始化方法始終以init開頭. 該方法返回一個動態類型的對象(id), 或者, 如果初始化失敗, 則返回nil.
如果某個類實現了初始化方法, 則第一步應調用其父類的初始化程序. 比如:

- (instancetype)init
{
    self = [super init];
    if (self) {
        // 初始化self的屬性和方法
    }
    return self;
}

此要求可確保從根對象開始在繼承鏈中對對象進行一系列初始化.

initialization.png

工廠方法
工廠方法是一種類方法, 其將分配內存alloc和初始化init結合在一起, 並返回一個自動釋放的類實例.
例如:

+ (instancetype)stringWithString:(NSString *)string;
+ (NSNumber *)numberWithInt:(int)value;

我們單例類的獲取實例方法, 就是使用工廠方法返回的, 形式如sharedClassType.

new
我們常常看到一個對象的創建方法如下:

NSString *string = [NSString new];

在蘋果文檔中:

This method is a combination of alloc and init.

其實, new也是工廠方法, new = alloc + init.

4. 單例寫法的討論過程

創建一個SingleObject類, 繼承於NSObject. 給這個SingleObject寫一個工廠方法用於返回類的實例對象.
SingleObject.h

@interface SingleObject : NSObject

+ (instancetype)sharedInstance;

@end

SingleObject.m

+ (instancetype)sharedInstance {
    
    return [[self alloc] init];
}

OK, 雛形有了, 現在它能返回一個實例. 但僅是這樣肯定是不行的, 因爲每次調用sharedObject都會重新創建一個實例.
之前說過, 單例的原理是讓單例類的創建實例方法只執行一次. 而創建方法分兩步: alloc和init. 所以, 創建單例, 我們需做到以下兩點(tag=10001):

  • alloc只調用一次
  • init只調用一次

可是alloc和init方法都是外部可以訪問的, 我們不能控制別人使用的次數. 比如外部可以調用多次:

SingleObject *obj1 = [[SingleObject alloc] init];
SingleObject *obj2 = [[SingleObject alloc] init];
...

我們在對象的創建小節裏討論過, 如果某個類實現了初始化方法, 則第一步應調用其父類的初始化程序. 也就是說, 雖然該類的init無法控制, 但是通過重寫init方法, 它的父類init是可控的. 所以, tag=10001那兩點應該改寫表述成如下兩點(tag=10086):

  • [super alloc]只調用一次
  • [super init]只調用一次

問題又來了, 我們之前不是說過分配內存有兩種方法嗎? 假如創建的時候不是用alloc而是用allocWithZone:方法, 那我們又得乾瞪眼了. 好在alloc最終都是調用allocWithZone:方法的, 所以tag=10086這兩點最終應修改成:

  • [super allocWithZone:]只調用一次
  • [super init]只調用一次

這兩點需求算是成熟可以告一段落了, 接下來討論怎樣讓一段代碼只執行一次呢?
蘋果給我們提供了dispatch_once這個方法:

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    // 不管調用多少次dispatch_once, 這裏的代碼只會執行一次
});

關於dispatch_once這裏不打算詳解, 我們只需要知道它的作用以及它是線程安全的.
我們就用這個方法去實現那兩點需求:

#import "SingleObject.h"

static SingleObject *_singleInstance = nil;

@implementation SingleObject

+ (instancetype)sharedInstance
{
    return [[self alloc] init];
}

+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _singleInstance = [super allocWithZone:zone];
    });
    return _singleInstance;
}

- (instancetype)init
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _singleInstance = [super init];
        if (_singleInstance) {
            // 在這裏初始化self的屬性和方法
        }
    });
    return _singleInstance;
}

@end

這裏allocWithZone:和init方法都需要返回同個對象, 所以使用static進行全局定義.

到這裏已經接近尾聲了, 我們可以驗證一下:

    SingleObject *obj1 = [[SingleObject alloc] init];
    SingleObject *obj2 = [[SingleObject allocWithZone:NULL] init];
    SingleObject *obj3 = [SingleObject new];
    SingleObject *obj4 = [SingleObject sharedInstance];
    SingleObject *obj5 = [SingleObject sharedInstance];
    
    NSLog(@"obj1:%@", obj1);
    NSLog(@"obj2:%@", obj2);
    NSLog(@"obj3:%@", obj3);
    NSLog(@"obj4:%@", obj4);
    NSLog(@"obj5:%@", obj5);

log:

2020-06-17 14:42:12.810826+0800 KKSingletonDemo[2168:109605] obj1:<SingleObject: 0x600000740580>
2020-06-17 14:42:12.811040+0800 KKSingletonDemo[2168:109605] obj2:<SingleObject: 0x600000740580>
2020-06-17 14:42:12.811162+0800 KKSingletonDemo[2168:109605] obj3:<SingleObject: 0x600000740580>
2020-06-17 14:42:12.811325+0800 KKSingletonDemo[2168:109605] obj4:<SingleObject: 0x600000740580>
2020-06-17 14:42:12.811430+0800 KKSingletonDemo[2168:109605] obj5:<SingleObject: 0x600000740580>

最後還得注意一點, 如果單例遵循了協議, 那麼創建單例的方法可能不走init而是走copy或者mutableCopy, 這種情況下我們還得添加如下處理:

#pragma mark - 如果遵循了 NSCopying / NSMutableCopying 協議

- (id)copyWithZone:(nullable NSZone *)zone
{
    return _singleInstance;
}

- (id)mutableCopyWithZone:(nullable NSZone *)zone
{
    return _singleInstance;
}

最後, 我們的成品代碼見篇頭.

參考

https://developer.apple.com/library/archive/documentation/General/Conceptual/DevPedia-CocoaCore/ObjectCreation.html#//apple_ref/doc/uid/TP40008195-CH39-SW1

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