什么是KVO??
KVO就是NSKeyValueObserving,請(qǐng)看官方文檔的解釋:

大概翻譯如下:
一種非正式協(xié)議,通知其他對(duì)象的指定屬性發(fā)生了改變。
簡(jiǎn)單理解就是,可以監(jiān)聽一個(gè)對(duì)象的某個(gè)屬性是否發(fā)生改變。
那么問題來了,什么是非正式協(xié)議??有正式協(xié)議嗎??
麻蛋,本來想找官方文檔的,找了半天沒找到。從Stackoverflow找到了答案,貌似原來官方文檔的鏈接失效了

大概翻譯如下:
非正式協(xié)議:非正式協(xié)議是NSObject的一個(gè)類別Category,幾乎所有的對(duì)象都隱含的采用(類別是OC的語(yǔ)言特性,能夠給類對(duì)象添加方法而不需要?jiǎng)?chuàng)建子類),非正式協(xié)議的方法是可選的
正式協(xié)議: 一個(gè)正式協(xié)議聲明了類需要實(shí)現(xiàn)的方法列表,正式協(xié)議有自己的聲明、采用和類型檢查語(yǔ)法。你可以使用@required或者optional關(guān)鍵字指定方法是否必須實(shí)現(xiàn)。子類繼承父類采用的協(xié)議。正式協(xié)議也可以遵守其他協(xié)議
KVO實(shí)現(xiàn)
- 監(jiān)聽某個(gè)對(duì)象的某個(gè)屬性
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
- 實(shí)現(xiàn)非正式協(xié)議
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
- 移除監(jiān)聽
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
簡(jiǎn)單代碼演示:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
self.person = [[ZJPerson alloc] init];
[self.person setName:@"zhangsan"];
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[self.person setName:@"lisi"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"%@", change);
}
- (void)dealloc{
[self.person removeObserver:self forKeyPath:@"name"];
}
打印結(jié)果:

用法其實(shí)很簡(jiǎn)單,接下來重點(diǎn)來了,KVO為什么能夠監(jiān)聽到屬性變化,底層做了什么??
KVO底層實(shí)現(xiàn)探究
首先,我們利用runtime在添加監(jiān)聽之前和之后分別打印一下類對(duì)象
NSLog(@"%@", object_getClass(self.person));
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
NSLog(@"%@", object_getClass(self.person));
打印結(jié)果:
2018-05-19 22:48:18.726028+0800 KVO[33804:3059947] ZJPerson
2018-05-19 22:48:18.726535+0800 KVO[33804:3059947] NSKVONotifying_ZJPerson
我們發(fā)現(xiàn)添加監(jiān)聽之后,實(shí)例對(duì)象的類對(duì)象發(fā)生了變化,系統(tǒng)為我們動(dòng)態(tài)添加了一個(gè)NSKVONotifying_+類名的類,因?yàn)槲覀兏淖儗?duì)象屬性的值是通過setter方法實(shí)現(xiàn)了,所以很明顯是系統(tǒng)動(dòng)態(tài)生成的NSKVONotifying_ZJPerson類重寫了setter方法。不信的話,我們可以做一個(gè)實(shí)驗(yàn),自己手動(dòng)添加一個(gè)NSKVONotifying_ZJPerson類,看下會(huì)打印什么
2018-05-19 22:56:32.223288+0800 KVO[33919:3068985] [general] KVO failed to allocate class pair for name NSKVONotifying_ZJPerson, automatic key-value observing will not work for this class
錯(cuò)誤提示很明顯,告訴我們創(chuàng)建NSKVONotifying_ZJPerson類失敗,KVO失效
那么系統(tǒng)自動(dòng)創(chuàng)建重寫的的setter方法內(nèi)部做了什么呢??同樣在添加監(jiān)聽方法之前,利用runtime打印下方法的實(shí)現(xiàn),截圖如下:

發(fā)現(xiàn)方法實(shí)現(xiàn)變了,內(nèi)部調(diào)用了系統(tǒng)
Foundation框架下的_NSSetObjectValueAndNotify方法。那么這個(gè)框架內(nèi)部又是怎么實(shí)現(xiàn)的呢,我們可以下斷點(diǎn),查看下函數(shù)調(diào)用棧:
首先通過設(shè)置一個(gè)觀察點(diǎn),觀察屬性的變化:

繼續(xù)執(zhí)行,可以看到函數(shù)調(diào)用棧如下:

在結(jié)果發(fā)生改變的地方繼續(xù)下斷點(diǎn)調(diào)試:


由以上函數(shù)調(diào)用棧,我們大致可以猜測(cè)出,_NSSetObjectValueAndNotify函數(shù)內(nèi)部實(shí)現(xiàn)過程如下:
1. `-[NSObject(NSKeyValueObservingPrivate) _changeValueForKey:key:key:usingBlock:]:
2. -[NSObject(NSKeyValueObservingPrivate) _changeValueForKeys:count:maybeOldValuesDict:usingBlock:]:
3. [ZJPerson setName:];
4. `NSKeyValueDidChange:
5. `NSKeyValueNotifyObserver:
6. - (void)observeValueForKeyPath:ofObject:change:context
簡(jiǎn)化成OC的偽代碼大致如下:
- (void)setName:(NSString *)name{
_NSSetObjectValueAndNotify();
}
void _NSSetObjectValueAndNotify {
[self willChangeValueForKey:@"name"];
[super setName:name];
[self didChangeValueForKey:@"name"];
}
- (void)didChangeValueForKey:(NSString *)key{
[observe observeValueForKeyPath:key ofObject:self change:nil context:nil];
}
NSKVONotifying_ZJPerson內(nèi)部都重寫了哪些方法
可以利用runtime方法打印一下方法列表:
unsigned int count;
Method *methods = class_copyMethodList(object_getClass(self.person), &count);
for (NSInteger index = 0; index < count; index++) {
Method method = methods[index];
NSString *methodStr = NSStringFromSelector(method_getName(method));
NSLog(@"%@\n", methodStr);
}
打印結(jié)果:
2018-05-20 08:57:07.883400+0800 KVO[35888:3218908] setName:
2018-05-20 08:57:07.883571+0800 KVO[35888:3218908] class
2018-05-20 08:57:07.883676+0800 KVO[35888:3218908] dealloc
2018-05-20 08:57:07.883793+0800 KVO[35888:3218908] _isKVOA
簡(jiǎn)單分析下重寫這些方法的作用:
class:重寫這個(gè)方法,是為了偽裝蘋果自動(dòng)為我們生成的中間類。
dealloc:應(yīng)該是處理對(duì)象銷毀之前的一些收尾工作
_isKVOA:告訴系統(tǒng)使用了kvo
拓展
學(xué)任何東西,通過我們的思考一定會(huì)問出一些別的問題,通過深入了解kvo,下面兩個(gè)問題,是面試經(jīng)常會(huì)被問到的,也是我所能想到的:
- 如何動(dòng)態(tài)生成一個(gè)類??
- 知道了原理,能不能自己寫一個(gè)KVO??
動(dòng)態(tài)生成一個(gè)自己的類
既然是動(dòng)態(tài)生成,肯定是利用了蘋果的runtime機(jī)制,通過上面對(duì)KVO的學(xué)習(xí),也了解到了runtime的強(qiáng)大之處。
- 創(chuàng)建類
Class customClass = objc_allocateClassPair([NSObject class], "ZJCustomClass", 0);
- 添加實(shí)例變量
// 添加實(shí)例變量
class_addIvar(customClass, "age", sizeof(int), 0, "i");
- 添加方法,
V@:表示方法的參數(shù)和返回值
// 添加方法
class_addMethod(customClass, @selector(hahahha), (IMP)hahahha, "V@:");
需要實(shí)現(xiàn)方法:
void hahahha(id self, SEL _cmd)
{
NSLog(@"hahahha====");
}
- (void)hahahha{
}
- 注冊(cè)到運(yùn)行時(shí)環(huán)境
objc_registerClassPair(customClass);
打印方法列表和成員變量列表,查看是否創(chuàng)建成功
#pragma mark - Util
- (NSString *)copyMethodsByClass:(Class)cls{
unsigned int count;
Method *methods = class_copyMethodList(cls, &count);
NSString *methodStrs = @"";
for (NSInteger index = 0; index < count; index++) {
Method method = methods[index];
NSString *methodStr = NSStringFromSelector(method_getName(method));
methodStrs = [NSString stringWithFormat:@"%@ ", methodStr];
}
free(methods);
return methodStrs;
}
- (NSString *)copyIvarsByClass:(Class)cls{
unsigned int count;
Ivar *ivars = class_copyIvarList(cls, &count);
NSMutableString *ivarStrs = [NSMutableString string];
for (NSInteger index = 0; index < count; index++) {
Ivar ivar = ivars[index];
NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)]; //獲取成員變量的名字
NSString *ivarType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)]; //獲取成員變量的數(shù)據(jù)類型
[ivarStrs appendString:@"\n"];
[ivarStrs appendString:ivarName];
[ivarStrs appendString:@"-"];
[ivarStrs appendString:ivarType];
}
free(ivars);
return ivarStrs;
}
調(diào)用方法可看到創(chuàng)建成功:
NSLog(@"%@", [self copyMethodsByClass:customClass]);
NSLog(@"%@", [self copyIvarsByClass:customClass]);

動(dòng)態(tài)創(chuàng)建類大致就這些步驟。。。
自己動(dòng)手寫一個(gè)KVO
KVO底層實(shí)現(xiàn)還是很復(fù)雜的,下面我只是簡(jiǎn)單的寫下實(shí)現(xiàn)過程:
- 因?yàn)樗且粋€(gè)非正式協(xié)議,給
NSObject新建一個(gè)Category,NSObject+kvo.h,添加監(jiān)聽方法:
.h文件
#import <Foundation/Foundation.h>
@interface NSObject (kvo)
- (void)zj_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
@end
.m文件
#import "NSObject+kvo.h"
#import <objc/runtime.h>
#import <objc/message.h>
@implementation NSObject (kvo)
- (void)zj_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context{
//動(dòng)態(tài)添加一個(gè)類
NSString *originClassName = NSStringFromClass([self class]);
NSString *newClassName = [@"ZJKVO_" stringByAppendingString:originClassName];
const char *newName = [newClassName UTF8String];
// 繼承自當(dāng)前類,創(chuàng)建一個(gè)子類
Class kvoClass = objc_allocateClassPair([self class], newName, 0);
// 添加setter方法
class_addMethod(kvoClass, @selector(setName:), (IMP)setName, "v@:@");
//注冊(cè)新添加的這個(gè)類
objc_registerClassPair(kvoClass);
// 修改isa指針,由ZJPerson指向ZJKVO_Person
object_setClass(self, kvoClass);
// 保存觀察者屬性到當(dāng)前類中
objc_setAssociatedObject(self, (__bridge const void *)@"observer", observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
#pragma mark - 重寫父類方法
void setName(id self, SEL _cmd, NSString *name) {
// 保存當(dāng)前KVO的類
Class kvoClass = [self class];
// 將self的isa指針指向父類ZJPerson,調(diào)用父類setter方法
object_setClass(self, class_getSuperclass([self class]));
// 調(diào)用父類setter方法,重新復(fù)制
objc_msgSend(self, @selector(setName:), name);
// 取出ZJKVO_Person觀察者
id objc = objc_getAssociatedObject(self, (__bridge const void *)@"observer");
// 通知觀察者,執(zhí)行通知方法
objc_msgSend(objc, @selector(observeValueForKeyPath:ofObject:change:context:), name, self, nil, name);
// 重新修改為ZJKVO_Person類
object_setClass(self, kvoClass);
}
注意一
要修改下xcode中的一個(gè)配置,將它改為NO,否則會(huì)報(bào)參數(shù)太多的錯(cuò)誤:

注意二
解釋下代碼中v@:@的意思:
- 第一個(gè)
v表示方法返回值void - 第二三個(gè)
@:一般是一塊的,因?yàn)楹瘮?shù)至少有兩個(gè)參數(shù)self和_cmd,一般是固定寫法 - 最后一個(gè)
@表示參數(shù)類型,是一個(gè)對(duì)象
下面在代碼中實(shí)驗(yàn),看下我們自己寫的kvo有沒有執(zhí)行:
修改添加監(jiān)聽者的方法,改成我們自己的
[self.person zj_addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
看下回調(diào)中的打?。?/p>

發(fā)現(xiàn)確實(shí)監(jiān)聽到了。。。
代碼地址
總結(jié)
kvo用法其實(shí)非常簡(jiǎn)單,但是深入了解,深入思考的話,知識(shí)點(diǎn)非常多?;艘惶於嗟臅r(shí)間,期間查閱了很多文檔(發(fā)現(xiàn)官方文檔真的是非常有用),總算是寫完了,對(duì)KVO有了一個(gè)更深入的認(rèn)識(shí)和理解。今天是520,感謝女朋友的理解,終于可以陪她出去玩了,哈哈。。。