Objective-C

KVC&KVO

2018-03-15  本文已影响6人  MichaelLedger
基本概念

KVC (Key-value coding)

//键值编码是间接访问对象属性的一种机制,使用字符串来标识属性,而不是通过调用访问器方法或直接通过实例变量访问它们。
//实质上,键值编码定义了应用程序访问器方法实现的模式和方法签名。
Key-value coding is a mechanism for accessing an object’s properties indirectly, using strings to identify properties, rather than through invocation of an accessor method or accessing them directly through instance variables. 
In essence, key-value coding defines the patterns and method signatures that your application’s accessor methods implement.

KVO (Key-value observing)

//键值观察提供了一种机制,允许通知对象更改其他对象的特定属性。 这对于应用程序中的模型层和控制器层之间的通信特别有用。
Key-value observing provides a mechanism that allows objects to be notified of changes to specific properties of other objects. It is particularly useful for communication between model and controller layers in an application.
使用

KVC

动态设置: setValue:属性值 forKey:属性名(用于简单路径)、setValue:属性值 forKeyPath:属性路径(用于复合路径,例如Person有一个Account类型的属性,那么person.account就是一个复合属性)

动态读取: valueForKey:属性名 、valueForKeyPath:属性名(用于复合路径)

具体查找规则(假设现在要利用KVC对a进行读取):

如果是动态设置属性,则优先考虑调用setA方法,如果没有该方法则优先考虑搜索成员变量_a,如果仍然不存在则搜索成员变量a,如果最后仍然没搜索到则会调用这个类的setValue:forUndefinedKey:方法

如果是动态读取属性,则优先考虑调用a方法(属性a的getter方法),如果没有搜索到则会优先搜索成员变量_a,如果仍然不存在则搜索成员变量a,如果最后仍然没搜索到则会调用这个类的valueforUndefinedKey:方法

注意:搜索过程中不管这些方法、成员变量是私有的还是公共的,无论是否提供getter/setter方法,无论可见性是怎样,是否有readonly修饰,都能正确设置和读取

KVO

在ObjC中使用KVO操作常用的方法如下:

注册指定Key路径的监听器: addObserver: forKeyPath: options:  context:
删除指定Key路径的监听器: removeObserver: forKeyPath、removeObserver: forKeyPath: context:
回调监听: observeValueForKeyPath: ofObject: change: context:

KVO的使用步骤也比较简单:

通过addObserver: forKeyPath: options: context:为被监听对象(它通常是数据模型)注册监听器
重写监听器的observeValueForKeyPath: ofObject: change: context:方法
在dealloc中移除监听者(特别要注意避免重复移除):removeObserver: forKeyPath、removeObserver: forKeyPath: context:
进阶

KVC与runtime应用

原来所有原生的类都是实现了NSCoding协议,在归档的过程中进行了转码,所以才可以归档成功。
特别提醒:下面的方法只适用于归档&解档简单的数据类型,会丢失父类的属性。(只支持继承于NSObject的模型类)

//
//  NSObject+KVC.h
//  Runtime+KVC
//
//  Created by MountainX on 2018/3/15.
//  Copyright © 2018年 MTX Software Technology Co.,Ltd. All rights reserved.
//

#import <Foundation/Foundation.h>

@interface NSObject (KVC) <NSCoding>

@end

//
//  NSObject+KVC.m
//  Runtime+KVC
//
//  Created by MountainX on 2018/3/15.
//  Copyright © 2018年 MTX Software Technology Co.,Ltd. All rights reserved.
//

#import "NSObject+KVC.h"
#import <objc/runtime.h>

@implementation NSObject (KVC)

#pragma mark - 解档
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    if (self = [self init]) {
        unsigned int count = 0;
        //获取类中所有成员变量名
        Ivar *ivar = class_copyIvarList([self class], &count);
        for (int i = 0; i<count; i++) {
            Ivar iva = ivar[i];
            const char *name = ivar_getName(iva);
            NSString *strName = [NSString stringWithUTF8String:name];
            //进行解档取值
            id value = [aDecoder decodeObjectForKey:strName];
            //利用KVC对属性赋值
            [self setValue:value forKey:strName];
        }
        free(ivar);
    }
    return self;
}

