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.
翻譯過來就是:
- 在App運行期間, 單例類有且僅有一個實例對象;
- 這個單例對象是可以全局訪問的.
2.2 幾個官方單例
- NSFileManager
- NSWorkspace
- UIApplication
- 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. 對象的創建
對象的創建分兩步: 分配內存和初始化.
常規操作如下:
TheClass *newObject = [[TheClass alloc] init];
分配內存
爲對象分配內存有兩種方法: alloc 和 allocWithZone:
其實, 使用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;
}
此要求可確保從根對象開始在繼承鏈中對對象進行一系列初始化.
工廠方法
工廠方法是一種類方法, 其將分配內存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;
}
最後, 我們的成品代碼見篇頭.