你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發綜合 >> iOS開發-Objective-C單例在ARC環境下的實現和理解

iOS開發-Objective-C單例在ARC環境下的實現和理解

編輯:IOS開發綜合
在23種設計模式裡面,單例是最好理解的那一類。追MM與設計模式是這麼解釋的:SINGLETON—俺有6個漂亮的老婆,她們的老公都是我,我就是我們家裡的老公Singleton,她們只要說道“老公”,都是指的同一個人,那就是我…   在app開發中,單例的生命周期是從創建出來到app被kill掉。也就是說singleton自初始化對象之後,直到關閉應用,才會被釋放。在生命周期期間,應該無論用什麼方法初始化,拿到的都應該是地址相同的同一個singleton對象。  

1.對象的構造

在oc中,構造方法很多,習慣上使用share後者manager命名的類方法來創建單例,但這並不能保證每個開發都知道這個習慣。為了確保無論用什麼方法創建出來的對象都是同一個,要使得所有構造方法的出口一致。  
1.1 oc中的構造方法
單例的父類一般都是NSObject,在此也只討論這種情況   查看NSObject.h 官方文檔 初始化對象的方法有:   +alloc   使用方法: You must use aninit...method to complete the initialization process. For example:
TheClass *newObject = [[TheClass alloc] init];

Do not overrideallocto include initialization code. Instead, implement class-specific versions ofinit...methods.

 

For historical reasons,allocinvokesallocWithZone:.

結論:alloc方法必須與init方法合用,來完成初始化過程。不要復寫alloc方法,可以復寫init方法來實現特別的需求。由於歷史原因,alloc會調用方法+allocWithZone:   +allocWithZone:   使用方法:

You must use aninit...method to complete the initialization process. For example:

Code Listing 3
TheClass *newObject = [[TheClass allocWithZone:nil] init];

Do not overrideallocWithZone:to include any initialization code. Instead, class-specific versions ofinit...methods.

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

與alloc方法類似,此方法也需要與init方法合用。內存空間不再被oc使用,所以zone參數傳nil即可。   -init   Implemented by subclasses to initialize a new object (the receiver) immediately after memory for it has been allocated. 由子類實現該方法,在得到內存分配之後立即初始化一個新的對象。   使用方法: Aninitmessage is coupled with analloc(orallocWithZone:) message in the same line of code: 與alloc或者allocWithZone組合使用。   如果要復寫,格式如下:
- (instancetype)init {
 self = [super init];
 if (self) {
 // Initialize self
 }
 return self;
}
  -copy   Returns the object returned bycopyWithZone:. 該方法的返回值是從copyWithZone:返回的   +copyWithZone:   This method exists so class objects can be used in situations where you need an object that conforms to theNSCopyingprotocol. For example, this method lets you use a class object as a key to anNSDictionaryobject. You should not override this method. 只有准守了NSCopying協議的類對象,才能調用該方法。例如,使用該方法可以將類對象作為字典對象的key。不能override。   -mutableCopy   Returns the object returned bymutableCopyWithZone:where the zone isnil. 與 -copy 同理   +mutableCopyWithZone:   This method exists so class objects can be used in situations where you need an object that conforms to theNSMutableCopyingprotocol. For example, this method lets you use a class object as a key to anNSDictionaryobject. You should not override this method. 與 -copyWithZone:同理。   +new   This method is a combination ofallocandinit. Likealloc, it initializes theisainstance variable of the new object so it points to the class data structure. It then invokes theinitmethod to complete the initialization process. 該方法是alloc和init兩個方法的結合版。和alloc方法類似,它初始化新對象的isa指針(指向類數據結構)實例變量。然後自動調用init方法來完成該實例化過程。   綜上所述 單例的初始化只要保證,- alloc -init 、-new、-copy、mutableCopy、以上四個方法創建出來的對象是同一個就ok了。   Demo 代碼如下
#import 

@interface SingleInstance : NSObject

+ (instancetype)shareInstance;

@property (nonatomic ,assign) NSInteger factor1;    //測試用

@end

#import "SingleInstance.h"

@implementation SingleInstance

static SingleInstance *instance = nil;