#pragma mark - 归档
- (void)encodeWithCoder:(NSCoder *)aCoder {
    unsigned int count;
    Ivar *ivar = class_copyIvarList([self class], &count);
    for (int i=0; i<count; i++) {
        Ivar iv = ivar[i];
        const char *name = ivar_getName(iv);
        NSString *strName = [NSString stringWithUTF8String:name];
        //利用KVC取值
        id value = [self valueForKey:strName];
        [aCoder encodeObject:value forKey:strName];
    }
    free(ivar);
}

@end

//
//  ViewController.m
//  Runtime+KVC
//
//  Created by MountainX on 2018/3/15.
//  Copyright © 2018年 MTX Software Technology Co.,Ltd. All rights reserved.
//

#import "ViewController.h"

/*NSObject分类中已经遵循了NSCoding,并利用runtime实现了协议方法进行归档和解档
 - (void)encodeWithCoder:(NSCoder *)aCoder;
 - (nullable instancetype)initWithCoder:(NSCoder *)aDecoder; // NS_DESIGNATED_INITIALIZER
 */
@interface Dog: NSObject

@property (nonatomic, copy) NSString *name;

@end

@implementation Dog
@end

@interface Person: NSObject

@property (nonatomic, assign) NSInteger age;

@property (nonatomic, copy) NSString *name;

@property (nonatomic, strong)Dog *dog;


@end

@implementation Person
@end


@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    
    Person *person = [[Person alloc] init];
    person.age = 24;
    person.name = @"MountainX";
    
    Dog *dog = [[Dog alloc] init];
    dog.name = @"Jim";
    person.dog = dog;
    
    //设置文件保存的目录
    NSString* docPatn = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
    NSString* path = [docPatn stringByAppendingPathComponent:@"person.archiver"];
    NSLog(@"path:%@",path);
    
    //自定义对象存到文件中
    [NSKeyedArchiver archiveRootObject:person toFile:path];
    
    //解档:
    Person *p = [NSKeyedUnarchiver unarchiveObjectWithFile:path];
    NSLog(@"name = %@, age = %ld, dog.name = %@", p.name, (long)p.age, p.dog.name);
}


- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}


@end

运行后控制台打印:

2018-03-15 16:03:56.013976+0800 Runtime+KVC[17125:341423] path:/Users/mxr/Library/Developer/CoreSimulator/Devices/F0A41D22-52BE-4F30-A200-F9DD0FA44D3F/data/Containers/Data/Application/B0BAEBF7-CC73-4D5B-A0FE-AFAB4075695B/Documents/person.archiver
2018-03-15 16:03:56.016603+0800 Runtime+KVC[17125:341423] name = MountainX, age = 24, dog.name = Jim

KVC底层实现

[person setValue:@"MountainX" forKey:@"name"];

就会被编译器处理成:

SEL sel = sel_get_uid ("setValue:forKey:");
IMP method = objc_msg_lookup (person->isa,sel);
method(person, sel, @"MountainX", @"name");

KVC的消息传递

valueForKey:的使用并不仅仅用来取值那么简单,还有很多特殊的用法,集合类也覆盖了这个方法,通过调用valueForKey:给容器中每一个对象发送操作消息,并且结果会被保存在一个新的容器中返回,这样我们能很方便地利用一个容器对象创建另一个容器对象。另外,valueForKeyPath:还能实现多个消息的传递。

//
//  main.m
//  Runtime
//
//  Created by MountainX on 2018/3/15.
//  Copyright © 2018年 MTX Software Technology Co.,Ltd. All rights reserved.
//

#import <Foundation/Foundation.h>

@interface Dog: NSObject

@property(nonatomic, copy) NSString *name;

@property(nonatomic, assign) NSInteger age;

@end

@interface Dog()//类拓展
{
//    NSString *_sex;//如果定义了成员变量_sex或者sex则不会崩溃
}

@end

@implementation Dog

/*Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<Dog 0x100701860> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key sex.'*/
/*一般需要声明此方法(保证代码的健壮性),否则使用setValuesForKeysWithDictionary查找不到setter方法和同名成员变量则会崩溃*/
-(void)setValue:(id)value forUndefinedKey:(NSString *)key {
    NSLog(@"Warning:can not setValue forUndefinedKey:'%@'", key);
}

@end

@interface Person : NSObject
{
    int weight;
}
@property(nonatomic,readonly,copy) NSString *name;
@property(nonatomic,readonly, assign) NSInteger age;
@property(nonatomic,strong) Dog * dog;
@property(nonatomic,assign) id ID;

@end

@implementation Person
{
    int _height;
}

