iOS-底層-KVO和KVC

一. KVO

1. KVO的基本使用

KVO的全稱是Key-Value Observing,俗稱“鍵值監聽”,可以用于監聽某個對象屬性值的改變

KVO.png

添加監聽:

// 給person1對象添加KVO監聽
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
[self.person1 addObserver:self forKeyPath:@"height" options:options context:@"456"];

值改變:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    self.person1.age = 20;
    self.person2.age = 20;
    
    self.person1.height = 30;
    self.person2.height = 30;
}

監聽改變:

//context:@"123" 作用:在添加監聽的時候傳入,傳到下面這個方法里面
/**
 當監聽對象的屬性值發生改變時,就會調用
 @param keyPath 監聽的KeyPath
 @param object 被監聽的對象
 @param change 改變
 @param context 監聽時傳入的context
 */
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    NSLog(@"監聽到%@的%@屬性值改變了 - %@ - %@", object, keyPath, change, context);
}

移除監聽:

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

2. KVO底層是怎么實現的

為了探究KVO的底層是怎么實現的,我們創建person1和person2,其中person1添加監聽,person2不添加監聽,代碼如下:

#import "ViewController.h"
#import "MJPerson.h"

@interface ViewController ()
@property (strong, nonatomic) MJPerson *person1;
@property (strong, nonatomic) MJPerson *person2;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person1 = [[MJPerson alloc] init];
    self.person1.age = 1;
    
    self.person2 = [[MJPerson alloc] init];
    self.person2.age = 2;
    
    // 給person1對象添加KVO監聽
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
//    self.person1.age = 21;
//    self.person2.age = 22;
    
    // NSKVONotifying_MJPerson是使用Runtime動態創建的一個類,是MJPerson的子類
    //如果你自己寫了這個類,就會報動態生成失敗
    //KVO效率沒代理高,因為代理是直接調用,KVO還要動態生成一個類
    
    // self.person1.isa == NSKVONotifying_MJPerson
    [self.person1 setAge:21];
    
    // self.person2.isa = MJPerson
    [self.person2 setAge:22];
}

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

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

打斷點,分別po它們的isa

(lldb) po self.person1.isa
NSKVONotifying_MJPerson

  Fix-it applied, fixed expression was: 
    self.person1->isa
(lldb) po self.person2.isa
MJPerson

  Fix-it applied, fixed expression was: 
    self.person2->isa
(lldb) 

可以發現,person1添加監聽后isa是NSKVONotifying_MJPerson,person2不添加監聽isa還是MJPerson。

  1. 其實NSKVONotifying_MJPerson是系統利用Runtime動態創建的一個類,是MJPerson的子類。
  2. 如果你自己寫了這個類,就會報動態生成失敗。
  3. KVO效率沒代理高,因為代理是直接調用,KVO還要動態生成一個類。

既然NSKVONotifying_MJPerson也是一個類,那么它肯定也有自己的isa和superclass,未使用KVO和使用KVO,實例對象和類對象內存結構如下:

未使用KVO.png
使用KVO.png

解釋:
① 當person2不添加監聽的時候,值改變,會通過person2的isa找到MJPerson,然后再找到MJPerson里面的setAge方法調用,完成。
② 當person1添加監聽的時候,值改變,會通過person1的isa找到NSKVONotifying_MJPerson,然后調用NSKVONotifying_MJPerson的setAge方法(方法內部會調用Foundation框架的_NSSetIntValueAndNotify),不會調用MJPerson的setAge方法了。

由于無法查看Foundation框架的實現,我們寫一些偽代碼,來表明添加監聽后的方法調用順序,創建NSKVONotifying_MJPerson類繼承于MJPerson。

#import "MJPerson.h"

@interface NSKVONotifying_MJPerson : MJPerson

@end

#import "NSKVONotifying_MJPerson.h"

@implementation NSKVONotifying_MJPerson

- (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];
}
@end

總結:

  1. 使用KVO,系統會使用Runtime動態創建的一個NSKVONotifying_MJPerson類,這個類是MJPerson的子類
  2. 添加監聽的屬性的值改變的時候,會調用NSKVONotifying_MJPerson類的setAge方法,setAge方法里面會調用_NSSetIntValueAndNotify方法,_NSSetIntValueAndNotify里面走如下步驟:
    ① willChangeValueForKey 將要改變
    ② setAge(原來的set方法) 真的去改變
    ③ didChangeValueForKey 已經改變
    ④ observeValueForKeyPath:ofObject:change:context: 監聽到MJPerson的age屬性改變了

3. 驗證_NSSetIntValueAndNotify內部方法調用流程

驗證過程也很簡單,重寫MJPerson類的三個方法,如下:

- (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");
}

賦值之后運行,打印結果如下:

willChangeValueForKey
setAge:
didChangeValueForKey - begin
監聽到<MJPerson: 0x6000017c90f0>的age屬性值改變了 - {
    kind = 1;
    new = 21;
    old = 1;
} - 123
didChangeValueForKey - end

4. 驗證生成了NSKVONotifying_MJPerson

結論我們知道了,接下來還是用代碼驗證一下:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person1 = [[MJPerson alloc] init];
    self.person1.age = 1;
    
    self.person2 = [[MJPerson alloc] init];
    self.person2.age = 2;
    
    NSLog(@"person1添加KVO監聽之前 - %@ %@",
          object_getClass(self.person1),
          object_getClass(self.person2));
    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:@"123"];
    
    NSLog(@"person1添加KVO監聽之后 - %@ %@",
          object_getClass(self.person1),
          object_getClass(self.person2));
    NSLog(@"person1添加KVO監聽之后 - %p %p",
          [self.person1 methodForSelector:@selector(setAge:)],
          [self.person2 methodForSelector:@selector(setAge:)]);
}

如上,我們在添加KVO之前和之后分別打印它的類對象和它的setAge方法實現,結果如下:

person1添加KVO監聽之前 - MJPerson MJPerson
person1添加KVO監聽之前 - 0x10ede8590 0x10ede8590
person1添加KVO監聽之后 - NSKVONotifying_MJPerson MJPerson
person1添加KVO監聽之后 - 0x10f143216 0x10ede8590

可以發現:
person1添加KVO之前,person1和person2的類對象和setAge方法都是一樣的。
person1添加KVO之后,person1的isa指向的類對象變成了NSKVONotifying_MJPerson,setAge方法地址也變了,person2什么都沒變。

接下來,我們通過p (IMP)指令打印某個地址對應的實現:

(lldb) p (IMP)0x10ede8590
(IMP) $0 = 0x000000010ede8590 (Interview01`-[MJPerson setAge:] at MJPerson.m:13)
(lldb) p (IMP)0x10f143216
(IMP) $1 = 0x000000010f143216 (Foundation`_NSSetIntValueAndNotify)
(lldb) 

打印發現,添加KVO之后果然調用的是Foundation框架下的_NSSetIntValueAndNotify函數,說明我們上面的結論是正確的。

5. NSKVONotifying_MJPerson類對象的isa指向哪里?

下面還有最后一個問題,NSKVONotifying_MJPerson類對象的isa指向哪里呢?這個驗證起來也很簡單,用如下代碼打印:

//獲取類對象
NSLog(@"類對象 - %p %p",
      object_getClass(self.person1),  // 相當于獲取person1的類對象(self.person1.isa)
      object_getClass(self.person2)); // 相當于獲取erson2的類對象(self.person2.isa)

//獲取元類對象
NSLog(@"元類對象 - %p %p",
      object_getClass(object_getClass(self.person1)), // 相當于獲取person1類對象的元類對象(self.person1.isa.isa)
      object_getClass(object_getClass(self.person2))); // 相當于獲取相當于獲取person1類對象的元類對象(self.person2.isa.isa)

打印結果如下;

類對象 - 0x6000008f0090 0x100d91158
元類對象 - 0x6000008f1050 0x100d91180

可以發現person1和person2的類對象和元類對象的地址都不一樣,說明他們是不同的類。這和我們以前的結論一致,所以NSKVONotifying_MJPerson類對象的isa也是指向它自己的元類對象

注意:當屬性是int類型的時候調用的是_NSSetIntValueAndNotify方法,當屬性是float類型的時候調用的是_NSSetFloatValueAndNotify方法,其他類比_NSSetObjectValueAndNotify,_NSSetLongValueAndNotify等等......

