iOS的KVO和KVC底層原理

1. KVO

一.KVO原理的使用與證明

我們在開發的過程中經常使用KVO和KVC,但是我們并不了解其底層原理和功能,今天我們來詳細了解下底層原理.
KVO的機制比較隱蔽,所以我們通過寫代碼的方式去驗證:
新建類Person

#import <Foundation/Foundation.h>
@interface Person : NSObject
@property (nonatomic, assign) int age;
@end

#import "Person.h"
@implementation Person
- (void)setAge:(int)age
{
    _age = age;
}
@end

給新建的Person類創建對象person1與person2,并對person1的age屬性添加observer(鍵值觀察)。

- (void)viewDidLoad {
    [super viewDidLoad];
    self.person1 = [[Person alloc] init];
    self.person1.age = 1;
    
    self.person2 = [[Person alloc] init];
    self.person2.age = 2;
    /* 
    options: 有4個值,分別是:
    NSKeyValueObservingOptionOld 把更改之前的值提供給處理方法 
    NSKeyValueObservingOptionNew 把更改之后的值提供給處理方法 
    NSKeyValueObservingOptionInitial 把初始化的值提供給處理方法,一旦注冊,立馬就會調用一次。通常它會帶有新值,而不會帶有舊值。 
    NSKeyValueObservingOptionPrior 分2次調用。在值改變之前和值改變之后。 
   */
    // 給person1對象添加KVO監聽
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.person1 addObserver:self forKeyPath:@"age" options:options context:@"觀察者"];
}

為了測試方便,點擊屏幕改變age的值,在- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event方法里面修改person1的age屬性值。

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    self.person1.age = 22;
}
//當key路徑對應的屬性值發生改變時,監聽器就會回調自身的監聽方法,如下
- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary<NSKeyValueChangeKey,id> *)change
                       context:(void *)contex
    
}

控制器銷毀了,應當及時移除觀察者。

- (void)dealloc {
    [self.person1 removeObserver:self forKeyPath:@"age"];
}

觸摸手機屏幕,獲得log.

2019-07-16 15:18:33.167839+0800 Student[1390:114826] 監聽到<Person: 0x60000392c570>的age屬性值改變了 - {
    kind = 1;
    new = 22;
    old = 1;
} - 觀察者

想知道KVO都做了什么我們可以通過觀察isa和對象的指針.


person1-isa的指針的變化.png

person1的isa指針居然由Person變成了NSKVONotifying_Person,我們知道實例對象(person1、person2)的isa指針指向類對象(關于isa指針方面的知識,可以參考這篇文章,講得比較容易理解。淺談Objective-C的對象本質的理解),這樣一來也就說明person1的直接類對象并不是Person,而是NSKVONotifying_Person這個類。
我們還可以進一步的確實是否生成了NSKVONotifying_Person這個類,我們在項目中創建一個NSKVONotifying_Person的類,再次運行項目的時候會報錯:

2019-07-16 15:39:45.191295+0800 Student[1576:124208] [general] KVO failed to allocate 
class pair for name NSKVONotifying_Person, automatic key-value observing will not
 work for this class

同過這兩種方式說明了當我們為person1的屬性添加了觀察者模式的之后,系統通過runtime會動態為我們創建一個繼承自Person的類NSKVONotifying_Person.

