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 的调用原理:
如果我们给某个类定义一个属性,那么编译器会自动生成 getter 和 setter 方法,如果通过 KVC 给该属性进行赋值操作,默认会调用 setter 方法进行赋值。但是这不能完全搞清楚 KVC 是如何工作的。
我们定义一个 Person 类,但是我们并不给 Person 定义任何的属性。接下来创建 person 对象,通过 KVC 的方式给 person 的 age 属性进行赋值操作。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        Person *person = [[Person alloc] init];
        
        [person setValue:@(20) forKey:@"age"];
    }
    return 0;
}

去 Person 类中查找有没有 - (void)setAge: 方法,如果有那么就进行赋值操作;如果没有再去查找有没有 - (void)_setAge: 方法,如果有就进行赋值的操作。
如果以上两个方法都没找到,那么就会调用 - (Bool)accessInstanceVariablesDirectly 方法,该方法是询问是否可以直接访问成员变量,返回 NO 就直接抛出异常未定义的 Key
如果 - (Bool)accessInstanceVariablesDirectly 返回的是 YES(如果不实现该方法默认返回的就是 YES),那么就直接去成员变量中按顺序查找以下成员变量:_age 、_isAge、age、 isAge。如果找到4个成员变量中的1位,那么就进行赋值,否则抛出异常未定义的 Key

// Person.h
#import 

@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 的取值操作也会按照一定的顺序进行操作的。

在 Person 的实现文件中,按照 -(int)getAge 、- (int)age 、- (int)isAge 、-(int)_age 顺序进行,看有没有实现这4个方法中的其中1个,如果有那么调用
如果没有实现上面的4个方法,继续查看 + (BOOL)accessInstanceVariablesDirectly 方法的返回值是否为 YES
如果 + (BOOL)accessInstanceVariablesDirectly 方法返回值为 NO,直接抛出异常,如果为 YES,那么就去按顺序查找 Person 的成员变量是不是 _age 、_isAge、age、isAge 中的一个,如果有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: 方法,在该方法中可以处理一些业务逻辑。
2.1 KVO 的基本使用

// Person.h
#import 

@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 *)touches withEvent:(UIEvent *)event {
    self.person1.age = 18;
    self.person2.age = 33;
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    
    NSLog(@"%@的%@属性改变了 - %@ - %@", object, keyPath, change, context);
}

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

// 打印结果:
的age属性改变了 - {
    kind = 1;
    new = 18;
    old = 11;
} - man

创建一个 Person 类,定义一个 age 属性,定义属性后,编译器会自动生成 getter 和 setter 方法以及带下划线的成员变量
创建两个 person 对象,分别给 age 属性赋值(本质是调用 setAge: 方法)同时给 person1 添加观察者 self (即该控制器对象)
监听 age 属性,监听它的新值和旧值
实现 - (void)observeValueForKeyPath:ofObject:change:context: 方法,当 age 发生改变后会回调到该方法。
在控制器对象销毁时候,将 person1 的观察者移除

以上就是 KVO 的基本使用。接下来我们就研究一下 KVO 的本质
2.2 KVO 的本质
上面的代码,我们改变 age 的值,本质是调用 setter 方法进行 age 的值修改,我们可能会认为程序在运行时 setter 方法做了手脚来实现监听,其实不是的,问题出在 person 对象上。
我们可以通过在为 person1 添加观察者之后来打印一下 person1 和 person2 的 isa 指向来获取他们的类对象

- (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_Person,NSKVONotifying_Person就是 person1 的类对象
person2 进行 KVO 监听,所以 person2 的 isa 指向没有改变
ps;iOS开发交流技术群:欢迎你的加入,不管你是大牛还是小白都欢迎入驻 ,分享BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长

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 *)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
的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 方法,还有三个方法,分别为 class,dealloc 方法和 _isKVOA 方法。

ps;iOS开发交流技术群:欢迎你的加入,不管你是大牛还是小白都欢迎入驻 ,分享BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长

重写 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

你可能感兴趣的:(iOS之KVC和KVO)