上篇講了類的本質,咱們知道實例實際是存儲了成員變量的值和指向類的isa
指針,class
對象和meta-class
對象包含 isa
、superclass
和class_rw_t
這幾種結構體,只是數據不同,isa
須要ISA_MASK
&以後纔是真正的值。那麼今天咱們在看一下Key-Value Observing的本質。git
首先須要瞭解KVO基本使用,KVO的全稱 Key-Value Observing,俗稱「鍵值監聽」,能夠用於監聽某個對象屬性值的改變。下面咱們展現一下KVO的基本使用。github
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface FYPerson : NSObject
@property (nonatomic,assign) NSInteger age;
@end
NS_ASSUME_NONNULL_END
#import "ViewController.h"
#import "FYPerson.h"
@interface ViewController ()
@property (nonatomic,strong)FYPerson *person;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.person=[FYPerson new];
self.person.age = 10;
[self.person addObserver:self
forKeyPath:@"age"
options:NSKeyValueObservingOptionNew
context:nil];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.person.age += 1;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"監聽到了age變化: %@",change);
}
-(void)dealloc{
[self.person removeObserver:self forKeyPath:@"age"];
}
@end
//下邊是輸出結果
監聽到了age變化: {
kind = 1;
new = 12;
old = 11;
}
複製代碼
從上述代碼能夠看出,添加監聽以後,當值改變時,會觸發函數observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
。bash
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
// self.person.age += 1;
[self.person willChangeValueForKey:@"age"];
[self.person didChangeValueForKey:@"age"];
}
複製代碼
當把age
具體值的改變,變成手動調用willChangeValueForKey
和didChangeValueForKey
的時候,結果以下:app
監聽到了age變化: {
kind = 1;
new = 10;
old = 10;
}
複製代碼
new
和old
的值居然同樣,經測試只有同時前後調用willChangeValueForKey
和didChangeValueForKey
,會觸發回調函數observeValueForKeyPath
,由此可知觸發條件是willChangeValueForKey
和didChangeValueForKey
配合使用。框架
經過上述代碼咱們發現,一旦age屬性的值發生改變時,就會通知到監聽者,而且咱們知道賦值操做都是調用 set方法,咱們能夠來到Person類中重寫age的set方法,觀察是不是KVO在set方法內部作了一些操做來通知監聽者。 咱們發現即便重寫了set方法,p1對象和p2對象調用一樣的set方法,可是咱們發現p1除了調用set方法以外還會另外執行監聽器的observeValueForKeyPath方法。 說明KVO在運行時獲取對p1對象作了一些改變。至關於在程序運行過程當中,對p1對象作了一些變化,使得p1對象在調用setage方法的時候可能作了一些額外的操做,因此問題出在對象身上,兩個對象在內存中確定不同,兩個對象可能本質上並不同。接下來來探索KVO內部是怎麼實現的。 KVO底層實現分析 首先咱們對上述代碼中添加監聽的地方打斷點,看觀察一下,addObserver方法對p1對象作了什麼處理?也就是說p1對象在通過addObserver方法以後發生了什麼改變,咱們經過打印isa指針:函數
@interface ViewController ()
@property (nonatomic,strong)FYPerson *person;
@property (nonatomic,strong)FYPerson *person2;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.person=[FYPerson new];
self.person2 =[FYPerson new];
self.person.age = 10;
[self.person addObserver:self
forKeyPath:@"age"
options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld
context:nil];
Class superclass = NSStringFromClass( class_getSuperclass(NSClassFromString(@"NSKVONotifying_FYPerson")));
Class NSKVONotifying_FYPerson = objc_getClass("NSKVONotifying_FYPerson");
fy_objc_class* NSKVONotifying_FYPerson_class = (__bridge fy_objc_class *)NSKVONotifying_FYPerson;
//此處打斷點
//p 命令輸出isa指針
(lldb) p self.person2->isa
(Class) $0 = FYPerson
(lldb) p self.person->isa
(Class) $1 = NSKVONotifying_FYPerson
(lldb) p superclass
(Class) $0 = FYPerson
(lldb) p NSKVONotifying_FYPerson_class->superclass
(Class) $4 = FYPerson
}
複製代碼
從輸出的isa指針看來,通過【person addObserver】
以後,person
的isa
指針指向了NSKVONotifying_FYPerson
,而person2
的isa
是FYPerson
,能夠看出系統是對instance
對象的isa
進行了賦值操做。經過p NSKVONotifying_FYPerson_class->superclass==FYPerson
能夠看出isa是指向了子類,那麼子類NSKVONotifying_FYPerson
到底作了那些事情呢?post
看下邊代碼查看函數isa改變過程:學習
self.person=[FYPerson new];
self.person2 =[FYPerson new];
self.person.age = 10;
//打斷點 輸出 po [_person methodForSelector:@selector(setAge:)]
[self.person addObserver:self
forKeyPath:@"age"
options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld
context:nil];
//打斷點 輸出 po [_person methodForSelector:@selector(setAge:)]
(lldb) po [_person methodForSelector:@selector(setAge:)]
0x000000010666b720
(lldb) po [_person methodForSelector:@selector(setAge:)]
0x00000001069c63d2
//查看IMP指針對應地址和內容
(lldb) p (IMP)0x000000010666b720
(IMP) $2 = 0x000000010666b720 (day03-KVO本質`::-[FYPerson setAge:](int) at FYPerson.h:14)
(lldb) p (IMP)0x00000001069c63d2
(IMP) $3 = 0x00000001069c63d2 (Foundation`_NSSetIntValueAndNotify)
複製代碼
能夠看出來兩次的函數地址不一致,添加KVO以前是[FYPerson setAge:]
,添加以後是(Foundation_NSSetIntValueAndNotify)
。咱們將age
的類型改爲double
,再看一下結果:測試
(lldb) po [_person methodForSelector:@selector(setAge:)]
0x00000001080c4710
(lldb) po [_person methodForSelector:@selector(setAge:)]
0x000000010841f18c
(lldb) p (IMP)0x00000001080c4710
(IMP) $2 = 0x00000001080c4710 (day03-KVO本質`::-[FYPerson setAge:](double) at FYPerson.h:14)
(lldb) p (IMP)0x000000010841f18c
(IMP) $3 = 0x000000010841f18c (Foundation`_NSSetDoubleValueAndNotify)
複製代碼
age
是int
的時候添加以後是Foundation _NSSetIntValueAndNotify
,改爲double
以後,是Foundation _NSSetDoubleValueAndNotify
。那麼咱們能夠推測Foundation
框架中還有不少例如_NSSetBoolValueAndNotify、_NSSetCharValueAndNotify、_NSSetFloatValueAndNotify、_NSSetLongValueAndNotify
等等函數。 運行nm Foundation | grep ValueAndNotify
結果以下:ui
nm Foundation | grep ValueAndNotify
__NSSetBoolValueAndNotify
__NSSetCharValueAndNotify
__NSSetDoubleValueAndNotify
__NSSetFloatValueAndNotify
__NSSetIntValueAndNotify
__NSSetLongLongValueAndNotify
__NSSetLongValueAndNotify
__NSSetObjectValueAndNotify
__NSSetPointValueAndNotify
__NSSetRangeValueAndNotify
__NSSetRectValueAndNotify
__NSSetShortValueAndNotify
__NSSetSizeValueAndNotify
複製代碼
在macOS中可使用
//開始記錄日誌
instrumentObjcMessageSends(YES);
// Do stuff...
instrumentObjcMessageSends(NO);//結束記錄日誌
複製代碼
若是將NSObjCMessageLoggingEnabled
環境變量設置爲YES
,則Objective-C
運行時會將全部已分派的Objective-C
消息記錄到名爲/tmp/msgSends-<pid>
的文件中。每一次運行會生成一個文件,咱們進入到該文件內部:
//初始化
+ FYPerson NSObject initialize
+ FYPerson NSObject new
- FYPerson NSObject init
- FYPerson NSObject addObserver:forKeyPath:options:context:
- FYPerson NSObject _isKVOA
****
//子類設置age [NSKVONotifying_FYPerson setAge:]
- NSKVONotifying_FYPerson NSKVONotifying_FYPerson setAge:
- NSKVONotifying_FYPerson NSObject _changeValueForKey:key:key:usingBlock:
- NSKVONotifying_FYPerson NSObject _changeValueForKeys:count:maybeOldValuesDict:maybeNewValuesDict:usingBlock:
- NSKeyValueUnnestedProperty NSKeyValueUnnestedProperty keyPathIfAffectedByValueForKey:exactMatch:
- NSKeyValueUnnestedProperty NSKeyValueUnnestedProperty _keyPathIfAffectedByValueForKey:exactMatch:
//will changeValueForKey
- NSKeyValueUnnestedProperty NSKeyValueUnnestedProperty object:withObservance:willChangeValueForKeyOrKeys:recurse:forwardingValues:
//FYPerson 設置age
- FYPerson FYPerson setAge:
// didChangeValueForKeyOrKeys
- NSKeyValueUnnestedProperty NSKeyValueUnnestedProperty object:withObservance:didChangeValueForKeyOrKeys:recurse:forwardingValues:
- NSKeyValueUnnestedProperty NSKeyValueProperty keyPath
//找到key 發送 具體的key對應的value 到observe
- NSKVONotifying_FYPerson NSObject valueForKeyPath:
- NSKVONotifying_FYPerson NSObject valueForKey:
+ NSKVONotifying_FYPerson NSObject _createValueGetterWithContainerClassID:key:
-
+ NSKVONotifying_FYPerson NSObject resolveInstanceMethod:
+ NSKVONotifying_FYPerson NSObject resolveInstanceMethod:
- NSKVONotifying_FYPerson FYPerson age
+ NSKeyValueMethodGetter NSObject alloc
- NSKeyValueMethodGetter NSKeyValueMethodGetter initWithContainerClassID:key:method:
- NSKeyValueGetter NSKeyValueAccessor initWithContainerClassID:key:implementation:selector:extraArguments:count:
- NSKVONotifying_FYPerson NSObject respondsToSelector:
- NSKVONotifying_FYPerson NSKVONotifying_FYPerson class
- NSKVONotifying_FYPerson NSKVONotifying_FYPerson _isKVOA
+ FYPerson NSObject class
+ FYPerson NSObject resolveInstanceMethod:
+ FYPerson NSObject resolveInstanceMethod:
//數據字典
+ NSDictionary NSObject self
+ NSMutableDictionary NSObject self
- NSKeyValueChangeDictionary NSKeyValueChangeDictionary initWithDetailsNoCopy:originalObservable:isPriorNotification:
- NSDictionary NSObject init
// 執行觀察者回調函數
- NSKVONotifying_FYPerson FYPerson observeValueForKeyPath:ofObject:change:context:
+ Student NSObject alloc
- Student NSObject init
- Student NSObject dealloc
***//省略一部分代碼
NSKVONotifying_FYPerson NSObject release
- NSKeyValueChangeDictionary NSObject release
- NSKeyValueChangeDictionary NSKeyValueChangeDictionary dealloc
- NSDictionary NSObject dealloc
- NSKeyValueObservationInfo NSObject release
- NSKVONotifying_FYPerson NSObject release
複製代碼
通過仔細把重要的函數過濾出來,咱們能夠了解到person.age = 12
的執行過程是NSKVONotifying_FYPerson setAge:
->NSKeyValueUnnestedProperty object:withObservance:willChangeValueForKeyOrKeys:recurse:forwardingValues
->FYPerson FYPerson setAge:
->NSKeyValueUnnestedProperty NSKeyValueUnnestedProperty object:withObservance:didChangeValueForKeyOrKeys:recurse:forwardingValues:
->NSKVONotifying_FYPerson NSObject valueForKeyPath:
->NSMutableDictionary NSObject self
->- NSKVONotifying_FYPerson FYPerson observeValueForKeyPath:ofObject:change:context:
,咱們來用僞代碼實現一遍:
//person.age = 12
[NSKVONotifying_FYPerson setAge:12];
willChangeValueForKey@"age";
[FYPerson setAge:12];
didChangeValueForKey@"age";
[[NSMutableDictionary alloc] init];
[NSKVONotifying_FYPerson observeValueForKeyPath:ofObject:change:context];
複製代碼
NSKVONotifyin_Person內部結構是怎樣的? 首先咱們知道,NSKVONotifyin_Person做爲Person的子類,其superclass指針指向Person類,而且NSKVONotifyin_Person內部必定對setAge方法作了單獨的實現,那麼NSKVONotifyin_Person同Person類的差異可能就在於其內存儲的對象方法及實現不一樣。 咱們經過runtime分別打印Person類對象和NSKVONotifyin_Person類對象內存儲的對象方法
- (void)viewDidLoad {
[super viewDidLoad];
Person *p1 = [[Person alloc] init];
p1.age = 1.0;
Person *p2 = [[Person alloc] init];
p1.age = 2.0;
// self 監聽 p1的 age屬性
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[p1 addObserver:self forKeyPath:@"age" options:options context:nil];
[self printMethods: object_getClass(p2)];
[self printMethods: object_getClass(p1)];
[p1 removeObserver:self forKeyPath:@"age"];
}
- (void) printMethods:(Class)cls
{
unsigned int count ;
Method *methods = class_copyMethodList(cls, &count);
NSMutableString *methodNames = [NSMutableString string];
[methodNames appendFormat:@"%@ - ", cls];
for (int i = 0 ; i < count; i++) {
Method method = methods[i];
NSString *methodName = NSStringFromSelector(method_getName(method));
[methodNames appendString: methodName];
[methodNames appendString:@" "];
}
NSLog(@"%@",methodNames);
free(methods);
}
//結果以下:
NSKVONotifying_FYPerson - setAge: class dealloc _isKVOA
FYPerson - setAge: age
複製代碼
經過上述代碼咱們發現NSKVONotifyin_Person中有4個對象方法。分別爲setAge: class dealloc _isKVOA,那麼至此咱們能夠畫出NSKVONotifyin_Person的內存結構以及方法調用順序。
這裏NSKVONotifyin_Person重寫class方法是爲了隱藏NSKVONotifyin_Person。不被外界所看到。咱們在p1添加過KVO監聽以後,分別打印p1和p2對象的class能夠發現他們都返回Person。若是NSKVONotifyin_Person不重寫class方法,那麼當對象要調用class對象方法的時候就會一直向上找來到nsobject,而nsobect的class的實現大體爲返回本身isa指向的類,返回p1的isa指向的類那麼打印出來的類就是NSKVONotifyin_Person,可是apple不但願將NSKVONotifyin_Person類暴露出來,而且不但願咱們知道NSKVONotifyin_Person內部實現,因此在內部重寫了class類,直接返回Person類,因此外界在調用p1的class對象方法時,是Person類。這樣p1給外界的感受p1仍是Person類,並不知道NSKVONotifyin_Person子類的存在。
那麼咱們能夠猜想NSKVONotifyin_Person內重寫的class內部實現大體爲
- (Class) class {
// 獲得類對象,在找到類對象父類
return class_getSuperclass(object_getClass(self));
}
複製代碼
最後本身寫代碼驗證一下:
@implementation FYPerson
-(void)willChangeValueForKey:(NSString *)key{
NSLog(@"%s 開始",__func__);
[super didChangeValueForKey:key];
NSLog(@"%s 結束",__func__);
}
- (void)didChangeValueForKey:(NSString *)key{
NSLog(@"%s 開始",__func__);
[super didChangeValueForKey:key];
NSLog(@"%s 結束",__func__);
}
- (void)setAge:(double)age{
_age = age;
NSLog(@"%s",__func__);
}
@end
複製代碼
執行以後結果以下:
-[FYPerson willChangeValueForKey:] 開始
-[FYPerson willChangeValueForKey:] 結束
-[FYPerson setAge:]
-[FYPerson didChangeValueForKey:] 開始
監聽到了age變化: {
kind = 1;
new = 11;
old = 10;
}
-[FYPerson didChangeValueForKey:] 結束
複製代碼
KVO實際上是一個經過runtime註冊創建子類,經過修改instance的isa指針,指向新的子類,重寫instace的class方法來掩蓋,子類擁有本身的set方法,調用順序是willChangeValueForKey方法、原來的setter方法實現、didChangeValueForKey方法,而didChangeValueForKey方法內部又會調用監聽器的observeValueForKeyPath:ofObject:change:context:監聽方法。
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;
複製代碼
其實當Obj調用(void)setValue:(id)value forKey:(NSString *)key
的時候,obj
會主動尋找方法setKey
和_setKey
兩個方法,沒有找到這兩個方法會再去尋找accessInstanceVariablesDirectly
,返回值爲NO
則拋出異常,返回YES
則去按照_key
、_isKey
、key
、isKey
的查找優先級查找成員變量,找到以後直接複製,不然拋出異常。 咱們使用這段代碼來驗證:
@interface FYPerson(){
}
@end
@implementation FYPerson
//code1
- (void)setAge:(NSInteger)age{
NSLog(@"%s %ld",__func__,(long)age);
}
//code2
- (void)_setAge:(NSInteger)age{
NSLog(@"%s %ld",__func__,(long)age);
}
@end
FYPerson *p=[[FYPerson alloc]init];
[p setValue:@(2) forKey:@"age"];
複製代碼
當執行code1
和code2
都有的時候,輸出-[FYPerson setAge:] 2
,當code1
註釋掉,輸出-[FYPerson _setAge:] 2
,能夠看出執行順序是setAge
,沒有setAge
的時候再去執行_setAge
。
如今新增FYPerson
4個成員變量,依次註釋掉他們來測試尋找成員變量的順序。
@interface FYPerson : NSObject
{
@public
NSInteger _age;
NSInteger _isAge;
NSInteger age;
NSInteger isAge;
}
@end
FYPerson *p=[[FYPerson alloc]init];
[p setValue:@(2) forKey:@"age"];
NSLog(@"age:%d _age:%d isAge:%d _isAge:%d",(int)p->age,(int)p->_age,(int)p->isAge,(int)p->_isAge);
複製代碼
age:0 _age:2 isAge:0 _isAge:0
_age
輸出 age:0 isAge:0 _isAge:2
_isAge
輸出 age:2 isAge:0
age
輸出 isAge:2
咱們知道KVC本質也是調用setter方法,那麼會出發KVO嗎?
FYPerson *p=[[FYPerson alloc]init];
[p addObserver:p
forKeyPath:@"age"
options:NSKeyValueChangeNewKey
context:nil];
[p setValue:@2 forKey:@"age"];
[p removeObserver:p forKeyPath:@"age"];
@interface FYPerson(){
@public
NSInteger _age;
NSInteger _isAge;
NSInteger age;
NSInteger isAge;
}
@end
@implementation FYPerson
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"%@",change);
}
@end
//結果
{
kind = 1;
new = 2;
old = 0;
}
複製代碼
通過測試,能夠看出KVC能觸發KVO的。那麼valueForKey:key
底層是怎麼運行的呢?其實底層是按照順序查找四個方法_age
->_isAge
->age
->isAge
。咱們測試一下:
FYPerson *p=[[FYPerson alloc]init];
p->_age = 1;
p->_isAge = 2;
p->age = 3;
p->isAge = 4;
NSLog(@"value:%@",[p valueForKey:@"age"]);
//依次註釋1,2,3,依次輸出是1->2->3->4
複製代碼
KVC其實本質是執行4個set方法和4個get方法,當使用setValue:forKey:key
會觸發KVO,找不到4個方法的時候會拋出異常。
以前看的沒有手動去試驗一下,而後再寫出來,如今總結一下,參考了不少文章,還有macOS中日誌記錄是無心搜索出來了一個老外的blog,你們能夠了解下,之後會有用,後邊會講如何hook objc_msgsend
,感受這個挺好玩的。
本文章之因此圖片比較少,我以爲仍是跟着代碼敲一遍,印象比較深入。
最怕一輩子碌碌無爲,還安慰本身平凡難得。
廣告時間