[iOS] dispatch_once_t 的单例模式完整实现

1. 单例模式

单例模式是什么?

引用一下菜鸟教程的定义——单例模式

这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

注意:

  • 单例类只能有一个实例。
  • 单例类必须自己创建自己的唯一实例。
  • 单例类必须给所有其他对象提供这一实例。

意图:

  • 保证一个类仅有一个实例,并提供一个访问它的全局访问点。

主要解决:

  • 一个全局使用的类频繁地创建与销毁。

何时使用:

  • 当您想控制实例数目,节省系统资源的时候。

如何解决:

  • 判断系统是否已经有这个单例,如果有则返回,如果没有则创建。

优点:

  • 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。
  • 避免对资源的多重占用(比如写文件操作)。

缺点:

  • 没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。

2. dispatch_once_t 实现单例模式

在 iOS 中,单例模式的实现非常简单,使用 GCD 几行代码就完事了。比起 Java 五花八门的实现而言,用 dispatch_once 来实现既简洁易用,又能保证多线程安全。

@implementation SingletonObject

+ (instancetype)defaultSingletonObject {
    static SingletonObject *_instance = nil;
    static dispatch_once_t token;
    dispatch_once(&token, ^{
        _instance = [[self alloc] init];
    });
    return _instance;
}

@end

但是,这样就完成了吗?

3. 单例模式完善

仔细考虑一下,使用这个接口访问的都是 _instance 对象,自然是不会有问题的。但是此时,其它开发者仍然可以通过 [[SingletonObject alloc] init]; 创建新的对象。这就违背了单例对象的原则。

SingletonObject *object1 = [SingletonObject defaultSingletonObject];
SingletonObject *object2 = [[SingletonObject alloc] init];    
NSLog(@"object1: %p, object2: %p", object1, object2);

// 控制台输出
// object1: 0x280867070, object2: 0x280867080

从输出结果可见,使用 [SingletonObject defaultSingletonObject]; 和 [[SingletonObject alloc] init]; 创建的对象的内存地址并不相同,可以认定访问的并非是一个实例。

那么,该如何处理这个问题呢?

(1) 禁用初始化函数

第一种方式,可以直接禁用初始化函数,防止其它开发者访问 init、new 等可以创建新对象的方法。禁用的方式,可以参考 AFNetworkReachabilityManager,使用 NS_UNAVAILABLE 宏定义来实现。

// 在头文件.h中对应的函数后添加 NS_UNAVAILABLE
@interface SingletonObject : NSObject

+ (instancetype)defaultSingletonObject;

+ (instancetype)new NS_UNAVAILABLE;
- (instancetype)init NS_UNAVAILABLE;

@end

这种情况下在外部调用 init 或者 new 方法时,xcode 会出现 “'****' is unavailable”的警告,编译也无法通过。当然,在 SingletonObject.m 中仍然可以调用 init 和 new 方法,否则无法创建对象,也谈不上什么单例模式了。

引申:宏定义 NS_UNAVAILABLE 和 NS_DESIGNATED_INITIALIZER

NS_UNAVAILABLE:禁用该初始化方法。

NS_DESIGNATED_INITIALIZER:指定构造器。简单来说,其它初始化方法都会调用这个方法。

详细了解的话,可以看一下这篇文章 『Apple API』NS_UNAVAILABLE 与 NS_DESIGNATED_INITIALIZER

(2) 使 alloc 返回同一个对象地址

第二种方式,就是通过修改单例模式的实现方式,使得调用 init、new 等方法时,返回的都是同一个内存地址的对象。

在这之前,先来了解一下 alloc、init、new 的实现。

翻阅一下这三者的官方文档。以下节选 NSObject 类对应方法的部分文档内容。

alloc

Returns a new instance of the receiving class.

This is an instance variable of the new instance that is initialized to a data structure describing the class; memory for all other instance variables is set to 0.

For historical reasons, alloc invokes allocWithZone:.

allocWithZone

Returns a new instance of the receiving class.