/*Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<Person 0x1005959d0> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key id.'*/
/*id为系统关键字,于是使用大写的ID代替,KVC是区分大小写的我们不用担心。这时我们只需在setValue:forUndefinedKey或者在setValue:forKey中把id的key值赋值给ID的key值,就可以避免关键字的尴尬。*/
- (void)setValue:(id)value forKey:(NSString *)key {
    if ([key isEqualToString:@"id"]) {
        _ID = value;
    } else {
        [super setValue:value forKey:key];
    }
}

//- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
//    if ([key isEqualToString:@"id"])
//    {
//        self.ID = value;
//    }
//}

//当key的值是没有定义时,取值时执行的方法
- (id)valueForUndefinedKey:(NSString *)key {
    if ([key isEqualToString:@"id"]) {
        return self.ID;
    }
    return [NSNull null];
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person * p1 = [[Person alloc]init];
        [p1 setValue:@"MountainX" forKeyPath:@"name"];
        //等价于[p1 setValue:@(24) forKeyPath:@"age"];
        //使用KVC间接修改对象属性时,系统会自动判断对象属性的类型,并完成转换。
        [p1 setValue:@"24" forKeyPath:@"age"];
        NSLog(@"p1_age:%ld", (long)p1.age);
        [p1 setValue:@"888" forKey:@"id"];
        NSLog(@"p1_ID:%@", p1.ID);
        
        Dog * d1 = [[Dog alloc] init];
        //等价于d1.name = @"Jim";[d1 setValue:@(3) forKeyPath:@"age"];
        [d1 setValuesForKeysWithDictionary:@{@"name": @"Jim", @"age" : @3, @"sex": @"male"}];//KVC的正向使用
        p1.dog = d1;
        
        NSDictionary *dict1 = [p1 dictionaryWithValuesForKeys:@[@"name", @"age"]];
        NSLog(@"p1:%@,class:%@", dict1, [dict1 class]);//KVC的逆向使用
        
        Person * p2 = [[Person alloc]init];
        [p2 setValue:@"Daisy" forKeyPath:@"name"];
        [p2 setValue:@(25) forKeyPath:@"age"];
        
        
        Dog * d2 = [[Dog alloc] init];
        //等价于d2.age = 2;
        [d2 setValue:@(2) forKeyPath:@"age"];
        //等价于p2.dog = d2;
        [p2 setValue:d2 forKeyPath:@"dog"];//简单路径
        //等价于d2.name = @"Tony";用KVC取一个嵌套层次很深的路径的时候,只要给它一个路径�就能把想要的�属性给拿出来。
        [p2 setValue:@"Tony" forKeyPath:@"dog.name"];//复合路径
        
        NSDictionary *dict2 = [p2 dictionaryWithValuesForKeys:@[@"name", @"age", @"dog"]];
        NSLog(@"p2:%@,class:%@", dict2, [dict2 class]);//KVC的逆向使用
        
        NSLog(@"p2_dog:%@,class:%@", [dict2 valueForKey:@"dog"], [[dict2 valueForKey:@"dog"] class]);
        
        NSNumber *dogAge = [dict2 valueForKeyPath:@"dog.age"];
        NSLog(@"p2_dog_age:%@,class:%@", dogAge, [dogAge class]);
        NSString *dogName = [dict2 valueForKeyPath:@"dog.name"];
        NSLog(@"p2_dog_name:%@,class:%@", dogName, [dogName class]);
        
        //NSArray/NSSet等都支持KVC
        NSArray *personArray=@[p1,p2];
        id value1 = [personArray valueForKeyPath:@"dog.name"];
        NSLog(@"dogNames:%@,class:%@", value1, NSStringFromClass([value1 class]));
        
        NSSet *personSet = [[NSSet alloc] initWithObjects:p1, p2, nil];
        id value2 = [personSet valueForKeyPath:@"dog.age"];
        NSLog(@"dogAges:%@,class:%@", value2, NSStringFromClass([value2 class]));
        
        //KVC计算
        NSNumber *personCount = [personArray valueForKeyPath:@"@count"];
        NSLog(@"personCount:%@,class:%@", personCount, [personCount class]);
        
        NSNumber *dogCount = [personArray valueForKeyPath:@"dog.@count"];
        NSLog(@"dogCount:%@,class:%@", dogCount, [dogCount class]);
        
        NSNumber *personNameSum = [personArray valueForKeyPath:@"@sum.name"];
        NSLog(@"personNameSum:%@,class:%@", personNameSum, [personNameSum class]);
        if ([personNameSum isEqualToNumber:[NSDecimalNumber notANumber]]) {
            NSLog(@"[personArray valueForKeyPath:@\"sum.name\"] is not a number");
        }
        
        NSNumber *personAgeSum = [personArray valueForKeyPath:@"@sum.age"];
        NSLog(@"personAgeSum:%@,class:%@", personAgeSum, [personAgeSum class]);
        
        NSNumber *dogAgeSum = [personArray valueForKeyPath:@"dog.@sum.age"];
        NSLog(@"dogAgeSum:%@,class:%@", dogAgeSum, [dogAgeSum class]);
        
        NSNumber *personAgeAverage = [personArray valueForKeyPath:@"@avg.age"];
        NSLog(@"personAgeAverage:%@, %@", personAgeAverage, [personAgeAverage class]);
        
        NSNumber *dogAgeAverage = [personArray valueForKeyPath:@"dog.@avg.age"];
        NSLog(@"dogAgeAverage:%@, %@", dogAgeAverage, [dogAgeAverage class]);
        
        NSNumber *personAgeMax = [personArray valueForKeyPath:@"@max.age"];
        NSLog(@"personAgeMax:%@, %@", personAgeMax, [personAgeMax class]);
        
        NSNumber *dogAgeMax = [personArray valueForKeyPath:@"dog.@max.age"];
        NSLog(@"dogAgeMax:%@, %@", dogAgeMax, [dogAgeMax class]);
        
        NSNumber *personAgeMin = [personArray valueForKeyPath:@"@min.age"];
        NSLog(@"personAgeMin:%@, %@", personAgeMin, [personAgeMin class]);
        
        NSNumber *dogAgeMin = [personArray valueForKeyPath:@"dog.@min.age"];
        NSLog(@"dogAgeMin:%@, %@", dogAgeMin, [dogAgeMin class]);
    }
    return 0;
}

