iOS中的消息传递机制有以下几种:
- 代理(Delegation)
- 通知(NSNotification)
- BLOCK
- KVO(key-value observing)
- Target-Action
这么多的消息传递机制,我们该如何选择呢?
最最基本的消息传递机制
其实,除了以上列举的5种以外,还有一类,往往被我们忽视,那就是
方法调用本身就是一种消息传递机制
方法调用可以代入参数,执行完成后,会有返回值,告诉调用者方法执行的结果。
由于返回值只能有一个,并且只在方法执行完成后才“返回”,因此对于需要反映执行过程的任务,比如下载进度,方法调用的局限就十分明显了,另外,从线程角度来看,方法调用是在调用者当前线程开辟的,属于一种同步方法,调用者会被“卡死”,直到方法执行完成。
代理(Delegation)
代理机制就是为了解决任务执行中,需要与调用者“交互”而出现的,UITableView的“三大问题”就是典型的“交互式”调用:
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
每次 reloadData ,以上方法都会被“自动”调用一次或多次,因此给了调用者一个机会,可以改变行数或是Cell的样式,给用户一种“动态变化”的效果。
代理机制有以下缺点:
- 定义繁琐
代理机制使用的是OC语言的协议, 首先需要定义协议:
@protocol MyDelegate
- (int)funWithArg:(int)arg1;
@end
@interface My : NSObject {
id<MyDelegate> deleage;
}
@property(assign,nonatomic) id<MyDelegate> delegate;
@end
调用者需要实现该协议:
@interface Caller : NSObject<MyDelegate>
@end
#import "Caller.h"
@implementation Business
- (int)funWithArg:(int)arg1 {
return arg1+1'
}
@end
被调用者需要调用代理对象:
// in my.m
int n = [deleage funWithArg:1];
- 释放代理对象时,要将被调用者的delegate属性设为nil
- 当使用多个同类对象时,代理机制本身没有提供区分的办法
设想一个controller中添加了两个tableview,controller本身作为代理,代理方法只能使用同一套,要么使用tag作为区分,要么将delegate单独成类,编程都比较繁琐,因此,我们需要一种一对一的便捷方法。
Block
回顾一下方法调用,调用时可以指定参数值,并获取对应的返回值,是很单纯的一对一调用模式,如果我们将一些执行方法,“打包”成参数传入,是对这种一对一模式很自然的补充,这就是Block的本质:
Block允许将方法作为参数传递给方法
因此Block在执行那些一对一的交互任务时,显得得心应手,比如AFNetworking中处理网络请求的返回值和失败信息,无需担心多次网络请求会导致信息“互串”。
但由于Block解决了代理定义繁琐,一对多的问题,自然也带来了副作用:
Block本身是匿名函数,因此复用的办法只能是copy,这是编程大忌
另外Block的调用是有延迟的,这点往往会被初学者误解,如下面的代码:
Record *record = [[Record alloc] init];
[drivingRecord save:^{
record.createTime = [NSDate date];
}];
NSLog(@"%@", record.createTime)
输出的createTime是nil !因为Block可能是使用异步线程调用的。
通知
NSNotification是系统Foundation提供的消息机制,使用起来是很直接的:
- 观察者注册
- 被观察者发消息
- 观察者注销
这是一种对消息发送者来说很“轻松”的消息机制,除了调用
- postNotification:
外,几乎不用做任何事情。
因为使用简单,因此通知也有一些天生的缺点:
- 一对多传递消息,消息发送者不能指定接收者。
- 通知发出后,被观察者不能从观察者获得任何的反馈信息,所以这不是一个双向传输的消息机制。
- 对观察者来说,调用和得到的结果并没有明显的对应关系,甚至不在同一个类中,调用关系不直观。
-
- (void)addObserver:(id)observer selector:(SEL)aSelector name:(nullable NSString *)aName object:(nullable id)anObject;
被调用多少次,回调就会被执行多少次,该特性往往导致一次post,多次调用;在ViewController中添加注销时,要注意添加和注销是否一一对应,错误往往在ViewController没有被正确释放时出现!
另外,还有些语法上带来的缺陷:
- 消息的主要由消息名称来区分,没有良好的编程习惯,很容易导致消息混淆,使观察者处理了不是发给自己的消息。
- 如果没有使用
- postNotificationName:object:
的object参数告知消息发送者,观察者无法确定消息来源。
KVO
添加监听者:
- addObserver:forKeyPath:options:context:
所有监听值的变化都会由一个方法获取,必须重写父类的同名方法:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
主要缺陷:
- 所有返回结果都由一个方法处理,导致一连串的if判断,修改和阅读都不是十分清晰。如果父类也实现了
observeValueForKeyPath...
会进一步导致这种混乱。 - 由于
addObserver...
无法指定单独的回调方法,单独为每次调用指定方法是不可能的。 - 事实上,正如KVO的名称所暗示的——键-值-监听——该机制只能适用于值的变化,尝试处理一些没有值(比如按钮事件),或一连串的相关事务时(比如网络下载时,先获取头,再根据头确定下载),用KVO实现就会变得非常臃肿,逻辑不清。
- 从代码清晰度角度看,KVO声明的接口是隐性的,与普通属性没有明显的区分(当然,你应该添加注释说明),除非编码尝试,调用者无法获知一个属性是否能够被监听。
Target-Action
最典型的就是UIButton的
- (void)addTarget:(nullable id)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents;
SEL中指定的方法是固定格式的
-(IBAction)doSometing:(id)sender
可以自己实现一个Target-Action机制,如下声明一个SEL:
SEL aSelector = @selector(methodName);
执行一个SEL:
SEL aSelector = @selector(run);
[aDog performSelector:aSelector];
[anAthlete performSelector:aSelector];
根据实现原理不难发现,Target-Action机制有以下缺陷:
- selector只能查找指定类(含子类)的方法。
- SEL 查找的方法不支持类方法
- 最大的缺陷就是不能传参数,唯一参数用于传递发消息的控件对象,因此所有传递的值必须放在指定对象中,也因为这个参数是可选的,较容易出错。
- 这也是一种单向传输的机制,执行SEL无法获得返回值。
综上所述,每种消息机制各有自己的适用情形,不存在谁比谁优秀先进的问题,一个复杂系统中往往是各种机制协同工作。
网友评论