美文网首页
单例的理解

单例的理解

作者: 小四_5e7b | 来源:发表于2021-03-02 17:46 被阅读0次

    什么是单例?

    通过单例方法获取到的实例,在系统中是唯一的。 为什么说通过单例方法获取到的实例是唯一的呢?这个问题接下在分析。

    什么时候适合使用单例?

    系统中某个对象只需要存在一个实例(多了就浪费内存)

    单例的创建

    创建单例,有两种方式(😂我知道的就两种),一种是通过@synchronized来创建,一种是通过GCD来创建。这里不考虑单线程下创建单例。

    1、@synchronized方式创建
     static UserInfo *manager;
    + (instancetype)sharedManager {
      if(!manager) {
        @synchronized (self) {
            if (!self) {
                manager = [[UserInfo alloc] init];
            }
        }
      }
        return manager;
    }
    

    2、GCD方式创建

    + (instancetype)shareManager{
        static dispatch_once_t onceToken;
        static UserInfo *manager;
        dispatch_once(&onceToken, ^{
            if (manager == nil) {
                manager = [[UserInfo alloc] init];
            }
        });
        return manager;
    }
    

    两种方式创建分析:

    通过@synchronized方式创建的单例,加锁的目的是为了防止多线程同时去创建单例实例

    通过GCD方式创建的单例,我们申明的静态局部变量static dispatch_once_t onceToken;的初始值为0(tip:dispatch_once_t是一个长整形),当获取实例的时候dispatch_once会判断onceToken的值,当值为0的时候,才会走dispatch_once的block方法,创建完成之后会将onceToken的值赋为非0。下一次获取实例的时候dispatch_once会判断onceToken的值,当值不为0的时候就不在走block方法,也就不会再创建实例。通过一下源码大致分析dispatch_once的流程:

    // dispatch_once_t 的定义
    typedef long dispatch_once_t;
    
    // dispatch_once 的定义
    void dispatch_once(dispatch_once_t *val, void (^block)(void)){
        struct Block_basic *bb = (void *)block;
        dispatch_once_f(val, block, (void *)bb->Block_invoke);
    }
    
    // 核心点dispatch_once_f函数
    void dispatch_once_f(dispatch_once_t *val, void *ctxt, void (*func)(void *)){
        
        volatile long *vval = val;
        if (dispatch_atomic_cmpxchg(val, 0l, 1l)) {
            func(ctxt);
            dispatch_atomic_barrier();
            *val = ~0l;
        } else {
            do {
                _dispatch_hardware_pause();
            } while (*vval != ~0l);
            dispatch_atomic_barrier();
        }
    }
    

    dispatch_atomic_cmpxchg(val, 0l, 1l)函数判断val的值是否等于0,等于则将val值赋为1,返回true,否则返回false

    在多线程环境中,如果某一个线程Thread1首次进入dispatch_once_f*val0,这个时候直接将其设为1,然后执行func(ctxt);方法(这个就是我们block里边的代码),然后调用dispatch_atomic_barrier(这函数干啥暂时没有搞懂,网搜了一下说是一个编译器操作,意思为前后指令的顺序不能颠倒.这里是防止编译器优化将原本的指令语义破坏),最后将*val的值修改为~0l(非零)。

    如果其它的线程也进入dispatch_once_f,那么这个时候if的判断返回false,就进入else分支,于是执行了do~while空循环,一直等待。_dispatch_hardware_pause(这个函数具体干啥暂时未知,网上说有助于提高性能和节省CPU耗电,延迟空等),直到首个线程已经将func(ctxt);执行完毕且将*val修改为~0l,调用dispatch_atomic_barrier后退出。所以多线程的时候block也是无法同时执行的,这就保证了在dispatch_once_fblock的执行的唯一性,生成的单例也是唯一的。

    如何创建唯一的单例实例

    上面两种方式创建的单例,其实在系统中并不是唯一的,并不唯一的意思是 当前类可以通过其他方式创建实例。 并不是说 通过shareManager方法获取的实例不是唯一的。注意区分喔(tip:系统实现的单例其实也是可以通过alloc、new等方式去重新获取实例的)。
    还是可以通过alloc 、new、copy、mutableCopy等方法获取到当前类的实例。

    那么如何避免通过alloc 、new、copy、mutableCopy获取到实例呢?也有两种实现方式。

    1、通过 NS_UNAVAILABLE实现

    我们把能创建实例的方法全部加入 NS_UNAVAILABLE

    + (instancetype)alloc NS_UNAVAILABLE;
    + (instancetype)new NS_UNAVAILABLE;
    - (id)copy NS_UNAVAILABLE;
    - (id)mutableCopy NS_UNAVAILABLE;
    

    这样定义了之后,在外部通过alloc 、new、copy、mutableCopy获取实例系统会报错找不到方法(tip:假如当前类没有实现NSCopying, NSMutableCopying协议时,可以不用限制copy、mutableCopy)。

    2、通过重写方法的方式实现
    // 重写+ (instancetype)allocWithZone:(struct _NSZone *)zone; 方法
    + (instancetype)allocWithZone:(struct _NSZone *)zone {
        return [UserInfo shareManager];
    }
    
    // 注意:重写+ (instancetype)allocWithZone:(struct _NSZone *)zone; 方法后,需要调用父类allocWithZone方法来分配空间
    // GCD创建方式需要改写为
    + (instancetype)shareManager{
        static dispatch_once_t onceToken;
        static UserInfo *manager;
        dispatch_once(&onceToken, ^{
            if (manager == nil) {
                manager = [[super allocWithZone:NULL] init];
            }
        });
        return manager;
    }
    
    // 如果实现了NSCopying, NSMutableCopying协议,还需要重写copy、mutableCopy方法
    - (id)copyWithZone:(NSZone *)zone {
        return [UserInfo shareManager];
    }
    
    - (id)mutableCopyWithZone:(NSZone *)zone {
        return [UserInfo shareManager];
    }
    

    注意:通过 NS_UNAVAILABLE方式实现的,也不一定是无法获取当前类其它实例,还是可以通过runtime获取:

    UserInfo *allocInfo = [UserInfo performSelector:@selector(alloc)];
    

    通过上述的方式,还是可以获取。

    单例使用注意

    初始化要尽量简单,不要太复杂;
    单例尽量考虑使用场景,不要随意实现单例,单例一旦初始化就会一直占着资源不能释放,造成资源浪费。

    相关文章

      网友评论

          本文标题:单例的理解

          本文链接:https://www.haomeiwen.com/subject/uakqqltx.html