這兩天在研究KVO,首先要吐槽的當然是官方提供的api,用起來實在是麻煩,所以想著封裝一下,增加一個block回調什么的。這是成果:ZNKVOManager
寫完之后發現FB在幾年前就已經寫好了。。。
關于KVO的基本知識,這里就不介紹了,對KVO一竅不通的可以先看看蘋果的官方文檔。這幾天研究的主要內容是:憑什么只要調用一下屬性的set方法,觀察者就能收到通知呢?
參考一下notification,起碼還有一個NSNotificationCenter負責轉發,KVO可什么都沒有提供,這么想來,一定是:
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
這個方法里面做了一些手腳,下面就是記錄一下研究的過程。內容不多。
在看蘋果的官方文檔時,有這么一段話:
Automatic key-value observing is implemented using a technique called isa-swizzling.
...
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.
翻譯:在addObserver的時候,會生成一個中間類,被觀察對象的isa指針實際指向這個中間類。
中間類究竟是什么樣?在KVO中起到什么作用?通過代碼看一看。首先是被觀察的對象,很簡單,就一個屬性:
// ZNObject.h
@interface ZNObject : NSObject
@property (nonatomic, copy) NSString *name;
@end
//ZNObject.m
@implementation ZNObject
@end
再看看具體測試的代碼:
- (void)viewDidLoad {
[super viewDidLoad];
self.obj = [ZNObject new];
NSLog(@"%p", self.obj);
NSLog(@"%p", [ZNObject class]);
self.kvoManager = [[ZNKVOManager alloc] initWithObserver:self];
[self.kvoManager observe:self.obj
forKeyPath:@"name"
callBack:^(id observer, id object, NSString *keyPath, NSDictionary<NSKeyValueChangeKey,id> *change) {
NSLog(@"new value is: %@", change[NSKeyValueChangeNewKey]);
NSLog(@"old value is: %@", change[NSKeyValueChangeOldKey]);
}];
NSLog(@"%@", [self.obj class]);
NSLog(@"%@", object_getClass(self.obj));
self.obj.name = @"haha";
}
這里使用了ZNKVOManager,實現代碼可以參考文章開始給出的鏈接。
其中這個方法:
- (void)observe:(id)object
forKeyPath:(NSString *)keyPath
callBack:(ZNKVOCallBack)callBack;
內部調用了addObserver方法,所以在調用完之后,獲取了一下className,看看中間類是什么。最后一行設置了name的值,會進入block中,打印出新值和舊值。
這里需要關注的是類名打印的結果:
...
2017-03-17 10:59:12.655055 TestOSX[88627:4489127] ZNObject
2017-03-17 10:59:12.655117 TestOSX[88627:4489127] NSKVONotifying_ZNObject
...
通過class方法獲取到的類依然是ZNObject,但是通過runtime提供的object_getClass(id obj)方法獲取到的卻是一個叫做NSKVONotifying_ZNObject的類??雌饋磉@就是所謂的中間類。
看看class方法的源碼:
// class方法
- (Class)class {
return object_getClass(self);
}
class方法內部就是調用的object_getClass(id obj)。這就引出了一個疑問:為什么通過class方法和object_getClass(id obj)方法獲取到的類不同呢?
在這之前,先看看這個中間類的信息,根據上面的官方信息,對象的isa指針指向的就是這個中間類,把代碼稍微修改一下,獲取中間類的地址:
NSLog(@"%p", object_getClass(self.obj));
2017-03-17 11:23:54.737805 TestOSX[89053:4504224] 0x6000001033c0
先看看這個類有哪些方法:
如果這一段看不懂,請先看一看Runtime源碼 —— 對象、類和isa以及Runtime源碼 —— 方法加載的過程
(lldb) p (objc_object *)0x6000001033c0
(objc_object *) $0 = 0x00006000001033c0
(lldb) p (class_data_bits_t *)0x00006000001033e0
(class_data_bits_t *) $1 = 0x00006000001033e0
(lldb) p $1->data()
(class_rw_t *) $2 = 0x000060000007c080
(lldb) p (*$2).methods
(method_array_t) $3 = {
list_array_tt<method_t, method_list_t> = {
= {
list = 0x000060000004b1c1
arrayAndFlag = 105553116574145
}
}
}
(lldb) p $3.beginCategoryMethodLists()
(method_list_t **) $4 = 0x000060000004b1c8
(lldb) p $4[0][0]
(method_list_t) $5 = {
entsize_list_tt<method_t, method_list_t, 3> = {
entsizeAndFlags = 26
count = 1
first = {
name = "setName:"
types = 0x00000001000029eb "v24@0:8@16"
imp = 0x00007fff980d4496 (Foundation`_NSSetObjectValueAndNotify)
}
}
}
(lldb) p $4[1][0]
(method_list_t) $6 = {
entsize_list_tt<method_t, method_list_t, 3> = {
entsizeAndFlags = 26
count = 1
first = {
name = "class"
types = 0x0000000100104da8 "#16@0:8"
imp = 0x00007fff980bfcea (Foundation`NSKVOClass)
}
}
}
(lldb) p $4[2][0]
(method_list_t) $7 = {
entsize_list_tt<method_t, method_list_t, 3> = {
entsizeAndFlags = 26
count = 1
first = {
name = "dealloc"
types = 0x0000000100104d52 "v16@0:8"
imp = 0x00007fff980dc5bf (Foundation`NSKVODeallocate)
}
}
}
(lldb) p $4[3][0]
(method_list_t) $8 = {
entsize_list_tt<method_t, method_list_t, 3> = {
entsizeAndFlags = 26
count = 1
first = {
name = "_isKVOA"
types = 0x00007fff983e2b70 "c16@0:8"
imp = 0x00007fff981e0c68 (Foundation`NSKVOIsAutonotifying)
}
}
}
(lldb) p $4[4][0]
error: Couldn't apply expression side effects : Couldn't dematerialize a result variable: couldn't read its memory
一共有4個方法,分別是setName,class,dealloc,_isKVOA.
到這里其實已經可以做出一些猜測了。
- 之前那個疑問應該就是中間類重寫了class方法導致的,因為這個class方法的imp是:Foundation`NSKVOClass。在這個方法內部肯定做了一些事情。
- 中間類重寫了setName方法,所以KVO應該在這個方法的imp:Foundation`_NSSetObjectValueAndNotify中做了一些事情來實現通知的功能。
那這個中間類和原有的類有什么關系呢?繼續測試一下:
// 中間類的地址是0x6000001033c0,superclass的地址在此基礎上偏移8
(lldb) p (objc_class *)0x00006000001033c8
(objc_class *) $10 = 0x00006000001033c8
(lldb) p *$10
(objc_class) $11 = {
isa = ZNObject
}
中間類的superclass就是原類,這么一想還是很合理的。中間類重寫了一些方法,對這些方法的調用會使用重寫之后的版本。而調用其他的方法,在中間類中查找不到就會到父類中查找,所以原類中的方法依然可以正常調用。
關于方法調用的順序,可以看這篇:Runtime源碼 —— 方法調用的過程
現在對中間類已經有一些了解了,再來看看文章開始的那個問題:set方法究竟如何觸發KVO的通知?
先回顧一下中間類的setName方法:
name = "setName:"
types = 0x00000001000029eb "v24@0:8@16"
imp = 0x00007fff980d4496 (Foundation`_NSSetObjectValueAndNotify)
實現是Foundation框架的_NSSetObjectValueAndNotify方法,沒法看到源碼,但是可以通過watchpoint看看調用棧:
繼續運行到命中:
對name的設置最終還是通過原類(ZNObject)的set方法來進行的,在_NSSetObjectValueAndNotify方法之后,調用了NSObject一個分類中的方法:
-[NSObject(NSKeyValueObservingPrivate) _changeValueForKeys:count:maybeOldValuesDict:usingBlock:]
看這個方法的時候,我發現這么一個方法:
這個方法后面會調用NSKeyValueDidChange這個方法,這個方法名字看起來有點眼熟,有點像NSObject提供的這個方法:
// Informs the observed object that the value of a given property has changed.
- (void)didChangeValueForKey:(NSString *)key;
那么NSKeyValueDidChange是不是起這個作用呢?進入這個方法看一看:
在這個方法內部又有新發現,有這么一個方法名字引起了我的注意:
NSKeyValueNotifyObserver,看起來就是用來通知觀察者的。
這個時候在這個方法中添加一個斷點,這個方法就是觀察者收到通知的地方,應該都很熟悉:
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey,id> *)change
context:(void *)context
結果如下圖:
果然,上面那個方法在NSKeyValueNotifyObserver方法內部被調用了。這就完成了整個通知的過程。
總結
稍微總結一下KVO方法的調用順序,就是這個樣子的:
- self.obj.name = @"haha";
- _NSSetObjectValueAndNotify
- -[NSObject(NSKeyValueObservingPrivate) _changeValueForKeys:count:maybeOldValuesDict:usingBlock:]
- - [ZNObject setName]
- NSKeyValueDidChange
- NSKeyValueNotifyObserver
- - observeValueForKeyPath:ofObject:change:context:
之前還有個疑問,調用class方法和object_getClass(id obj)方法為什么返回結果不同??梢苑抡丈厦娴牧鞒?,研究class方法的調用順序,結果是這樣的:
- [self.obj class]
- NSKVOClass
- object_getClass(id obj)
- _NSKVONotifyingOriginalClassForIsa
- _isKVOA
- NSKVOIsAutonotifying
- [ZNObject class]
- + [NSObject class]
最后還是會調用ZNObject的class方法,所以返回的結果是ZNObject也就不奇怪了。
如果直接使用object_getClass(id obj),并沒有那么復雜的邏輯,就只調用了這個方法,也就是獲取obj的isa,所以返回的是中間類。
最后再說一下這幾個方法的用法吧:
- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;
正常情況下是不需要使用的,因為前面也看到了,中間類的set方法會幫我們調用NSKeyValueDidChange來觸發通知。
如果不想自動觸發,可以使用上面這兩個方法來手動觸發,但是需要注意的是,如果僅僅是把set方法修改為這個樣子:
- (void)setName:(NSString *)name {
[self willChangeValueForKey:@"name"];
_name = name;
[self didChangeValueForKey:@"name"];
}
這樣會觸發兩次通知,第一次是手動觸發,第二次是中間類自動觸發,這個時候需要關閉自動觸發機制,很簡單,只要配套使用這個方法就可以了:
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
if ([key isEqualToString:@"name"]) {
return NO;
}
return YES;
}
將想要手動觸發的key返回NO就可以了,這個方法默認返回YES。