控制台输出:

2018-03-15 15:25:44.209683+0800 Runtime[15895:301993] p1_age:24
2018-03-15 15:25:44.209969+0800 Runtime[15895:301993] p1_ID:888
2018-03-15 15:25:44.210222+0800 Runtime[15895:301993] Warning:can not setValue forUndefinedKey:'sex'
2018-03-15 15:25:44.210517+0800 Runtime[15895:301993] p1:{
    age = 24;
    name = MountainX;
},class:__NSDictionaryI
2018-03-15 15:25:44.210817+0800 Runtime[15895:301993] p2:{
    age = 25;
    dog = "<Dog: 0x1007116e0>";
    name = Daisy;
},class:__NSDictionaryI
2018-03-15 15:25:44.210930+0800 Runtime[15895:301993] p2_dog:<Dog: 0x1007116e0>,class:Dog
2018-03-15 15:25:44.211055+0800 Runtime[15895:301993] p2_dog_age:2,class:__NSCFNumber
2018-03-15 15:25:44.211136+0800 Runtime[15895:301993] p2_dog_name:Tony,class:__NSCFConstantString
2018-03-15 15:25:44.211268+0800 Runtime[15895:301993] dogNames:(
    Jim,
    Tony
),class:__NSArrayI
2018-03-15 15:25:44.211447+0800 Runtime[15895:301993] dogAges:{(
    3,
    2
)},class:__NSSetI
2018-03-15 15:25:44.211517+0800 Runtime[15895:301993] personCount:2,class:__NSCFNumber
2018-03-15 15:25:44.211564+0800 Runtime[15895:301993] dogCount:2,class:__NSCFNumber
2018-03-15 15:25:44.211745+0800 Runtime[15895:301993] personNameSum:nan,class:NSDecimalNumber
2018-03-15 15:25:44.211796+0800 Runtime[15895:301993] [personArray valueForKeyPath:@"sum.name"] is not a number
2018-03-15 15:25:44.211997+0800 Runtime[15895:301993] personAgeSum:49,class:NSDecimalNumber
2018-03-15 15:25:44.212153+0800 Runtime[15895:301993] dogAgeSum:5,class:NSDecimalNumber
2018-03-15 15:25:44.212276+0800 Runtime[15895:301993] personAgeAverage:24.5, NSDecimalNumber
2018-03-15 15:25:44.212357+0800 Runtime[15895:301993] dogAgeAverage:2.5, NSDecimalNumber
2018-03-15 15:25:44.212657+0800 Runtime[15895:301993] personAgeMax:25, __NSCFNumber
2018-03-15 15:25:44.212714+0800 Runtime[15895:301993] dogAgeMax:3, __NSCFNumber
2018-03-15 15:25:44.213047+0800 Runtime[15895:301993] personAgeMin:24, __NSCFNumber
2018-03-15 15:25:44.213133+0800 Runtime[15895:301993] dogAgeMin:2, __NSCFNumber
Program ended with exit code: 0

