IOS基础:Delegate与Protocol

原创:知识点总结性文章
创作不易,请珍惜,之后会持续更新,不断完善
个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
温馨提示:由于不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容

目录

  • 一、比较
    • 1、代理和block的选择
    • 2、Delegate、Notification、KVO的优缺点比较
  • 二、跨层传值
    • 1、属性正向传值
    • 2、KVC正向传值
    • 3、Delegate逆向传值
    • 4、Block逆向传值
    • 5、KVO逆向传值
    • 6、Notification反向传值
    • 7、NSUserDefaults传值
  • 三、Delegate与Protocol的用法
    • 1、简介
    • 2、计算人数工具类
    • 3、员工
    • 4、技术总监
    • 5、调用方式
  • Demo
  • 参考文献

一、比较

1、代理和block的选择

多个消息传递,应该使用delegate,这个时候block反而不便于维护,而且看起来非常臃肿,很别扭。如果UITableView中很多代理都换成block实现,我们脑海里想一下这个场景是很可怕的。

一个委托对象的代理属性只能有一个代理对象,如果想要委托对象调用多个代理对象的回调应该用block,因为delegate只是一个保存某个代理对象的地址,如果设置多个代理相当于重新赋值,只有最后一个设置的代理才会被真正赋值。

单例对象最好不要用delegate。单例对象由于始终都只是同一个对象,如果使用delegate,就会造成我们上面说的delegate属性被重新赋值的问题,最终只能有一个对象可以正常响应代理方法。

代理是可选的,而block在方法调用的时候只能通过将某个参数传递一个nil进去实现同样的效果,只不过这并不是什么大问题,没有代码洁癖的可以忽略。

从设计模式的角度来说,代理更佳面向过程,而block更佳面向结果。例如我们使用NSXMLParserDelegate代理进行XML解析,NSXMLParserDelegate中有很多代理方法,NSXMLParser会不间断调用这些方法将一些转换的参数传递出来,这就是NSXMLParser解析流程,这些通过代理来展现比较合适。而例如一个网络请求回来,就通过successfailure代码块来展示就比较好。

从性能上来说,block的性能消耗要略大于delegate,因为block会涉及到栈区向堆区拷贝等操作,时间和空间上的消耗都大于代理。而代理只是定义了一个方法列表,在运行时向遵守协议的对象发送消息即可。


2、Delegate、Notification、KVO的优缺点比较

Delegate
优势
  • 如果delegate中的一个方法没有实现那么就会出现编译警告/错误
  • 一个控制器中可以实现多个不同的协议
  • 能够接收返回值
  • 一对一的通信
缺点: 需要定义很多代码
  • 协议定义
  • controllerdelegate属性
  • 实现delegate方法
Notification
优势
  • 代码量少,实现简单
  • 1对多的通信
  • 可以携带自定义消息
缺点
  • 需要在不用的时候注销通知
  • 调试难以追踪
  • 通知发送后,不能从观察者得到任何反馈信息
  • 代码可读性不强
  • notifacationName必须相同,否则无法接受消息
KVO
优势
  • key paths来观察属性,因此可以观察嵌套对象
  • 能够提供一种简单的方法实现两个对象间的同步
  • 能够对非我们创建的对象,即内部对象的状态改变做出响应,而且不需要改变内部对象的实现
缺点
  • 观察的属性必须使用 string 来定义,因此编译器不会出现警告
  • 对属性重构将导致我们的观察代码不再可用

二、跨层传值

1、属性正向传值

当从第一个页面push到第二个页面时,第二个页面需要使用到第一个页面的数据,这时就可以使用正向传值。

界面一

这样传递是有问题的,因为子页面中的textfield是在viewDidLoad中进行初始化和布局的,但在这时候textfield还没有初始化,为nil,所以赋值是失效的。

- (void)proprety
{
    SecondViewController *postVC = [[SecondViewController alloc] init];
    postVC.content = @"刘盈池";
    postVC.contentTextField.text = @"谢佳培";
    
    [self.navigationController pushViewController:postVC animated:YES];
}
界面二
- (void)proprety
{
    NSLog(@"属性正向传值,content内容为:%@",self.content);
    NSLog(@"属性正向传值,contentTextField内容为:%@",self.contentTextField.text);
}

输出结果为:

