探索KVC和KVO的本質

  • 原文連接: 探索KVC和KVO的本質
  • 這篇文章主要介紹KVOKVC, 機器底層是如何實現的
  • KVO的全稱是Key-Value Observing,俗稱鍵值監聽,能夠用於監聽某個對象屬性值的改變
  • KVO是使用獲取其餘對象的特定屬性變化的通知機制,控制器層的綁定技術就是嚴重依賴鍵值觀察得到模型層和控制器層的變化通知的
  • 對於不依賴控制器層類的應用程序,鍵值觀察提供了一種簡化的方法來實現檢查器並更新用戶界面值
  • KVCKVO都是基於OC的動態特性和Runtime機制的

KVO

添加監聽

以下所示, 咱們爲person對象添加一個監聽html

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person = [[Person alloc]init];
    self.person.age = 10;
    
    // 給person添加KVO監聽
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.person addObserver:self forKeyPath:@"age" options:options context:nil];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    self.person.age = 10;
}


// 當監聽的對象發生改變時就會調用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    
}
複製代碼

上面添加監聽的方法微信

addObserver:forKeyPath:options:context:
監聽方法各個參數的做用分別是什麼

[object addObserver: observer forKeyPath: @"frame" options: 0 context: nil];
/** object: 被觀察者 observer: 觀察者 KeyPath: 被觀察者索貝觀察的屬性 options: 有四個值 一、NSKeyValueObservingOptionNew 把更改以前的值提供給處理方法 二、NSKeyValueObservingOptionOld 把更改以後的值提供給處理方法 三、NSKeyValueObservingOptionInitial 把初始化的值提供給處理方法,一旦注 冊,立馬就會調用一次。一般它會帶有新值,而不會帶有舊值。 四、NSKeyValueObservingOptionPrior 分2次調用。在值改變以前和值改變以後。 context:上下文,能夠帶一些參數,任何類型均可以 */
複製代碼

當被監聽的對象的屬性發生改變時就會調用下面的方法測試

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

/* 1. keyPath: 被監聽的屬性 2. object: 被監聽的對象 3. change 屬性變化字典(新/舊) 4. 上下文,與監聽的時候傳遞的一致 */
複製代碼

KVO的本質

這裏咱們建立兩個pweson對象, 可是隻對person1實行監聽ui

self.person1 = [[Person alloc]init];
    self.person2 = [[Person alloc]init];
    self.person1.age = 10;
    self.person2.age = 10;
    
    // 給person添加KVO監聽
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.person1 addObserver:self forKeyPath:@"age" options:options context:nil];
複製代碼

下面咱們能夠在touchesBegan方法中分別添加斷點打印兩個對象的isa, 以下編碼

image

  • 從上面能夠看出,未添加監聽的pweson2對象的isa依然是Person, 可是添加KVO監聽的person1isa變成了NSKVONotifying_Person
  • NSKVONotifying_Person這個類是由Runtime在運行狀態下動態建立的一個類, 是Person的一個子類
  • 當咱們對age屬性進行賦值操做的時候, 其實調用的是Person類的setAge方法
    • person1經過isa找到其對應的類對象Person類, 並調用Person類的setAge方法
    • person2經過isa找到其對應的類對象NSKVONotifying_Person類, 並調用NSKVONotifying_Person類的setAge方法
    • 兩個類的setAge方法的實現是不同的, 後面會詳解
  • PersonNSKVONotifying_Person對應的類對象以下所示

image

使用了KVO監聽的對象動態生成的NSKVONotifying_Personatom

image

實際上NSKVONotifying_Person類中的setAge:方法內部是調用了Foundation_NSSetIntValueAndNotify方法, 有興趣的能夠反編譯一下Foundation.framwork的源碼, 查看其僞代碼, 大體的能夠推出內部方法的實現, 代碼大體以下spa

- (void)setAge:(int)age
{
    _NSSetIntValueAndNotify();
}

// 僞代碼
void _NSSetIntValueAndNotify()
{
    [self willChangeValueForKey:@"age"];
    [super setAge:age];
    [self didChangeValueForKey:@"age"];
}

- (void)didChangeValueForKey:(NSString *)key
{
    // 通知監聽器,某某屬性值發生了改變
    [oberser observeValueForKeyPath:key ofObject:self change:nil context:nil];
}
複製代碼
  • 從上面的代碼能夠看出_NSSetIntValueAndNotify其實重寫了willChangeValueForKeydidChangeValueForKey兩個方法
  • 並且監聽屬性值變化的是在didChangeValueForKey方法中實現的
  • 下面咱們就來驗證一下上述代碼

首先咱們在Person類內部重寫willChangeValueForKeydidChangeValueForKey兩個方法, 在運行的過程當中分別加斷點進行調試, 以下調試

- (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");
}
複製代碼

而後在以下代碼中加斷點code

// 當監聽對象的屬性值發生改變時,就會調用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    NSLog(@"監聽到%@的%@屬性值改變了 - %@ - %@", object, keyPath, change, context);
}
複製代碼

在輸出結果中能夠看到代碼的執行順序, 從下面的代碼能夠看出監聽屬性的改變實際上是在didChangeValueForKey方法中實現的cdn

setAge:

didChangeValueForKey - begin

監聽到<MJPerson: 0x60000389b680>的age屬性值改變了 

didChangeValueForKey - end
複製代碼