+ (instancetype)shareInstance
{
    static SingleInstance *instance;
    static dispatch_once_t onceToken;
//dispatch_once (If called simultaneously from multiple threads, this function waits synchronously until the block has completed. 由官方解釋,該函數是線程安全的)
    dispatch_once(&onceToken, ^{
        instance = [[super allocWithZone:NULL] init];
    });
    return instance;
}
//保證從-alloc-init和-new方法返回的對象是由shareInstance返回的
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
    return [SingleInstance shareInstance];
}
//保證從copy獲取的對象是由shareInstance返回的
- (id)copyWithZone:(struct _NSZone *)zone
{
    return [SingleInstance shareInstance];
}
//保證從mutableCopy獲取的對象是由shareInstance返回的
- (id)mutableCopyWithZone:(struct _NSZone *)zone
{
    return [SingleInstance shareInstance];
}
@end
以上:保證了無論用何種方式獲取單例對象,獲取到的都是同一個對象。   驗證代碼如下
    SingleInstance *single1 = [[SingleInstance alloc] init];
    SingleInstance *single2 = [SingleInstance new];
    SingleInstance *single3 = [SingleInstance shareInstance];
    SingleInstance *single4 = [single1 copy];//調用此方法獲取對象,需要該類重寫過copyWithZone:方法,不然會crash
    SingleInstance *single5 = [single1 mutableCopy];//需重寫mutableCopyWithZone:,否則crash
    
    single1.factor1 = 1;
    single2.factor1 = 2;
    single3.factor1 = 3;
    single4.factor1 = 4;
    single5.factor1 = 5;
    
    NSLog(@"s1 value = %ld \n",single1.factor1);
    NSLog(@"s2 value = %ld \n",single2.factor1);
    NSLog(@"s3 value = %ld \n",single3.factor1);
    NSLog(@"s4 value = %ld \n",single4.factor1);
    NSLog(@"s5 value = %ld \n",single5.factor1);
    
    NSLog(@"memory address \n %@ \n %@ \n %@  \n %@  \n %@",single1,single2,single3,single4,single5);
  控制台打印結果為: ttt[44738:1478271] s1 value = 5
ttt[44738:1478271] s2 value = 5
ttt[44738:1478271] s3 value = 5
ttt[44738:1478271] s4 value = 5
ttt[44738:1478271] s5 value = 5
ttt[44738:1478271] memory address




可以看到,各種方法創建出來的對象的內存地址是一樣的,並且屬性值也是一樣的。  

2.線程安全問題