其他證明KVO機制的方法
- (void)viewDidLoad {
    [super viewDidLoad];
    self.person1 = [[Person alloc] init];
    self.person1.age = 1;
    
    self.person2 = [[Person alloc] init];
    self.person2.age = 2;
    
    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:@"kvo監聽"];
    NSLog(@"person1添加KVO監聽之后 - %p %p",
          [self.person1 methodForSelector:@selector(setAge:)],
          [self.person2 methodForSelector:@selector(setAge:)]);
}
//log日志:添加kvo機制前后isa指向的變化
2019-07-17 09:29:40.806381+0800 Student[1395:28559] person1添加KVO監聽之前 - 0x10a930570 0x10a930570
2019-07-17 09:29:40.806719+0800 Student[1395:28559] person1添加KVO監聽之后 - 0x10ac8b3d2 0x10a930570
(lldb) p IMP(0x10a930570)
(IMP) $0 = 0x000000010a930570 (Student`-[Person setAge:] at Person.m:13)
(lldb) p IMP(0x10ac8b3d2)
(IMP) $1 = 0x000000010ac8b3d2 (Foundation`_NSSetIntValueAndNotify)
- (void)viewDidLoad {
    [super viewDidLoad];
    self.person1 = [[Person alloc] init];
    self.person1.age = 1;
    
    self.person2 = [[Person alloc] init];
    self.person2.age = 2;

    // 給person1對象添加KVO監聽
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.person1 addObserver:self forKeyPath:@"age" options:options context:@"kvo監聽"];
    NSLog(@"類對象 - %@ %@",
          object_getClass(self.person1),  // self.person1.isa
          object_getClass(self.person2)); // self.person2.isa
    
    NSLog(@"元類對象 - %@ %@",
          object_getClass(object_getClass(self.person1)), // self.person1.isa.isa
          object_getClass(object_getClass(self.person2))); // self.person2.isa.isa
}
//log日志:添加kvo機制前后isa指向的變化
2019-07-17 09:37:58.106744+0800 Student[1477:31924] 類對象 - NSKVONotifying_Person Person
2019-07-17 09:37:58.106895+0800 Student[1477:31924] 元類對象 - NSKVONotifying_Person Person

二. KVO的結構

kvo的全稱是Key-Value Observing,俗稱"鍵值監聽",可以用與監聽某個對象屬性值的改變.


未使用kvo.png
使用kvo.png
kvo的原理.png

NSKVONotifying_MJperson中的class是重寫父類的class方法,原因是屏蔽內部實現,隱藏NSKVONotifying_MJperson類.
NSKVONotifying_MJperson是Runtime動態創建的一個類,是MJperson的一個子類.NSKVONotifying_MJperson的set方法會調用.
子類的set方法的實現:

-(void)setAge:(int) age{
    //Foundation框架的_NSSetIntValueAndNotify的方法.
    _NSSetIntValueAndNotify();
}

_NSSetIntValueAndNotify中調用了:

[self willChangeValueForkey:@"age"];
[super setAge:age];
[self didChangeValueForkey:@"age"];

didChangeValueForkey的實現:

-(void)didChangeValueForkey:(NSString *)key{
    //通知監聽器,某某屬性發生了改變
    [oberser observeValueForKeyPath:key ofObject:self change:nil context:nil];
}
佐證NSSetIntValueAndNotify的原理
#import "Person.h"
@implementation Person
- (void)setAge:(int)age {
    _age = age;
}
- (void)willChangeValueForKey:(NSString *)key {
    [super willChangeValueForKey:key];
 
    NSLog(@"willChangeValueForKey");
}
- (void)didChangeValueForKey:(NSString *)key {
    NSLog(@"didChangeValueForKey - begin");
   
    [super didChangeValueForKey:key];
   
    NSLog(@"didChangeValueForKey - end");
}
@end
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self.person1 setAge:21];
}
//log日志:
2019-07-17 09:41:45.186419+0800 Student[1513:33394] willChangeValueForKey
2019-07-17 09:41:45.186572+0800 Student[1513:33394] didChangeValueForKey - begin
2019-07-17 09:41:45.186850+0800 Student[1513:33394] 監聽到<Person: 0x600003013860>的age屬性值改變了 - {
    kind = 1;
    new = 21;
    old = 1;
} - kvo監聽
2019-07-17 09:41:45.186967+0800 Student[1513:33394] didChangeValueForKey - end
2019-07-17 09:41:45.187052+0800 Student[1513:33394] person1 age = 21,person2 age = 22

補充的問題

_NSSet*ValueAndNotify的存在.png

