一. KVO
1. KVO的基本使用
KVO的全稱是Key-Value Observing,俗稱“鍵值監聽”,可以用于監聽某個對象屬性值的改變
添加監聽:
// 給person1對象添加KVO監聽
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
[self.person1 addObserver:self forKeyPath:@"height" options:options context:@"456"];
值改變:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
self.person1.age = 20;
self.person2.age = 20;
self.person1.height = 30;
self.person2.height = 30;
}
監聽改變:
//context:@"123" 作用:在添加監聽的時候傳入,傳到下面這個方法里面
/**
當監聽對象的屬性值發生改變時,就會調用
@param keyPath 監聽的KeyPath
@param object 被監聽的對象
@param change 改變
@param context 監聽時傳入的context
*/
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
NSLog(@"監聽到%@的%@屬性值改變了 - %@ - %@", object, keyPath, change, context);
}
移除監聽:
- (void)dealloc {
[self.person1 removeObserver:self forKeyPath:@"age"];
[self.person1 removeObserver:self forKeyPath:@"height"];
}
2. KVO底層是怎么實現的
為了探究KVO的底層是怎么實現的,我們創建person1和person2,其中person1添加監聽,person2不添加監聽,代碼如下:
#import "ViewController.h"
#import "MJPerson.h"
@interface ViewController ()
@property (strong, nonatomic) MJPerson *person1;
@property (strong, nonatomic) MJPerson *person2;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[MJPerson alloc] init];
self.person1.age = 1;
self.person2 = [[MJPerson alloc] init];
self.person2.age = 2;
// 給person1對象添加KVO監聽
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
// self.person1.age = 21;
// self.person2.age = 22;
// NSKVONotifying_MJPerson是使用Runtime動態創建的一個類,是MJPerson的子類
//如果你自己寫了這個類,就會報動態生成失敗
//KVO效率沒代理高,因為代理是直接調用,KVO還要動態生成一個類
// self.person1.isa == NSKVONotifying_MJPerson
[self.person1 setAge:21];
// self.person2.isa = MJPerson
[self.person2 setAge:22];
}
- (void)dealloc {
[self.person1 removeObserver:self forKeyPath:@"age"];
}
// 當監聽對象的屬性值發生改變時,就會調用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
NSLog(@"監聽到%@的%@屬性值改變了 - %@ - %@", object, keyPath, change, context);
}
@end
打斷點,分別po它們的isa
(lldb) po self.person1.isa
NSKVONotifying_MJPerson
Fix-it applied, fixed expression was:
self.person1->isa
(lldb) po self.person2.isa
MJPerson
Fix-it applied, fixed expression was:
self.person2->isa
(lldb)
可以發現,person1添加監聽后isa是NSKVONotifying_MJPerson,person2不添加監聽isa還是MJPerson。
- 其實NSKVONotifying_MJPerson是系統利用Runtime動態創建的一個類,是MJPerson的子類。
- 如果你自己寫了這個類,就會報動態生成失敗。
- KVO效率沒代理高,因為代理是直接調用,KVO還要動態生成一個類。
既然NSKVONotifying_MJPerson也是一個類,那么它肯定也有自己的isa和superclass,未使用KVO和使用KVO,實例對象和類對象內存結構如下:
解釋:
① 當person2不添加監聽的時候,值改變,會通過person2的isa找到MJPerson,然后再找到MJPerson里面的setAge方法調用,完成。
② 當person1添加監聽的時候,值改變,會通過person1的isa找到NSKVONotifying_MJPerson,然后調用NSKVONotifying_MJPerson的setAge方法(方法內部會調用Foundation框架的_NSSetIntValueAndNotify),不會調用MJPerson的setAge方法了。
由于無法查看Foundation框架的實現,我們寫一些偽代碼,來表明添加監聽后的方法調用順序,創建NSKVONotifying_MJPerson類繼承于MJPerson。
#import "MJPerson.h"
@interface NSKVONotifying_MJPerson : MJPerson
@end
#import "NSKVONotifying_MJPerson.h"
@implementation NSKVONotifying_MJPerson
- (void)setAge:(int)age
{
_NSSetIntValueAndNotify();
}
// 偽代碼
void _NSSetIntValueAndNotify()
{
[self willChangeValueForKey:@"age"];
[super setAge:age];
[self didChangeValueForKey:@"age"];
}
- (void)didChangeValueForKey:(NSString *)key
{
// 通知監聽器,某某屬性值發生了改變
[oberser observeValueForKeyPath:key ofObject:self change:nil context:nil];
}
@end
總結:
- 使用KVO,系統會使用Runtime動態創建的一個NSKVONotifying_MJPerson類,這個類是MJPerson的子類。
- 添加監聽的屬性的值改變的時候,會調用NSKVONotifying_MJPerson類的setAge方法,setAge方法里面會調用_NSSetIntValueAndNotify方法,_NSSetIntValueAndNotify里面走如下步驟:
① willChangeValueForKey 將要改變
② setAge(原來的set方法) 真的去改變
③ didChangeValueForKey 已經改變
④ observeValueForKeyPath:ofObject:change:context: 監聽到MJPerson的age屬性改變了
3. 驗證_NSSetIntValueAndNotify內部方法調用流程
驗證過程也很簡單,重寫MJPerson類的三個方法,如下:
- (void)setAge:(int)age
{
_age = age;
NSLog(@"setAge:");
}
- (void)willChangeValueForKey:(NSString *)key
{
[super willChangeValueForKey:key];
NSLog(@"willChangeValueForKey");
}
- (void)didChangeValueForKey:(NSString *)key
{
NSLog(@"didChangeValueForKey - begin");
[super didChangeValueForKey:key];
NSLog(@"didChangeValueForKey - end");
}
賦值之后運行,打印結果如下:
willChangeValueForKey
setAge:
didChangeValueForKey - begin
監聽到<MJPerson: 0x6000017c90f0>的age屬性值改變了 - {
kind = 1;
new = 21;
old = 1;
} - 123
didChangeValueForKey - end
4. 驗證生成了NSKVONotifying_MJPerson
結論我們知道了,接下來還是用代碼驗證一下:
- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[MJPerson alloc] init];
self.person1.age = 1;
self.person2 = [[MJPerson alloc] init];
self.person2.age = 2;
NSLog(@"person1添加KVO監聽之前 - %@ %@",
object_getClass(self.person1),
object_getClass(self.person2));
NSLog(@"person1添加KVO監聽之前 - %p %p",
[self.person1 methodForSelector:@selector(setAge:)],
[self.person2 methodForSelector:@selector(setAge:)]);
// 給person1對象添加KVO監聽
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
NSLog(@"person1添加KVO監聽之后 - %@ %@",
object_getClass(self.person1),
object_getClass(self.person2));
NSLog(@"person1添加KVO監聽之后 - %p %p",
[self.person1 methodForSelector:@selector(setAge:)],
[self.person2 methodForSelector:@selector(setAge:)]);
}
如上,我們在添加KVO之前和之后分別打印它的類對象和它的setAge方法實現,結果如下:
person1添加KVO監聽之前 - MJPerson MJPerson
person1添加KVO監聽之前 - 0x10ede8590 0x10ede8590
person1添加KVO監聽之后 - NSKVONotifying_MJPerson MJPerson
person1添加KVO監聽之后 - 0x10f143216 0x10ede8590
可以發現:
person1添加KVO之前,person1和person2的類對象和setAge方法都是一樣的。
person1添加KVO之后,person1的isa指向的類對象變成了NSKVONotifying_MJPerson,setAge方法地址也變了,person2什么都沒變。
接下來,我們通過p (IMP)指令打印某個地址對應的實現:
(lldb) p (IMP)0x10ede8590
(IMP) $0 = 0x000000010ede8590 (Interview01`-[MJPerson setAge:] at MJPerson.m:13)
(lldb) p (IMP)0x10f143216
(IMP) $1 = 0x000000010f143216 (Foundation`_NSSetIntValueAndNotify)
(lldb)
打印發現,添加KVO之后果然調用的是Foundation框架下的_NSSetIntValueAndNotify函數,說明我們上面的結論是正確的。
5. NSKVONotifying_MJPerson類對象的isa指向哪里?
下面還有最后一個問題,NSKVONotifying_MJPerson類對象的isa指向哪里呢?這個驗證起來也很簡單,用如下代碼打印:
//獲取類對象
NSLog(@"類對象 - %p %p",
object_getClass(self.person1), // 相當于獲取person1的類對象(self.person1.isa)
object_getClass(self.person2)); // 相當于獲取erson2的類對象(self.person2.isa)
//獲取元類對象
NSLog(@"元類對象 - %p %p",
object_getClass(object_getClass(self.person1)), // 相當于獲取person1類對象的元類對象(self.person1.isa.isa)
object_getClass(object_getClass(self.person2))); // 相當于獲取相當于獲取person1類對象的元類對象(self.person2.isa.isa)
打印結果如下;
類對象 - 0x6000008f0090 0x100d91158
元類對象 - 0x6000008f1050 0x100d91180
可以發現person1和person2的類對象和元類對象的地址都不一樣,說明他們是不同的類。這和我們以前的結論一致,所以NSKVONotifying_MJPerson類對象的isa也是指向它自己的元類對象。
注意:當屬性是int類型的時候調用的是_NSSetIntValueAndNotify方法,當屬性是float類型的時候調用的是_NSSetFloatValueAndNotify方法,其他類比_NSSetObjectValueAndNotify,_NSSetLongValueAndNotify等等......
6. 為什么重寫class、dealloc、isKVO方法
在第三張圖中我們可以看出,創建NSKVONotifying_MJPerson之后會重寫setAge、class、dealloc、isKVO 這四個方法,setAge方法我們知道為什么重寫,但是為什么要重寫后面三個方法呢?
首先我們先驗證NSKVONotifying_MJPerson的確有這四個方法:
//獲取一個類里面所有的方法
- (void)printMethodNamesOfClass:(Class)cls
{
unsigned int count;
// 獲得方法數組
Method *methodList = class_copyMethodList(cls, &count);
// 存儲方法名
NSMutableString *methodNames = [NSMutableString string];
// 遍歷所有的方法
for (int i = 0; i < count; i++) {
// 獲得方法
Method method = methodList[i];
// 獲得方法名
NSString *methodName = NSStringFromSelector(method_getName(method));
// 拼接方法名
[methodNames appendString:methodName];
[methodNames appendString:@", "];
}
//c語言中,如果數組是create或者copy出來的要free OC中ARC不用管
// 釋放
free(methodList);
// 打印方法名
NSLog(@"%@ %@", cls, methodNames);
}
- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[MJPerson alloc] init];
self.person1.age = 1;
self.person2 = [[MJPerson alloc] init];
self.person2.age = 2;
// 給person1對象添加KVO監聽
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
[self printMethodNamesOfClass:object_getClass(self.person1)];
[self printMethodNamesOfClass:object_getClass(self.person2)];
}
打印結果如下:
打印結果:
NSKVONotifying_MJPerson setAge:, class, dealloc, _isKVOA,
MJPerson setAge:, age,
由打印結果可知:
NSKVONotifying_MJPerson里面的確有setAge:、class、dealloc、_isKVOA四個方法
MJPerson里面有setAge:、age兩個方法
接下來如果想知道為什么要重寫class、dealloc、_isKVOA三個方法,我們先打印:
NSLog(@"%@ %@",object_getClass(self.person1),object_getClass(self.person2));
NSLog(@"%@ %@",[self.person1 class],[self.person2 class]);
打印結果:
NSKVONotifying_MJPerson MJPerson
MJPerson MJPerson
在OC對象的分類中,我們知道上面兩種方式都可以獲取類對象,但是為什么獲取的結果不一樣呢?
其實,因為NSKVONotifying_MJPerson是內部創建的,不想讓用戶看到,所以用戶調用class方法要把NSKVONotifying_MJPerson轉成MJPerson,所以系統才重寫了class方法。使用object_getClass函數(RuntimeAPI)獲取的就是真實的,不會被轉成MJPerson。
如果NSKVONotifying_MJPerson沒有實現class方法,最后會調用到NSObject的class方法,會直接返回NSKVONotifying_MJPerson,因為NSObject內部這樣實現的:
@implementation NSObject
- (Class)class
{
return object_getClass(self);
}
@end
我們可以寫NSKVONotifying_MJPerson的偽代碼:
#import "NSKVONotifying_MJPerson.h"
@implementation NSKVONotifying_MJPerson
//NSKVONotifying_MJPerson內部實現了setKey class dealloc isKVO 方法
- (void)setAge:(int)age
{
_NSSetIntValueAndNotify();
}
// 屏蔽內部實現,隱藏了NSKVONotifying_MJPerson類的存在
- (Class)class
{
return [MJPerson class];
}
- (void)dealloc
{
// 收尾工作
}
- (BOOL)_isKVOA
{
return YES;
}
@end
7. 面試題
下面我們就可以回答面試題了:
問題一:iOS用什么方式實現對一個對象的KVO?(KVO的本質是什么?)
答:
- 利用RuntimeAPI動態生成一個子類,并且讓instance對象的isa指向這個全新的子類
- 當修改instance對象的屬性時,會先調用這個新子類的setter方法,這個新子類的setter方法內部會調用Foundation的_NSSet*ValueAndNotify函數(內部調用如下方法)
① willChangeValueForKey:
② 父類原來的setter
③ didChangeValueForKey:
④ 內部會觸發監聽器(Oberser)的監聽方法(observeValueForKeyPath:ofObject:change:context:)
問題二:如何手動觸發KVO?(就算沒有人修改age值,也想觸發監聽方法observeValueForKeyPath)
答:手動調用willChangeValueForKey:和didChangeValueForKey:
比如:
[self.person1 willChangeValueForKey:@"age"];
self.person1->_age = 2;
[self.person1 didChangeValueForKey:@"age"];
//didChangeValueForKey內部會判斷willChangeValueForKey是否調用,所以兩個都要調用
會打印:
2019-04-11 15:34:31.968473+0800 Interview01[16217:3596188] 監聽到<MJPerson: 0x6000000180d0>的age屬性值改變了 - {
kind = 1;
new = 2;
old = 1;
} - 123
問題三:直接修改成員變量會觸發KVO嗎?
答:不會觸發KVO,因為沒調用重寫后的set方法。
比如,如下代碼,不會觸發
self.person1->_age = 2;
二. KVC
1. KVC的基本使用
KVC的全稱是Key-Value Coding,俗稱“鍵值編碼”,可以通過一個key來訪問某個屬性
常見的API有:
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
- (void)setValue:(id)value forKey:(NSString *)key;
- (id)valueForKeyPath:(NSString *)keyPath;
- (id)valueForKey:(NSString *)key;
KVC的基本使用:
person.age = 10;
NSLog(@"%@", [person valueForKey:@"age"]);
NSLog(@"%@", [person valueForKeyPath:@"cat.weight"]);
NSLog(@"%d", person.age);
//[person setValue:[NSNumber numberWithInt:10] forKey:@"age"];
[person setValue:@10 forKey:@"age"];
person.cat = [[MJCat alloc] init];
[person setValue:@10 forKeyPath:@"cat.weight"];
//setValue:@10 forKeyPath 更強大推薦使用.
NSLog(@"%d", person.age);
2. 通過KVC給屬性賦值能觸發KVO嗎?
能不能試一下就知道了:
MJObserver *observer = [[MJObserver alloc] init];
MJPerson *person = [[MJPerson alloc] init];
// 添加KVO監聽
[person addObserver:observer forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL];
[person setValue:@10 forKey:@"age"];
打印如下:
2019-04-11 15:55:00.568868+0800 Interview01-KVC[16345:3639722] observeValueForKeyPath - {
kind = 1;
new = 10;
old = 0;
}
發現,通過KVC修改age屬性會觸發KVO。為什么呢?先往下看
接下來看看setValue:forKey:設值原理和valueForKey:取值原理。
3. KVC設值原理
設值原理解釋:
1.尋找setAge方法
- (void)setAge:(int)age
{
NSLog(@"setAge: - %d", age);
}
2.尋找_setAge方法
- (void)_setAge:(int)age
{
NSLog(@"_setAge: - %d", age);
}
3.找不到上面兩個方法就調用accessInstanceVariablesDirectly問問能不能直接訪問成員變量
默認的返回值就是YES
+ (BOOL)accessInstanceVariablesDirectly
{
return YES;
}
4.1 如果返回NO,就調用setValue:forUndefinedKey:并拋出異常NSUnknownKeyException
4.2如果返回YES,會按順序_key,_isKey,key,isKey賦值,如果四個都找不到就報上面的錯
上面我們知道KVC設值會觸發KVO,但是如果沒有set方法呢?通過驗證(驗證過程省略)可知,就算沒有set方法只有成員變量,通過KVC進行賦值也會觸發KVO,可以理解它們是配套使用的。
其實KVC內部調用了下面方法才會觸發KVO的,可自行驗證。
[person willChangeValueForKey:@"age"];
person->_age = 10;
[person didChangeValueForKey:@"age"];
4. KVC取值原理
KVC取值原理解釋,可自行驗證:
1.getAge
- (int)getAge
{
return 11;
}
2.age
- (int)age
{
return 12;
}
3.isAge
- (int)isAge
{
return 13;
}
4._age
- (int)_age
{
return 14;
}
5.3.找不到上面四個方法就調用accessInstanceVariablesDirectly問問能不能直接訪問成員變量
默認的返回值就是YES
+ (BOOL)accessInstanceVariablesDirectly
{
return YES;
}
5.1 如果返回NO,就調用valueforUndefinedKey:并拋出異常NSUnknownKeyException
5.2如果返回YES,會按順序_key,_isKey,key,isKey賦值,如果四個都找不到就報上面的錯
Demo地址:KVO和KVC原理