介绍:
KVO -- 值改变 + 两要求
当对象中的某个属性值发生了改变,可以对这些值的观察者做出通知。
有两个要求:
首先,接收者必须知道发送者。
另外,接收者同样还需要知道发送者的生命周期,因为在销毁发送者对象之前,需要取消观察者的注册。如果这两个要求都满足了,消息传递过程中可以是1对多(多个观察者可以注册某个对象中的值)。
实例:
观察者: 处理监听事件的对象
观察的属性 (对象的属性,可以是实例变量)
观察的选项 (prior、initial、new、old)
上下文 (传递自定义数据)- 在AF中用于标识哪一个KVO
观察的属性
观察的对象
变化字典
上下文 (与监听时传递的一致)
移除观察者,否则我们的 app 会崩溃。
Notification -- 不相关代码 + 广播(任意消息) + 发送接收者 + 单向
在不相关的两部分代码中要想进行消息传递,它可以对消息进行广播,通知可以用来发送任意的消息。
通知的独特之处就在于发送者和接收者双方并不需要相互知道。
这种消息传递机制是单向的,作为接收者是不可以回复消息的。
实例:
在OSC3.x源码中运用了大量的Notification,比如,在LoginVC的login方法中,发送关于"userRefresh"的广播
在MyInfoVC的initWithStyle:中 和 SideMenuVC的viewDidLoad中分别注册的观察者会收到广播
之后分别执行各自的触发方法。
最后,在dealloc方法中移除观察者
delegation -- 定制 + 发送接收者 + 可双向(返回值) + 接近的模块
delegation允许我们定制某个对象的行为,并且可以收到某些确定的事件。为了使用delegation模式,消息的发送者需要知道消息的接收者(delegate),反过来就不用了。
你可以用函数参数的形式来处理消息内容,delegate还可以通过返回值的形式给发送者做出回应。如果只需要在相对接近的两个模块之间进行消息传递,那么Delegation是一种非常灵活和直接方式。
不过,过度使用delegation也有一定的风险,如果两个对象的耦合程度比较紧密,内部相互之间不能独立存在,那么此时就没有必要使用delegate协议了,针对这种情况,对象之间可以知道相互间的类型,进而直接进行消息传递。例如UICollectionViewLayout和NSURLSessionConfiguration。
block -- 会引用循环 + 一次性 + completion, error
block可以满足用delegation实现的消息传递机制。不过这两种机制都有各自的需求和优势。
当不考虑使用block时,一般主要是考虑到block极易引起引用循环。如果发送者需要retain block,而又不能确保这个引用什么时候被置nil,这样就会发生潜在的引用循环。
如果我们发送的消息属于一次性的(具体到某个方法的调用),由于这样可以打破潜在的引用循环,那么使用block是非常不错的选择。另外,如果为了让代码可读性更强,更有连贯性,那最好是使用block了。根据这个思路,block经常可以用于completion handler、error handler等。
实例:
OSC3.x当中 根VC 定义了众多block,给继承它的子类实现,返回不同的业务逻辑(如,解析额外的信息,生成对应页面的请求URL,根据数据量判断是否可刷新)。又因为所有网络请求都封装在 根VC 中,所以 根VC 可以利用block的返回值的URL做网络请求。
block定义
block实现 -- 在子类中
block调用
Target-Action -- 界面事件 + 响应链 + 局限
主要被用于响应用户界面事件时所需要传递的消息中。消息接收者和发送者可以互相不知道。
如果target是nil,action会在响应链(responder chain)中被传递,直到找到某个能够响应该action的对象。在iOS中,每个控件都能关联多个target-action。
有一个局限就是发送的消息不能携带自定义的payload。
在iOS中,可以选择性的将发送者和触发action的事件作为参数。除此之外,没有别的办法可以对发送action消息内容做控制。
PS: iOS响应者链 - iOS通过视图层级关系用来传送触摸事件的机制
每一个应用有一个响应者链,我们的视图结构是一个N叉树(一个视图可以有多个子视图,一个子视图同一时刻只有一个父视图),而每一个继承UIResponder的对象都可以在这个N叉树中扮演一个节点。当叶节点成为最高响应者的时候,从这个叶节点开始往其父节点开始追朔出一条链,那么对于这一个叶节点来讲,这一条链就是当前的响应者链。响应者链将系统捕获到的UIEvent与UITouch从叶节点开始层层向下分发,期间可以选择停止分发,也可以选择继续向下分发。
响应者链事件分发的原理是,iOS系统检测到手指触摸(Touch)操作时会将其打包成一个UIEvent对象,并放入当前活动Application的事件队列,单例的UIApplication会从事件队列中取出触摸事件并传递给单例的UIWindow来处理,UIWindow对象首先会使用hitTest:withEvent:方法寻找此次Touch操作初始点所在的视图(View),即需要将触摸事件传递给其处理的视图,这个过程称之为hit-test view。(UIEvent->Application->UIApplication->UIWindow-hitTest:..)
UIWindow实例对象会首先在它的内容视图上调用hitTest:withEvent:,此方法会在其视图层级结构中的每个视图上调用pointInside:withEvent:(该方法用来判断点击事件发生的位置是否处于当前视图范围内,以确定用户是不是点击了当前视图),如果pointInside:withEvent:返回YES,则继续逐级调用,直到找到touch操作发生的位置,若返回NO, 则hitTest:withEvent:返回nil。
至此,本次点击事件的第一响应者就通过响应者链的事件分发逻辑成功的找到了。
(树中的叶节点比父节点永远更优先被分发事件,但是并不是说他就能在时间上先响应)(由此可以推理出我们响应者树的构造过程是在ViewDidLoad周期中来完成的,这个函数会将当前实例的构成的响应者子树合并到我们整个根树中)
appDelegate虽然优先级别不如ViewController,但是他响应的时间上面一般比ViewController早。
如果我们不super,那么我们在这里写响应代码。事件到这里就不继续分发了。
应用时机:
上图中:sender is KVO compliant。这不仅意味着当值发生改变时,发送者会发送KVO通知,并且观察者还需要知道发送者的生命周期(两要求)。如果发送者被存储在一个weak属性中,那么发送者有可能被nil掉,进而引起观察者发生leak。
另外底部的一个盒子说到:message is direct response to method call(消息直接在方法的调用代码中响应)。也就是说 处理消息的代码 跟 方法的调用代码 处于相同的地方。
最后,在左下角,处于一个决策问题的判断状态:sender can guarantee to nil out reference to block?(发送者能够确保置nil到block的引用吗?),这实际上涉及到之前我们讨论到基于block 的APIs已经潜在的引用循环。使用block时,如果发送者不能保证在某个时机能够把对block的引用置nil,那么将会遇到引用循环的问题。
iOS Framework中的应用
KVO
NSOperationQueue
队列中operation状态属性的改变 ---> 队列会收到一个KVO通知。
消息的接收者(operation queue)明确的知道发送者(opertation),以及通过retain来控制operation的生命周期。
Notifications
Core Data使用notification来传递事件(例如一个managed object context内部的改变——NSManagedObjectContextDidChangeNotification)。
Delegation
Table View的delegate有多种功能。
Blocks
[NSURLSession dataTaskWithURL:completionHandler:]
NSURLConnection在block问世之前就已经存在了,所以它并没有利用block进行消息传递,而是使用delegation机制。
替代NSURLConnection 的 NSURLSession 运用的block。
Target-Action
Target-Action用的最明显的一个地方就是button。button除了需要发送一个click事件以外,并不需要再发送别的信息了。所以Target-Action在用户界面事件传递过程中,是最佳的选择。
虽然notification最接近这种在发送者和接收者解耦关系,但是target-action可以用于响应链(responder chain)——只有一个对象获得action并作出响应,并且action可以在响应链中传递,直到遇到能够响应该action的对象。
Extension
KVO 与 KVC
Key-value coding (KVC) 和 key-value observing (KVO) 是两种能让我们驾驭 Objective-C 动态特性并简化代码的机制。
(1) KVO:
KeyValueObserver辅助类封装了-addObserver:forKeyPath:options:context:,-observeValueForKeyPath:ofObject:change:context:和-removeObserverForKeyPath:的调用,让视图控制器远离杂乱的代码。
实践:
我们实现一个类把它自己(self)注册为观察者的话,用
然后传递地址过去。
这样避免子类和父类能安全的观察同样的键值而不冲突。
关于options选项:
NSKeyValueObservingOptionInitial -- 第一次触发 && 正常触发
NSKeyValueObservingOptionPrior -- 两个通知:一个在值变更前,另一个在变更之后。
变更前后的通知将会在change字典中有不同的键。
获取方式:
NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld -- 需要改变前后的值
获取方式:
KVO缺陷:API太固定,不能传递selector或者block。
(2) KVC:
可以让一个类动态的支持一些键(属性)的访问
键路径(Key Path)
一个常常被忽视的 KVC 特性是它对集合操作的支持。
通过集合代理对象来实现 KVC -- 高端技巧 -- KVC 和 KVO
PS:
(1) 若一个类有实例变量NSString *_foo,调用setValue:forKey时,可以以foo还是_foo作为key?
foo和_foo都可以作为key。这题涉及到KVC的实现原理哦
KVC如何访问属性值
KVC在某种程度上提供了存取方法(getter & setter)的替代方案。不过存取方法是一个很好的东西,以至于只要是有可能,KVC也尽量在存取方法的帮助下工作。为了设置或者返回对象属性,KVC按顺序使用如下技术:
<1> 检查是否存在-<key>、-is<Key>(只针对布尔值有效)或者-get<Key>的存取方法,如果有可能,就是用这些方法返回值;
检查是否存在名为-set<Key>:的方法,并使用它做设置值。对于-get<Key>和-set<Key>:方法,将大写Key字符串的第一个字母,并与Cocoa的方法命名保持一致;
<2> 如果上述方法不可用,则检查名为-_<key>、-_is<Key>(只针对布尔值有效)、-_get<Key>和-_set<Key>:方法;
<3> 如果没有找到存取方法,可以尝试直接访问实例变量。实例变量可以是名为:
<key>或_<key>;
<4> 如果仍为找到,则调用valueForUndefinedKey:和setValue:forUndefinedKey:方法。这些方法的默认实现都是抛出异常,我们可以根据需要重写它们。
(2) KVO自动&手动触发原理
自动触发:当对象中被观察的属性值改变时,会自动触发KVO,通知观察对象。
手动触发:KVO依赖NSObject两个方法,willChangeValueForKey: 和 didChangeValueForKey:。在被观察属性发生改变后,willChangeValueForKey:一定会被调用,记录旧值。当改变发生后,didChangeValueForKey:会被调用,之后 observeValueForKey:ofObject:change:context:会被调用。手动实现这两个方法,即手动调用。
(3)Apple用什么方式实现对一个对象的KVO?(KVO实现原理)
Apple使用 isa-swizzling来实现KVO。
当观察一个对象时,一个新的类会被动态的创建,它继承自该对象所属的类,并重写了被观察属性的setter方法(在开头和末尾分别加上 willChangeValueForKey: 和 didChangeValueForKey:)。重写的setter方法通过这两个方法在前后通知所观察对象,值的改变。最后通过isa-swizzling把这个对象的isa指针指向新建的这个类,对象就变成这个类的实例了。
(4) KVC的 keyPath 中的集合运算符如何使用?
<1> 必须是在集合对象上或普通对象的集合属性上
<2> 简单集合运算 @avg,@count,@max,@min,@sum
<3> 格式 @"@sum.age" 、 @" 集合属性.@max.age"
引文:
iOS中消息的传递机制 --- 原文特别好
Cocoa Touch事件处理流程--响应者链 -- "事件分发" 讲的很好,hit-test view的流程
点击事件的第一响应者就通过响应者链的事件分发找到
KVC 和 KVO -- 介绍的很深入吶。。Contact Editor很不错!
KVC/KVO原理详解及编程指南 -- 有些地方比objc介绍的还详细!