iOS单例及其底层原理

这是我参与8月更文挑战的第17天,活动详情查看: 8月更文挑战

单例在不同的语言中实现方式可能不一定完全相同,但其宗旨应该是一样的:单例类在整个程序运行期间有且仅有一个实例;所以,在开发过程中,根据需求合理使用单例类;

单例的优缺点

优点

  • 一个类只被实例化一次,提供了对唯一实例的受控访问
  • 节省系统资源
  • 允许可变数目的实例

因为其上面的特点,对于项目中的个别场景的传值,存储状态等等更加方便

缺点

  • 一个类只有一个对象,可能造成责任过重,在一定程度上违背了单一职责原则
  • 由于单例模式中没有抽象层,因此单例类的扩展有很大困难
  • 滥用单例将带来一些负面问题,如:为了节省资源将数据库连接池对象设计为单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失
  • 单例实例一旦创建,对象指针是保存在静态区的,那么在堆区分配空间只有在应用程序终止后才会被释放

单例的实现

Objective-C

第一种方式

  • 创建单例类
+ (instancetype)shareInstance { 
    static AppManager *instance = nil ; 
    static dispatch_once_t onceToken; 
    dispatch_once(&onceToken, ^{ 
        if (instance == nil) { 
            instance = [[AppManager alloc] init]; 
        } 
    }); 
    return instance; 
}
复制代码
  • 防止alloc,init,new引起的错误
// 防止外部调用alloc 或者 new
+ (instancetype)allocWithZone:(struct _NSZone *)zone {
    return [AppManager shareInstance];
}
复制代码
  • 防止NSCopying引起的错误
// 防止外部调用copy
- (id)copyWithZone:(nullable NSZone *)zone {
    return [AppManager shareInstance];
}
复制代码
  • 防止mutableCopy引起的错误
// 防止外部调用mutableCopy
- (id)mutableCopyWithZone:(nullable NSZone *)zone {
    return [AppManager shareInstance];
}
复制代码

第二种方式

创建之后,我们直接禁止外部使用initnew等操作;

我们在单例.h文件中添加一下代码:

- (instancetype)init NS_UNAVAILABLE;
+ (instancetype)new NS_UNAVAILABLE;
- (id)copy NS_UNAVAILABLE; // 没有遵循协议可以不写
- (id)mutableCopy NS_UNAVAILABLE; // 没有遵循协议可以不写
复制代码

Swift

class AppManager: NSObject {
    static let sharedInstance = AppManager()
    // 私有化构造方法,防止其他对象使用这个类的默认方法‘()’来进行初始化
    private override init() {}
}
复制代码

注意事项

  • 单例必须是唯一的,所以它才被称为单例。在一个应用程序的生命周期里,有且只有一个实例存在。单例的存在给我们提供了一个唯一的全局状态。比如NSNotificaton,Application,NSuserDefault都是单例.
  • 为了保持一个单例的唯一性,单例的构造器必须是私有的。这防止其他对象也能创建出单例类的实例。
  • 为了确保单例在应用程序的整个生命周期是唯一的,它就必须是线程安全的。简单来说,如果你写单例的方式是错误的,就有可能会有两个线程尝试在同一时间初始化同一个单例,这样就有潜在的风险得到两个不同额单例。这就意味着我们需要用到GCDdispatch_once来确保初始化单例的代码在运行时只执行一次。

那么为什么用Swift创建的单例没有看到dispatch_once呢?

根据官方文档中的说明:

全局变量延迟构造器(也适用于结构和枚举的静态成员)在第一次访问全局变量时运行,并作为dispatch_once启动,以确保初始化是原子的。这使得在代码中使用dispatch_once有了一个很酷的方法:只要声明一个带有初始化式的全局变量并将其标记为private即可。

单例dispatch_once的底层原理

我们经常写单例的实现时,经常会这么写:

+ (instancetype)shareInstance {
    static XXXXX *instance = nil ;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        if (instance == nil) {
            instance = [[XXXXX alloc] init];
        }
    });
    return instance;
}
复制代码

那么dispatch_once是如何实现的呢?我们来看一下:

我们发现dispatch_once就是通过dispatch_once_f这个函数来实现单例类的;

  • val就是&onceToken,也就是全局静态变量;(作为条件控制来使用)
  • block是要执行的任务;
  • _dispatch_Block_invoke(block)是对任务的封装;

接下来,我们来看一下dispatch_once_f的具体实现:

  • dispatch_once_gate_t:将val封装成一个开关,进行强制类型转换为l
  • _dispatch_once_gate_tryenter(l):我们第一次创建时,将会执行至此;

进入_dispatch_once_gate_tryenter函数内部:

  • os_atomic_cmpxchg进行原子操作,进行锁的处理,说明其实有线程控制的;
  • _dispatch_lock_value_for_self当前队列中的线程空间的锁,防止多线程(单例类的线程安全问题);

接下来进入_dispatch_once_callout函数:

  • 函数_dispatch_client_callout进行任务ctxt的执行;
  • _dispatch_once_gate_broadcast:任务执行完毕之后进行广播;

两个宏定义,会有一个执行;我们针对_dispatch_once_mark_done分析一下:

dog设置dgo_onceDLOCK_ONCE_DONE;

然后在dispatch_once_f的实现中:

第二次进来时,判断dgo_onceDLOCK_ONCE_DONE的话直接return

如果没有设置DLOCK_ONCE_DONE,并且_dispatch_once_gate_tryenter被上锁,那么将会执行_dispatch_once_wait(l),进行无限期等待,等待开锁;

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享