KVC

  • KVC全稱是Key Value Coding(鍵值編碼),是一個基於NSKeyValueCoding非正式協議實現的機制,它能夠直接經過key值對對象的屬性進行存取操做,而不需經過調用明確的存取方法
  • 這樣就能夠在運行時動態在訪問和修改對象的屬性,而不是在編譯時肯定
  • KVC提供了一種間接訪問屬性方法或成員變量的機制,能夠經過字符串來訪問對象的的屬性方法或成員變量
  • 相關常見的API有
// 通用的訪問方法
- (id)valueForKey:(NSString *)key; 
- (void)setValue:(id)value forKey:(NSString *)key;
// 衍生的keyPath方法, 用來進行深層訪問(key使用點語法),也可單層訪問:
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
- (id)valueForKeyPath:(NSString *)keyPath;
複製代碼

通用訪問方法使用示例

// 使用示例
Person *person = [[Person alloc] init];
 
 // 賦值
[person setValue:@"titan" forKey:@"name"];

// 取值
NSLog(@"-------name = %@",person.name);
NSLog(@"-------name = %@",[person valueForKey:@"name"]);
複製代碼

keyPath方法使用示例

//注意,這裏要想使用keypath對adress的屬性進行賦值,必須先給myself賦一個Address對象
Address *myAddress = [[Address alloc] init];
   
[myself setValue:myAddress forKey:@"address"];
   
//KeyPath爲多級訪問
[myself setValue:@"rizhao" forKeyPath:@"address.city"];
 
//取值
NSLog(@"-------city = %@",myself.address.city);
NSLog(@"-------city = %@",[myself valueForKeyPath:@"address.city"]);
複製代碼

底層原理

setValue:forKey:

image

0. 咱們先建立一個Person類, 並在Person.h文件中聲明一個age屬性, 以下

#import <Foundation/Foundation.h>

@interface Person : NSObject

@property (assign, nonatomic) int age;

@end
複製代碼

下面咱們在ViewController.m裏面調用一下看看

- (void)viewDidLoad {
    [super viewDidLoad];
    
    Person *person = [[Person alloc]init];
    // 這種方式調用的是setAge方法
    person.age = 10;
    
    // 內部實際上是調用的setAge方法
    [person setValue:@20 forKey:@"age"];
    NSLog(@"%d", person.age);
    // 打印結果20
}
複製代碼
  • 若是在Person.h文件中沒有聲明age屬性,也就是在Person.m文件中沒有默認生成的setAgegetAge方法
  • 那麼調用setValue方法對age存值的時候就會致使程序崩潰, 並會報出setValue:forUndefinedKey:]的錯誤
  • 如同上圖中所示, setValue:forKey:的原理實際上就是先按照setAge:_setAge:順序查找方法, 若是找到了對應方法中的一個, 則代碼能夠執行成功, 下面咱們就一個個驗證一下吧

1. 驗證setKey_setKey方法, 代碼以下

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface Person : NSObject

// .h文件中不添加age屬性
//@property (assign, nonatomic) int age;

@end
複製代碼

.m文件中分別添加一下兩個方法, 側其中一個方法的時候, 能夠先註釋掉另一個方法

#import "Person.h"

@implementation Person


- (void)setAge:(int)age {

    NSLog(@"setAge--");
}

- (void)_setAge:(int)age {
    
    NSLog(@"_setAge--");
}
@end
複製代碼

而後在ViewController.m調用setValue方法的時候, 能夠看到打印對應的輸出, 當上述兩個方法同事存在的時候, 則會默認執行setAge方法

[person setValue:@20 forKey:@"age"];
複製代碼

2. 若是沒有setKey:_setKey:兩個方法, 則會繼續查找Person.m文件中是否有accessInstanceVariablesDirectly方法, 若是沒有程序會奔潰

#import "Person.h"

@implementation Person

+ (BOOL)accessInstanceVariablesDirectly {
    // 默認返回值是YES
    return YES;
}
@end
複製代碼
  • accessInstanceVariablesDirectly方法默認是返回YES的, 若是return NO, 則程序一樣會崩潰, 並拋出NSUnknownKeyException異常
  • return YES的狀況下, 會按照順序查找_key、_isKey、key、isKey等成員變量, 若是找不到依然會拋出NSUnknownKeyException異常
  • 下面在Person.h文件中, 分別聲明四個變量
#import <Foundation/Foundation.h>

@interface Person : NSObject
{
    @public
    int age;
    int isAge;
    int _age;
    int _isAge;
}
@end
複製代碼

ViewController.m中添加以下代碼, 執行結果以下所示

- (void)viewDidLoad {
    [super viewDidLoad];
    
    Person *person = [[Person alloc]init];

    [person setValue:@20 forKey:@"age"];
    NSLog(@"-----------");
}
複製代碼

image

  • 當咱們在Person.h中聲明age、isAge、_age、_isAge四個變量的時候, 上述代碼會默認賦值給_age變量
  • 當咱們不聲明_age屬性時, 則會默認賦值給_isAge屬性, 以此類推依次是ageisAge變量, 有興趣的能夠親自測試一番

valueForKey

image

valueForKey經過key進行取值的時候, 取值流程和setValue相似, 途中也比較清晰, 這裏就不在贅述了


歡迎您掃一掃下面的微信公衆號,訂閱個人博客!

微信公衆號
相關文章
相關標籤/搜索