`Objective-C`的`Singleton`模式

Objective-CSingleton模式

Overview

Singleton 模式的定义

// 确保一个类有且仅有唯一一个单例, 并且提供一个全局的访问点.
Ensures a class has only one instance, and provide a global point of access.

Objective-C 开发

Singleton 模式是 iOS 开发中非常有用的设计模式, 可用于对象之间方便地共享(或传递)数据. 相对于对象之间手动传递数据, Singleton 模式更加的方便直接, 不需要对象之间有任何的联系.


Implementation

技术要点

  1. 仅有一个对象 -> 用一个全局的东西保存这个对象.
  2. 全局的访问点 -> 工厂方法创建对象.
  3. 线程安全 -> dispatch_once

Show code

<br />+ (SQISingleton *)shareInstance {

    static SQISingleton * s_instance_sqi_singleton = nil ;
    
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        if (s_instance_sqi_manager == nil) {
            s_instance_sqi_manager = [[SQISingleton alloc] init];
        }
    });
    return (SQISingleton *)s_instance_sqi_singleton;
}

关于static关键字 & extern关键字

  1. static关键字修饰局部变量:①. 当static关键字修饰局部变量时,该局部变量只会初始化一次,在系统中只有一份内存; ②. static关键字不可以改变局部变量的作用域,但是可延长局部变量的生命周期,该变量直到整个项目结束的时候才会被销毁

  2. static修饰的全局变量:作用域仅限于当前文件,外部类不可以访问到该变量

  3. extern-引用关键字,当某一个全局变量,没有用static修饰时,其作用域为整个项目文件,若是在其他类想引用该变量,则用extern关键字,例如,想引用其他类的全局变量,int age = 10;则在当前类中实现,extern int age;也可以在外部修改该变量,extern int age = 40;,若某个文件中的全局变量不想被外界修改,则用static修饰该变量,则其作用域只限于该文件.


代码鲁棒性

看起来很完美了。可是Objective-C毕竟是Objective-C。别的语言,诸如C++,java,构造方法可以隐藏。Objective-C中的方法,实际上都是公开的,虽然我们提供了一个方便的工厂方法的访问入口,但是里面的alloc方法依旧是可见的,可以调用到的。也就是说,虽然你给了我一个工厂方法,调皮的小伙伴可能依旧会使用alloc的方式创建对象。这样会导致外面使用的时候,依旧可能创建多个实例。

关于这个事情的处理,可以分为两派。一个是冷酷派,技术上实现无论你怎么调用,我都给你同一个单例对象;一个是温柔派,是从编译器上给调皮的小伙伴提示,你不能这么造对象,温柔的指出有问题,但不强制约束。

冷酷派的实现

冷酷派的实现从OC的对象创建角度出发,就是把创建对象的各种入口给封死了。alloc,copy等等,无论是采用哪种方式创建,我都保证给出的对象是同一个。

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

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

温柔派的实现

温柔派就直接告诉外面,alloc,new,copy,mutableCopy方法不可以直接调用。否则编译不过。