6. 為什么重寫class、dealloc、isKVO方法

在第三張圖中我們可以看出,創建NSKVONotifying_MJPerson之后會重寫setAge、class、dealloc、isKVO 這四個方法,setAge方法我們知道為什么重寫,但是為什么要重寫后面三個方法呢?

首先我們先驗證NSKVONotifying_MJPerson的確有這四個方法:

//獲取一個類里面所有的方法
- (void)printMethodNamesOfClass:(Class)cls
{
    unsigned int count;
    // 獲得方法數組
    Method *methodList = class_copyMethodList(cls, &count);
    
    // 存儲方法名
    NSMutableString *methodNames = [NSMutableString string];
    
    // 遍歷所有的方法
    for (int i = 0; i < count; i++) {
        // 獲得方法
        Method method = methodList[i];
        // 獲得方法名
        NSString *methodName = NSStringFromSelector(method_getName(method));
        // 拼接方法名
        [methodNames appendString:methodName];
        [methodNames appendString:@", "];
    }
    
    //c語言中,如果數組是create或者copy出來的要free  OC中ARC不用管
    // 釋放
    free(methodList);
    
    // 打印方法名
    NSLog(@"%@ %@", cls, methodNames);
}

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person1 = [[MJPerson alloc] init];
    self.person1.age = 1;
    
    self.person2 = [[MJPerson alloc] init];
    self.person2.age = 2;
    
    // 給person1對象添加KVO監聽
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
    
    [self printMethodNamesOfClass:object_getClass(self.person1)];
    [self printMethodNamesOfClass:object_getClass(self.person2)];
}

打印結果如下:

打印結果:
NSKVONotifying_MJPerson setAge:, class, dealloc, _isKVOA,
MJPerson setAge:, age,

由打印結果可知:
NSKVONotifying_MJPerson里面的確有setAge:、class、dealloc、_isKVOA四個方法
MJPerson里面有setAge:、age兩個方法

接下來如果想知道為什么要重寫class、dealloc、_isKVOA三個方法,我們先打印:

NSLog(@"%@ %@",object_getClass(self.person1),object_getClass(self.person2));
NSLog(@"%@ %@",[self.person1 class],[self.person2 class]);

打印結果:

NSKVONotifying_MJPerson MJPerson
MJPerson MJPerson

OC對象的分類中,我們知道上面兩種方式都可以獲取類對象,但是為什么獲取的結果不一樣呢?

其實,因為NSKVONotifying_MJPerson是內部創建的,不想讓用戶看到,所以用戶調用class方法要把NSKVONotifying_MJPerson轉成MJPerson,所以系統才重寫了class方法。使用object_getClass函數(RuntimeAPI)獲取的就是真實的,不會被轉成MJPerson。

如果NSKVONotifying_MJPerson沒有實現class方法,最后會調用到NSObject的class方法,會直接返回NSKVONotifying_MJPerson,因為NSObject內部這樣實現的:

@implementation NSObject
- (Class)class
{
    return object_getClass(self);
}
@end

我們可以寫NSKVONotifying_MJPerson的偽代碼:

#import "NSKVONotifying_MJPerson.h"

@implementation NSKVONotifying_MJPerson

//NSKVONotifying_MJPerson內部實現了setKey class dealloc isKVO 方法

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

// 屏蔽內部實現,隱藏了NSKVONotifying_MJPerson類的存在
- (Class)class
{
    return [MJPerson class];
}

- (void)dealloc
{
    // 收尾工作
}

- (BOOL)_isKVOA
{
    return YES;
}
@end

7. 面試題

下面我們就可以回答面試題了:

問題一:iOS用什么方式實現對一個對象的KVO?(KVO的本質是什么?)
答:

  1. 利用RuntimeAPI動態生成一個子類,并且讓instance對象的isa指向這個全新的子類
  2. 當修改instance對象的屬性時,會先調用這個新子類的setter方法,這個新子類的setter方法內部會調用Foundation的_NSSet*ValueAndNotify函數(內部調用如下方法)
    ① willChangeValueForKey:
    ② 父類原來的setter
    ③ didChangeValueForKey:
    ④ 內部會觸發監聽器(Oberser)的監聽方法(observeValueForKeyPath:ofObject:change:context:)

