iOS 底层探索系列html
- iOS 底层探索 - alloc & init
- iOS 底层探索 - calloc 和 isa
- iOS 底层探索 - 类
- iOS 底层探索 - cache_t
- iOS 底层探索 - 方法
- iOS 底层探索 - 消息查找
- iOS 底层探索 - 消息转发
- iOS 底层探索 - 应用加载
- iOS 底层探索 - 类的加载
- iOS 底层探索 - 分类的加载
- iOS 底层探索 - 类拓展和关联对象
- iOS 底层探索 - KVC
- iOS 底层探索 - KVO
iOS 查漏补缺系列git
在
Objective-C
和Cocoa
中,有许多事件之间进行通讯的方式,而且每一个都有不一样程度的形式和耦合:NSNotification
&NSNotificationCenter
提供了一个中央枢纽,一个应用的任何部分均可能通知或者被通知应用的其余部分的变化。惟一须要作的是要知道在寻找什么,主要是通知的名字。例如,UIApplicationDidReceiveMemoryWarningNotification
是给应用发了一个内存不足的信号。Key-Value Observing
键值观察经过侦听特定键路径上的更改,能够在特定对象实例之间进行特殊的事件自省。例如:一个ProgressView
能够观察 网络请求的numberOfBytesRead
来更新它本身的progress
属性。Delegate
是一个流行的传递事件的设计模式,经过定义一系列的方法来传递给指定的处理对象。例如:UIScrollView
每次它的scroll offset
改变的时候都会发送scrollViewDidScroll:
到它的代理Callbacks
无论是像NSOperation
里的completionBlock
(当isFinished==YES
的时候会触发),仍是C
里边的函数指针,传递一个函数钩子好比SCNetworkReachabilitySetCallback(3)
。github
根据苹果官方文档的定义,KVO
(Key Value Observing) 键值观察是创建在 KVC
基础之上的,因此若是对 KVC
不是很了解的读者能够查看上一篇 KVC
底层探索的文章。编程
我相信大多数开发者应该对于 KVO
都能熟练掌握,不过咱们仍是回顾一下官网对于 KVO
的解释吧。设计模式
KVO
?KVO
提供了一种当其余对象的属性发生变化就会通知观察者对象的机制。根据官网的定义,属性的分类能够分为下列三种:api
NSNumber
和其它一些不可变类型好比 NSColor
也能够被认为是简单属性Account
对象可能具备一个 owner
属性,该属性是 Person
对象的实例,而 Person
对象自己具备 address
属性。owner
的地址能够更改,但却而无需更改 Account
持有的 owner
属性。也就是说 Account
的 owner
属性未被更改,只是 address
被更改了。NSArray
或 NSSet
的实例来持有此集合。而 KVO
对于这三种属性都能适用。下面举一个例子:数组
如上所示,Person
对象有一个 Account
属性,而 Account
对象又有 balance
和 interestRate
两个属性。而且这两个属性对于 Person
对象来讲都是可读写的。若是想实现一个功能:当余额或利率变化的时候须要通知到用户。通常来讲可使用轮询的方式,Person
对象按期从 Account
属性中取出 balance
和 interestRate
。但这种方式是效率低下且不切实际的,更好的方式是使用 KVO
,相似于余额或利率变更时, Person
对象收到了通知同样。安全
要实现 KVO
的前提是要确保被观察对象是符合 KVO
机制的。通常来讲,继承于 NSObject
根类的对象及其属性都自动符合 KVO
这一机制。固然也能够本身去实现 KVO
符合。也就是说实际上 KVO
机制分为自动符合和手动符合。bash
一旦肯定了对象和属性是 KVO
符合的话,就须要历经三个步骤:markdown
Person
对象须要将本身注册到 Account
的某一个具体属性上。这个过程是经过 addObserver:forKeyPath:options:context:
实现的,这个方法须要指定监听者(observer
)、监听谁(keypath
)、监听策略(options
)、监听上下文(context
)
Person
对象要接收 Account
被监听属性改动后发出的通知,须要自身实现 observeValueForKeyPath:ofObject:change:context:
方法来接收通知。
在观察者不须要再监听或自身生命周期结束的时候,须要取消注册。具体实现是经过向被观察对象发出 removeObserver:forKeyPath:
消息。
KVO
机制的最大好处你不须要本身去实现一个机制来获取对象属性什么时候改变以及改变后的结果。
KVO
三大流程解析- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context 复制代码
observer:注册
KVO
通知的对象。观察者必须实现key-value observing
方法observeValueForKeyPath:ofObject:change:context:
。 keyPath:被观察者的属性的keypath
,相对于接受者,值不能是nil
。 options:NSKeyValueObservingOptions
的组合,它指定了观察通知中包含了什么 context:在observeValueForKeyPath:ofObject:change:context:
传给observer
参数的上下文
前两个参数很好理解,而 options
和 context
参数则须要额外注意。
options
表明 NSKeyValueObservingOptions
的位掩码,须要注意 NSKeyValueObservingOptionNew & NSKeyValueObservingOptionOld
,由于这些是你常常要用到的,能够跳过 NSKeyValueObservingOptionInitial & NSKeyValueObservingOptionPrior:
。
NSKeyValueObservingOptionNew
: 代表通知中的更改字典应该提供新的属性值,如何能够的话。NSKeyValueObservingOptionOld
: 代表通知中的更改字典应该包含旧的属性值,如何能够的话。NSKeyValueObservingOptionInitial
: 这个枚举值比较特殊,若是指定了这个枚举值, 在属性发生变化后当即通知观察者,这个过程甚至早于观察者注册。若是在注册的时候配置了NSKeyValueObservingOptionNew
,那么在通知的更改字典中也会包含NSKeyValueChangeNewKey
,可是不会包括NSKeyValueChangeOldKey
。(在初始通知中,观察到的属性值多是旧的,可是对于观察者来讲是新的)其实简单来讲就是这个枚举值会在属性变化前先触发一次observeValueForKeyPath
回调。NSKeyValueObservingOptionPrior
: 这个枚举值会前后连续出发两次observeValueForKeyPath
回调。同时在回调中的可变字典中会有一个布尔值的key
-notificationIsPrior
来标识属性值是变化前仍是变化后的。若是是变化后的回调,那么可变字典中就只有new
的值了,若是同时制定了NSKeyValueObservingOptionNew
的话。若是你须要启动手动KVO
的话,你能够指定这个枚举值而后经过willChange
实例方法来观察属性值。在出发observeValueForKeyPath
回调后再去调用willChange
可能就太晚了。
这些选项容许一个对象在发生变化的先后获取值。在实践中,这不是必须的,由于从当前属性值获取的新值通常是可用的 也就是说
NSKeyValueObservingOptionInitial
对于在反馈KVO
事件的时候减小代码路径是颇有好处的。好比,若是你有一个方法,它可以动态的使一个基于text
值的按钮有效,传NSKeyValueObservingOptionInitial
可使事件随着它的初始化状态触发一旦观察者被添加进去的话。
如何设置一个好的
context
值呢?这里有个建议:
static void * XXContext = &XXContext; 复制代码
就是这么简单:一个静态变量存着它本身的指针。这意味着它本身什么也没有,使 <NSKeyValueObserving>
更完美。
咱们简单测试一下在注册观察者时指定不一样的枚举值会有怎么样的结果:
NSKeyValueObservingOptionNew
NSKeyValueObservingOptionOld
NSKeyValueObservingOptionInitial
能够看到,只指定了 NSKeyValueObservingOptionInitial
后触发了两个回调,而且一次是在属性值变化前,一次是在属性值变化后。同时而且没有新值和旧值返回,咱们加一个 NSKeyValueObservingOptionNew
和 NSKeyValueObservingOptionOld
:
在咱们加上新值和旧值的枚举以后,新值在两次回调后被返回,可是第一次的新值实际上是最开始的属性值,第二次才是改变以后的属性值,而旧值在第二次真正属性值被改变后返回。
NSKeyValueObservingOptionPrior
能够看到,NSKeyValueObservingOptionPrior
枚举值是在属性值发生变化后触发了两次回调,同时也没有新值和旧值的返回,咱们加一个 NSKeyValueObservingOptionNew
和 NSKeyValueObservingOptionOld
:
能够看到,在第一次回调里没有新值,第二次才有,而旧值在两次回调里面都有。
咱们在注册观察者的时候,要求传入的 keyPath
是字符串类型,若是咱们拼写错误的话,编译器是不能帮咱们检查出来的,全部最佳实践应该是使用 NSStringFromSelector(SEL aSelector)
,好比咱们要观察 tableView
的 contentSize
属性,咱们能够这样使用:
NSStringFromSelector(@selector(contentSize)) 复制代码
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context 复制代码
这个方法就是观察者接收通知的地方,除了 change
参数以外,其余三个参数都与观察者注册的时候传入的三个参数一一对应。
keypath
默认状况下,咱们在 addObserver:forKeyPath:options:context:
方法的最后一个参数传入的是 NULL
,由于这个方法签名中最后一个参数 context
是 void *
,因此须要传入一个空指针,而根据下图咱们可知,nil
只是一个对象的字面零值,这里须要的是一个指针,因此须要传 NULL
。
可是若是是不一样的对象都监听同一属性,咱们就须要给 context
传入一个能够区分不一样对象的字符串指针:
static void *StudentNameContext = &StudentNameContext; static void *PersonNameContext = &PersonNameContext; [self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:PersonNameContext]; [self.student addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:StudentNameContext]; - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{ if (context == PersonNameContext) { } else if (context == StudentNameContext) { } } 复制代码
superclass
的 observe
事务对于
Objective-C
,不少时候Runtime
系统都会自动帮助处理superclass
的方法。譬如对于dealloc
,假设类Father
继承自NSObject
,而类Son
继承自Father
,建立一个Son
的实例aSon
,在aSon
被释放的时候,Runtime
会先调用Son#dealloc
,以后会自动调用Father#dealloc
,而无需在Son#dealloc
中显式执行[super dealloc]
;。但KVO
不会这样,因此为了保证父类(父类可能也会本身observe
事务要处理)的observe
事务也能被处理。- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if (object == _tableView && [keyPath >isEqualToString:@"contentSize"]) { [self configureView]; } else { [super observeValueForKeyPath:keyPath ofObject:object >change:change context:context]; } } 复制代码
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(void *)context; - (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath; 复制代码
取消注册有两个方法,不过建议仍是跟注册和通知两个流程统一,选用带有 context
参数的方法。
一旦对某个对象上的属性注册了键值观察,能够选择在收到属性值变化后取消注册,也能够在观察者声明周期结束以前(好比:dealloc
方法) 取消注册,若是忘记调用取消注册方法,那么一旦观察者被销毁后,KVO
机制会给一个不存在的对象发送变化回调消息致使野指针错误。
取消注册也不能对同一个观察者重复屡次,为了不 crash
,能够把取消注册的代码包裹在 try&catch
代码块中:
static void * ContentSizeContext = &ContentSizeContext; - (void)viewDidLoad { [super viewDidLoad]; // 1. subscribe [_tableView addObserver:self forKeyPath:NSStringFromSelector(@selector(contentSize)) options:NSKeyValueObservingOptionNew context:ContentSizeContext]; } // 2. responding - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if (context == ContentSizeContext) { // configure view } else { [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; } } - (void)dealloc { @try { // 3. unsubscribe [_tableView removeObserver:self forKeyPath:NSStringFromSelector(@selector(contentSize)) context:ContentSizeContext]; } @catch (NSException *exception) { } } 复制代码
默认状况下,咱们只须要按照前面说的 三步曲
的方式来实现对属性的键值观察,不过这属因而 「自动挡」,什么意思呢?就是说属性值变化彻底是由系统控制,咱们只须要告诉系统监听什么属性,而后就直接等系统告诉咱们就完事了。而实际上,KVO
还支持「手动挡」。
要让系统知道咱们想开启手动挡,须要修改类方法 automaticallyNotifiesObserversForKey:
的返回值,这个方法若是返回 YES
就是自动挡,返回 NO
就是手动挡。同时该类方法还能精准实策,让咱们选择对哪些属性是自动,哪些属性是手动。
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey { BOOL automatic = NO; if ([theKey isEqualToString:@"balance"]) { automatic = NO; } else { automatic = [super automaticallyNotifiesObserversForKey:theKey]; } return automatic; } 复制代码
一样的,如上代码所示,咱们使用 automaticallyNotifiesObserversForKey
的最佳实践仍然须要把咱们须要手动或自动的代码排除后去调用下父类的方法来确保不会有问题出现。
KVO
触发方式// Call the accessor method. [account setName:@"Savings"]; // Use setValue:forKey:. [account setValue:@"Savings" forKey:@"name"]; // Use a key path, where 'account' is a kvc-compliant property of 'document'. [document setValue:@"Savings" forKeyPath:@"account.name"]; // Use mutableArrayValueForKey: to retrieve a relationship proxy object. Transaction *newTransaction = <#Create a new transaction for the account#>; NSMutableArray *transactions = [account mutableArrayValueForKey:@"transactions"]; [transactions addObject:newTransaction]; 复制代码
如上代码所示是自动 KVO
的触发方式
KVO
触发方式其实手动 KVO
能够帮助咱们将多个属性值的更改合并成一个,这样在回调的时候就有一次了,同时也能最大程度地减小处于应用程序特定缘由而致使的通知发生。
- (void)setBalance:(double)theBalance { [self willChangeValueForKey:@"balance"]; _balance = theBalance; [self didChangeValueForKey:@"balance"]; } 复制代码
如上代码所示,最朴素的手动 KVO
使用方法就是在属性值改变前对观察者发送 willChangeValueForKey
实例方法,在属性值改变以后对观察者发送 didChangeValueForKey
实例方法,参数都是所观察的键。 固然,上面这种方式不是最佳的,为了性能最佳,能够在属性的 setter
中判断是否要执行 will
+ did
:
- (void)setBalance:(double)theBalance { if (theBalance != _balance) { [self willChangeValueForKey:@"balance"]; _balance = theBalance; [self didChangeValueForKey:@"balance"]; } } 复制代码
可是,若是对一个属性的改变会影响到多个键的话,则须要以下的操做:
- (void)setBalance:(double)theBalance { [self willChangeValueForKey:@"balance"]; [self willChangeValueForKey:@"itemChanged"]; _balance = theBalance; _itemChanged = _itemChanged+1; [self didChangeValueForKey:@"itemChanged"]; [self didChangeValueForKey:@"balance"]; } 复制代码
对于有序的一对多关系属性,不只必须指定已更改的键,还必须指定更改的类型和所涉及对象的索引。 更改的类型是 NSKeyValueChange
,它指定 NSKeyValueChangeInsertion
,NSKeyValueChangeRemoval
或 NSKeyValueChangeReplacement
,受影响的对象的索引做为 NSIndexSet
对象传递:
- (void)removeTransactionsAtIndexes:(NSIndexSet *)indexes {
[self willChange:NSKeyValueChangeRemoval
valuesAtIndexes:indexes forKey:@"transactions"];
// Remove the transaction objects at the specified indexes.
[self didChange:NSKeyValueChangeRemoval
valuesAtIndexes:indexes forKey:@"transactions"];
}
复制代码
KVO
所谓从属关系,指的是一个对象的某个属性的值取决于另外一个对象的一个或多个属性。对于不一样类型的属性,有不一样的方式来实现。
要触发 一对一 类型属性的自动 KVO
,有两种方式。一种是重写 keyPathsForValuesAffectingValueForKey
方法,一种是实现一个合适的方法。
- (NSString *)fullName { return [NSString stringWithFormat:@"%@ %@",firstName, lastName]; } 复制代码
好比上面的代码,fullName
由 firstName
和 lastName
组成,因此重写 fullName
属性的 getter
方法。这样,不管是 firstName
仍是 lastName
发生了改变,监听 fullName
属性的观察者都会收到通知。
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key { NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key]; if ([key isEqualToString:@"fullName"]) { NSArray *affectingKeys = @[@"lastName", @"firstName"]; keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys]; } return keyPaths; } 复制代码
如上代码所示,经过实现类方法 keyPathsForValuesAffectingValueForKey
来返回一个集合。值得注意的是,这里须要先对父类发送 keyPathsForValuesAffectingValueForKey
消息,以避免干扰父类中对此方法的重写。
实际上还有一个便利的方法,就是 keyPathsForValuesAffecting<Key>
,Key
是属性的名称(须要首字母大写)。这个方法的效果和 keyPathsForValuesAffectingValueForKey
是同样的,但针对的某个具体属性。
+ (NSSet *)keyPathsForValuesAffectingFullName { return [NSSet setWithObjects:@"lastName", @"firstName", nil]; } 复制代码
相对来讲,在分类中去使用 keyPathsForValuesAffectingFullName
更合理,由于分类中是不容许重载方法的,因此 keyPathsForValuesAffectingValueForKey
方法确定是不能在分类中使用的。
keyPathsForValuesAffectingValueForKey
:方法不支持包含一对多关系的 Key Path
。例如,假设你有一个 Department
对象,该对象与 Employee
有一对多关系(即 employees
属性),而 Employee
具备 salary
属性。 若是须要在 Department
对象上增长totalSalary
属性,而该属性取决于关系中全部 Employees
的薪水。例如,您不能使用 keyPathsForValuesAffectingTotalSalary
和返回 employees.salary
做为键来执行此操做。
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if (context == totalSalaryContext) { [self updateTotalSalary]; } else // deal with other observations and/or invoke super... } - (void)updateTotalSalary { [self setTotalSalary:[self valueForKeyPath:@"employees.@sum.salary"]]; } - (void)setTotalSalary:(NSNumber *)newTotalSalary { if (totalSalary != newTotalSalary) { [self willChangeValueForKey:@"totalSalary"]; _totalSalary = newTotalSalary; [self didChangeValueForKey:@"totalSalary"]; } } - (NSNumber *)totalSalary { return _totalSalary; } 复制代码
如上代码所示,将 Department
实例对象注册为观察者,而后观察对象为 totalSalary
属性,可是在通知回调中会手动调用 totalSalary
属性的 setter
方法,而且传入值是经过 KVC
的集合运算符的方式取出 employees
属性所对应的集合中全部 sum
值之和。而后在 totalSalary
属性的 setter
方法中,会相应的调用 willChangeValueForKey:
和 didChangeValueForKey:
方法。
若是使用的是 Core Data
,你还能够把 Department
注册到 NSNotificationCenter
中来做为托管对象上下文的观察者。Department
应以相似于观察键值的方式响应 Employee
发布的相关变动通知。
Automatic key-value observing is implemented using a technique called isa-swizzling. 【译】自动的键值观察的实现基于
isa-swizzling
。The isa pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data. 【译】
isa
指针,顾名思义,指向的是对象所属的类,这个类维护了一个哈希表。这个哈希表基本上存储的是方法的SEL
和IMP
的键值对。When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.
【译】当一个观察者注册了对一个对象的某个属性键值观察以后,被观察对象的
isa
指针所指向的内容发生了变化,指向了一个中间类而不是真正的类。这也致使isa
指针并不必定是指向实例所属的真正的类。
You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.
【译】你永远不该依靠
isa
指针来肯定类成员身份。相反,你应该使用class
方法来肯定对象实例所属的类。
根据官网文档的内容,咱们初步判断,在 KVO
底层实现中,会有一个所谓的中间类生成。而这个中间类会让对象的 isa
指针发生变化。咱们不妨测试一下:
如上图所示,person
对象和 personForTest
对象都是属于 JHPerson
类的,而 person
对象又实现了 KVO
,可是在控制台打印结果里面能够看到它们两者的类都是 JHPerson
类。不是说会有一个中间类生成吗?难道是这个中间类生成又被干掉了?咱们直接LLDB
大法测试一下:
Bingo~,所谓的中间类 NSKVONotifying_JHPerson
被咱们找出来了。那么其实这里显然,系统是重写了中间类 NSKVONotifying_JHPerson
的 class
方法,让咱们觉得对象的 isa
指针一直指向的都是 JHPerson
类。那么这个中间类和原来的类是什么关系呢?咱们能够测试一下:
其中 printClasses
实现以下:
- (void)printClasses:(Class)cls{ int count = objc_getClassList(NULL, 0); NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls]; Class* classes = (Class*)malloc(sizeof(Class)*count); objc_getClassList(classes, count); for (int i = 0; i<count; i++) { if (cls == class_getSuperclass(classes[i])) { [mArray addObject:classes[i]]; } } free(classes); NSLog(@"classes = %@", mArray); } 复制代码
最终打印结果以下:
classes = ( JHPerson ) classes = ( JHPerson, "NSKVONotifying_JHPerson" ) 复制代码
结果很清晰,中间类 NSKVONotifying_JHPerson
是做为原始真正的类 JHPerson
的子类的角色。
KVO
观察的是什么?KVO
所关注的是属性值的变化,而属性值本质上是成员变量+getter
+setter
,getter
是用来获取值的,而显然只有 setter
和成员变量赋值两种方式能够改变属性值。咱们测试一下这两种方式:
// JHPerson.h @interface JHPerson : NSObject { @public NSString *_nickName; } @property (nonatomic, copy) NSString *name; @end 复制代码
如上图所示,setter
方法对属性 name
作了修改被 KVO
监听到了,而成员变量 _nickName
的修改并无被监听到,说明 KVO
底层其实观察的是 setter
方法。
咱们能够经过打印原始类和中间类的方法列表来验证:
printClassAllMethod
方法实现以下:
- (void)printClassAllMethod:(Class)cls{ NSLog(@"*********************"); unsigned int count = 0; Method *methodList = class_copyMethodList(cls, &count); for (int i = 0; i<count; i++) { Method method = methodList[i]; SEL sel = method_getName(method); IMP imp = class_getMethodImplementation(cls, sel); NSLog(@"%@-%p",NSStringFromSelector(sel),imp); } free(methodList); } 复制代码
能够看到如上图所示,原始类和中间类都有 setter
方法。根据咱们前面所探索的消息发送以及转发流程,这里的中间类应该是重写了 setName:
、class
、 dealloc
和 _isKVOA
方法。
由咱们上一小节的测试结果可知,中间类重写的 class
方法结果仍然是返回的是原始类,显然系统这样作的目的就是隐藏中间类的存在,让调用者调用 class
方法结果先后一致。
KVO
中间类什么时候指回去?咱们推断 KVO
注册观察者到移除观察者这一个流程里面,被观察对象的 isa
指针才会指向中间类,咱们用代码测试一下:
由上图可知,观察者的 dealloc
方法中的移除观察者以后,对象的 isa
指针已经指回了原始的类。那么是否是此时中间类就被销毁了呢,咱们不妨打印一下此时原始类的全部子类信息:
结果代表中间类仍然存在,也就是说移除观察者并不会致使中间类销毁,显然这样对于屡次添加和移除观察者来讲性能上更好。
KVO
调用顺序而咱们前面说了,有一个中间类的存在,既然要生成中间类,确定是有意义的,咱们梳理一下整个 KVO
的流程,从注册观察者到观察者的回调通知,既然有回调通知,那么确定是在某个地方发出回调的,而因为中间类是不能编译的,因此咱们对中间类的父类也就是 JHPerson
类,咱们重写一下相应的 setter
方法,咱们不妨测试一下:
// JHPerson.m - (void)setName:(NSString *)name { _name = name; } - (void)willChangeValueForKey:(NSString *)key{ [super willChangeValueForKey:key]; NSLog(@"willChangeValueForKey"); } - (void)didChangeValueForKey:(NSString *)key{ NSLog(@"didChangeValueForKey - begin"); [super didChangeValueForKey:key]; NSLog(@"didChangeValueForKey - end"); } 复制代码
打印结果以下:
也就是说 KVO
的调用顺序是:
willChangeValueForKey:
setter
实现didChangeValueForKey:
也就是说 didChangeValueForKey:
内部必然是调用了 observer
的observeValueForKeyPath:ofObject:change:context:
方法。
咱们已经初步了解了 KVO
底层原理,接下来咱们尝试本身简单实现一下 KVO
。 咱们直接跳转到 addObserver:forKeyPath:options:context:
方法的声明处:
能够看到,跟 KVC
同样,KVO
在底层也是以分类的形式加载的,这个分类叫作 NSKeyValueObserverRegistration
。咱们不妨也以这种方式来自定义实现一下 KVO
。
// NSObject+JHKVO.h @interface NSObject (JHKVO) // 观察者注册 - (void)jh_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(JHKeyValueObservingOptions)options context:(nullable void *)context; // 回调通知观察者 - (void)jh_observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context; // 移除观察者 - (void)jh_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context; @end 复制代码
这里为了不与系统的方法冲突,因此添加了一个方法前缀。同时对于观察策略,为了简化实现,这里只声明了新值和旧值两种策略。
在开始以前,咱们回忆下自定义 KVC
的时候的第一个步骤就是判断 key
或者 keyPath
,那么 KVO
是否也须要进行这样的判断呢?通过笔者实际测试,若是观察对象的一个不存在的属性的话,并不会报错,也不会来到 KVO
回调方法,因而可知,判断 keyPath
是否存在并无必要。可是,咱们回想一下上一节 KVO
底层原理,KVO
关注的是属性的 setter
方法,那其实判断对象所属的类是否有这样的 setter
就至关于同时判断了 keyPath
是否存在。接着咱们就须要去动态的建立子类,建立子类的过程当中包括了重写 setter
等一系列方法。而后就须要保存观察者和 keyPath
等信息,这里咱们借助关联对象来实现,咱们把传入的观察者对象、keyPath
和观察策略封装成一个新的对象存储在关联对象中。由于同一个对象的属性能够被不一样的观察者所观察,因此这里实质上是以对象数组的方式存储在关联对象里面。 话很少说,直接上代码:
// JHKVOInfo.h typedef NS_OPTIONS(NSUInteger, JHKeyValueObservingOptions) { JHKeyValueObservingOptionNew = 0x01, JHKeyValueObservingOptionOld = 0x02, }; @interface JHKVOInfo : NSObject @property (nonatomic, weak) NSObject *observer; @property (nonatomic, copy) NSString *keyPath; @property (nonatomic, assign) JHKeyValueObservingOptions options; - (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(JHKeyValueObservingOptions)options; @end // JHKVOInfo.m @implementation JHKVOInfo - (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(JHKeyValueObservingOptions)options { if (self = [super init]) { _observer = observer; _keyPath = keyPath; _options = options; } return self; } @end 复制代码
上面的代码是自定义的 JHKVOInfo
对象。
static NSString *const kJHKVOPrefix = @"JHKVONotifying_"; static NSString *const kJHKVOAssiociateKey = @"kJHKVO_AssiociateKey"; - (void)jh_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(JHKeyValueObservingOptions)options context:(void *)context { // 1.判断 getter 是否存在 SEL setterSelector = NSSelectorFromString(setterForGetter(keyPath)); Method setterMethod = class_getInstanceMethod([self class], setterSelector); if (!setterMethod) { NSString *reason = [NSString stringWithFormat:@"对象 %@ 的 key %@ 没有 setter 实现", self, keyPath]; @throw [NSException exceptionWithName:NSInvalidArgumentException reason:reason userInfo:nil]; return; } // 2.动态建立中间子类 Class newClass = [self createChildClassWithKeyPath:keyPath]; // 3.将对象的isa指向为新的中间子类 object_setClass(self, newClass); // 4.保存观察者 JHKVOInfo *info = [[JHKVOInfo alloc] initWitObserver:observer forKeyPath:keyPath options:options]; NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kJHKVOAssiociateKey)); if (!observerArr) { observerArr = [NSMutableArray arrayWithCapacity:1]; [observerArr addObject:info]; objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kJHKVOAssiociateKey), observerArr, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } } 复制代码
上面的代码是完整的添加观察者的流程:
keyPath
对应的 setter
方法这里的
setterForGetter
实现以下:static NSString * setterForGetter(NSString *getter) { // 判断 getter 是否为空字符串 if (getter.length <= 0) { return nil; } // 取出 getter 字符串的第一个字母并转大写 NSString *firstLetter = [[getter substringToIndex:1] uppercaseString]; // 取出剩下的字符串内容 NSString *remainingLetters = [getter substringFromIndex:1]; // 将首字母大写的字母与剩下的字母拼接起来获得 `set<KeyPath>` 格式的字符串 NSString *setter = [NSString stringWithFormat:@"set%@%@:", firstLetter, remainingLetters]; return setter; } 复制代码
setter
方法,那么就建立有对应前缀的中间子类这里的
createChildClassWithKeyPath
实现以下:- (Class)createChildClassWithKeyPath:(NSString *)keyPath{ // 得到原始类的类名 NSString *oldClassName = NSStringFromClass([self class]); // 在原始类名前添加中间子类的前缀来得到中间子类名 NSString *newClassName = [NSString stringWithFormat:@"%@%@",kJHKVOPrefix,oldClassName]; // 经过中间子类名来判断是否建立过 Class newClass = NSClassFromString(newClassName); // 若是建立过中间子类,直接返回 if (newClass) return newClass; // 若是没有建立过,则须要建立一下, objc_allocateClassPair 方法的三个参数分别为: 1.父类 2.新类的名字 3.建立新类所需额外的空间 newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0); // 注册中间子类 objc_registerClassPair(newClass); // 从父类上拿到 `class` 方法的 `SEL` 以及类型编码,而后在中间子类上添加一个新的子类实现 `jh_class` SEL classSEL = NSSelectorFromString(@"class"); Method classMethod = class_getInstanceMethod([self class], classSEL); const char *classTypes = method_getTypeEncoding(classMethod); class_addMethod(newClass, classSEL, (IMP)jh_class, classTypes); // 从父类上拿到 `getter` 方法的 `SEL` 以及类型编码,而后在中间子类上添加一个新的子类实现 `jh_setter` SEL setterSEL = NSSelectorFromString(setterForGetter(keyPath)); Method setterMethod = class_getInstanceMethod([self class], setterSEL); const char *setterTypes = method_getTypeEncoding(setterMethod); class_addMethod(newClass, setterSEL, (IMP)jh_setter, setterTypes); return newClass; } 复制代码
jh_class
的实现以下:Class jh_class(id self,SEL _cmd) { // 经过 class_getSuperclass 来返回父类的 `Class`,达到对调用者隐藏中间子类的效果 return class_getSuperclass(object_getClass(self)); } 复制代码
jh_setter
的实现以下:static void jh_setter(id self,SEL _cmd,id newValue){ // 由于 `_cmd` 做为方法的第二个参数其实就是 `setter` 的 `SEL`,这里反向得到对应 `getter` 字符串形式做为 `keyPath`,而后经过 `KVC` 来获取到旧的属性值 NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd)); id oldValue = [self valueForKey:keyPath]; // 由于是重写父类的 `setter`,因此还须要经过消息发送的方式手动执行如下父类的 `setter` 方法 // 经过强转的方式将 `objc_msgSendSuper` 转成 `jh_msgSendSuper` 函数指针,同时,因为 `objc_msgSendSuper` 要比咱们常见的 `objc_msgSend` 多一个父类结构体参数,因此须要手动构建一下这个父类结构体,结构体有两个属性,分别是实例对象以及实例对象的类的父类 void (*jh_msgSendSuper)(void *, SEL, id) = (void *)objc_msgSendSuper; // void /* struct objc_super *super, SEL op, ... */ struct objc_super superStruct = { .receiver = self, .super_class = class_getSuperclass(object_getClass(self)), }; // 准备工做完成后手动调用 `jh_msgSendSuper`,由于 `superStruct` 是结构体类型,而 `jh_msgSendSuper` 的第一个参数是空指针对象,因此这里须要加取地址符来把结构体地址赋值给指针对象 jh_msgSendSuper(&superStruct, _cmd, newValue); // 调用完父类的 `setter` 以后,从关联对象中取出存储了自定义的对象数组 NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kJHKVOAssiociateKey)); // 循环遍历自定义的对象 for (JHKVOInfo *info in observerArr) { // 若是 `keyPath` 匹配则进入下一步 if ([info.keyPath isEqualToString:keyPath]) { // 基于线程安全的考虑,使用 `GCD` 的全局队列异步执行下面的操做 dispatch_async(dispatch_get_global_queue(0, 0), ^{ // 初始化一个通知字典 NSMutableDictionary<NSKeyValueChangeKey,id> *change = [NSMutableDictionary dictionaryWithCapacity:1]; // 判断存储的观察策略,若是是新值,则在通知字典中设置新值 if (info.options & JHKeyValueObservingOptionNew) { [change setObject:newValue forKey:NSKeyValueChangeNewKey]; } // 若是是旧值,在通知字典中设置旧值 if (info.options & JHKeyValueObservingOptionOld) { [change setObject:@"" forKey:NSKeyValueChangeOldKey]; if (oldValue) { [change setObject:oldValue forKey:NSKeyValueChangeOldKey]; } } // 取得通知观察者方法的 `SEL` SEL observerSEL = @selector(jh_observeValueForKeyPath:ofObject:change:context:); // 经过 `objc_msgSend` 手动发送消息,达到观察者收到回调的效果 ((void(*)(id, SEL, id, id, NSMutableDictionary *, void *))objc_msgSend)(info.observer, observerSEL, keyPath, self, change, NULL); }); } } } 复制代码
getterForSetter
实现以下:static NSString *getterForSetter(NSString *setter){ // 判断传入的 `setter` 字符串长度是否大于 0,以及是否有 `set` 的前缀和 `:` 的后缀 if (setter.length <= 0 || ![setter hasPrefix:@"set"] || ![setter hasSuffix:@":"]) { return nil;} // 排除掉 `setter` 字符串中的 `set:` 部分以取得 getter 字符串 NSRange range = NSMakeRange(3, setter.length-4); NSString *getter = [setter substringWithRange:range]; // 对 getter 字符串首字母小写处理 NSString *firstString = [[getter substringToIndex:1] lowercaseString]; return [getter stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:firstString]; } 复制代码
咱们接着开始自定义移除观察者,首先,咱们须要把 isa
指回原来的类,而后须要对关联对象中存储的自定义对象数组对应的观察者移除掉。
- (void)jh_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(void *)context { // 从关联对象中取出数组 NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kJHKVOAssiociateKey)); // 若是数组中没有内容,说明没有添加过观察者,那么直接返回 if (observerArr.count<=0) { return; } // 遍历取出的全部自定义对象 for (JHKVOInfo *info in observerArr) { // 若是 `keyPath` 匹配上了 则从数组中移除响应对象,而后存储最新的数组到关联对象上 if ([info.keyPath isEqualToString:keyPath]) { [observerArr removeObject:info]; objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kJHKVOAssiociateKey), observerArr, OBJC_ASSOCIATION_RETAIN_NONATOMIC); break; } } // 要将 `isa` 指回原来的类的前提条件是,被观察属性的对象已经没有任何观察者在观察了,那么就须要指回去 if (observerArr.count<=0) { Class superClass = [self class]; object_setClass(self, superClass); } } 复制代码
如今咱们自定义的 KVO
已经能够实现简单的通知观察者新值和旧值的变化了,但其实对于 api
的使用者来讲,仍是要严格的执行 addObserver
和 removeObserver
的配套操做,不免有些繁琐。虽然通常来讲为了方便起见,都是在观察者的 dealloc
方法中去手动调用 removeObserver
方法,但仍是太麻烦了。所以,咱们能够借助 methodSwizzling
的技术来替换默认 dealloc
方法的实现,直接上代码:
+ (BOOL)jh_hookOrigInstanceMenthod:(SEL)oriSEL newInstanceMenthod:(SEL)swizzledSEL { // 获取 Class 对象 Class cls = self; // 经过 `SEL` 获取原始方法 Method oriMethod = class_getInstanceMethod(cls, oriSEL); // 经过 `SEL` 获取要替换的方法 Method swiMethod = class_getInstanceMethod(cls, swizzledSEL); // 若是要替换的方法不存在,返回 NO if (!swiMethod) { return NO; } // 若是原始方法不存在,那么就直接在 Class 上添加要替换的方法,注意,添加的方法实现为要替换的方法,可是方法 `SEL` 仍是原始方法的 `SEL` if (!oriMethod) { class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod)); method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){ })); } // 判断是否添加成功 BOOL didAddMethod = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod)); if (didAddMethod) { // 若是成功,说明 Class 上已经存在了要替换的方法的实现,那么就把原始方法实现替换掉 `swizzledSEL` 对应的方法实现 class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod)); }else{ // 若是不成功,说明原始方法已经存在,则直接交换方法实现 method_exchangeImplementations(oriMethod, swiMethod); } return YES; } + (void)load{ static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [self jh_hookOrigInstanceMenthod:NSSelectorFromString(@"dealloc") newInstanceMenthod:@selector(myDealloc)]; }); } - (void)myDealloc{ Class superClass = [self class]; object_setClass(self, superClass); // 这里并不会形成循环引用的递归,由于 `myDealloc` 的方法实现是真正的原始 `dealloc` [self myDealloc]; } 复制代码
经过实现自动移除观察者,api
的使用者能够彻底放心的只使用 addObserver
来添加观察者以及 observeValueForKeyPath
来接收回调。
咱们虽然已经实现了自动的移除观察者,可是从函数式编程思想来看,如今的设计还不是很完美,对同一个属性的观察的代码散落在不一样的地方,若是业务一旦增多,对于可读性和可维护性都有很大的影响。因此,咱们能够把如今这种回调的形式重构为 Block
的方式。
// NSObject+JHBlockKVO.h typedef void(^JHKVOBlock)(id observer,NSString *keyPath,id oldValue,id newValue); @interface NSObject (JHBlockKVO) - (void)jh_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(JHKVOBlock)block; - (void)jh_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath; @end // NSObject+JHBlockKVO.m @interface JHBlockKVOInfo : NSObject @property (nonatomic, weak) NSObject *observer; @property (nonatomic, copy) NSString *keyPath; @property (nonatomic, copy) JHKVOBlock handleBlock; @end @implementation JHBlockKVOInfo - (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath handleBlock:(JHKVOBlock)block{ if (self=[super init]) { _observer = observer; _keyPath = keyPath; _handleBlock = block; } return self; } @end @implementation NSObject (JHBlockKVO) - (void)jh_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(JHKVOBlock)block{ // 1.判断 getter 是否存在 SEL setterSelector = NSSelectorFromString(setterForGetter(keyPath)); Method setterMethod = class_getInstanceMethod([self class], setterSelector); if (!setterMethod) { NSString *reason = [NSString stringWithFormat:@"对象 %@ 的 key %@ 没有 setter 实现", self, keyPath]; @throw [NSException exceptionWithName:NSInvalidArgumentException reason:reason userInfo:nil]; return; } // 2.动态建立中间子类 Class newClass = [self createChildClassWithKeyPath:keyPath]; // 3.将对象的isa指向为新的中间子类 object_setClass(self, newClass); // 4.保存观察者 JHBlockKVOInfo *info = [[JHBlockKVOInfo alloc] initWitObserver:observer forKeyPath:keyPath handleBlock:block]; NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kJHKVOAssiociateKey)); if (!mArray) { mArray = [NSMutableArray arrayWithCapacity:1]; objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kJHKVOAssiociateKey), mArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } [mArray addObject:info]; } 复制代码
这里咱们直接经过传入分类一个 block
,而后存储在对应的自定义观察对象中,而后咱们还须要在重写 setter
方法中作出修改,原来是直接经过发送消息来实现回调,如今须要改为 block
回调
static void jh_setter(id self,SEL _cmd,id newValue){ NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd)); id oldValue = [self valueForKey:keyPath]; void (*jh_msgSendSuper)(void *,SEL , id) = (void *)objc_msgSendSuper; struct objc_super superStruct = { .receiver = self, .super_class = class_getSuperclass(object_getClass(self)), }; jh_msgSendSuper(&superStruct,_cmd,newValue); NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kJHKVOAssiociateKey)); for (JHBlockKVOInfo *info in mArray) { if ([info.keyPath isEqualToString:keyPath] && info.handleBlock) { info.handleBlock(info.observer, keyPath, oldValue, newValue); } } } 复制代码
通过探索 KVC
和 KVO
的底层,咱们能够看到 KVO
是创建在 KVC
基础之上的。KVO
做为观察者设计模式在 iOS
中的具体落地,其原理到实现咱们都探索完了。其实咱们能够看出来在早期设计 api
的时候,原生的 KVO
其实并很差用,因此诸如 FaceBook
的库 KVOController 会大受欢迎。固然本文的自定义 KVO
实现并不严谨,感兴趣的读者能够查看这两个代码库:
KVC
和 KVO
反汇编而成 DIS_KVC_KVOGNUStep
的 libs-base
gnustep/libs-base咱们的 iOS
底层探索系列接下来将会进入多线程篇章,敬请期待~
Key-Value Observing Programming Guide - Apple 官方文档