設置模式之單例模式(附上一個Objective-C編寫的播放音樂的單例類)


在查閱Cocoa Touch開發文檔時,會發現框架中隨處可見的大量單例類,比如說,UIApplication、NSFileManager 等。

  1. UIApplication

框架中極為常用的一個單例類,它提供了一個控制並協調iOS應用程序的集中點。每一個應用程序有且只有一個UIApplication的實例,它由UIApplicationMain函數在應用程序啟動的時候創建為單例對象。之后,對於同一UIApplication實例可以通過sharedApplication類方法進行訪問。

UIApplication對象為應用程序處理許多內務管理任務,包括傳入用戶事件的最初路由,以及為UIControl分發動態消息給合適的目標對象,它還維護應用程序中所有打開UIWindows對象的列表。應用程序對象總是會被分配一個UIApplicationDelegate對象,應用程序會把重要的運行時事件通知給它,比如iOS程序中的應用程序啟動、內存不足時的警告、應用程序終止和后台進程執行。這讓委托(delegate)有機會作出適當的響應。

[UIApplication sharedApplication].windows;
[UIApplication sharedApplication].keyWindow;
[UIApplication sharedApplication].version;

 2. NSFileManager

NSFileManager是文件管理常用的一個單例類,對於它可以通過defaultManager類方法進行訪問。剛好前兩天封裝了一個它的使用,下面是這個類的使用的部分代碼:

#import <Foundation/Foundation.h>

typedef enum {
    ZYFileToolTypeDocument,
    ZYFileToolTypeCache,
    ZYFileToolTypeLibrary,
    ZYFileToolTypeTmp
} ZYFileToolType;

@interface ZYFileTool : NSObject
/**  獲取Document路徑  */
+ (NSString *)getDocumentPath;
/**  獲取Cache路徑  */
+ (NSString *)getCachePath;
/**  獲取Library路徑  */
+ (NSString *)getLibraryPath;
/**  獲取Tmp路徑  */
+ (NSString *)getTmpPath;
/**  此路徑下是否有此文件存在  */
+ (BOOL)fileIsExists:(NSString *)path;

/**
 *  創建目錄下文件
 *  一般來說,文件要么放在Document,要么放在Labrary下的Cache里面
 *  這里也是只提供這兩種存放路徑
 *
 *  @param fileName 文件名
 *  @param type     路徑類型
 *  @param context  數據內容
 *
 *  @return 文件路徑
 */
+ (NSString *)createFileName:(NSString *)fileName  type:(ZYFileToolType)type context:(NSData *)context;
@end



#import "ZYFileTool.h"

@implementation ZYFileTool

+ (NSString *)getRootPath:(ZYFileToolType)type
{
    switch (type) {
        case ZYFileToolTypeDocument:
            return [self getDocumentPath];
            break;
        case ZYFileToolTypeCache:
            return [self getCachePath];
            break;
        case ZYFileToolTypeLibrary:
            return [self getLibraryPath];
            break;
        case ZYFileToolTypeTmp:
            return [self getTmpPath];
            break;
        default:
            break;
    }
    return nil;
}

+ (NSString *)getDocumentPath
{
    return [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
    
}

+ (NSString *)getCachePath
{
    return [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
}

+ (NSString *)getLibraryPath
{
    return [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) lastObject];
}

+ (NSString *)getTmpPath
{
    return NSTemporaryDirectory();
}

+ (BOOL)fileIsExists:(NSString *)path
{
    if (path == nil || path.length == 0) {
        return false;
    }
    return [[NSFileManager defaultManager] fileExistsAtPath:path];
}


+ (NSString *)createFileName:(NSString *)fileName  type:(ZYFileToolType)type context:(NSData *)context
{
    if (fileName == nil || fileName.length == 0) {
        return nil;
    }
    NSString *path = [[self getRootPath:type] stringByAppendingString:fileName];
    if ([self fileIsExists:path]) {
        if (![[NSFileManager defaultManager] removeItemAtPath:path error:nil]) {
            return nil;
        }
    }
    [[NSFileManager defaultManager] createFileAtPath:path contents:context attributes:nil];
    return path;
}
@end

 

3. 單例的概念以及在iOS開發中的實現一個單例類

概念:保證一個類只有一個實例,並且提供一個訪問它的全局訪問點。

通常我們可以讓一個全局變量使得一個對象並訪問,但它不能防止你實例化多個對象,一個最好的方法是,讓類自身負責保存它的唯一實例。這個類可以保證沒有其他實例被創建,並且它可以提供一個訪問該實例的方法。

 基於上面的原理,我們可以很快的實現一個“簡單的單例”(但事實上,它是有很大問題的)

簡單單例代碼:

#import <Foundation/Foundation.h>

@interface ZYSimpleManager : NSObject
+ (instancetype)defaultManager;
@end



#import "ZYSimpleManager.h"

static ZYSimpleManager *_instance = nil;
@implementation ZYSimpleManager
+ (instancetype)defaultManager
{
    if (_instance == nil) {
        _instance = [[self alloc] init];
    }
    return _instance;
}

- (instancetype)init
{
    if (_instance) return _instance;
    
    if (self = [super init]) {
        /**
         *  初始化
         */
    }
    return self;
}
@end

 初看之下,只要_instance被實例化一次之后,就不會再給它分配存儲空間了,這樣保證了一個類只實例化一次,好像是對的?

 單例,一個常見的擔憂是它往往不是線程安全的,也就是上面代碼出現的情況了,這個擔憂是十分合理的,基於它們的用途:單例常常被多個控制器同時訪問。

當前狀態下,上面的代碼非常簡單,然而if的條件分支不是線程安全的,如果想要在控制台看到輸出,可以這樣調試下上面的代碼:

#import "ZYSimpleManager.h"

static ZYSimpleManager *_instance = nil;
@implementation ZYSimpleManager
+ (instancetype)defaultManager
{
    if (_instance == nil) {
        _instance = [[self alloc] init];
        
        NSLog(@"%d",(int)[NSThread currentThread]);     //加上這句,判斷當前是哪條線程
    }
    return _instance;
}

- (instancetype)init
{
    if (_instance) return _instance;
    
    if (self = [super init]) {
        /**
         *  初始化
         */
    }
    return self;
}
@end

 在viewController里面調用如下代碼:

#import "ViewController.h"
#import "ZYSimpleManager.h"
@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
        [ZYSimpleManager defaultManager];
    });
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
        [ZYSimpleManager defaultManager];
    });
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
        [ZYSimpleManager defaultManager];
    });
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
        [ZYSimpleManager defaultManager];
    });
    
}

 會看到如下打印:

可以發現,有多個線程同時實例化了這個單例對象。

為什么會出現這樣的錯誤?就是因為這樣的代碼是線程不安全的,多個線程可以在同一時間訪問,從而導致多個對象被創建。

這個輸出展示了臨界區被執行多次,而它本來只應該執行一次。現在,固然是我強制這樣的狀況發生,但可以想像一下這個狀況在無意間發生會導致什么?

要糾正這個狀況,實例化代碼應該只執行一次,並阻塞其它實例在 if 條件的臨界區運行。這剛好就是 dispatch_once 能做的事。

正確代碼:

 

#import "ZYSimpleManager.h"

static ZYSimpleManager *_instance = nil;
@implementation ZYSimpleManager
+ (instancetype)defaultManager
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _instance = [[self alloc] init];
        
        NSLog(@"%@",[NSThread currentThread]);
    });
    return _instance;
}

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

- (instancetype)init
{
    __block ZYSimpleManager *temp = self;
    
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        if ((temp = [super init]) != nil) {
            /**
             *  初始化數據
             */
        }
    });
    self = temp;
    return self;
}
@end

 當你將代碼改成這樣,再運行上面viewController里面的代碼,不論多少異步線程訪問這個單例,永遠只會有一次打印。有且僅有一個單例的實例——這就是我們對單例的期望!

 

 基本上,iOS開發中,單例的實現以及為什么要這樣實現已經總結完了,還得說說,單例的命名規范,一般我們將實例化它的那個類方法以shared、manager等開頭,我這里是仿照NSFileManager的defaultManager開頭的。下面是一個音樂播放單例的實現:

#import <Foundation/Foundation.h>
#import <AVFoundation/AVFoundation.h>
@interface ZYAudioManager : NSObject
+ (instancetype)defaultManager;

//播放音樂
- (AVAudioPlayer *)playingMusic:(NSString *)filename;
- (void)pauseMusic:(NSString *)filename;
- (void)stopMusic:(NSString *)filename;

//播放音效
- (void)playSound:(NSString *)filename;
- (void)disposeSound:(NSString *)filename;
@end



#import "ZYAudioManager.h"

@interface ZYAudioManager ()
@property (nonatomic, strong) NSMutableDictionary *musicPlayers;
@property (nonatomic, strong) NSMutableDictionary *soundIDs;
@end

static ZYAudioManager *_instance = nil;

@implementation ZYAudioManager
+ (instancetype)defaultManager
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _instance = [[self alloc] init];
    });
    return _instance;
}

- (instancetype)init
{
    __block ZYAudioManager *temp = self;
    
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        if ((temp = [super init]) != nil) {
            _musicPlayers = [NSMutableDictionary dictionary];
            _soundIDs = [NSMutableDictionary dictionary];
        }
    });
    self = temp;
    return self;
}

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

//播放音樂
- (AVAudioPlayer *)playingMusic:(NSString *)filename
{
    if (filename == nil || filename.length == 0)  return nil;
    
    AVAudioPlayer *player = self.musicPlayers[filename];      //先查詢對象是否緩存了
    
    if (!player) {
        NSURL *url = [[NSBundle mainBundle] URLForResource:filename withExtension:nil];
        
        if (!url)  return nil;
        
        player = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:nil];
        
        if (![player prepareToPlay]) return nil;
        
        self.musicPlayers[filename] = player;            //對象是最新創建的,那么對它進行一次緩存
    }
    
    if (![player isPlaying]) {                 //如果沒有正在播放,那么開始播放,如果正在播放,那么不需要改變什么
        [player play];
    }
    return player;
}

- (void)pauseMusic:(NSString *)filename
{
    if (filename == nil || filename.length == 0)  return;
    
    AVAudioPlayer *player = self.musicPlayers[filename];
    
    if ([player isPlaying]) {
        [player pause];
    }
}
- (void)stopMusic:(NSString *)filename
{
    if (filename == nil || filename.length == 0)  return;
    
    AVAudioPlayer *player = self.musicPlayers[filename];
    
    [player stop];
}

//播放音效
- (void)playSound:(NSString *)filename
{
    if (!filename) return;
    
    //取出對應的音效ID
    SystemSoundID soundID = (int)[self.soundIDs[filename] unsignedLongValue];
    
    if (!soundID) {
        NSURL *url = [[NSBundle mainBundle] URLForResource:filename withExtension:nil];
        if (!url) return;
        
        AudioServicesCreateSystemSoundID((__bridge CFURLRef)(url), &soundID);
        
        self.soundIDs[filename] = @(soundID);
    }
    
    // 播放
    AudioServicesPlaySystemSound(soundID);
}

//摧毀音效
- (void)disposeSound:(NSString *)filename
{
    if (!filename) return;
    
    
    SystemSoundID soundID = (int)[self.soundIDs[filename] unsignedLongValue];
    
    if (soundID) {
        AudioServicesDisposeSystemSoundID(soundID);
        
        [self.soundIDs removeObjectForKey:filename];    //音效被摧毀,那么對應的對象應該從緩存中移除
    }
}
@end

 viewController里面的代碼:

#import "ViewController.h"
#import "ZYAudioManager.h"
@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    [[ZYAudioManager defaultManager] playingMusic:@"12309111.mp3"];
    
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [[ZYAudioManager defaultManager] stopMusic:@"12309111.mp3"];
}
@end

 這個單例是ARC情況下的寫法,在MRC里面,只需要對單例類加上內存管理語句即可。

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM