在查閱Cocoa Touch開發文檔時,會發現框架中隨處可見的大量單例類,比如說,UIApplication、NSFileManager 等。
- 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里面,只需要對單例類加上內存管理語句即可。

