iOS底層原理 KVO和KVC本質與聯繫 --(3)

上篇講了類的本質,咱們知道實例實際是存儲了成員變量的值和指向類的isa指針,class對象和meta-class對象包含 isasuperclassclass_rw_t這幾種結構體,只是數據不同,isa須要ISA_MASK&以後纔是真正的值。那麼今天咱們在看一下Key-Value Observing的本質。git

KVO本質

首先須要瞭解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 *)contextbash

觸發條件

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
//	self.person.age += 1;
   [self.person willChangeValueForKey:@"age"];
   [self.person didChangeValueForKey:@"age"];
}
複製代碼

當把age具體值的改變,變成手動調用willChangeValueForKeydidChangeValueForKey的時候,結果以下:app

監聽到了age變化: {
   kind = 1;
   new = 10;
   old = 10;
}
複製代碼

newold的值居然同樣,經測試只有同時前後調用willChangeValueForKeydidChangeValueForKey,會觸發回調函數observeValueForKeyPath,由此可知觸發條件是willChangeValueForKeydidChangeValueForKey配合使用。框架

探尋KVO底層實現原理

經過上述代碼咱們發現,一旦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】以後,personisa指針指向了NSKVONotifying_FYPerson,而person2isaFYPerson,能夠看出系統是對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)
複製代碼

ageint的時候添加以後是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的本質

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_isKeykeyisKey的查找優先級查找成員變量,找到以後直接複製,不然拋出異常。 咱們使用這段代碼來驗證:

@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"];

複製代碼

當執行code1code2都有的時候,輸出-[FYPerson setAge:] 2,當code1註釋掉,輸出-[FYPerson _setAge:] 2,能夠看出執行順序是setAge,沒有setAge的時候再去執行_setAge

如今新增FYPerson4個成員變量,依次註釋掉他們來測試尋找成員變量的順序。

@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和KVO聯繫

咱們知道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,感受這個挺好玩的。

本文章之因此圖片比較少,我以爲仍是跟着代碼敲一遍,印象比較深入。


最怕一輩子碌碌無爲,還安慰本身平凡難得。

廣告時間

相關文章
相關標籤/搜索