因為單例的對象只有一個,而且可以在應用的任何時機讀寫,所以很有可能在多個地方同時讀寫,會出現數據錯亂。例如:將12306比作一個單例,a站-b站的票為單例的數據,全國有34個地方同時買a站到-b站的票。票的庫存只有1張,假設同時獲取12306的余票,都是1,那麼都會交易成功,實際的1張票,賣出去34張,其中有33張都是並不存在的票。   轉換成程序層面上就是,多線程對同一個對象進行操作,此處要有線程同步來保證數據的正確性。所謂線程同步就是:在一個線程操作的同時,其他線程只能等待,上一個線程操作完畢之後,才輪到下一個線程。   模擬代碼:以火車票出售模擬
//開始模擬
- (void)simulateStart {
    SingleInstance *single1 = [[SingleInstance alloc] init];
    single1.factor1 = 10;//總共有10張票
    [self performSelectorInBackground:@selector(sellTickets) withObject:nil];
    [self performSelectorInBackground:@selector(sellTickets) withObject:nil];
    [self performSelectorInBackground:@selector(sellTickets) withObject:nil];
}
- (void)sellTickets
{
    SingleInstance *single1 = [SingleInstance shareInstance];
    
    NSThread *thread = [NSThread currentThread];
    thread.name = [NSString stringWithFormat:@"%.6f",[[NSDate date] timeIntervalSince1970]];
    
    dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
        //檢查票數
        for (int i = 0; i<10; i++) {
            NSInteger leftTicketsCount = single1.factor1;
            if (leftTicketsCount <= 0) {
                NSLog(@"賣光了 \n");
                break;
            }
            else
            {
                [NSThread sleepForTimeInterval:0.02];
                NSInteger remain = single1.factor1;
                single1.factor1--;
                NSInteger left = single1.factor1;
                NSLog(@"線程名:%@ 余票數 %ld , 賣出一張 , 剩余票數 %ld \n",thread.name,remain,left);
            }
        }
    });
}
控制台打印的結果為: 線程名:1484826739.634151余票數 9 ,賣出一張 ,剩余票數 8
線程名:1484826739.634131余票數 10 ,賣出一張 ,剩余票數 9
線程名:1484826739.634141余票數 8 ,賣出一張 ,剩余票數 7
線程名:1484826739.634141余票數 7 ,賣出一張 ,剩余票數 6
線程名:1484826739.634151余票數 6 ,賣出一張 ,剩余票數 5
線程名:1484826739.634131余票數 5 ,賣出一張 ,剩余票數 4
線程名:1484826739.634141余票數 4 ,賣出一張 ,剩余票數 3
線程名:1484826739.634151余票數 3 ,賣出一張 ,剩余票數 2
線程名:1484826739.634131余票數 2 ,賣出一張 ,剩余票數 1
線程名:1484826739.634141余票數 1 ,賣出一張 ,剩余票數 0
賣光了
線程名:1484826739.634151余票數 0 ,賣出一張 ,剩余票數 -1
賣光了
線程名:1484826739.634131余票數 -1 ,賣出一張 ,剩余票數 -2
賣光了   可以看到,數據出現了錯誤,當票數為0時依然賣了票。因為多線程同時對同一個對象進行讀寫,導致獲取票數的時候是正確的值,但是在賣票的過程中,有可能其他線程已經將票賣光了。 所以,在多線程讀寫數據的時候,要注意線程安全,保證在該線程操作數據的時候,其他線程只能看著,不能夠同時讀寫   此處可以使用同步鎖 @synchronized (<#token#>) {
<#statements#>
}   修改的代碼如下
- (void)sellTickets
{
    SingleInstance *single1 = [SingleInstance shareInstance];
    
    NSThread *thread = [NSThread currentThread];
    thread.name = [NSString stringWithFormat:@"%.6f",[[NSDate date] timeIntervalSince1970]];
    
    dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
        //注意一定要把線程對數據操作的代碼放在同步鎖的花括號裡面
        @synchronized (single1) {
            //檢查票數
            for (int i = 0; i<11; i++) {
                NSInteger leftTicketsCount = single1.factor1;
                if (leftTicketsCount <= 0) {
                    NSLog(@"賣光了 \n");
                    break;
                }
                else
                {
                    [NSThread sleepForTimeInterval:0.02];
                    NSInteger remain = single1.factor1;
                    single1.factor1--;
                    NSInteger left = single1.factor1;
                    NSLog(@"線程名:%@ 余票數 %ld , 賣出一張 , 剩余票數 %ld \n",thread.name,remain,left);
                }
            }
        }
    });
}
控制台打印數據為: 線程名:1484827607.308329余票數 10 ,賣出一張 ,剩余票數 9
線程名:1484827607.308329余票數 9 ,賣出一張 ,剩余票數 8
線程名:1484827607.308329余票數 8 ,賣出一張 ,剩余票數 7
線程名:1484827607.308329余票數 7 ,賣出一張 ,剩余票數 6
線程名:1484827607.308329余票數 6 ,賣出一張 ,剩余票數 5
線程名:1484827607.308329余票數 5 ,賣出一張 ,剩余票數 4
線程名:1484827607.308329余票數 4 ,賣出一張 ,剩余票數 3
線程名:1484827607.308329余票數 3 ,賣出一張 ,剩余票數 2
線程名:1484827607.308329余票數 2 ,賣出一張 ,剩余票數 1
線程名:1484827607.308329余票數 1 ,賣出一張 ,剩余票數 0
賣光了
賣光了
賣光了   與預期的結果相同,沒有出現數據錯誤。   以上為本人對OC語言ARC環境下單例的理解和一點使用,有錯誤的地方,或者理解不夠的地方,還請看官們指出來。歡迎交流~
  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved