阅读 709

iOS基础之KVC和KVO

一、KVC

在开发中,我们可以通过使用 KVC 的方式来对某个对象的属性进行赋值/取值操作。

经常会用到以下 API

// 设置值
- (void)setValue:(nullable id)value forKey:(NSString *)key;
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
// 获取值
- (nullable id)valueForKeyPath:(NSString *)keyPath;
- (nullable id)valueForKey:(NSString *)key;
复制代码

1.1 赋值操作

接下来我们就研究一下 KVC 的调用原理:

如果我们给某个类定义一个属性,那么编译器会自动生成 gettersetter 方法,如果通过 KVC 给该属性进行赋值操作,默认会调用 setter 方法进行赋值。但是这不能完全搞清楚 KVC 是如何工作的。

我们定义一个 Person 类,但是我们并不给 Person 定义任何的属性。接下来创建 person 对象,通过 KVC 的方式给 personage 属性进行赋值操作。


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        Person *person = [[Person alloc] init];
        
        [person setValue:@(20) forKey:@"age"];
    }
    return 0;
}
复制代码
  1. Person 类中查找有没有 - (void)setAge: 方法,如果有那么就进行赋值操作;如果没有再去查找有没有 - (void)_setAge: 方法,如果有就进行赋值的操作。
  2. 如果以上两个方法都没找到,那么就会调用 - (Bool)accessInstanceVariablesDirectly 方法,该方法是询问是否可以直接访问成员变量,返回 NO 就直接抛出异常未定义的 Key
  3. 如果 - (Bool)accessInstanceVariablesDirectly 返回的是 YES(如果不实现该方法默认返回的就是 YES),那么就直接去成员变量中按顺序查找以下成员变量:_age_isAgeageisAge。如果找到4个成员变量中的1位,那么就进行赋值,否则抛出异常未定义的 Key
// Person.h
#import <Foundation/Foundation.h>

@interface Person : NSObject {
    @public
    int _age; // 最先查找
    int _isAge; // 老2
    int age; // 老3
    int isAge; // 老小
  
  	// 如果以上4个成员变量都没有,抛异常
}

@end
  
// Person.m 
#import "Person.h"

@implementation Person
  
// 如果有最先调用
- (void)setAge:(int)age {
    NSLog(@"setAge - %d", age);
}

// 如果没有 setAge 方法,调用该方法
- (void)_setAge:(int)age {
    NSLog(@"_setAge - %d", age);
}

// 如果以上两个方法都没有,且该方法返回 YES,就去查找 成员变量
// 如果以上两个方法都没有,且该方法返回 NO,直接抛异常
+ (BOOL)accessInstanceVariablesDirectly {
    return YES;
}

@end
复制代码

1.2 取值操作

KVC 的取值操作也会按照一定的顺序进行操作的。

  1. Person 的实现文件中,按照 -(int)getAge- (int)age- (int)isAge-(int)_age 顺序进行,看有没有实现这4个方法中的其中1个,如果有那么调用
  2. 如果没有实现上面的4个方法,继续查看 + (BOOL)accessInstanceVariablesDirectly 方法的返回值是否为 YES
  3. 如果 + (BOOL)accessInstanceVariablesDirectly 方法返回值为 NO,直接抛出异常,如果为 YES,那么就去按顺序查找 Person 的成员变量是不是 _age_isAgeageisAge 中的一个,如果有4个成员变量中的1个,那么就取他们的值。
// Person.m
#import "Person.h"

@implementation Person
- (int)getAge {
    return 11;
}

- (int)age {
    return 12;
}

- (int)isAge {
    return 13;
}

- (int)_age {
    return 14;
}

+ (BOOL)accessInstanceVariablesDirectly {
    return YES;
}

@end
  
// main.m
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        Person *person = [[Person alloc] init];
        person->age = 11;
        person->_age = 12;
        person->isAge = 13;
        person->_isAge = 14;
        
        NSLog(@"%@", [person valueForKey:@"age"]);
    }
    return 0;
}
复制代码

二、KVO

KVO 即键值观察,可以用来监听一个对象的属性的变化,当该对象的属性的值发生改变的时候,会回调 - (void)observeValueForKeyPath:ofObject:change:context: 方法,在该方法中可以处理一些业务逻辑。

KVO 的一些实现细节可以查看这个文档:KVO的实现细节

KVO 的实现就是利用了在运行时动态的修改 isa 的指向的技术。

2.1 KVO 的基本使用

// Person.h
#import <Foundation/Foundation.h>

@interface Person : NSObject
@property (nonatomic, assign) int age;
@end
  
// ViewController.m
  
#import "ViewController.h"
#import "Person.h"

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

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person1 = [[Person alloc] init];
    self.person1.age = 11;
    
    self.person2 = [[Person alloc] init];
    self.person2.age= 22;
    
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.person1 addObserver:self forKeyPath:@"age" options:options context:@"man"];
    
}

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

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

// 打印结果:
<Person: 0x6000019a8270>的age属性改变了 - {
    kind = 1;
    new = 18;
    old = 11;
} - man
复制代码
  • 创建一个 Person 类,定义一个 age 属性,定义属性后,编译器会自动生成 gettersetter 方法以及带下划线的成员变量
  • 创建两个 person 对象,分别给 age 属性赋值(本质是调用 setAge: 方法)同时给 person1 添加观察者 self (即该控制器对象)
  • 监听 age 属性,监听它的新值和旧值
  • 实现 - (void)observeValueForKeyPath:ofObject:change:context: 方法,当 age 发生改变后会回调到该方法。
  • 在控制器对象销毁时候,将 person1 的观察者移除

