【iOS】单例、通知、代理

1 单例模式

1.1 什么是单例

单例模式在整个工程中,相当于一个全局变量,就是不论在哪里需要用到这个类的实例变量,都可以通过单例方法来取得,而且一旦你创建了一个单例类,不论你在多少个界面中初始化调用了这个单例方法取得对象,它们所有的对象都是指向的同一块内存的存储空间(即单例类保证了该类的实例对象是唯一存在的一个)。

1.2 单例模式的优缺点

  • 优点

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

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

1.3 单例的实现

单例的实现分为两种:懒汉式和饿汉式。

  • 懒汉式:顾名思义,不到万不得已就不会去实例化类,也就是说在第一次用到类实例的时候才会去实例化。
  • 饿汉式:饿了肯定会饥不择食,所以在单例类加载的时候就进行实例化。

特点和选择:

  • 由于要进行线程同步,所以在访问量比较大,或者可能访问的线程比较多时,采用饿汉实现,可以实现更好的性能,这是以空间换时间。
  • 在访问量较小时,采用懒汉实现,这是以时间换空间。

1.3.1 懒汉式

  1. 使用@synchronized
objectivec 复制代码
static id manager = nil;
+ (instancetype)shareInstance {
	// 防止多次加锁
    if (!manager) {
        @synchronized (self) {
            if (!manager) {
                manager = [[super allocWithZone:NULL] init];
            }
        }
    }
    return manager;
}

第一次if(!manager)判断是为了避免在对象创建后多次访问导致的多次加锁,浪费性能。第二次if(!manager)判断就是判断此时单例是否存在,不存在就重新创建。

  1. 使用GCD
objectivec 复制代码
+ (instancetype)shareInstance {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        manager = [[super allocWithZone:NULL] init];
    });
    return manager;
}

dispatch_once它没有使用重量级的同步机制,性能也优于前者,并且更加高效。

dispatch_once无论使用多线程还是单线程,都只执行一次,在安全的前提下也保证了性能。

dispatch_once主要是根据onceToken的值来决定怎么执行代码:

  • onceToken为0时,线程执行dispatch_onceblock中的代码。
  • onceToken为-1时,线程跳过dispatch_onceblock中的代码。
  • onceToken为其他值时,线程被阻塞,等待onceToken值改变。

dispatch_once的执行流程:

  1. 当线程调用shareInstance,此时onceToken为0,执行dispatch_onceblock中的代码,此时onceToken中的值为其他值。
  2. 这时如果有其他线程再调用shareInstance方法时,onceToken值为其他值,线程阻塞。
  3. block线程执行完block后,onceToken变为-1。其他线程不再阻塞,跳过block
  4. 下次再调用shareInstance时,onceToken为-1,直接跳过block

1.3.2 饿汉式

当类被加载的时候就创建,因为一个类在整个生命周期中只会被加载一次,所以它肯定只有一个线程对其进行访问,此时再创建他就是线程安全的,就不需要使用线程锁来保证其不会被多次创建。

objectivec 复制代码
static id manager = nil;

+ (void)load {
    [super load];
    manager = [[super allocWithZone:NULL] init];
}

+ (instancetype)shareInstance {
    return manager;
}

- (instancetype)copyWithZone:(NSZone *)zone {
    return manager;
}

- (instancetype)mutableCopyWithZone:(NSZone *)zone {
    return manager;
}

+ (instancetype)allocWithZone:(struct _NSZone *)zone {
    return manager;
}

1.4 关于复写

根据单例的定义,我们只能允许存在一个该单例对象,所以我们要扼制其创建出新的该单例类的对象,保证对象的唯一性。所以我们要复写一些可以自己创建对象的方法例如copymutableCopy。为了彻底保证用户无法创建新的该单例类的对象,我们一般会重写到XXXWithZone:方法。

objectivec 复制代码
- (instancetype)copyWithZone:(NSZone *)zone {
    return manager;
}

- (instancetype)mutableCopyWithZone:(NSZone *)zone {
    return manager;
}