The is a instance variable of the new instance is initialized to a data structure that describes the class; memory for all other instance variables is set to 0.

This method exists for historical reasons; memory zones are no longer used by Objective-C.

init

Implemented by subclasses to initialize a new object (the receiver) immediately after memory for it has been allocated.

An object isn’t ready to be used until it has been initialized.

In a custom implementation of this method, you must invoke super’s Initialization then initialize and return the new object. If the new object can’t be initialized, the method should return nil.

new

Allocates a new instance of the receiving class, sends it an init message, and returns the initialized object.

This method is a combination of alloc and init. Like alloc, it initializes the isa instance variable of the new object so it points to the class data structure. It then invokes the init method to complete the initialization process.

通过官方文档可以了解,alloc 和 allocWithZone: 用来为新对象分配内存地址,init 则用来初始化这个新的对象,为这个对象赋默认值,而 new 本质上就是调用 alloc 和 init。

单例模式的最终目的,就是返回唯一一个实例,那么就可以通过重写 alloc 和 allocWithZone:, 使得在为新对象分配内存时,返回的也是已经存在的单例对象。文档中还提到了,由于历史原因,alloc 会调用 allocWithZone:,那么只需要重写 allocWithZone: 方法即可。

@implementation SingletonObject

+ (instancetype)defaultSingletonObject {
    static SingletonObject *_instance = nil;
    static dispatch_once_t token;
    dispatch_once(&token, ^{
        // MARK: 下面这行代码是错误写法
        _instance = [[self alloc] init];
    });
    return _instance;
}

+ (instancetype)allocWithZone:(struct _NSZone *)zone {
    return [SingletonObject defaultSingletonObject];
}

@end

但是,这样也还没有完成。由于重写了 allocWithZone: 方法使得该方法调用 [SingletonObject defaultSingletonObject] ,在 [SingletonObject defaultSingletonObject] 中又调用 [self alloc],alloc 方法会调用 allocWithZone:,因此,在初始化单例对象的时候,就形成了死循环。

既然在初始化单例对象时 [self alloc] 和 [self allocWithZone:nil] 都不能用,改成 [super alloc] 总可以吧?

很遗憾,仍然出错。这其中就涉及到了 ObjectIve-C 的 runtime 特性、self 和 super 的原理,推荐看下 sunnyxx 老师的这篇博客——iOS 程序员 6 级考试(答案和解释)和 Sam_Lau 老师的这篇博客——Objective-C特性:Runtime

简单来说,[self alloc] 和 [super alloc] 在没有任何一个父类重写 alloc 方法的情况下,最后找到的方法实现都是 NSObject 类的 alloc 实现。这两者,[self alloc] 和 [super alloc] 在这种情况下是等价的。

那么,[super alloc] 也不能使用。最后,就剩下 [super allocWithZone:nil] 这个方法可选了。

测试一下改成 [super allocWithZone:nil] 后的结果,能否运行,是否是唯一对象。

SingletonObject *object1 = [SingletonObject defaultSingletonObject];
SingletonObject *object2 = [[SingletonObject alloc] init];
NSLog(@"object1: %p, object2: %p", object1, object2);

// 控制台输出
// object1: 0x281c6c060, object2: 0x281c6c060

那么,问题又来了。为什么 [super alloc] 不可以 ,但是 [super allocWithZone:nil] 却没有问题呢?

这个就需要从 runtime 的源码的角度解释起来可能比较容易理解。runtime 的源码可以从这个苹果开发者官网的 opensource 下载,选择最新的版本即可。以下源代码来自 objc4-779.1.tar.gz

// 节选自 NSObject.mm
@implementation NSObject

+ (id)alloc {
    return _objc_rootAlloc(self);
}

// Replaced by ObjectAlloc
+ (id)allocWithZone:(struct _NSZone *)zone {
    return _objc_rootAllocWithZone(self, (malloc_zone_t *)zone);
}

@end

id
_objc_rootAlloc(Class cls)
{
    return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}

