廢話不多說先來幾個面試題:
一,iOS用什么方式實現(xiàn)對一個對象的KVO?(KVO的本質(zhì)是什么?)
二,如何手動觸發(fā)KVO
三,直接修改成員變量會觸發(fā)KVO嗎?
通過挖掘KVO的本質(zhì),就會發(fā)現(xiàn),這幾個面試題就跟切菜一樣
那么什么是KVO呢?
什么是KVO呢?
KVO的全稱是Key-Value-Observing,即“鍵值監(jiān)聽”,可以用于監(jiān)聽某個對象屬性值的變化
用一張簡單的圖就可以表示:
這就是KVO最簡單的是使用,那么它到底是怎么實現(xiàn)的呢,下面我們一步步解開這個KVO底層的神秘面紗。
一,代碼準(zhǔn)備
1. 創(chuàng)建一個Person 類 繼承NSObject
// 聲明
@interface Person : NSObject
@property (nonatomic, assign) int age;
@end
// 實現(xiàn)
@implementation Person
- (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");
}
@end
2. ViewController代碼
// 導(dǎo)入頭文件
#import <objc/runtime.h>
#import "Person.h"
// 聲明person對象
@interface ViewController ()
@property (nonatomic, strong) Person *person1;
@property (nonatomic, strong) Person *person2;
@end
- (void)viewDidLoad {
[super viewDidLoad];
person1 = [[Person alloc]init];
person1 = 18;
self.person2 = [[Person alloc]init];
self.person2.age = 28;
// 給person1添加一個KVO
[person1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:@"123"];
}
// 當(dāng)監(jiān)聽對象的屬性值發(fā)生改變是,就會調(diào)用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"監(jiān)聽到%@的%@屬性值改變了 - %@ - %@",
object, keyPath, change, context);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.person1.age = 20;
}
二,開始研究KVO本質(zhì)
點擊屏幕后,會觸發(fā)observeValueForKeyPath這監(jiān)聽事件
// 打印結(jié)果
監(jiān)聽到<Person: 0x600002e2c510>的age屬性值改變了 - {
kind = 1;
new = 20;
old = 18;
}
發(fā)現(xiàn)age的值確實發(fā)生了變化
1. 那么age值的改變是否與setAge:方法有關(guān)
在touchesBegan給person2重新賦值
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// self.person1.age = 21;
// self.person2.age = 22;
// 上面兩句代碼,其實就是調(diào)用Person的 set 方法,都在調(diào)用同一個方法
[self.person1 setAge:21];
[self.person2 setAge:22];
}
當(dāng)觸發(fā)touchesBegan事件時,發(fā)現(xiàn)調(diào)用 [self.person1 setAge:21];后會觸發(fā)observe監(jiān)聽器,而調(diào)用 [self.person2 setAge:22];后不會觸發(fā)observe監(jiān)聽器。
既然都是在調(diào)用 setAge方法,但是為啥只有person1的age發(fā)生改變時,才會給observe發(fā)通知呢?而person2的age發(fā)生改變時,不會通知observe?
先看兩張圖:
從上面兩張圖,可以看出來:
其實本質(zhì)上就是兩個實例對象的isa指向不一樣
現(xiàn)在看下兩個實例的isa有什么不同
lldb結(jié)果:
self.person1.isa -> NSKVONotifying_Person
self.person2.isa -> Person
差異:
1. 添加了KVO監(jiān)聽的實例person1,通過isa指向的class是NSKVONotifying_Person
2. 沒有添加KVO監(jiān)聽的實例person2,通過isa指向的class是Person
那么,NSKVONotifying_Person這個類是怎么產(chǎn)生的呢?
本質(zhì)上是利用Runtime動態(tài)創(chuàng)建的一個類(可以利用truntime機制自己實現(xiàn)KVO監(jiān)聽)
結(jié)論:由此可見,age值的改變與setAge:方法沒有關(guān)系,而是因為派生出了新的類,此時的setAge:方法的實現(xiàn)也就不一樣了,具體有啥不易樣的,接著往下看哈。
2. person1添加KVO前后person1和person2對象的變化
驗證person1在添加監(jiān)聽前后,person1實例的isa指向的【class】和具體的【對象方法】實現(xiàn)到底發(fā)生了哪些變化
- 利用runtime的 object_getClass() 查看person1添加監(jiān)聽前后的,person1和person2的isa指向類對象;
- 利用runtime的 object_getClass() 查看person1添加監(jiān)聽前后的,person1和person2的isa指向類對象的地址;
- 利用runtime的 methodForSelector這個方法來獲取setAge:這個實例方法的具體實現(xiàn)
根據(jù)以上3點,添加一些打印信息:
NSLog(@"person1添加監(jiān)聽之前類對象:%@, %@",
object_getClass(self.person1),
object_getClass(self.person2));
NSLog(@"person1添加監(jiān)聽之前類對象地址:%p, %p",
object_getClass(self.person1),
object_getClass(self.person2));
NSLog(@"person1添加監(jiān)聽之前類的實例方法實現(xiàn):%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添加監(jiān)聽之后類對象:%@, %@",
object_getClass(self.person1),
object_getClass(self.person2));
NSLog(@"person1添加監(jiān)聽之后類對象地址:%p, %p",
object_getClass(self.person1),
object_getClass(self.person2));
NSLog(@"person1添加監(jiān)聽之后類的實例方法實現(xiàn):%p, %p",
[self.person1 methodForSelector:@selector(setAge:)],
[self.person2 methodForSelector:@selector(setAge:)]);
分析打印信息:
打印結(jié)果:
person1添加監(jiān)聽之前類對象:Person, Person
person1添加監(jiān)聽之后類對象:NSKVONotifying_Person, Person
person1添加監(jiān)聽之前類對象地址:0x1089bd708, 0x1089bd708
person1添加監(jiān)聽之后類對象地址:0x6000021d87e0, 0x1089bd708
結(jié)論:
由此可見,person1在添加監(jiān)聽后,isa所指向的類確實改變了,那么setAge:這個對象方法到底是怎么實現(xiàn)的呢,繼續(xù)分析第三個打印結(jié)果
打印結(jié)果:
person1添加監(jiān)聽之前類的實例方法實現(xiàn):0x1061e1ed0, 0x1061e1ed0
person1添加監(jiān)聽之后類的實例方法實現(xiàn):0x7fff257223da, 0x1061e1ed0
利用lldb查看
(lldb) p (IMP)0x1061e1ed0
(IMP) $0 = 0x00000001061e1ed0 (06-KVO初探`-[Person setAge:] at Person.m:13)
(lldb) p (IMP)0x7fff257223da
(IMP) $1 = 0x00007fff257223da (Foundation`_NSSetIntValueAndNotify)
結(jié)論:
由此可見,person1添加監(jiān)聽后,setAge:的實現(xiàn)的確改變了,是在Foundation框架下的_NSSetIntValueAndNotify(c語言方法)實現(xiàn)的,_NSSetIntValueAndNotify具體實現(xiàn)見自己實現(xiàn)的NSKVONotifying_Person 類。
下面可以看下_NSSetIntValueAndNotify實現(xiàn)的偽代碼
// 聲明一個添加了KVO的派生類 NSKVONotifying_Person
@interface NSKVONotifying_Person : Person
@end
// 實現(xiàn)
@implementation NSKVONotifying_Person
- (void)setAge:(int)age {
_NSSetIntValueAndNotify();
}
// 偽代碼 大概流程
void _NSSetIntValueAndNotify() {
[self willChangeValueForKey:@"age"];
[super setAge:age];
[self didChangeValueForKey:@"age"];
}
- (void)didChangeValueForKey:(NSString *)key {
// 偽代碼
// 通知監(jiān)聽器,某某屬性發(fā)生了改變
[observe observeValueForKeyPath:key ofObject:self change:nil context:nil];
}
@end
3. 查看person1添加KVO后,都有哪些調(diào)用
在Person類里,實現(xiàn)兩個父類的方法,并且給setAge加上打印信息
- (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");
}
觸發(fā)touchesBegan方法
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self.person1 setAge:21];
// [self.person2 setAge:22];
}
打印結(jié)果是:
哥們 == willChangeValueForKey
哥們 == setAge
哥們 == didChangeValueForKey-begin
監(jiān)聽到<Person: 0x600001108590>的age屬性值改變了 - {
kind = 1;
new = 21;
old = 1;
} - 123
哥們 == didChangeValueForKey-end
打印結(jié)果分析:添加了KVO的對象,屬性改變的話,都有下面一些方法調(diào)用
當(dāng)修改instance對象的屬性時,先調(diào)用setter方法,然后實現(xiàn)Foundation中的_NSSet****ValueAndotify函數(shù)
a> willChangeValueForKey:
b> 父類原來的setter
c> didChangeValueForKey:
內(nèi)部會觸發(fā)監(jiān)聽器(Observe)的監(jiān)聽方法(observeValueForKeyPath:ofObject:change:context:)
如果person2調(diào)用了setAge:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// [self.person1 setAge:21];
[self.person2 setAge:22];
}
打印結(jié)果只有一個
哥們 == setAge
說明:沒有添加監(jiān)聽的對象,willChangeValueForKey和didChangeValueForKey方法不會調(diào)用
4. 查看NSKVONotifying_Person類的元類
驗證一下,添加KVO后,新生成的NSKVONotifying_Person類對象的isa指向的元類對象及對象地址是什么
利用runtime的 Class object_getClass(id obj) 來查看對象地址和對象名
1> 傳入的obj可能是instance對象、class對象、meta-class對象
2> 返回值
a) 如果是instance對象,返回class對象
b) 如果是class對象,返回meta-class對象
c) 如果是meta-class對象,返回NSObject(基類)的meta-class對象
為了驗證在添加KVO后添加一些打印信息
NSLog(@"類對象地址:%p, %p",
// 拿到person1.isa,相當(dāng)于Person類地址(isa & ISA_MASK)
object_getClass(self.person1),
// 拿到person2.isa
object_getClass(self.person2));
NSLog(@"元類對象地址:%p, %p",
// 拿到person1.isa.isa
object_getClass(object_getClass(self.person1)),
// 拿到person2.isa.isa
object_getClass(object_getClass(self.person2)));
NSLog(@"類對象:%@, %@",
// 拿到person1的isa指向的類
object_getClass(self.person1),
// 拿到person2的isa指向的類
object_getClass(self.person2));
NSLog(@"元類對象:%@, %@",
// 拿到person1的isa指向的類的元類
object_getClass(object_getClass(self.person1)),
// 拿到person2的isa指向的類的元類
object_getClass(object_getClass(self.person2)));
打印信息分析:
類對象地址:0x600001284120, 0x1081ea710
元類對象地址:0x6000012841b0, 0x1081ea6e8
類對象:NSKVONotifying_Person, Person
元類對象:NSKVONotifying_Person, Person
結(jié)論:
從打印結(jié)果來看,派生出來的NSKVONotifying_Person類的元類就是它本身
三, 派生出來的NSKVONotifying_Person都有哪些方法
先看一張示例圖,紅框狂起來的部分
1. 利用runtime來獲取方法實現(xiàn)
Class cls = object_getClass(self.person1);
unsigned int count;
Method *methodList = class_copyMethodList(cls, &count);
// 遍歷方法
for (int i = 0; i < count; i++) {
// 獲得方法
Method method = methodList[i];
NSString *methodName = NSStringFromSelector(method_getName(method));
NSLog(@"methodName == %@", methodName);
}
打印結(jié)果:
setAge:
class
dealloc
_isKVOA
所以派生出來的NSKVONotifying_Person類里面,確實存在setAge:/class/dealloc/_isKVOA這些方法
2. 研究下為啥要重寫class方法呢?
添加兩個打印信息
NSLog(@"類對象:%@, %@", object_getClass(self.person1), object_getClass(self.person2));
NSLog(@"類對象:%@, %@", [self.person1 class], [self.person2 class]);
上面打印信息是利用兩種方法來獲取person1和person2的類對象
打印結(jié)果:
類對象:NSKVONotifying_Person, Person
類對象:Person, Person
結(jié)果分析:
只有通過runtime的object_getClass這中方式,取出來的才是person1的isa指向的類對象,
而通過self.person1 class]取出來的,不一定是person1的isa指向的類對象
為什么要重寫class呢?
猜測:系統(tǒng)為了直接返回當(dāng)前類,屏蔽內(nèi)部實現(xiàn),
如果不重寫,person1找到isa,通過isa找到對應(yīng)的類對象,在這個類里面發(fā)現(xiàn)沒有-class的這個方法,那么就通過superclass找到父類,
如果父類還沒有,就通過superclass一直找到基類,基類(NSObject)里面就會通過object_getClass(sef)返回當(dāng)前的類,即Person
四,回答面試題
一,iOS用什么方式實現(xiàn)對一個對象的KVO?(KVO的本質(zhì)是什么?)
1. 利用runtimeAPI動態(tài)生成一個子類,并且讓instance的isa指向一個全新的子類
2. 當(dāng)修改instance對象的屬性時,現(xiàn)調(diào)用setter方法,然后實現(xiàn)Foundation中的_NSSet****ValueAndotify函數(shù)
a> willChangeValueForKey:
b> 父類原來的setter
c> didChangeValueForKey:
內(nèi)部會觸發(fā)監(jiān)聽器(Observe)的監(jiān)聽方法(observeValueForKeyPath:ofObject:change:context:)
二,如何手動觸發(fā)KVO
意思就是不通過改變屬性的值,怎么觸發(fā)KVO
手動調(diào)用willChangeValueForKey:和didChangeValueForKey:
三,直接修改成員變量會觸發(fā)KVO嗎?
不會觸發(fā)KVO,因為只有通過setter方法才能觸發(fā),而成員變量不會調(diào)用setter方法
要想通過修改成員變量來觸發(fā)KVO,也很簡單
手動依次調(diào)用:
1.willChangeValueForKey:
2.修改成員變量
3.didChangeValueForKey: