iOS鍵值編碼(KVC)與鍵值監(jiān)聽(KVO)、

描述

KVO全稱KeyValueObserving。俗稱“鍵值監(jiān)聽”。
利用Key來找到某個(gè)對(duì)象并監(jiān)聽其屬性的改變。也是一種典型的觀察者模式。
在某個(gè)對(duì)象注冊(cè)監(jiān)聽者后,被監(jiān)聽對(duì)象的屬性發(fā)生改變時(shí),會(huì)發(fā)送一個(gè)通知給監(jiān)聽者。以便監(jiān)聽者執(zhí)行回調(diào)操作。

本文演示代碼地址

KVO方法介紹

1、通過addObserver:forKeyPath:options:context:方法注冊(cè)觀察者。

/**
 添加KVO監(jiān)聽

 @param observer 添加觀察者,被觀察者屬性變化通知的目標(biāo)對(duì)象

 @param keyPath  監(jiān)聽的屬性路徑

 @param options  監(jiān)聽類型 - options支持按位或來監(jiān)聽多個(gè)事件類型

 @param context  監(jiān)聽上下文context主要用于在多個(gè)監(jiān)聽器對(duì)象監(jiān)聽相同keyPath時(shí)進(jìn)行區(qū)分

 */

- (void)addObserver:(NSObject *)observer
         forKeyPath:(NSString *)keyPath
            options:(NSKeyValueObservingOptions)options
            context:(nullable void *)context;

2、通過 observeValueForKeyPath:ofObject:change:context:獲得回調(diào),從而做出事件處理。

/**
 監(jiān)聽器對(duì)象的監(jiān)聽回調(diào)方法

 @param keyPath 監(jiān)聽的屬性路徑

 @param object 被觀察者

 @param change 監(jiān)聽內(nèi)容的變化

 @param context context為監(jiān)聽上下文,由add方法回傳

 */
- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary<NSKeyValueChangeKey,id> *)change
                       context:(void *)context;

3、當(dāng)觀察者不需要監(jiān)聽時(shí),調(diào)用可以removeObserver:forKeyPath:方法將KVO移除。需要注意的是:調(diào)用removeObserver需要在觀察者消失之前,否則會(huì)導(dǎo)致Crash。

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

簡(jiǎn)單示例

@interface ViewController ()

@property (nonatomic,strong) Animal * ani;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.ani = [[Animal alloc] init];

    self.ani.age = 10;
    
  // 添加鍵值監(jiān)聽
    [self.ani addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];
    

}

// 點(diǎn)擊事件,觸發(fā)屬性修改
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.ani.age += 5;
}

// 獲得回調(diào),實(shí)時(shí)監(jiān)聽屬性改變、
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    NSLog(@"監(jiān)聽到了%@對(duì)象的%@屬性由%@變成了%@屬性",object,keyPath,change,change);
}

// 需要在不使用的時(shí)候,移除監(jiān)聽
- (void)dealloc{
    [self.ani removeObserver:self forKeyPath:@"age"];
}

@end

KVO原理探究

1、利用RuntimeAPI動(dòng)態(tài)生成一個(gè)子類NSKVONotifying_XXX,并且讓當(dāng)前instance對(duì)象isa指針指向這個(gè)全新子類。
2、當(dāng)修改instance對(duì)象的屬性時(shí),會(huì)觸發(fā)setter方法,調(diào)用Foundation的_NSSetXXXValueAndnotify函數(shù)

  • 調(diào)用willChangeValueForKey:
  • 調(diào)用原來的setter實(shí)現(xiàn)(父類原來的setter方法)
  • 調(diào)用didChangeValueForKey
    此時(shí)內(nèi)部觸發(fā)監(jiān)聽器(Oberser)的監(jiān)聽方法 - observeValueForKeyPath: ofObject: change: context:

代碼驗(yàn)證上述流程

第一步:通過runtime查看isa指針指向的 class對(duì)象

如果觀察 Animal的age屬性。
系統(tǒng)會(huì)在運(yùn)行時(shí)生成NSKVONotifying_Animal
在NSKVONotifying_Animal中重寫setter、class、dealloc等方法。
使Animal實(shí)例對(duì)象的isa指針指向NSKVONotifying_Animal
NSKVONotifying_Animal的superclass指向Animal

探究過程

    // 注冊(cè)成為觀察者
    NSLog(@"添加KVO之前,Animal的class是 = %s",object_getClassName(self.ani));
    [self.ani addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];
    NSLog(@"添加KVO之后,Animal的class是 = %s",object_getClassName(self.ani));

結(jié)果如下:

 添加KVO之前,Animal的class是 = Animal
 添加KVO之后,Animal的class是 = NSKVONotifying_Animal

注冊(cè)成為觀察者之后,類變成了NSKVONotifying_Animal而再是 Animal 。
我們先看一下NSKVONotifying_Animal類內(nèi)部的方法。

 #import <objc/runtime.h>

