如推理般层层递进——从一个单例设计说起

解决问题是一件有意思的事,不过遇到难题,初学者常苦恼于选择哪条路走下去,因为每条路都看似合理却又不知尽头的成败,往往在抉择间犹豫不决。那么,何不化繁为简,先把步子迈出去,在行走的过程中不断修正,正如推理般层层递进。下面结合一个单例模式的学习过程,分享给大家。

问题很简单,设计一个单例。对经验丰富的程序员来说自然是闭着眼睛信手拈来,可是初学者脑海中就会蹦出很多想法——“用全局变量来做?” “在构造对象的时候做处理?” “多线程要怎么考虑?” “打印变量地址来看异同?”纠结于诸多考虑往往不知从何下手,再加上查阅前辈的博客、代码,什么“懒汉式”,“饿汉式”等一系列更加复杂的术语越发让初学者困惑。那么,为什么不先写点啥呢

用最熟悉的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方法实现。。。
笔者从这个单例设计的例子说起,和大家分享一种解决问题的思想,由于水平有限,不足之处还请多多指教。想写博客的念头也有好久了,今天终于迈出了这一步,也是很欣喜的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值