KVO的缺点

AFNetworking作者Mattt Thompson在《Key-Value Observing》中说:

Ask anyone who’s been around the NSBlock a few times: Key-Value Observing has the worst API in all of Cocoa.

缺点1:所有的observe处理都放在一个方法里

假设我们要观察一个tableView的contentSize,看上去使用KVO是个不错的选择,因为没有其他方法去获知这个属性被改变。Ok,首先,添加观察者:

[_tableView addObserver:self forKeyPath:@"contentSize" options:0 context:NULL];

很好,实现属性被改变的响应:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    [self configureView];
}

完成!Just kidding.考虑到该方法中可能有其他的observe事务,所以我们要这样修改:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if (object == _tableView && [keyPath isEqualToString:@"contentSize"]) {
        [self configureView];
    }
}

如果KVO处理的事务繁多,就会造成该方法代码特别长,并且十分不优雅,我们还没辙。

缺点2:严重依赖于string

KVO严重依赖于string,换句话说如果KVO的keyPath如果错误编译器无法查出,比如我们可能会把@“contentSize”写成@“contentsize”,然后你就只能傻傻的找半个小时bug。

因此我们不得不使用NSStringFromSelector(@selector(contentSize))编译器就能判断是否存在这个属性,并且我们也好修改,但是代码这么长,逼死强迫症。

而且,我们也不能用KVO观察多值路径,比如我们观察一个viewController并且想获得scrollView的contentOffset,我们是不能用这样的keyPath:scrollview.contentOffset来得到的。

因此上面的代码又得变成这样:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if (object == _tableView && [keyPath isEqualToString:NSStringFromSelector(@selector(contentSize))]) {
        [self configureView];
    }
}

缺点3:需要自己处理superclass的observe事务

对于Objective-C,很多时候runtime系统都会自动帮忙处理superClass的方法,比如dealloc。在ARC下,调用子类的dealloc方法,runtime会自动调用父类的dealloc。但是KVO不会,或者说是不行,因为runtime并不知道父类有没有observe事务,并且由于它是用协议实现的,一次只能触发一个observeValueForKeyPath:ofObject:change:context:方法,所以如果子类和父类都有observe事务,我们要这样做:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if (object == _tableView && [keyPath isEqualToString:@"contentSize"]) {
        [self configureView];
    } else {
        // 如果我们忘记这句,那么父类不会收到通知
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

缺点4:多次相同KVO的removeObserve会导致crash

写过KVO代码的人都知道,对同一个对象执行两次removeObserver操作会导致程序crash。

在同一个文件中执行两次相同的removeObserver属于粗心,比较容易debug出来;但是跨文件执行两次相同的removeObserver就不是那么容易发现了。

我们一般会在dealloc中进行removeObserver操作(这也是Apple所推荐的)。

思考这样一个场景:一个JZTableView继承自UITableview,他们都监听了tableView的contentSize属性,那么UItableview和JZTableView的dealloc都会有这句代码:

- (void)dealloc {
     ...
    [_tableView removeObserver:self forKeyPath:@"contentSize" context:NULL];
    ...
}

那么当JZTableview的对象释放时,他和她父类的dealloc都会被调用,两次相同的removeObserve,自然就Crash了。

还有很多点,不过说出上面4个就差不多了。

参考

iOS 中KVC、KVO、NSNotification、delegate 总结及区别
谈KVC、KVO(重点观察者模式)机制编程
iOS开发系列--Objective-C之KVC、KVO
iOS开发之MVC设计模式 KVO模式 KVC模式 单例模式
iOS开发技巧系列---详解KVC(我告诉你KVC的一切)
青少年一定要读的KVO指南
一句代码,更加优雅的调用KVO和通知
KVC KVO高阶应用
Key-Value Coding and Observing
刨根问底KVC

上一篇 下一篇

猜你喜欢

热点阅读