//打印某個(gè)類中的所有方法
- (void)printMethonNamesFromClass:(Class)cls{
    
    unsigned int count;
    //獲取方法列表
    Method *methodList = class_copyMethodList(cls, &count);
    
    //保存方法名
    NSMutableString *methonNames = @"".mutableCopy;
    
    for (int i = 0; i < count; i++) {
        
        //獲取方法
        Method method = methodList[i];
        
        NSString *methodName = NSStringFromSelector(method_getName(method));
        
        [methonNames appendFormat:@"%@", [NSString stringWithFormat:@"%@, ",methodName]];
        
    }
    
    NSLog(@"methonNames = %@",methonNames);
    //c語(yǔ)音創(chuàng)建的list記得釋放
    free(methodList);
}

結(jié)果如下:

 [self printMethonNamesFromClass:object_getClass(self.ani)];
 ----------------------------------------------
 methonNames = setAge:, class, dealloc, _isKVOA,
畫圖分析KVO內(nèi)部結(jié)構(gòu)

第二步:- (void)setAge:(int)age方法

為了比較在注冊(cè)觀察者前后setter方法的變化,我們新創(chuàng)建一個(gè)實(shí)例ani1

    self.ani = [[Animal alloc] init];
    self.ani1 = [[Animal alloc] init];

    NSLog(@"添加KVO之前,ani的setAge是 = %p,未添加KVO的ani1的setAge是 = %p",
          [self.ani methodForSelector:@selector(setAge:)],
          [self.ani1 methodForSelector:@selector(setAge:)]);

    // 注冊(cè)成為觀察者
    [self.ani addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];
    
    NSLog(@"添加KVO之后,ani的setAge是 = %p,未添加KVO的ani1的setAge是 = %p",
          [self.ani methodForSelector:@selector(setAge:)],
          [self.ani1 methodForSelector:@selector(setAge:)]);

結(jié)果如下:

添加KVO之前,ani的setAge是 = 0x10d751460,未添加KVO的ani1的setAge是 = 0x10d751460
添加KVO之后,ani的setAge是 = 0x10daaacf2,未添加KVO的ani1的setAge是 = 0x10d751460

這里可以看到,添加KVO前后,setAge方法有所改變
我們進(jìn)入debugger來看看這第這個(gè)方法的實(shí)現(xiàn)到底是怎樣的:

(gdb) print (IMP) 0x10daaacf2 
$1 = (IMP) 0x96a1a550 <_NSSetIntValueAndNotify> 

原來在重寫的NSKVONotifying_Animal-setAge方法中會(huì)調(diào)用_NSSetIntValueAndNotify:

// 注:Foundation框架中類似_NSSetIntValueAndNotify的方法實(shí)現(xiàn)還有很多:
__NSSetBoolValueAndNotify
__NSSetCharValueAndNotify
__NSSetDoubleValueAndNotify
__NSSetFloatValueAndNotify
__NSSetIntValueAndNotify
__NSSetLongLongValueAndNotify
__NSSetLongValueAndNotify
__NSSet0bjectValueAndNotify
__NSSetPointValueAndNotify
__NSSetRangeValueAndNotify
__NSSetRectValueAndNotify
__NSSetShortValueAndNotify
__NSSetSizeValueAndNotify
查看_NSSet*ValueAndNotify的內(nèi)部實(shí)現(xiàn)
- (void)setAge:(int)age{
    _NSSet*ValueAndNotify();
}

// 因?yàn)開NSSetIntValueAndNotify在Foundation框架中,無法查看起具體實(shí)現(xiàn),根據(jù)實(shí)踐猜測(cè)大致為代碼如下:
void _NSSet*ValueAndNotify()
{
      [self willChangeValueForKey:@"age"];
      [super setAge:age];
      [self didChangeValueForKey:@"age"];
}
- (void)didChangeValueForKey:(NSString *)key{
          //通過監(jiān)聽器,監(jiān)聽屬性發(fā)生了改變
  [oberser observeValueForKeyPath:key ofObject:self change:nil context:nil];
}
在Animal類中驗(yàn)證
#import "Animal.h"

@implementation Animal

//Animal內(nèi)部代碼實(shí)現(xiàn)
- (void)setAge:(int)age{
    _age = age;
    NSLog(@"setAge");
}
- (void)willChangeValueForKey:(NSString *)key{
    [super willChangeValueForKey:key];
    NSLog(@"willChangeValueForKey");
}
- (void)didChangeValueForKey:(NSString *)key{
    NSLog(@"didChangeValueForKey == begin");
    [super didChangeValueForKey:key];
    NSLog(@"didChangeValueForKey == end");
}
@end

結(jié)果如下:

kvoAndkvoDemo[2391:50226] willChangeValueForKey
kvoAndkvoDemo[2391:50226] setAge
kvoAndkvoDemo[2391:50226] didChangeValueForKey == begin
kvoAndkvoDemo[2391:50226] 監(jiān)聽到了<Animal: 0x600000da3690>對(duì)象的age屬性由{
    kind = 1;
    new = 15;
    old = 10;
}變成了{(lán)
    kind = 1;
    new = 15;
    old = 10;
}屬性
kvoAndkvoDemo[2391:50226] didChangeValueForKey == end

