单例实例化对象存放问题的拓展

你好,我是Emma,今天我们针对单例进行简单的介绍和剖析。

前言:单例的介绍,比较有名是GOF的23中设计模式:
单例(Singleton)模式:某个类只能生成一个实例,该类提供了一个全局访问点供外部获取该实例,其拓展是有限多例模式。Ensures a class has only one instance, and provide a global point of access to it.保证一个类只有一个实例,并且提供一个全局的访问入口访问这个实例。

1.23种设计模式图简介。

范围\目的 创建型模式 结构型模式 行为型模式
类模式 工厂方法 (类)适配器 模板方法、解释器
对象模式 单例 原型 抽象工厂 建造者 代理 (对象)适配器 桥接 装饰 外观 享元 组合 策略 命令 职责链 状态 观察者 中介者 迭代器 访问者 备忘录

2.什么时候选择单例模式呢?

官方说法:
一个类必须只有一个对象。客户端必须通过一个众所周知的入口访问这个对象。
这个唯一的对象需要扩展的时候,只能通过子类化的方式。客户端的代码能够不需要任何修改就能够使用扩展后的对象。

举个栗子:在建模的时候,如果这个东西确实只需要一个对象,多余的对象都是无意义的,那么就考虑用单例模式。比如定位管理(CLLocationManager),硬件设备就只有一个,弄再多的逻辑对象意义不大。所以就会考虑用单例。

3.单例模式有什么优缺点或者需要注意的地方?

  • 内存问题
    • 单例其实延长了对象的声明周期,在整个程序的声明周期中都是存在的,在我的理解中常驻线程可以和他类比。当单例比较大的时候会占用更多内存,造成了内存压力。或者另议中情况就是单例本身不是很大,但是他强引用了另外的比较大的对象的时候,那么那个比较大的对象就由于单例没有被释放,他也不会被释放,这样也造成了内存压力。
    • 针对这种单例本身不大,强引用对象比较大的情况,解决方式:比如一些可以重新加载的对象,需要的时候强引用,使用完成之后,不在强引用,释放掉内存,下次使用的时候再加载回来。
    • 问题来了举个栗子???
  • 循环依赖出现死锁问题
    • 举个栗子:在单例A中创建m属性,这个属性在单例A的init方法中进行初始化,初始化的时候m属性依赖于另一个单例B创建。而单例B中创建了n属性,这个属性在单例B的init中进行初始化,初始化的时候n属性依赖于前面的单例A创建。这样创建出来会使用其中一个单例进行初始化的时候会出现死锁问题。

    • 处理方式(前两个方式我理解,第三种方式看书得来没有很好的栗子来阐述先标记在这里):

      1. 设计的时候就尽量不去产生这种互相依赖的关系。
      2. 如果真的产生这种依赖关系就适当的打破死锁链。
      3. 实在要形成环或者无法控制,那么就异步初始化的方式,先过去,内容再填,内部需要做个标识,标识这个单例创建的是时候不能立即被使用或者不能完整被使用。

面试实例:

1.单例实例化的对象存放在内存区域的哪个区?

单例对象一旦建立,对象指针保存在静态区,单例对象在堆中分配的内存空间,只在应用程序终止后才会被释放

单例模式创建的对象能够一直存在于内存中不被释放,并不只是由于持有一个自身的引用,本质是因为这个引用是静态,也就是说,如果成员变量是非静态的,它持有一个自身的引用,那么这个对象还是被回收。

"系统内至少保持一个对象的引用",这个引用指的是从栈区或方法区中发出的引用,也就是安全的引用

类被实例化之后,是放在堆区中的,而我们是无法直接操作堆内存的,因此需要一个引用,指向堆区的某个区域,而这个引用,必须是从栈中(或方法区中)发出的,因为我们可以直接访问栈内存,如果是从堆中发出的引用,是无意义的引用,我们根本访问不到,因此会被回收。

类的成员变量恰恰是放在堆内存中,因此由类的成员变量持有一个对象引用,这个引用是不安全的

2.内存区域分为那几个区?内存区域的划分和分配:

  • 栈:局部变量,当局部变量的作用域执行完毕之后,局部变量被挥手
  • 堆:程序媛手动申请的字节空间。malloc,realloc, calloc
  • BBS:全局变量或者是静态变量,初始化时候会被回收,这时这些变量就转存到数据段
  • 数据段(常量区):初始化之后的全局变量,静态变量和常量数据,直到程序结束才会被回收。
  • 代码段: 存储程序代码
    3.如何来验证上面的理论?lldb如何研究对象存在于哪一块内存区域?

1.常用的单例应该如何去写?
单例比较方便的也是常用的方法是写成宏。

#define MYY_SINGLETON_DEF(_type_) + (_type_ *)sharedInstance; \
+(instancetype) alloc __attribute__((unavailable("call sharedInstance instead"))); \
+(instancetype) new __attribute__((unavailable("call sharedInstance instead"))); \
-(instancetype) copy __attribute__((unavailable("call sharedInstance instead"))); \
-(instancetype) mutableCopy __attribute__((unavailable("call sharedInstance instead"))); \

#define MYY_SINGLETON_IMP(_type_) + (_type_ *)sharedInstance{ \
static _type_ * theSharedInstance = nil; \
static dispatch_once_t onceToken; \
dispatch_once(&onceToken, ^{ \
theSharedInstance = [[super alloc] init]; \
}); \
return theSharedInstance; \
}

使用方法:

//引用
@interface DJSingleton : NSObject
MYY_SINGLETON_DEF(DJSingleton);
@end
//实现
@implementation DJSingleton
MYY_SINGLETON_IMP(DJSingleton);
@end

另一种比较直接写代码的书写方法是:原理:dispatch_once_t来保证线程安全,从对象的创建角度出发:把创建对象的各种入口封死,比如:alloc,new,copy,无论通过哪种方式创建保证对象只创建一个。

具体实现:在对象创建的时候,无论是alloc还是new,都会调用到 allocWithZone方法。在通过拷贝的时候创建对象时,会调用到-(id)copyWithZone:(NSZone *)zone
-(id)mutableCopyWithZone:(NSZone *)zone方法。因此,可以重写这些方法,让创建的对象唯一。

+(id)allocWithZone:(NSZone *)zone{
    return [DJSingleton sharedInstance];
}
+(DJSingleton *) sharedInstance{
    static DJSingleton * s_instance_dj_singleton = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        s_instance_dj_singleton = [[super allocWithZone:nil] init];
    });
    return s_instance_dj_singleton;
}
-(id)copyWithZone:(NSZone *)zone{
    return [DJSingleton sharedInstance];
}
-(id)mutableCopyWithZone:(NSZone *)zone{
    return [DJSingleton sharedInstance];
}

参考文献:

  1. iOS中的单例模式
posted @ 2019-09-06 15:07  小虫博美  阅读(1038)  评论(0编辑  收藏  举报