問題二:如何手動觸發KVO?(就算沒有人修改age值,也想觸發監聽方法observeValueForKeyPath)
答:手動調用willChangeValueForKey:和didChangeValueForKey:

比如:

[self.person1 willChangeValueForKey:@"age"];
self.person1->_age = 2;
[self.person1 didChangeValueForKey:@"age"];
//didChangeValueForKey內部會判斷willChangeValueForKey是否調用,所以兩個都要調用

會打印:

2019-04-11 15:34:31.968473+0800 Interview01[16217:3596188] 監聽到<MJPerson: 0x6000000180d0>的age屬性值改變了 - {
    kind = 1;
    new = 2;
    old = 1;
} - 123

問題三:直接修改成員變量會觸發KVO嗎?
答:不會觸發KVO,因為沒調用重寫后的set方法。

比如,如下代碼,不會觸發

self.person1->_age = 2; 

二. KVC

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

KVC的基本使用:

person.age = 10;

NSLog(@"%@", [person valueForKey:@"age"]);
NSLog(@"%@", [person valueForKeyPath:@"cat.weight"]);
NSLog(@"%d", person.age);

//[person setValue:[NSNumber numberWithInt:10] forKey:@"age"];
[person setValue:@10 forKey:@"age"];

person.cat = [[MJCat alloc] init];
[person setValue:@10 forKeyPath:@"cat.weight"];
//setValue:@10 forKeyPath 更強大推薦使用.
NSLog(@"%d", person.age);

2. 通過KVC給屬性賦值能觸發KVO嗎?

能不能試一下就知道了:

MJObserver *observer = [[MJObserver alloc] init];
MJPerson *person = [[MJPerson alloc] init];
// 添加KVO監聽
[person addObserver:observer forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL];
[person setValue:@10 forKey:@"age"];

打印如下:

2019-04-11 15:55:00.568868+0800 Interview01-KVC[16345:3639722] observeValueForKeyPath - {
    kind = 1;
    new = 10;
    old = 0;
}

發現,通過KVC修改age屬性會觸發KVO。為什么呢?先往下看

接下來看看setValue:forKey:設值原理和valueForKey:取值原理。

3. KVC設值原理

設值原理.png

設值原理解釋:

1.尋找setAge方法
- (void)setAge:(int)age
{
    NSLog(@"setAge: - %d", age);
}

2.尋找_setAge方法
- (void)_setAge:(int)age
{
    NSLog(@"_setAge: - %d", age);
}

3.找不到上面兩個方法就調用accessInstanceVariablesDirectly問問能不能直接訪問成員變量
 默認的返回值就是YES
+ (BOOL)accessInstanceVariablesDirectly
{
    return YES;
}

4.1 如果返回NO,就調用setValue:forUndefinedKey:并拋出異常NSUnknownKeyException
4.2如果返回YES,會按順序_key,_isKey,key,isKey賦值,如果四個都找不到就報上面的錯

上面我們知道KVC設值會觸發KVO,但是如果沒有set方法呢?通過驗證(驗證過程省略)可知,就算沒有set方法只有成員變量,通過KVC進行賦值也會觸發KVO,可以理解它們是配套使用的。

其實KVC內部調用了下面方法才會觸發KVO的,可自行驗證。

[person willChangeValueForKey:@"age"];
person->_age = 10;
[person didChangeValueForKey:@"age"];

4. KVC取值原理

取值原理.png

KVC取值原理解釋,可自行驗證:

1.getAge
- (int)getAge
{
    return 11;
}

2.age
- (int)age
{
    return 12;
}

3.isAge
- (int)isAge
{
    return 13;
}

4._age
- (int)_age
{
    return 14;
}

5.3.找不到上面四個方法就調用accessInstanceVariablesDirectly問問能不能直接訪問成員變量
 默認的返回值就是YES
+ (BOOL)accessInstanceVariablesDirectly
{
    return YES;
}

5.1 如果返回NO,就調用valueforUndefinedKey:并拋出異常NSUnknownKeyException
5.2如果返回YES,會按順序_key,_isKey,key,isKey賦值,如果四個都找不到就報上面的錯

Demo地址:KVO和KVC原理

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。