+(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 SQI_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 SQI_SINGLETON_IMP(_type_) + (_type_ *)sharedInstance{\
static _type_ *theSharedInstance = nil;\
static dispatch_once_t onceToken;\
dispatch_once(&onceToken, ^{\
theSharedInstance = [[super alloc] init];\
});\
return theSharedInstance;\
}

那么,在定义和实现的时候就很简单了:

@interface SQISingleton : NSObject
    SQI_SINGLETON_DEF(SQISingleton);
@end
@implementation SQISingleton
    SQI_SINGLETON_IMP(SQISingleton);
@end

单例模式潜在的问题

Singleton 模式延长了对象的生命周期, 若单例对象本身比较大, 或者强引用了比较大的对象, 持久的内存占用是一个不得不考虑的问题.

属性修饰符 atomic & nonatomic

目前为止, 在苹果的官方文档上, 已经找不到对这两个属性修饰符的介绍, 网络上对其细节说明也是千奇百怪, 各执一词.

atomic(原子性)

  1. 顾名思义, 表示原子性, 这个概念其实并不新鲜, 早在linux系统下编程本身也是有这个东西的, 所谓原子, 就是不可再化分, 已经是最小的操作单位(所谓操作指的是对内存的读写), 网上很多地方都在讨论 Objective-C 下的 atomic 不安全, 不能保证数据的并发性, 实际上有一点误导了大家, 认为 atomic 本身是不安全的. 实际上, 并非 atomic 不安全, 而是网上一些说法有问题.

  2. atomic关键字会保证属性生成的成员变量在同一时间只能有一条线程对其进行读/写操作, 本质上是在 setter 方法和 getter 方法中添加了一个.

// @property(retain) UITextField *userName;
// 系统生成的代码如下:

- (UILabel *) titleLabel {
    UITextField *label = nil;
    @synchronized(self) {
        label = [[_titleLabel retain] autorelease];
    }
    return label;
}

- (void) setTitleLabel:(UILabel *)titleLabel {
    @synchronized(self) {
      [_titleLabel release];
      _titleLabel = [titleLabel retain];
    }
}
  1. 这里需要说明的是 atomic 仅仅是保证了 setter 方法和 getter 方法的完整性, 确保任何线程在获取数据的时候, 都会得到一个完整的数据.

nonatomic(非原子性)

  1. 没有保证 getter 和 setter 存取方法的线程安全.
- (void)setCurrentImage:(UIImage *)currentImage
{
    if (_currentImage != currentImage) {
        [_currentImage release];
        _currentImage = [currentImage retain];
    }
}

- (UIImage *)currentImage
{
    return _currentImage;
}

Others

  1. atomic要比nonatomic慢大约20倍. 一般如果条件允许, 我们可以让服务器来进行加锁操作.
  2. 虽然 atomic 保证了 setter & getter 的读写安全, 但不能保证其它方法造成的线程安全问题, 比如NSArry对象调用addObject:方法在 atomic 下是没有线程安全保证的.

使用UIView实现视图的转场动画

1. 声明动画块(animation Block)

[UIView beginAnimations:@"View Flip" content:NULL];

beginAnimations: content:方法接受两个参数,第一个是动画块标题,只有在更直接地使用Core Animation(即用于实现动画的框架)时才会用到这个标题,对于上面的实例,可以使用nil替换。第二个参数是一个指针,指向关联到这个动画快的对象(或者任何C语言数据类型)。这里使用空数据(NULL或者nil),因为没有必要指定对象。

2. 指明动画的持续时间

[UIView setAnimationDuration:0.25];

类方法setAnimationDuration:设定了动画的持续时间,它告诉了 UIView 动画应该运行多长时间(以秒为单位)。

3. 设置动画曲线(animation curve)

[UIView setAnimationCure:UIViewAnimationCureEaseInOut];

这决定了动画的运行速度。默认情况下,动画曲线是一条线性曲线,使动画匀速运行。示例中的UIViewAnimationCureEaseInOut指定了“慢速开始,中间加速,慢慢停止”。这样动画看起来会更加自然,不那么呆板。

4. 指定要使用的转场动画类型

[UIView setAnimationTransition:UIViewAnimationFlipFromRight forView:xxxView cache:YES];

在写此篇博客时,iOS提供了5种视图转场类型:

typedef NS_ENUM(NSInteger, UIViewAnimationTransition) {
    UIViewAnimationTransitionNone,  //一个视图突然变成另一个视图(若需要此种效果,根本无需创建一个动画快)
    UIViewAnimationTransitionFlipFromLeft,  // 左侧翻入
    UIViewAnimationTransitionFlipFromRight,  // 右侧翻入
    UIViewAnimationTransitionCurlUp,  // 向上卷起
    UIViewAnimationTransitionCurlDown,  // 向下卷起
};

cache会在动画开始生成一个快照,并在动画执行过程的每一步使用这个快照(对这个快照做动画变换),而不是重新绘制视图。应该始终对动画进行缓存(也就是说此参数应该始终传 YES),除非视图外观在动画执行过程中需要改变。

5. 调用 commitAnimations 方法

typedef NS_ENUM(NSInteger, UIViewAnimationTransition) {
    [UIView commitAnimations];
};

在第一步创建动画块开始一直到调用commitAnimations,中间所有的动作都会被制成动画。
由于 Cocoa Touch 在后台使用了 Core Animation(说人话其实就是底层封装了 Core Animation),所以我们能够使用极少的代码制作出非常复杂而精美的动画。