2020-09-23 17:03:02.553646+0800 Demo[92767:17573694] 属性正向传值,content内容为:刘盈池
2020-09-23 17:03:02.553825+0800 Demo[92767:17573694] 属性正向传值,contentTextField内容为:

2、KVC正向传值

通过Key名给对象的属性赋值,而不需要调用明确的存取方法,这样就可以在运行时动态地访问和修改对象的属性。

界面一
- (void)useKVC
{
    SecondViewController *postVC = [[SecondViewController alloc] init];
    [postVC setValue:@"刘盈池" forKey:@"content"];
    
    [self.navigationController pushViewController:postVC animated:YES];
}
界面二
- (void)useKVC
{
    NSLog(@"KVC正向传值,content内容为:%@",self.content);
}

输出结果为:

2020-09-23 17:57:13.984068+0800 Demo[93502:17615940] KVC正向传值,content内容为:刘盈池

3、Delegate逆向传值

在从第二个页面返回第一个页面的时候,第二个页面会释放掉内存,如果需要使用子页面中的数据就用到了逆向传值。

界面一

在第一个页面中遵从该代理。

@interface FirstViewController ()

第二个页面的代理是第一个页面自身self

- (void)useDelegate
{
    SecondViewController *postVC = [[SecondViewController alloc] init];
    postVC.delegate = self;
    [self.navigationController pushViewController:postVC animated:YES];
}

实现代理中定义的方法,第二个页面调用的时候会回调该方法。在方法的实现代码中将参数传递给第一个页面的属性

- (void)transferString:(NSString *)content
{
    self.title = content;
    NSLog(@"Delegate反向传值,第一个页面接收到的content为:%@",content);
}
界面二

声明代理

@protocol contentDelegate 

/** 代理方法 */
- (void)transferString:(NSString *)content;

@end

声明代理属性

@interface SecondViewController : UIViewController

@property(nonatomic, weak) id delegate;

@end

返回第一个页面之前调用代理中定义的数据传递方法,方法参数就是要传递的数据。如果当前的代理存在,并且实现了代理方法,则调用代理方法进行传递数据。

- (void)backDelegate
{
    if (self.delegate && [self.delegate respondsToSelector:@selector(transferString:)])
    {
        [self.delegate transferString:@"刘盈池"];
        [self.navigationController popViewControllerAnimated:YES];
    }
}

输出结果为:

2020-09-23 17:21:45.923769+0800 Demo[92974:17584680] Delegate反向传值,第一个页面接收到的content为:刘盈池

4、Block逆向传值

界面一

通过子页面的block回传拿到数据后进行处理,赋值给当前页面的textfield

- (void)useBlock
{
    SecondViewController *postVC = [[SecondViewController alloc] init];
    postVC.transDataBlock = ^(NSString * _Nonnull content) {
        self.title = content;
        NSLog(@"Block逆向传值,第一个页面接收到的content为:%@",content);
    };
    
    [self.navigationController pushViewController:postVC animated:YES];
}
界面二

声明block,用于回传数据

typedef void(^TransDataBlock)(NSString *content);

定义一个block属性,用于回传数据

@property(nonatomic, copy) TransDataBlock transDataBlock;

@property (nonatomic, copy) void(^ TransDataBlock)(NSString * content);

调用block并将数据作为参数回传。

- (void)backBlock
{
    if (self.transDataBlock)
    {
        self.transDataBlock(@"刘盈池");
    }
    [self.navigationController popViewControllerAnimated:YES];
}

输出结果为:

2020-09-23 17:29:40.121002+0800 Demo[93133:17594231] Block逆向传值,第一个页面接收到的content为:刘盈池

5、KVO逆向传值

界面一

在第一个页面注册观察者。

- (void)useKVO
{
    self.secondVC = [[SecondViewController alloc] init];

    [self.secondVC addObserver:self forKeyPath:@"content" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:nil];
    
    [self.navigationController pushViewController:self.secondVC animated:YES];
}

实现KVO的回调方法,当观察者中的数据有变化时会回调该方法。

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if ([keyPath isEqualToString:@"content"])
    {
        self.title = self.secondVC.content;
        NSLog(@"KVO反向传值,第一个页面接收到的content为:%@",self.secondVC.content);
    }
}

在第一个页面销毁时移除KVO观察者。

- (void)dealloc
{
    [self.secondVC removeObserver:self forKeyPath:@"content"];
}
界面二
- (void)backKVO
{
    // 修改属性的内容
    self.content = @"刘盈池";

    // 返回第一个界面回传数据
    [self.navigationController popViewControllerAnimated:YES];
}