如果不添加監(jiān)聽,則不會(huì)執(zhí)行willChangeValueForKeydidChangeValueForKey方法、驗(yàn)證成功!

匯總

1、當(dāng)你觀察一個(gè)對(duì)象時(shí),系統(tǒng)通過Runtime動(dòng)態(tài)的創(chuàng)建一個(gè)該類的派生類,這個(gè)類繼承自該對(duì)象的原本的類,并了重寫被觀察屬性的setter方法。
2、isa指針會(huì)指向這個(gè)新創(chuàng)建的類,該對(duì)象就變成新創(chuàng)建子類的實(shí)例了、
3、重寫的setter方法,執(zhí)行_NSSet*ValueAndNotify,會(huì)負(fù)責(zé)在調(diào)用原來的setter方法前后,通知所有觀察對(duì)象:值的改變。

拓展思考

1、用法聽起來和NSNotification很相似啊, 其實(shí)NSNotification也是觀察者模式,但是NSNotification是一種廣播機(jī)制,KVO是被觀察者直接發(fā)消息給觀察者,是對(duì)象間的相互溝通。NSNotification則是兩者都和通知中心對(duì)象交互,對(duì)象之間不知道彼此。
2、KVO行為是同步的,并且發(fā)生與觀察的值發(fā)生在同樣的線程上,沒有隊(duì)列或Run-Loop處理。【使用注意】

用途

常見運(yùn)用是監(jiān)聽ScrollViewcontentOffset屬性。當(dāng)用戶滾動(dòng)結(jié)束時(shí)動(dòng)態(tài)改變某些空間的實(shí)現(xiàn)效果。下拉刷新,漸變導(dǎo)航欄,頭像變大縮小等。


KVC

KVC (Key-Value-Coding )鍵值編碼。顧名思義:可以通過一個(gè)Key來訪問某個(gè)屬性。

常用方法
- (void)setValue:(id)value forKey:(NSString *)key;
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;

- (id)valueForKey:(NSString *)key; 
- (id)valueForKeyPath:(NSString *)    keyPath;
簡(jiǎn)單示例
    self.ani = [[Animal alloc] init];
    [self.ani setValue:@38 forKey:@"age"];
    NSLog(@"%d",self.ani.age);
    
    self.ani1 = [[Animal alloc] init];
    [self.ani1 setValue:@99 forKeyPath:@"cat.weight"];
    NSLog(@"%d",self.ani1.cat.weight);

    self.ani2 = [[Animal alloc] init];
    self.ani2.age = 10;
    NSLog(@"%@",[self.ani2 valueForKey:@"age"]);
setValue:forKey:的原理:

當(dāng)我們?cè)O(shè)置setValue:forKey:時(shí)
首先會(huì)查找setKey:、_setKey: (按順序查找)
如果有直接調(diào)用
如果沒有,先查看accessInstanceVariablesDirectly方法
如果可以訪問會(huì)按照 _key、_isKey、key、iskey的順序查找成員變量
找到直接復(fù)制
未找到報(bào)錯(cuò)NSUnkonwKeyException錯(cuò)誤

valueForKey:的原理:

kvc取值按照 getKey、key、iskey、_key 順序查找方法
存在直接調(diào)用
沒找到同樣,先查看accessInstanceVariablesDirectly方法
如果可以訪問會(huì)按照 _key、_isKey、key、iskey的順序查找成員變量
找到直接復(fù)制
未找到報(bào)錯(cuò)NSUnkonwKeyException錯(cuò)誤

思考

我們可以通過 self.ani.age = 10; 來賦值,也可通過上述代碼進(jìn)行賦值,看著多此一舉、
可是如果人這個(gè)類的屬性是沒有暴露在外面呢?比如現(xiàn)在給人這個(gè)類一個(gè)私有的身高的屬性。就可以通過KVC進(jìn)行賦值、

Key 和 KeyPath區(qū)別接聯(lián)系

Key:只能訪問當(dāng)前對(duì)象的屬性,如果按路徑找會(huì)報(bào)錯(cuò)。
KeyPath:相當(dāng)于根據(jù)路徑去尋找屬性,能利用運(yùn)算符一層一層往內(nèi)部訪問屬性。

用途

我們通過KVC可以直接對(duì)私有屬性并進(jìn)行賦值
字典轉(zhuǎn)模型

拓展

我們通過XIB或者SB拖線布局連線錯(cuò)誤的時(shí)候也會(huì)報(bào)錯(cuò)說找不到什么key,說明Storyboard在賦值的時(shí)候也是通過KVC來操作的。

試題

KVO相關(guān):
1. iOS用什么方式來實(shí)現(xiàn)對(duì)一個(gè)對(duì)象的KVO?(KVO的本質(zhì)是什么?)
2. 如何手動(dòng)出發(fā)KVO?
3. 直接修改成員變量會(huì)觸發(fā)KVO么?

KVC相關(guān): 
1. 通過KVC修改屬性會(huì)觸發(fā)KVO么?
2. KVC的賦值和取值過程是怎樣的?原理是什么?
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容