_NSSet*ValueAndNotify的內部實現:

[self willChangeValueForkey:@"age"];
 //原來set的實現
[self didChangeValueForkey:@"age"];

1.調用willChangeValueForkey.
2.調用原來的setter的實現.
3.調用didChangeValueForkey,didChangeValueForKey:內部會調用observer的observeValueForKeyPath:ofObject:change:context:方法.
面試問題
1.iOS用什么方式實現對一個對象的KVO?(KVO的本質是什么?)
利用RuntimeAPI動態生成一個子類,并且讓instance對象的isa指向這個全新的子類;
當修改instance對象的屬性時,會調用Foundation的_NSSetXXXValueAndNotify函數
willChangeValueForKey:
②父類原來的setter
didChangeValueForKey:
didChangeValueForKey內部會觸發監聽器(Oberser)的監聽方法( observeValueForKeyPath:ofObject:change:context:
2.如何手動觸發KVO?
對監聽的對象手動調用下面兩行代碼即可。

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [self.person1 willChangeValueForKey:@"age"];
    [self.person1 didChangeValueForKey:@"age"];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"監聽到%@的%@屬性值改變了 - %@ - %@", object, keyPath, change, context);
}

3.KVO與代理的效率問題?
KVO的效率比代理的效率低,因為KVO需要動態地生成一個類NSKVONotifying_className,耗時。
4.使用KVC給對象屬性賦值,能不能觸發KVO?
可以觸發KVO。因為KVC本質上會調用屬性的setXxx:方法。

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"調用帶下劃線的成員變量");
    self.person1.age = 10;
}

5.直接修改成員變量會觸發KVO嘛?
不會觸發KVO,因為修改成員變量不會觸發set方法。

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"調用帶下劃線的成員變量");
    self.person1->_age = 10;
}

2. 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;

key和keyPath的區別:
key:只能接受當前類所具有的屬性,不管是自己的,還是從父類繼承過來的,如view.setValue(CGRectZero(),key: "frame");
keypath: 除了能接受當前類的屬性,還能接受當前類屬性的屬性,即可以接受關系鏈,如view.setValue(5,keypath: "layer.cornerRadius");

一. KVC的賦值和取值過程和原理

1. KVC的賦值過程

KVC在賦值的時候,按照setKey:、_setKey:的順序查找對象是否有對應的方法實現,如果有的話就傳遞參數并調用方法,如過這兩個方法都沒有實現,則調用對象的+ (BOOL)accessInstanceVariablesDirectly方法,查看是否允許直接訪問成員變量。下面我們證明一下:
A:證明先調用- (void)setAge:(NSUInteger)age方法,新建一個Person類,不添加任何屬性,實現- (void)setAge:(NSUInteger)age、- (void)_setAge:(NSUInteger)age方法。初始化一個Person實例并對其進行KVC賦值,看系統調用結果。

#import <Foundation/Foundation.h>
@interface Person : NSObject
@property(nonatomic, assign) NSInteger age;
@end

#import "Person.h"
@implementation Person
- (void)setAge:(NSUInteger)age{
    NSLog(@"setAge : %lu",(unsigned long)age);
}
- (void)_setAge:(NSUInteger)age{
    NSLog(@"_setAge : %lu",(unsigned long)age);
}
@end

#import <Foundation/Foundation.h>
#import "Person.h"
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        [person setValue:@20 forKey:@"age"];
    }
    return 0;
}
//log日志
2019-07-16 16:47:55.228096+0800 Student[1841:143855] setAge : 20

B:將Person類中的- (void)setAge:(NSUInteger)age注釋掉,保留- (void)_setAge:(NSUInteger)age,看系統調用結果。

#import "Person.h"
@implementation Person
//- (void)setAge:(NSUInteger)age{
//    NSLog(@"setAge : %lu",(unsigned long)age);
//}
- (void)_setAge:(NSUInteger)age{
    NSLog(@"_setAge : %lu",(unsigned long)age);
}
@end
//log日志
//2018-08-02 23:15:08.754741+0800 Student[1841:544138] _setAge : 20