输出结果为:

2020-09-23 17:46:10.744468+0800 Demo[93363:17607725] KVO反向传值,第一个页面接收到的content为:刘盈池

6、Notification反向传值

界面一

点击跳转到第二个页面

- (void)notificationClick
{
    SecondViewController *postVC = [[SecondViewController alloc] init];
    [self.navigationController pushViewController:postVC animated:YES];
}

注册通知

- (void)registerNotification
{
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(infoAction:) name:@"info" object:nil];
}

实现收到通知时触发的方法

- (void)infoAction:(NSNotification *)notification
{
    NSLog(@"接收到通知,内容为:%@",notification.userInfo);
    self.title = notification.userInfo[@"name"];
}

在注册通知的页面消毁时一定要移除已经注册的通知,否则会造成内存泄漏。

- (void)dealloc
{
    // 移除所有通知
    [[NSNotificationCenter defaultCenter] removeObserver:self];
    
    // 移除某个通知
    [[NSNotificationCenter defaultCenter] removeObserver:self name:@"info" object:nil];
    
    // 在第一个页面销毁时移除KVO观察者
    [self.secondVC removeObserver:self forKeyPath:@"content"];
}
界面二

如果发送的通知指定了object对象,那么观察者接收的通知设置的object对象与其一样,才会接收到通知,但是接收通知如果将这个参数设置为了nil,则会接收一切通知。

// 返回到上个界面
- (void)backNotification
{
    // 1.创建字典,将数据包装到字典中
    NSDictionary *dict = [[NSDictionary alloc] initWithObjectsAndKeys:@"刘盈池",@"name",@"19",@"age", nil];
    
    // 2.创建通知
    NSNotification *notification = [NSNotification notificationWithName:@"info" object:nil userInfo:dict];
    
    // 3.通过通知中心发送通知
    [[NSNotificationCenter defaultCenter] postNotification:notification];
    
    // 4.回传数据
    [self.navigationController popViewControllerAnimated:YES];
}

输出结果为:

2020-09-23 18:08:28.526933+0800 Demo[93661:17626006] 接收到通知,内容为:{
    age = 19;
    name = "\U5218\U76c8\U6c60";
}

7、NSUserDefaults传值

页面一

需要使用值时通过NSUserDefaults从沙盒目录里面取值进行处理。

- (void)useDefaults
{
    NSString *content = [[NSUserDefaults standardUserDefaults] valueForKey:@"Girlfriend"];
    NSLog(@"NSUserDefaults传值,内容为:%@",content);
}
页面二

需要传值时将数据通过NSUserDefaults保存到沙盒目录里面,比如用户名之类,当用户下次登录或者使用app的时候,可以直接从本地读取此值。

- (void)userDefaultsClick
{
    [[NSUserDefaults standardUserDefaults] setObject:@"刘盈池" forKey:@"Girlfriend"];
    [[NSUserDefaults standardUserDefaults] synchronize];
    
    // 跳转到第一个界面
    [self.navigationController popViewControllerAnimated:YES];
}

输出结果为:

2020-09-23 18:19:10.065815+0800 Demo[93846:17636600] NSUserDefaults传值,内容为:刘盈池

三、Delegate与Protocol的用法

1、简介

Delegate
什么是Delegate与Protocol呢?

举个简单的例子,外卖app就是我的代理,我就是委托方,我买了一瓶红茶并付给外卖app钱,这就是购买协议。我只需要从外卖app上购买就可以,具体的操作都由外卖app去处理,我只需要最后接收这瓶红茶就可以。我付的钱就是参数,最后送过来的红茶就是处理结果。

有哪些特性呢?

如果只是某个类使用,我们常做的就是写在某个类中。如果多个类都是用同一个协议,建议创建一个Protocol文件,在这个文件中定义协议。遵循的协议可以被继承,例如我们常用的UITableView,由于继承自UIScrollView的缘故,所以也将UIScrollViewDelegate继承了过来,我们可以通过代理方法获取UITableView偏移量等状态参数。

协议只能定义公用的一套接口,类似于一个约束代理双方的作用。但不能提供具体的实现方法,实现方法需要代理对象去实现。协议可以继承其他协议,并且可以继承多个协议,在iOS中对象是不支持多继承的,而协议可以多继承。