// Call [cls alloc] or [cls allocWithZone:nil], with appropriate 
// shortcutting optimizations.
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
#if __OBJC2__
    if (slowpath(checkNil && !cls)) return nil;
    if (fastpath(!cls->ISA()->hasCustomAWZ())) {
        return _objc_rootAllocWithZone(cls, nil);
    }
#endif

    // No shortcuts available.
    if (allocWithZone) {
        return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);
    }
    return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
}


// 节选自 objc-runtime-new.mm
NEVER_INLINE
id
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused)
{
    // allocWithZone under __OBJC2__ ignores the zone parameter
    return _class_createInstanceFromZone(cls, 0, nil,
                                         OBJECT_CONSTRUCT_CALL_BADALLOC);
}

先根据源码解释以下 [self alloc] 和 [super alloc] 不可行的原因。[self allocWithZone:] 就不解释了,最直接的调用自身,导致死循环。

第一步,[SingletonObject defaultSingletonObject] 调用 [self alloc] 或 [super alloc],来为对象分配内存空间。

第二步,[self alloc] 和 [super alloc] 因为没有重写 alloc 方法,都会去调用 NSObject 类的 alloc 方法,也就是源代码中的 + (id)alloc{...}。

第三步,[NSObject alloc] 方法调用 id _objc_rootAlloc(Class cls) {...} 函数。注意,此处的参数 cls 正是 alloc 方法中传入的 self。而这个 self,这是 SingletonObject 类对象。

第四步,_objc_rootAlloc 方法调用了 static id callAlloc(Class cls, bool checkNil, bool allocWithZone=false) {...} 函数。其中各项参数分别为,cls - SingletonObject 类对象,checkNil - false,allocWithZone - true。

第五步,callAlloc 函数中因为传入参数 allocWithZone 为 true,调用 objc_msgSend(cls, @selector(allocWithZone:)),即调用 [SingletonObject allocWithZone:]。

第六步,[SingletonObject allocWithZone:] 由于被重写,会调用 [SingletonObject defaultSingletonObject]。

[SingletonObject defaultSingletonObject] 正是第一步,也就是陷入了死循环。

再根据源码来解释一下为什么 [super allocWithZone:nil] 是可行的。

第一步,[SingletonObject defaultSingletonObject] 调用 [super allocWithZone:nil] 方法。

第二步,[super allocWithZone:nil] 方法本质上调用的是 NSObject 类的 allocWithZone: 方法。

第三步,[NSObject allocWithZone:] 调用 id _objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused) {...} 方法。

第四步,_objc_rootAllocWithZone 调用 _class_createInstanceFromZone 方法,将其分配好内存地址的对象返回。

当使用 [super allocWithZone:nil] 来实现单例时,不会调用 [SingletonObject allocWithZone:] ,也就不会形成死循环。


2020.4.17 补充

由于从 opensource 下载的源代码无法直接进行调试,理解起来不够直观。

如果想要调试 objc 源码,可以使用 LGCooci 大佬已经调好的可编译的 objc 源码库,库地址为 objc4_debug


 4. 单例模式完整实现

@interface SingletonObject : NSObject

+ (instancetype)defaultSingletonObject;

+ (instancetype)new NS_UNAVAILABLE;
- (instancetype)init NS_UNAVAILABLE;

@end
@implementation SingletonObject

+ (instancetype)defaultSingletonObject {
    static SingletonObject *_instance = nil;
    static dispatch_once_t token;
    dispatch_once(&token, ^{
        _instance = [[super allocWithZone:nil] init];
    });
    return _instance;
}

+ (instancetype)allocWithZone:(struct _NSZone *)zone {
    return [SingletonObject defaultSingletonObject];
}

@end

如果文中尚存错误,亦或不足之处,烦请大佬之处。谢谢!


参考资料:

菜鸟教程-单例模式

『Apple API』NS_UNAVAILABLE 与 NS_DESIGNATED_INITIALIZER

Self & Super

iOS 中self和super如何理解?

iOS 程序员 6 级考试(答案和解释)

Objective-C特性:Runtime

objc4_debug

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值