iOS的KVO和KVC底層原理

1. KVO

一.KVO原理的使用與證明

我們在開發(fā)的過程中經(jīng)常使用KVO和KVC,但是我們并不了解其底層原理和功能,今天我們來詳細(xì)了解下底層原理.
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類創(chuàng)建對象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 把初始化的值提供給處理方法薪丁,一旦注冊,立馬就會調(diào)用一次。通常它會帶有新值,而不會帶有舊值。 
    NSKeyValueObservingOptionPrior 分2次調(diào)用袜啃。在值改變之前和值改變之后。 
   */
    // 給person1對象添加KVO監(jiān)聽
    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;
}
//當(dāng)key路徑對應(yīng)的屬性值發(fā)生改變時,監(jiān)聽器就會回調(diào)自身的監(jiān)聽方法发乔,如下
- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary<NSKeyValueChangeKey,id> *)change
                       context:(void *)contex
    
}

控制器銷毀了熟妓,應(yīng)當(dāng)及時移除觀察者。

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

觸摸手機屏幕,獲得log.

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

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


person1-isa的指針的變化.png

person1的isa指針居然由Person變成了NSKVONotifying_Person栏尚,我們知道實例對象(person1起愈、person2)的isa指針指向類對象(關(guān)于isa指針方面的知識,可以參考這篇文章译仗,講得比較容易理解抬虽。淺談Objective-C的對象本質(zhì)的理解),這樣一來也就說明person1的直接類對象并不是Person纵菌,而是NSKVONotifying_Person這個類阐污。
我們還可以進一步的確實是否生成了NSKVONotifying_Person這個類,我們在項目中創(chuàng)建一個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

同過這兩種方式說明了當(dāng)我們?yōu)閜erson1的屬性添加了觀察者模式的之后,系統(tǒng)通過runtime會動態(tài)為我們創(chuàng)建一個繼承自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監(jiān)聽之前 - %p %p",
          [self.person1 methodForSelector:@selector(setAge:)],
          [self.person2 methodForSelector:@selector(setAge:)]);
    // 給person1對象添加KVO監(jiān)聽
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.person1 addObserver:self forKeyPath:@"age" options:options context:@"kvo監(jiān)聽"];
    NSLog(@"person1添加KVO監(jiān)聽之后 - %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監(jiān)聽之前 - 0x10a930570 0x10a930570
2019-07-17 09:29:40.806719+0800 Student[1395:28559] person1添加KVO監(jiān)聽之后 - 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監(jiān)聽
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.person1 addObserver:self forKeyPath:@"age" options:options context:@"kvo監(jiān)聽"];
    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的結(jié)構(gòu)

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


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

NSKVONotifying_MJperson中的class是重寫父類的class方法,原因是屏蔽內(nèi)部實現(xiàn),隱藏NSKVONotifying_MJperson類.
NSKVONotifying_MJperson是Runtime動態(tài)創(chuàng)建的一個類,是MJperson的一個子類.NSKVONotifying_MJperson的set方法會調(diào)用.
子類的set方法的實現(xiàn):

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

_NSSetIntValueAndNotify中調(diào)用了:

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

didChangeValueForkey的實現(xiàn):