+ (instancetype)allocWithZone:(struct _NSZone *)zone {
    return manager;
}

2 通知

2.1 几个点

  • 观察者和被观察者都无需知晓对方,只需要通过标记在NSNotificationCenter中找到监听该通知所对应的类,从而调用该类的方法。
  • 并且在NSNotificationCenter中,观察者可以只订阅某一特定的通知,并对齐做出相应操作,而不用对某一个类发的所有通知都进行更新操作。
  • NSNotificationCenter对观察者的调用不是随机的,而是遵循注册顺序一一执行的,并且在该线程内是同步的。

2.2 通知使用步骤

总体分为三步走:

  1. 在要传递参数的地方,发送通知给通知中心:
objectivec 复制代码
[[NSNotificationCenter defaultCenter] postNotificationName:@"temp" object:nil userInfo:@{@"content": self.myTextField.text}];
  1. 在接收参数的地方注册通知,并实现定义方法
objectivec 复制代码
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(Notificate:) name:@"temp" object:nil];
  1. 在不需要通知的时候,移除通知
objectivec 复制代码
[[NSNotificationCenter defaultCenter] removeObserver:self];

2.3 一些问题

2.3.1 通知的发送时同步的,还是异步的?发送消息与接收消息的线程是同一个线程么?

通知中心发送通知给观察者是同步的,也可以用通知队列(NSNotificationQueue)异步发送通知。

在抛出通知以后,观察者在通知事件处理完成以后(可以通过休眠3秒来测试),抛出者才会往下继续执行,也就是说这个过程默认是同步的;当发送通知时,通知中心会一直等待所有的 observer 都收到并且处理了通知才会返回到 poster

接收通知的线程,和发送通知所处的线程是同一个线程。也就是说如果要在接收通知的时候更新 UI,需要注意发送通知的线程是否为主线程。

objectivec 复制代码
- (void)viewDidLoad {
    [super viewDidLoad];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(test) name:@"NotificationName" object:nil];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    dispatch_queue_t queue = dispatch_queue_create("test.queue", DISPATCH_QUEUE_SERIAL);
    dispatch_async(queue, ^{
        NSLog(@"--currect thread:%@", [NSThread currentThread]);
        NSLog(@"Begin post notification");
        [[NSNotificationCenter defaultCenter] postNotificationName:@"NotificationName" object:nil];
        NSLog(@"End");
    });
}

- (void)test {
    NSLog(@"--current thread:%@", [NSThread currentThread]);
    NSLog(@"Handle notification and sleep 3s");
    sleep(3);
}

2.3.2 如何使用异步发送通知?

  1. 让通知事件处理方法在子线程中执行。
objectivec 复制代码
- (void)viewDidLoad {
    [super viewDidLoad];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(test) name:@"NotificationName" object:nil];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"--currect thread:%@", [NSThread currentThread]);
    NSLog(@"Begin post notification");
    [[NSNotificationCenter defaultCenter] postNotificationName:@"NotificationName" object:nil];
    NSLog(@"End");
}

- (void)test {
    dispatch_queue_t queue = dispatch_queue_create("test.queue", DISPATCH_QUEUE_SERIAL);
    dispatch_async(queue, ^{
        NSLog(@"--currect thread:%@", [NSThread currentThread]);
        NSLog(@"Handle notification and sleep 3s");
        sleep(3);
        NSLog(@"Test End");
    });
}
  1. 可以通过 NSNotificationQueueenqueueNotification: postingStyle:enqueueNotification: postingStyle: coalesceMask: forModes: 方法将通告放入队列,实现异步发送,在把通告放入队列之后,这些方法会立即将控制权返回给调用对象。
objectivec 复制代码
- (void)viewDidLoad {
    [super viewDidLoad];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(test) name:@"NotificationName" object:nil];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"--current thread:%@", [NSThread currentThread]);
    NSLog(@"Begin post notification");
    NSNotification *notification = [NSNotification notificationWithName:@"NotificationName" object:nil];
    [[NSNotificationQueue defaultQueue] enqueueNotification:notification postingStyle:NSPostASAP];
    NSLog(@"End");
}

- (void)test {
    NSLog(@"--current thread:%@", [NSThread currentThread]);
    NSLog(@"Handle notification and sleep 3s");
    sleep(3);
    NSLog(@"Test End");
}

2.3.3 NSNotificationQueue 和 runloop 的关系?

postringStyle 参数就是定义通知调用和 runloop 状态之间关系。

该参数的三个可选参数:

1.NSPostWhenIdle:通知回调方法是等待到当下线程 runloop 进入等待状态才会调用。

2.NSPostASAP:通知回调方法是等待到当下线程 runloop 开始接收事件源的时候就会调用。

3.NSPostNow:其实和直接用默认的通知中心添加通知是一样的,通知马上调用回调方法。

2.3.3 页面销毁时不移除通知会崩溃吗?

在观察者对象释放之前,需要调用 removeOberver 方法将观察者从通知中心移除,否则程序可能会出现崩溃。但从 iOS9 开始,即使不移除观察者对象,程序也不会出现异常。

这是因为在 iOS9 以后,通知中心持有的观察者由 unsafe_unretained 引用变为weak引用。即使不对观察者手动移除,持有的观察者的引用也会在观察者被回收后自动置空。但是通过 addObserverForName:object: queue:usingBlock: 方法注册的观察者需要手动释放,因为通知中心持有的是它们的强引用。

2.3.4 多次添加同一个通知会是什么结果?多次移除通知呢?

  • 多次添加同一个通知,观察者方法会调用多次
  • 多次移除,没关系。

2.3.5 为什么注册通知时可以空名注册,但是发送通知时却不可以?

  • 当注册通知时,空的通知名称表示监听所有的通知。这在某些情况下是非常有用的,特别是当你希望监听某个对象发送的所有通知时,而不仅限于特定的通知名称。
  • 发送通知时不可以使用空的通知名称,这是因为发送通知时需要指定一个具体的通知名称,以便通知中心能够将通知正确地发送给对应的观察者。如果在发送通知时使用空的通知名称,通知中心将无法确定通知应该发送给哪些观察者,这样会导致通知无法被正确处理。

2.3.6 object是干嘛的?是不是可以用来传值?

在观察者的添加中,object 参数指定了通知的发送者,只有当通知的发送者与观察者添加时指定的 object 参数相同,才会接收到通知。如果观察者添加时指定了具体的 object 对象,那么在发送通知时,通知的 object 参数必须与观察者添加时的 object 参数相匹配,否则观察者将不会接收到通知。如果需要传值请用userInfo,而不是object

示例,以下方法就不会接收到通知:

objectivec 复制代码
// 添加观察
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:@"TestNotification" object:@1];
// 通知发送
[NSNotificationCenter.defaultCenter postNotificationName:@"TestNotification" object:nil];

这是因为在添加观察者时,指定了 object 参数为 @1,即一个 NSNumber 类型的对象。而在发送通知时,指定的 object 参数为 nil

3 代理

3.1 代理的使用步骤

  1. 在B视图控制器声明一份协议
objectivec 复制代码
@protocol MyViewControllerDelegate <NSObject>

- (void)changUILabelText:(NSString *)string;

@end
  1. 在B视图控制器中声明一个代理属性
objectivec 复制代码
@property (nonatomic, weak) id<MyViewControllerDelegate> delegate;
  1. 在B视图控制器中想要回传值的地方写代理执行的方法
objectivec 复制代码
[self.delegate changeUILabelText:self.myTextField.text];
  1. 在A视图控制器里签订代理协议
objectivec 复制代码
@interface ViewController : UIViewController <MyViewControllerDelegate>

@property (nonatomic, strong) MyViewController *myView;

@end
  1. A中签订代理人
objectivec 复制代码
    self.myView = [[MyViewController alloc] init];
    self.myView.delegate = self;
  1. 实现协议方法
