解决问题是一件有意思的事,不过遇到难题,初学者常苦恼于选择哪条路走下去,因为每条路都看似合理却又不知尽头的成败,往往在抉择间犹豫不决。那么,何不化繁为简,先把步子迈出去,在行走的过程中不断修正,正如推理般层层递进。下面结合一个单例模式的学习过程,分享给大家。
问题很简单,设计一个单例。对经验丰富的程序员来说自然是闭着眼睛信手拈来,可是初学者脑海中就会蹦出很多想法——“用全局变量来做?” “在构造对象的时候做处理?” “多线程要怎么考虑?” “打印变量地址来看异同?”纠结于诸多考虑往往不知从何下手,再加上查阅前辈的博客、代码,什么“懒汉式”,“饿汉式”等一系列更加复杂的术语越发让初学者困惑。那么,为什么不先写点啥呢?
用最熟悉的Person类举例。
// 创建两个Person对象
#import <Foundation/Foundation.h>
#import "Person.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p1 = [[Person alloc] init];
Person *p2 = [[Person alloc] init];
NSLog(@"p1 = %@", p1);
NSLog(@"p2 = %@", p2);
}
return 0;
}
Person是一个继承自NSObject的普通自定义类。显然,上述并非一个单例(单例的概念定义部分请初学者自行查阅专业资料)。原因很简单,通过打印p1 p2的值,两者不同,说明两者是两个不同的对象。
下一步该做的事接踵而至——如何让二者(甚至所有创建出的新对象)都为同一个对象?想到所学的知识,全局变量是可以在整个程序之间共享的,通俗地说就是“只有一份内存”。那么理所当然,在Person.m中创建一个全局的Person对象如下:
#import "Person.h"
Person *p = nil; // 不知道该怎么赋值的时候不妨试试nil
@implementation Person
// ...
@end
接下来,都能想到的,在创建其他Person对象的时候都返回全局变量p呗!创建对象需要调用alloc方法,显然,在Person.m中重写该方法如下:
+ (instancetype)alloc {
if (p == nil) {
p = [super alloc];
}
return p;
}
注意上述代码中的if判断,因为全局对象p初始为nil,所以需要在第一次创建的时候分配内存空间,剩下的非空时候返回p就行啦!
像之前一样通过打印测试,新创建的p1 p2都是同一个对象,好像成功了!别急着高兴,接下来的一步才是至关重要。正如推理中可能遗漏某些重要线索一样,我们的单例设计可能也会忽略某些情况。这时候,“站在巨人的肩膀上”就显得尤为重要。这时候再去查阅技术大牛关于单例设计的文章代码,你会欣喜的发现你的某些想法和大牛不谋而合,不过初学者会更惊讶于竟然忘了考虑许多重要的情况。
比如:copy的情况,ARC和非ARC的情况,多线程的情况等等。
copy提醒我们忘记了某种特殊情况——copy方法内部调用copyWithZone可能产生了新的对象,于是,在Person.m实现copyWithZone方法
- (id)copyWithZone:(NSZone *)zone {
return p;
}
很简单,这里没有考虑if p为空的情况是因为copy为对象方法,能够调用说明Person对象存在呗~
多线程的情况存在于当第一次创建p对象时,多个线程恰好同时执行到alloc方法中的if (p == nil)判断语句,这样就可能导致多个线程分别创建了Person对象,解决方法也很简单,加线程锁即可。
+ (instancetype)alloc {
@synchronized(self) { // 在这里加上线程锁
if (p == nil) {
p = [super alloc];
}
}
return p;
}
线程锁的详细说明不在本篇范围内,为不增加代码复杂性,上述代码中笔者也未做防止线程反复加锁的性能优化处理,读者有兴趣可以自行查阅相关文献资料。
接下来就要处理非ARC情况下的遗漏了。老规矩,一步步的写。首先能想到的ARC与非ARC之间最大的区别无外乎release retain等,而非ARC的一句秘诀就是“谁分配内存谁负责回收”。那么问题来了,如果出现如下代码:
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p1 = [[Person alloc] init];
Person *p2 = [[Person alloc] init];
[p1 release];
Person *p3 = [[Person alloc] init];
NSLog(@"p1 = %@", p1);
NSLog(@"p2 = %@", p2);
NSLog(@"p3 = %@", p3);
}
return 0;
}
由于单例,p1 p2 p3都是同一个对象,但中途p1做了release操作,相当于释放了全局变量p,而之后创建p3调用了alloc方法又返回了已经释放了的全局变量p,自然会引起错误。
解决方法也很容易想到,在Person.m中重写release方法,不做任何操作就行
- (oneway void)release {
// 这里不做任何释放操作,避免提前释放全局变量造成以后的坏内存访问
}
当然,别漏了autorelease
- (instancetype)autorelease {
return p;
}
下一步,考虑retain方法,既然是单例同一个对象,直接重写retain方法,返回全局变量p即可
- (instancetype)retain {
return p;
}
接着是retainCount方法,单例同一个对象,所以计数永远返回1即可
- (NSUInteger)retainCount {
return 1;
}
到这里,非ARC的问题也就解决了。还有个小问题,由于重写了release方法,导致该单例对象直到程序结束之后才会销毁,可能会引起内存泄漏。最简单的做法就是提供一个自定义方法,可以供调用单例的代码在恰当的地方(如调用单例对象的文件内部的dealloc方法中)执行,自定义方法可以定义如下:
- (void)destroySingleton {
[super release];
}
到这里,我们的单例设计可以告一段落了,还剩下一些小细节可以优化,比如:模仿系统的单例方法,提供一个以default或share等开头的类方法;全局变量p最好加上static修饰(防止被跨文件修改);线程锁也可以用GCD的dispatch_once方法实现。。。
笔者从这个单例设计的例子说起,和大家分享一种解决问题的思想,由于水平有限,不足之处还请多多指教。想写博客的念头也有好久了,今天终于迈出了这一步,也是很欣喜的。