-(void)didChangeValueForkey:(NSString *)key{
    //通知監(jiān)聽器,某某屬性發(fā)生了改變
    [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] 監(jiān)聽到<Person: 0x600003013860>的age屬性值改變了 - {
    kind = 1;
    new = 21;
    old = 1;
} - kvo監(jiān)聽
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的內(nèi)部實現(xiàn):

[self willChangeValueForkey:@"age"];
 //原來set的實現(xiàn)
[self didChangeValueForkey:@"age"];

1.調(diào)用willChangeValueForkey.
2.調(diào)用原來的setter的實現(xiàn).
3.調(diào)用didChangeValueForkey,didChangeValueForKey:內(nèi)部會調(diào)用observer的observeValueForKeyPath:ofObject:change:context:方法.
面試問題
1.iOS用什么方式實現(xiàn)對一個對象的KVO?(KVO的本質(zhì)是什么咱圆?)
利用RuntimeAPI動態(tài)生成一個子類疤剑,并且讓instance對象的isa指向這個全新的子類;
當(dāng)修改instance對象的屬性時闷堡,會調(diào)用Foundation的_NSSetXXXValueAndNotify函數(shù)
willChangeValueForKey:
②父類原來的setter
didChangeValueForKey:
didChangeValueForKey內(nèi)部會觸發(fā)監(jiān)聽器(Oberser)的監(jiān)聽方法( observeValueForKeyPath:ofObject:change:context:
2.如何手動觸發(fā)KVO?
對監(jiān)聽的對象手動調(diào)用下面兩行代碼即可疑故。

- (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(@"監(jiān)聽到%@的%@屬性值改變了 - %@ - %@", object, keyPath, change, context);
}

3.KVO與代理的效率問題杠览?
KVO的效率比代理的效率低,因為KVO需要動態(tài)地生成一個類NSKVONotifying_className纵势,耗時踱阿。
4.使用KVC給對象屬性賦值管钳,能不能觸發(fā)KVO?
可以觸發(fā)KVO软舌。因為KVC本質(zhì)上會調(diào)用屬性的setXxx:方法才漆。

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

5.直接修改成員變量會觸發(fā)KVO嘛?
不會觸發(fā)KVO佛点,因為修改成員變量不會觸發(fā)set方法醇滥。

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"調(diào)用帶下劃線的成員變量");
    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的區(qū)別:
key:只能接受當(dāng)前類所具有的屬性,不管是自己的超营,還是從父類繼承過來的鸳玩,如view.setValue(CGRectZero(),key: "frame");
keypath: 除了能接受當(dāng)前類的屬性演闭,還能接受當(dāng)前類屬性的屬性不跟,即可以接受關(guān)系鏈,如view.setValue(5,keypath: "layer.cornerRadius");

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

1. KVC的賦值過程

KVC在賦值的時候米碰,按照setKey:窝革、_setKey:的順序查找對象是否有對應(yīng)的方法實現(xiàn),如果有的話就傳遞參數(shù)并調(diào)用方法吕座,如過這兩個方法都沒有實現(xiàn)虐译,則調(diào)用對象的+ (BOOL)accessInstanceVariablesDirectly方法,查看是否允許直接訪問成員變量米诉。下面我們證明一下:
A:證明先調(diào)用- (void)setAge:(NSUInteger)age方法,新建一個Person類菱蔬,不添加任何屬性,實現(xiàn)- (void)setAge:(NSUInteger)age史侣、- (void)_setAge:(NSUInteger)age方法拴泌。初始化一個Person實例并對其進行KVC賦值,看系統(tǒng)調(diào)用結(jié)果惊橱。

#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,看系統(tǒng)調(diào)用結(jié)果。

#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

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

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

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

注意鼻忠,這里面為什么提到對象的成員變量,而不是屬性呢杈绸?
如果是屬性的話帖蔓,系統(tǒng)自動幫我們實現(xiàn)了set方法矮瘟,所以KVC總是可以找到它需要的`setKey:`方法。如果是
成員變量塑娇,系統(tǒng)就不會為你實現(xiàn)set方法了.

KVC在訪問成員變量時也嚴(yán)格按照_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

通過設(shè)置斷點觀察對象成員變量值得變化,證明了 ‘嚴(yán)格按照_key次坡、_isKey呼猪、key、isKey的順序查找’的結(jié)論.
setValue forkey的原理:

kvc的賦值的過程.png

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

3. KVC的取值過程

- (id)valueForKey:(NSString *)key;- (id)valueForKeyPath:(NSString *)keyPath;方法取值的時候砸琅,按照getKey宋距、key、isKey症脂、_key的順序查找對應(yīng)方法谚赎,一旦找到就調(diào)用方法獲取值。如果沒有找到以上四個方法诱篷,同樣會調(diào)用+(BOOL)accessInstanceVariablesDirectly方法壶唤,看是否具備直接訪問成員變量的權(quán)限。與KVC的賦值過程相同棕所,在查找成員變量的時候闸盔,也是嚴(yán)格按照 _key、_isKey琳省、key迎吵、isKey的順序查找的。找到了就直接取值针贬,都沒有找到的話击费,后果也是相同的,即調(diào)用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的取值時候調(diào)用的方法順序依次為:getKey批幌、key、isKey嗓节、_key荧缘。
Value forkey的原理:

kvc的取值過程.png

                            想了解更多iOS學(xué)習(xí)知識請聯(lián)系:QQ(814299221)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市拦宣,隨后出現(xiàn)的幾起案子截粗,更是在濱河造成了極大的恐慌,老刑警劉巖鸵隧,帶你破解...
    沈念sama閱讀 218,640評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件绸罗,死亡現(xiàn)場離奇詭異,居然都是意外死亡豆瘫,警方通過查閱死者的電腦和手機珊蟀,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,254評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來外驱,“玉大人育灸,你說我怎么就攤上這事£怯睿” “怎么了磅崭?”我有些...
    開封第一講書人閱讀 165,011評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長瓦哎。 經(jīng)常有香客問我砸喻,道長,這世上最難降的妖魔是什么蒋譬? 我笑而不...
    開封第一講書人閱讀 58,755評論 1 294
  • 正文 為了忘掉前任割岛,我火速辦了婚禮,結(jié)果婚禮上羡铲,老公的妹妹穿的比我還像新娘蜂桶。我一直安慰自己,他們只是感情好也切,可當(dāng)我...
    茶點故事閱讀 67,774評論 6 392
  • 文/花漫 我一把揭開白布扑媚。 她就那樣靜靜地躺著,像睡著了一般雷恃。 火紅的嫁衣襯著肌膚如雪疆股。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,610評論 1 305
  • 那天倒槐,我揣著相機與錄音旬痹,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛两残,可吹牛的內(nèi)容都是我干的永毅。 我是一名探鬼主播,決...
    沈念sama閱讀 40,352評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼人弓,長吁一口氣:“原來是場噩夢啊……” “哼沼死!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起崔赌,我...
    開封第一講書人閱讀 39,257評論 0 276
  • 序言:老撾萬榮一對情侶失蹤意蛀,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后健芭,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體县钥,經(jīng)...
    沈念sama閱讀 45,717評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,894評論 3 336
  • 正文 我和宋清朗相戀三年慈迈,在試婚紗的時候發(fā)現(xiàn)自己被綠了若贮。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,021評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡吩翻,死狀恐怖兜看,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情狭瞎,我是刑警寧澤细移,帶...
    沈念sama閱讀 35,735評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站熊锭,受9級特大地震影響弧轧,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜碗殷,卻給世界環(huán)境...
    茶點故事閱讀 41,354評論 3 330
  • 文/蒙蒙 一精绎、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧锌妻,春花似錦代乃、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,936評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至吭历,卻和暖如春堕仔,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背晌区。 一陣腳步聲響...
    開封第一講書人閱讀 33,054評論 1 270
  • 我被黑心中介騙來泰國打工摩骨, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留通贞,地道東北人。 一個月前我還...
    沈念sama閱讀 48,224評論 3 371
  • 正文 我出身青樓恼五,卻偏偏與公主長得像昌罩,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子灾馒,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,974評論 2 355