在iOS开发中,经常使用到单例。单例是Cocoa中被广泛使用的设计模式之一。单例使得某个类在整个application的生命周期中只有一个实例,减少内存开销,统一了某些具体操作的逻辑,方便管理。开发中常用构造单例的方法有两种@sychronized
和dispatch_once
。
@sychronized
使用@sychronized
构造单例通常如下:
static SomeClass * instance = nil;
+ (instancetype)shareInstance {
@synchronized(self) {
if (instance == nil) {
instance = [[SomeClass alloc] init];
}
}
return instance;
}
@sychronized
是一个编译器指令,方便我们对临界区提供互斥锁(mutex locks )。也就是说在多线程并发访问临界区(@synchronized(self) {//临界区}
)时,它保证同一时刻只有一个线程处于临界区中,其他线程阻塞等待。那么@sychronized
如何标识一个互斥锁?苹果文档中说了,@sychronized
可以使用任意的Objective-C对象作为互斥锁的标识符(lock token)。那么问题来了,如果互斥锁的标识符不一样呢(动态的)?比如下面这样:
- (void)instantMethod:(id)lockTocken
{
@synchronized(lockTocken)
{
//临界区
}
}
假设现在有多个线程并发调用上面的实例方法- (void)instantMethod:(id)lockTocken;
,它们分别为A、B、C和D线程,其中A、B调用该方法时候传入的lockTocken
一样,C、D传入的lockTocken
一样。那么答案是只有标识变量(lockTocken
)一样的线程才会互斥,标识变量(lockTocken
)不一样的线程相互之间没有影响。回到最早的例子,其中使用了self
(类对象)作为互斥锁的标识符,由此可见,多进程并发访问,使用的互斥锁是一样的,并且在第一个进入临界区的线程初始化instance
后,其后进入的线程就不会再次初始化(instance
不再是nil
),保证了SomeClass
类只有一个实例。
好奇的你一定会想,既然
@synchronized
是编译器指令,那么编译器对这段代码做了什么?
有如下代码:
id obj = ...
@synchronized(obj) {
//临界区
}
clang -rewrite-objc之后:
id obj = ...
{ id _rethrow = 0; id _sync_obj = (id)obj; objc_sync_enter(_sync_obj);
try {
struct _SYNC_EXIT { _SYNC_EXIT(id arg) : sync_exit(arg) {}
~_SYNC_EXIT() {objc_sync_exit(sync_exit);}
id sync_exit;
} _sync_exit(_sync_obj);
} catch (id e) {_rethrow = e;}
{ struct _FIN { _FIN(id reth) : rethrow(reth) {}
~_FIN() { if (rethrow) objc_exception_throw(rethrow); }
id rethrow;
} _fin_force_rethow(_rethrow);}
}
即编译器把@synchronized
相关代码转成下面这一大坨东西。代码的关键在两个函数的调用:objc_sync_exit
和objc_sync_enter
。代码的逻辑还是比较好理解,想深入了解的可以看下这篇博文。
dispatch_once
使用dispatch_once
构造单例通常如下:
static SomeClass * instance = nil;
+ (instancetype)shareInstance {
static dispatch_once_t onceTocken;
dispatch_once(&onceTocken, ^{
instance = [[SomeClass alloc] init];
});
return instance;
}
dispatch_once
是GCD中提供的函数,通常使用它来初始化全局数据(单例),它接受两个参数,dispatch_once_t *
类型的谓语和dispatch_block_t
类型的block(block中的代码就是临界区)。文档中说到,dispatch_once_t *
类型的谓语用于测试block是否已经执行结束或者还没与执行,它配合上dispatch_once
函数保证了在applecation的生命周期中block只会运行一次,并且是线程安全的。可以看到dispatch_once
和@synchronized
一样,是线程安全,不同指出在于@synchronized
的临界区代码可能在application生命周期中多次调用,而dispatch_once
只会调用一次(使用dispatch_once_t *
类型的谓语做判断)。因此@synchronized
的临界区代码要判断instance
是否是nil
,来判断是实例是否已经构造了。
注意:
dispatch_once_t *
类型的谓语必须是全局变量或者静态变量,如果使用自动或者动态变量(包括Objective-C实例变量),dispatch_once
的结果是无法预知的。
单例思考
单例用着用着就被滥用了。最近正在思考如何对公司APP的分享模块重构。其中有个类叫SSShareManager
,作为的单例,它管理着所有模块的分享逻辑。滥用点就在既然是单例,却使用它来保存来自不同模块的数据以及状态(比如:分享到微信还是QQ、友盟统计的数据等),无形中增加了不同模块之间的耦合。因为在某个模块调用SSShareManager
单例分享之前,还要记得清理其他模块可能留下的数据=,=。这篇博文给我很大启发,其中印象深刻的一段话:
The lesson here is that singletons should be preserved only for state that is global, and not tied to any scope. If state is scoped to any session shorter than “a complete lifecycle of my app,” that state should not be managed by a singleton.