objectivec 复制代码
- (void)changUILabelText:(NSString *)string {
    NSLog(@"%@", string);
}

3.2 代理的循环引用

代理的循环引用是指在代理模式中,由于相互强引用,导致两个对象(通常是委托方和代理方)之间形成了一个循环引用的关系,从而导致内存泄漏问题。由于这种循环引用,当委托方和代理方相互强引用对方时,它们的引用计数无法降为0,导致它们所占用的内存无法被正确释放,从而造成内存泄漏。

为了避免代理的循环引用,可以采用以下几种方法之一:

  1. 使用弱引用(weak reference):在委托方的属性声明中,将代理方的引用设为弱引用,这样就不会形成循环引用。例如:@property (nonatomic, weak) id delegate;。但需要确保代理方在使用期间不会被提前释放。
  2. 使用代理方的生命周期控制:在委托方中持有代理方的引用时,可以根据具体情况,在适当的时候解除对代理方的引用,避免循环引用。
  3. 使用 block 或通知来替代代理模式:在某些情况下,可以使用 block 或通知来替代代理模式,避免循环引用的问题。
    选择合适的方法取决于具体的业务逻辑和需求,确保代理模式的使用不会造成循环引用和内存泄漏。

4. KVO\KVC\单例模式\通知\代理\Block

4.1 代理和通知的区别

  • 效率:代理比通知高;
  • 关联:代理是强关联,委托和代理双方互相知道。通知是弱关联,不需要知道是谁发,也不需要知道是谁接收;
  • 代理是一对一的关系,通知是一对多的关系;代理要实现对多个类发出消息可以通过将代理者添加入集合类后遍历,或通过消息转发来实现。
  • 代理一般行为需要别人来完成,通知是全局通知;

4.2 KVO和通知的区别

  • 相同:都是一对多的关系;
  • 不同:通知是需要被观察者先主动发出通知,观察者注册监听再响应,比KVO多了发送通知这一步;
  • 监听范围:KVO是监听一个值的变化,通知不局限于监听属性的变化,还可以对多种多样的状态变化进行监听,通知的监听范围广,使用更灵活;
  • 使用场景:KVO的一般使用场景是监听数据变化,通知是全局通知;

4.3 block和代理的区别

  • 相同点:block和代理都是回调的方式。使用场景相同。
  • 不同点:
    • block集中代码块,而代理分散代码块,所以 block 更适用于轻便、简单的回调,如网络传输,代理适用于公共接口较多的情况,这样做也更易于解耦代码架构;
    • block运行成本高,block出栈时,需要将使用的数据从栈内存拷贝到堆内存。当然如果是对象就是加计数,使用完或block置为 nil 后才消除,而代理只是保存了一个对象指针,直接回调,并没有额外消耗,相对C的函数指针,只是多做了一个查表动作;

总结表:

方法 delegate NSNotification KVO block
优点 1.逻辑清楚2代码可读性较强3.编译器会检查是否实现了所有方法4.-个controller可以有多个协议5.减少代码耦合性 1.代码量小2.可以实现一对多3.传值方便快捷 1.可以简单的实现两个对象同步2.可以观察当前值和先前值3.能够对非我们创建的对象,即内部对象的状态改变作出响应,而且不需要改变内部对象(SDK对象)的实现 1.逻辑清晰2.同一函数需要低啊用多次的时候,节省代码量3.block可以存储在属性里4.增强代码可读性5.配合GCD优秀的解决多线程
缺点 1.定义需要的代码多2.释放时需要将delegate置为nil,否则调用释放的delegate会crash3.一个controller有多个同一协议的deleqate.难以区分4.跨层传值监听,会使程序层次混乱5.容易引起循环引用 1.编译器不会发现是否可以正确处理2.释放注册对象时,需要取消注册,否则可能会crash3.逻辑不清晰 1.观察的属性使用"string",编译器不会出现警告和检查(编译器无法在编译时捕获拼写错误或其他错误类型)2.属性重构后需要再次修改(属性名称变更、属性类型变更、属性移除或新增) 1.可能引起循环引用2.block中的代码会自动retain.容易造成内存泄漏
建议使用场景 controller与其他任何对象通信,回调方法中一对多的情况、UI响应事件 代码上需要处理的东西很简单,两个毫无关联的对象之间通信 需要监视一个属性 回调方法,简单值的传递