默认是@required状态的,无论是@optional还是@required,在委托方调用代理方法时都需要做一个判断,判断代理是否实现当前方法,否则会导致崩溃。

原理是什么呢?

其实委托方的代理属性本质上就是代理对象自身,设置委托代理就是代理属性指针指向代理对象,相当于代理对象只是在委托方中调用自己的方法,如果方法没有实现就会导致崩溃。

为什么我们设置代理属性都使用weak呢?

由于代理对象使用强引用指针,引用创建的委托方对象,并且成为委托方对象的代理。这就会导致委托方的delegate属性强引用代理对象,导致循环引用的问题,最终两个对象都无法正常释放。设置为弱引用属性。这样在代理对象生命周期存在时,可以正常为我们工作,如果代理对象被释放,委托方和代理对象都不会因为内存释放导致的Crash

实际运用场景举个例子?

控制器瘦身:UITableView的数据处理、展示逻辑和简单的逻辑交互都由代理对象去处理,传入一些参数,和控制器相关的逻辑处理传递出来,交由控制器来处理,这样控制器的工作少了很多,而且耦合度也大大降低了。


2、计算人数工具类

声明委托方法
@protocol CountToolDelegate 

- (void)willCountAllPerson;// 即将计算人数的委托方法
- (void)didCountedAllPerson;// 完成计算人数的委托方法

@end
声明数据源方法
@protocol CountToolDataSource 

- (NSArray *)personArray;// 返回包含所有人的数组

@end
声明委托和数据源属性
@property (nonatomic, weak) id delegate;// 委托
@property (nonatomic, weak) id dataSource;// 数据源
实现计数方法
- (void)count
{
    // 调用即将计数的委托方法
    if (self.delegate && [self.delegate conformsToProtocol:@protocol(CountToolDelegate)])
    {
        [self.delegate willCountAllPerson];
    }
    
    // 调用数据源进行计数
    NSArray *persons = [self.dataSource personArray];
    NSLog(@"人数:%@", @(persons.count));
    
    // 调用完成计数的委托方法
    if (self.delegate && [self.delegate respondsToSelector:@selector(didCountedAllPerson)])
    {
        [self.delegate didCountedAllPerson];
    }
}

3、员工

声明工号和职位的协议
@protocol WorkProtocol 

@property (nonatomic, strong) NSString *jobNumber;// 工号

@required
- (void)printJobNumber;// 打印工号

@optional
- (void)codingAsProgrammer;// 职位

@end
遵从协议并声明协议属性
@interface Person : NSObject 

@property (nonatomic, weak) id delegate;

@end
实现协议方法
@implementation Person

- (void)printJobNumber
{
    NSLog(@"打印工号为:%@", self.jobNumber);
}

- (void)codingAsProgrammer
{
    NSLog(@"编程者");
}

@end

4、技术总监

遵从工具类委托
@interface Administrator() 

_countTool.delegate = self;
_countTool.dataSource = self;
实现计算所有人的数目
- (void)countAllPerson
{
    [self.countTool count];
}
CountToolDelegate
- (void)willCountAllPerson
{
    NSLog(@"调用了即将计算人数的委托方法");
}

- (void)didCountedAllPerson
{
    NSLog(@"调用了完成计算人数的委托方法");
}
CountToolDataSource
- (NSArray *)personArray
{
    return self.allPersons;// 返回包含所有人的数组
}

5、调用方式

Person *aPerson = [[Person alloc] init];

// protocol
aPerson.jobNumber = @"10004847";
[aPerson printJobNumber];
[aPerson codingAsProgrammer];

// delegate, dataSource
Administrator *admin = [[Administrator alloc] init];
[admin countAllPerson];

输出结果为:

2020-10-20 17:23:35.042241+0800 DelegateDemo[27449:4938470] 打印工号为:10004847
2020-10-20 17:23:35.042349+0800 DelegateDemo[27449:4938470] 编程者
2020-10-20 17:23:35.042450+0800 DelegateDemo[27449:4938470] 调用了即将计算人数的委托方法
2020-10-20 17:23:35.042539+0800 DelegateDemo[27449:4938470] 人数:2
2020-10-20 17:23:35.042619+0800 DelegateDemo[27449:4938470] 调用了完成计算人数的委托方法

Demo

Demo在我的Github上,欢迎下载。
BasicsDemo

参考文献

你可能感兴趣的:(IOS基础:Delegate与Protocol)