以上就是 KVO 的基本使用。接下来我们就研究一下 KVO 的本质

2.2 KVO 的本质

上面的代码,我们改变 age 的值,本质是调用 setter 方法进行 age 的值修改,我们可能会认为程序在运行时 setter 方法做了手脚来实现监听,其实不是的,问题出在 person 对象上。

我们可以通过在为 person1 添加观察者之后来打印一下 person1person2isa 指向来获取他们的类对象

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person1 = [[Person alloc] init];
    self.person1.age = 11;
    
    self.person2 = [[Person alloc] init];
    self.person2.age= 22;
    
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.person1 addObserver:self forKeyPath:@"age" options:options context:@"man"];
    
  	// 获取类对象
    NSLog(@"%@", object_getClass(self.person1));
    NSLog(@"%@", object_getClass(self.person2));
    
}

// 打印结果:
NSKVONotifying_Person
Person
复制代码
  • person1 对象的 isa 指向发生了变化,指向了 NSKVONotifying_PersonNSKVONotifying_Person就是 person1 的类对象
  • person2 进行 KVO 监听,所以 person2isa 指向没有改变

NSKVONotifying_Person 是在程序运行时为我们动态添加的类,而该类是继承 Person 的,即它的 superclass 指针指向了 Person,调用下面的代码可以验证该结论。

NSLog(@"%@", [object_getClass(self.person1) superclass]);
// 打印结果:Person
复制代码

KVO 又是怎么对 person1 的 age 属性进行监听的呢?

  • person1 通过 isa找到它的类对象即 NSKVONotifying_Person,在 NSKVONotifying_Person内部也存储着一个 setAge: 方法,该方法内部调用了 _NSSetIntValueAndNotify 函数
  • _NSSetIntValueAndNotify 函数内部首先是调用了 - (void)willChangeValueForKey: 方法,然后通过 [super setAge:] 方法去调用父类真正的赋值操作,最后调用 - (void)didChangeValueForKey: 方法
  • - (void)didChangeValueForKey: 内部调用- (void)observeValueForKeyPath:ofObject:change:context: 方法最终完成属性值的监听操作。

怎么证明是调用了 _NSSetIntValueAndNotify 方法呢?

我们可以利用 lldb 命令来查看一下:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    self.person1.age = 18;
    self.person2.age = 33;
  	// 打印方法的地址
    NSLog(@"%p", [self.person1 methodForSelector:@selector(setAge:)]);
    NSLog(@"%p", [self.person2 methodForSelector:@selector(setAge:)]);
}

// 打印结果:
0x7fff207bc2b7
0x108a0ef30

lldb:
p (IMP)0x7fff207bc2b7 => $0 = 0x00007fff207bc2b7 (Foundation`_NSSetIntValueAndNotify)
p (IMP)0x108a0ef30 		=> $2 = 0x0000000108a0ef30 (KVODemo`-[Person setAge:] at Person.h:13)
复制代码

我们可以通过一些打印来观察一下具体是什么时候进行监听的:

// Person.m
#import "Person.h"

@implementation Person

- (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

// 打印结果:
willChangeValueForKey:
setAge:
didChangeValueForKey: => begin
<Person: 0x600000aac460>的age属性改变了 - {
    kind = 1;
    new = 18;
    old = 11;
} - man
didChangeValueForKey: => end
复制代码
  • 重写 Person.m 文件中的 setAge: 方法、willChangeValueForKey: 方法以及 didChangeValueForKey: 方法
  • 通过打印结果可以观察打印顺序,先调用 willChangeValueForKey: 再调用 setAge: 方法去修改值,最后再 didChangeForKey: 方法中来监听属性的改变

前面已经得出结论,person1 的类对象已经变成了 NSKVONotifying_Person 类,而且 NSKVONotifying_Person 中还重写了 setAge 方法,其实内部不仅仅有 setAge 方法,还有三个方法,分别为 classdealloc 方法和 _isKVOA 方法。

  • 重写 class 方法的目的是当我们调用 [person1 class] 方法时,返回的是 Person 类,从而防止 NSKVONotifying_Person 类暴露出来,因为苹果本身是不希望我们去过多关注 NSKVONotifying_Person 类的。
  • dealloc 方法在 NSKVONotifying_Person 类使用完毕后进行一些收尾的工作,因为是不开源的所以这里也只是一个猜测
  • _isKVOA 方法目的是返回布尔类型告诉系统是否和 KVO 有关。

我们可以利用 runtime 来查看一个类对象中的方法名称:

/// 传入类/元类对象,返回其中的方法名称
- (NSString *)printMethodNameOfClass:(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 appendFormat:@"%@ ", methodName];
    }
    
    return methodNames;
}

NSLog(@"%@ - %@", object_getClass(self.person1), [self printMethodNameOfClass:object_getClass(self.person1)]);
NSLog(@"%@ - %@", object_getClass(self.person2), [self printMethodNameOfClass:object_getClass(self.person2)]);

// 打印结果:
NSKVONotifying_Person - setAge: class dealloc _isKVOA
Person - setAge: age
复制代码

修改时间:2021年5月19日

文章分类
iOS
文章标签