5 设计模式总结

  • KVO/通知 -------> 观察者模式

观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态上发生变化时,会通知所有观察者对象,使它们能够自动更新自己。

优势:解耦合

接口隔离原则、开放-封闭原则

  • KVC --------> KVC模式

KVC 提供了一种更加灵活和强大的属性访问方式,使得开发者能够以简单的方式操作对象的属性和集合,减少了许多重复的代码和复杂性。它是 Cocoa 框架中的一个重要特性,为开发者带来了便利和高效性。但需要注意的是,使用 KVC 时需要保证对象的属性符合 KVC 规范,即属性通过一定的命名规则对应到键,这样才能正确地使用 KVC 进行属性访问和操作。

  • 单例模式

利用应用程序只有一个该类的实例对象这一特殊性来实现资源共享。

优势:使用简单,延时求值,易于跨模块

劣势:这块内存知道程序退出时才能释放

单一职责原则

  • 代理模式

委托方将不想完成的任务交给代理方处理,并且需要委托方通知代理方才能处理。

优势: 解耦合

开放-封闭原则

举例:tableview的数据源和代理

  • 策略模式

策略模式定义了一系列的算法,并将每一个算法封装起来,而且使它们还可以相互替换。策略模式让算法独立于使用它的客户而独立变化。

优势:使算法的变化独立于使用算法的用户

接口隔离原则、多用组合,少用继承、针对接口编程,而非实现

举例:账号密码输入格式的判断、NSArray的sortedArrayUsingSelector等等

  • MVC模式

将程序书写分为三层,分别为模型、视图、控制器,每层都有各自的职责完成各自的工作。

优势: MVC模式使系统,层次清晰,职责分明,易于维护

对扩展开放-对修改封闭

  • MVVM模式

用于解决MVC模式下C层代码冗杂的情况(过多的网络请求以及业务逻辑处理)而出现的MVVM模式,其相比于MVC多了一层ViweModel(业务处理和数据转化)层,专门用于处理数据。

当功能简单时,MVVM反而会增加很多代码,所以对于简单的功能,MVC更加的方便。

  • 三种工厂模式

通过给定参数来返回对应的实例,完全对用户隐藏其实现的原理。

优势:易于替换,面向抽象编程

依赖倒置原则

设计模式:MVC模式、单例模式、观察者模式、MVVM模式、工厂模式、代理模式、策略模式

适配器模式、模板模式、外观模式、创建模式

想要深入了解可以看看这个:iOS 设计模式

相关推荐
恋猫de小郭8 小时前
什么?Flutter 可能会被 SwiftUI/ArkUI 化?全新的 Flutter Roadmap
flutter·ios·swiftui
网安墨雨12 小时前
iOS应用网络安全之HTTPS
web安全·ios·https
福大大架构师每日一题14 小时前
37.1 prometheus管理接口源码讲解
ios·iphone·prometheus
BangRaJun1 天前
LNCollectionView-替换幂率流体
算法·ios·设计
刘小哈哈哈1 天前
iOS 多个输入框弹出键盘处理
macos·ios·cocoa
靴子学长1 天前
iOS + watchOS Tourism App(含源码可简单复现)
mysql·ios·swiftui
一如初夏丿2 天前
xcode15 报错 does not contain ‘libarclite‘
ios·xcode
杨武博2 天前
ios 混合开发应用白屏问题
ios
BangRaJun2 天前
LNCollectionView
android·ios·objective-c
二流小码农3 天前
鸿蒙元服务项目实战:终结篇之备忘录搜索功能实现
android·ios·harmonyos