由以上結果可見,我們調用方法
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
- (void)setValue:(id)value forKey:(NSString *)key;時,OC底層依次查找了setKey:_setKey:方法。

2. 如果沒有setKey:_setKey:方法怎么辦?

沒有實現setKey:_setKey:方法,系統將查看+(BOOL)accessInstanceVariablesDirectly方法的返回結果(該方法默認返回YES),這個方法決定是否可以直接訪問成員變量key。
注意:如果+(BOOL)accessInstanceVariablesDirectly方法返回了NO,那么就會調用setValue:forUndefinedKey:并拋出異常NSUnknownKeyException!

注意,這里面為什么提到對象的成員變量,而不是屬性呢?
如果是屬性的話,系統自動幫我們實現了set方法,所以KVC總是可以找到它需要的`setKey:`方法。如果是
成員變量,系統就不會為你實現set方法了.

KVC在訪問成員變量時也嚴格按照_key、_isKey、key、isKey的順序查找。下面我們將上面代碼中- (void)setAge:(NSUInteger)age、- (void)_setAge:(NSUInteger)age注釋掉,并添加四個成員變量_age、_isAge、age、isAge。

#import <Foundation/Foundation.h>
@interface Person : NSObject
{
    int _age;
    int _isAge;
    int age;
    int isAge;
}
@end

#import "Person.h"
@implementation Person
+(BOOL)accessInstanceVariablesDirectly{
    return YES;
}
@end

通過設置斷點觀察對象成員變量值得變化,證明了 ‘嚴格按照_key、_isKey、key、isKey的順序查找’的結論.
setValue forkey的原理:

kvc的賦值的過程.png

+(BOOL)accessInstanceVariablesDirectly的方法是用來確認是否可以訪問成員變量, +(BOOL)accessInstanceVariablesDirectly默認是Yes.
kvc的內部調用了①willChangeValueForkeydidChangeValueForkey兩個方法,從而觸發了kvo,所以不用實現set方法也可以調起kvo.

3. KVC的取值過程

- (id)valueForKey:(NSString *)key;- (id)valueForKeyPath:(NSString *)keyPath;方法取值的時候,按照getKey、key、isKey、_key的順序查找對應方法,一旦找到就調用方法獲取值。如果沒有找到以上四個方法,同樣會調用+(BOOL)accessInstanceVariablesDirectly方法,看是否具備直接訪問成員變量的權限。與KVC的賦值過程相同,在查找成員變量的時候,也是嚴格按照 _key、_isKey、key、isKey的順序查找的。找到了就直接取值,都沒有找到的話,后果也是相同的,即調用setValue:forUndefinedKey:并拋出異常NSUnknownKeyException*!

#import "Person.h"
@interface Person ()
@end

@implementation Person
+ (BOOL)accessInstanceVariablesDirectly{
    return  YES;
}
- (int)getAge{
    NSLog(@"getAge");
    return 5;
}
- (int)age{
    NSLog(@"age");
    return 10;
}
- (int)isAge{
    NSLog(@"isAge");
    return 15;
}
- (int)_age{
    NSLog(@"_age");
    return 20;
}
@end

#import <Foundation/Foundation.h>
#import "Person.h"
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];
          [person valueForKey:@"age"];
    }
    return 0;
}

依次對getKey、key、isKey、_key方法進行注釋,通過log日志可見KVC的取值時候調用的方法順序依次為:getKey、key、isKey、_key。
Value forkey的原理:

kvc的取值過程.png

                            想了解更多iOS學習知識請聯系:QQ(814299221)
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,646評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,595評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,560評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,035評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,814評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,224評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,301評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,444評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,988評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,804評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,998評論 1 370
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,544評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,237評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,665評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,927評論 1 287
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,706評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,993評論 2 374