iOS基础---KVC vs KVO

系列文章目录

iOS基础—Block
iOS基础—Protocol
iOS基础—KVC vs KVO
iOS网络—AFNetworking
iOS网络—NSURLSession
iOS内存管理—MRC vs ARC
iOS基础—Category vs Extension
iOS基础—多线程:GCD、NSThread、NSOperation
iOS基础—常用三方库:Masonry、SDWebImage
iOS基础—定时器:GCD、NSTimer、CADisplayLink



一、KVC

1.定义和用途

Key-Value Coding (KVC) 是一种在 Objective-C 中通过字符串标识符(键)访问对象属性的机制。它是 Cocoa 的一部分,允许开发者通过键来获取或设置对象的属性,而不需要直接调用明确的访问器或修改器方法。

假设有一个 Person 类,包含 firstName 和 lastName 属性:

@interface Person : NSObject
@property (nonatomic, strong) NSString *firstName;
@property (nonatomic, strong) NSString *lastName;
@end

使用 KVC 设置和获取属性值的示例:

Person *person = [[Person alloc] init];
[person setValue:@"John" forKey:@"firstName"];
[person setValue:@"Doe" forKey:@"lastName"];

NSString *firstName = [person valueForKey:@"firstName"];
NSString *lastName = [person valueForKey:@"lastName"];
NSLog(@"Full Name: %@ %@", firstName, lastName);

// 输出结果 -> Full Name: John Doe

在这个例子中,setValue:forKey: 和 valueForKey: 方法被用来分别设置和获取属性值,而不是直接使用属性的 setter 和 getter 方法。

使用场景

  1. 动态数据模型:在需要根据外部数据动态访问或修改对象属性时,KVC 是一种有效的工具。
  2. 绑定和模型层操作:在 MVC 模式中,KVC 可以用来简化视图和模型之间的数据绑定。
  3. 批量操作:KVC 可以与 Key-Value Observing (KVO) 结合使用,对对象属性的变化进行监听和响应,这在处理数据模型的批量更新时非常有用。

2.底层调用逻辑

a.赋值

我们先给出结论,再来验证整个流程,使用KVC给一个对象赋值时, 会有以下方法和属性的调用顺序:

  1. 查看setKey:方法是否存在, 如果存在直接调用, 如果不存在进入下一步
  2. 查看_setKey:方法是否存在, 如果存在直接调用, 如果不存在进入下一步
  3. 查看+ (BOOL)accessInstanceVariablesDirectly方法的返回值, 默认返回YES
  • YES: 可以访问成员变量, 进入下一步
  • NO: 不可以访问成员变量, 同时调用- (void)setValue:(id)value forUndefinedKey:(NSString *)key方法, 如果方法不存在会抛出异常
  1. 调用成员变量:_key, _isKey, key, isKey,调用顺序, 从左到右, 只有发现存在成员变量, 就不会在调用后续变量
  2. 如果没有成员变量, 会调用- (void)setValue:(id)value forUndefinedKey:(NSString *)key方法, 如果方法不存在会抛出异常

我们用下面的代码来验证方法的调用顺序

@interface Person : NSObject
@end

@implementation Person

-(void) setAge: (int)age
{
    NSLog(@"setAge: -%d",age);
}

-(void) _setAge: (int)age
{
    NSLog(@"_setAge: -%d",age);
}

+(BOOL) accessInstanceVariablesDirectly
{
    NSLog(@"accessInstanceVariablesDirectly");
    return NO;
}

-(void) setValue:(id)value forUndefinedKey:(NSString *)key
{
    NSLog(@"setValue:forUndefinedKey: -- %@",key);
}
@end

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

首先调用 setKey 方法:

在这里插入图片描述

如果 setKey 方法不存在,则调用 _setKey 方法:

在这里插入图片描述

如果 _setKey 方法不存在,查看+ (BOOL)accessInstanceVariablesDirectly方法的返回值,返回NO,不可以访问成员变量, 同时调用- (void)setValue:(id)value forUndefinedKey:(NSString *)key方法, 如果方法不存在会抛出异常:

在这里插入图片描述

返回YES, 可以访问成员变量, 进入下一步,调用成员变量,按照_key ,_isKey,,key,isKey的顺序访问:

在这里插入图片描述

没有方法和成员变量时,抛异常:

在这里插入图片描述

b.取值

KVC取值时, 方法和成员变量的调用顺序如下:

  1. 判断是否有这几个方法: getKey, key, isKey, _key,从左到右, 如果有方法, 直接调用,取值结束
  2. 如果没有进入下一步,调用+ (BOOL)accessInstanceVariablesDirectly查看是否可以访问成员变量,默认返回YES
  • YES: 可以访问成员变量,进入下一步
  • NO: 不可以访问成员变量,判断是否实现- (id)valueForUndefinedKey:(NSString *)key方法,实现时调用,未实现报错
  1. 判断是否有这几个成员变量: _key,_isKey, key, isKey,从左到右,如果有成员变量,直接访问,取值结束
  2. 如果没有这几个成员变量, 进入下一步,判断是否实现- (id)valueForUndefinedKey:(NSString *)key方法,实现时调用,未实现报错

我们用下面的代码来验证方法的调用顺序和成员变量的访问顺序:

@interface Person : NSObject
{
    @public
    int _isAge;
    int isAge;
    int age;
    int _age;
}
@end

@implementation Person
-(int) getAge {NSLog(@"getAge");return 0;}
-(int) age {NSLog(@"age");return 1;}
-(int) isAge {NSLog(@"isAge");return 2;}
-(int) _age {NSLog(@"_age");return 3;}

+(BOOL) accessInstanceVariablesDirectly
{
    NSLog(@"accessInstanceVariablesDirectly");
    return NO;
}

-(void) setValue:(id)value forUndefinedKey:(NSString *)key
{
    NSLog(@"setValue:forUndefinedKey: -- %@",key);
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        Person *obj = [[Person alloc] init];
        obj->_age = 10;
        obj->isAge = 13;
        obj->_isAge = 12;
        obj->age = 11;
        
        NSLog(@"%@",[obj valueForKey:@"age"]);
    }
    return 0;
}

getKey, key, isKey, _key,从左到右, 如果有方法, 直接调用,取值结束:

在这里插入图片描述

查看+ (BOOL)accessInstanceVariablesDirectly方法的返回值,返回NO,不可以访问成员变量,判断是否存在- (id)valueForUndefinedKey:(NSString *)key方法,方法存在, 直接调用,如果方法不存在会抛出异常:

在这里插入图片描述

返回YES, 可以访问成员变量, 进入下一步,调用成员变量,按照 _key,_isKey, key,isKey 的顺序访问:

在这里插入图片描述

没有这几个成员变量, 进入下一步,判断是否实现- (id)valueForUndefinedKey:(NSString *)key方法,实现时调用,未实现报错:

在这里插入图片描述

3.使用keyPath​

在开发过程中,一个类的成员变量有可能是自定义类或其他的复杂数据类型,你可以先用KVC获取该属性,然后再次用KVC来获取这个自定义类的属性,但这样是比较繁琐的,对此,KVC提供了一个解决方案,那就是键路径keyPath。顾名思义,就是按照路径寻找key。

- (nullable id)valueForKeyPath:(NSString *)keyPath;                  //通过KeyPath来取值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;  //通过KeyPath来设值

例如下面这种情况,对象 Test 中有一个 Test1 变量,Test1 变量中有变量 name:

@interface Test1: NSObject {
    NSString *_name;
}
@end

@implementation Test1
@end

@interface Test: NSObject {
    Test1 *_test1;
}
@end

@implementation Test
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        
        //Test生成对象
        Test *test = [[Test alloc] init];
        //Test1生成对象
        Test1 *test1 = [[Test1 alloc] init];
        //通过KVC设值test的"test1"
        [test setValue:test1 forKey:@"test1"];
        //通过KVC设值test的"test1的name"
        [test setValue:@"xiaoming" forKeyPath:@"test1.name"];
        //通过KVC取值age打印
        NSLog(@"test的\"test1的name\"是%@", [test valueForKeyPath:@"test1.name"]);
        
    }
    return 0;
}

//运行结果 -> test的"test1的name"是xiaoming

4.异常处理

KVC中最常见的异常就是不小心使用了错误的key,或者在设值中不小心传递了nil的值,KVC中有专门的方法来处理这些异常。

KVC处理nil异常

KVC 不允许我们向一个非对象传递一个 nil 值,因为值类型是不能为nil 的。如果你不小心传了,KVC 会调用 setNilValueForKey:方法。这个方法默认是抛出异常:

在这里插入图片描述

我们也可以重写该方法:

在这里插入图片描述

KVC处理UndefinedKey异常

KVC 不允许在调用 setValue:forKey:时对不存在的 key 进行操作。 否则会调用 setValue:forUndefinedKey: 。这个方法默认是抛出异常:

在这里插入图片描述

重写该方法避免崩溃:

在这里插入图片描述

5.KVC处理数值和结构体类型属性

不是每一个方法都返回对象,但是 valueForKey: 总是返回一个 id 对象,如果原本的变量类型是值类型或者结构体,返回值会封装成NSNumber 或者 NSValue 对象。 这两个类会处理从数字,布尔值到指针和结构体任何类型。然后开以者需要手动转换成原来的类型。 尽管 valueForKey: 会自动将值类型封装成对象,但是 setValue:forKey:却不行。你必须手动将值类型转换成 NSNumber 或者NSValue 类型,才能传递过去。 因为传递进去和取出来的都是 id 类型,所以需要开发者自己担保类型的正确性,运行时Objective-C在发送消息的会检查类型,如果错误会直接抛出异常。

- (id)valueForKey:(NSString *)key;
//参数:key:一个 NSString 对象,代表要访问的属性的名称。
//返回值:返回一个对象,该对象是指定键对应的属性值。如果属性不存在,将调用 -valueForUndefinedKey: 方法,并可能抛出异常。

- (void)setValue:(id)value forKey:(NSString *)key;
//参数:value:要设置的新值,类型为 id,因此可以是任何类型的对象。
//key:一个 NSString 对象,代表要设置的属性的名称。

举个例子,我们无法直接传一个字面值:

在这里插入图片描述

我们可以使用 @ 创建一个 NSNumber 对象:

在这里插入图片描述

我们不能直接将一个数值或结构体通过 KVC 赋值的,我们需要把数据转为 NSNumber 和 NSValue 类型传入,那到底哪些类型数据要用NSNumber 封装,哪些类型数据要用 NSValue 封装呢?看下面这些方法的参数类型就知道了:

//NSNumber就是一些常见的数值型数据。
+ (NSNumber*)numberWithChar:(char)value;
+ (NSNumber*)numberWithUnsignedChar:(unsignedchar)value;
+ (NSNumber*)numberWithShort:(short)value;
+ (NSNumber*)numberWithUnsignedShort:(unsignedshort)value;
+ (NSNumber*)numberWithInt:(int)value;
+ (NSNumber*)numberWithUnsignedInt:(unsignedint)value;
+ (NSNumber*)numberWithLong:(long)value;
+ (NSNumber*)numberWithUnsignedLong:(unsignedlong)value;
+ (NSNumber*)numberWithLongLong:(longlong)value;
+ (NSNumber*)numberWithUnsignedLongLong:(unsignedlonglong)value;
+ (NSNumber*)numberWithFloat:(float)value;
+ (NSNumber*)numberWithDouble:(double)value;
+ (NSNumber*)numberWithBool:(BOOL)value;
+ (NSNumber*)numberWithInteger:(NSInteger)valueNS_AVAILABLE(10_5,2_0);
+ (NSNumber*)numberWithUnsignedInteger:(NSUInteger)valueNS_AVAILABLE(10_5,2_0);

//NSValue主要用于处理结构体型的数据,它本身提供了如上集中结构的支持。任何结构体都是可以转化成NSValue对象的,包括其它自定义的结构体。
+ (NSValue*)valueWithCGPoint:(CGPoint)point;
+ (NSValue*)valueWithCGSize:(CGSize)size;
+ (NSValue*)valueWithCGRect:(CGRect)rect;
+ (NSValue*)valueWithCGAffineTransform:(CGAffineTransform)transform;
+ (NSValue*)valueWithUIEdgeInsets:(UIEdgeInsets)insets;
+ (NSValue*)valueWithUIOffset:(UIOffset)insetsNS_AVAILABLE_IOS(5_0);

6.KVC键值验证

KVC 提供了验证 Key 对应的 Value 是否可用的方法:

- (BOOL)validateValue:(inoutid*)ioValue forKey:(NSString*)inKey error:(outNSError**)outError;

@interface Test: NSObject {
    NSUInteger age;
}

@end

@implementation Test

- (BOOL)validateValue:(inout id  _Nullable __autoreleasing *)ioValue forKey:(NSString *)inKey error:(out NSError * _Nullable __autoreleasing *)outError {
    NSNumber *age = *ioValue;
    if (age.integerValue == 10) {
        return NO;
    }
    
    return YES;
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //Test生成对象
        Test *test = [[Test alloc] init];
        //通过KVC设值test的age
        NSNumber *age = @10;
        NSError* error;
        NSString *key = @"age";
        BOOL isValid = [test validateValue:&age forKey:key error:&error];
        if (isValid) {
            NSLog(@"键值匹配");
            [test setValue:age forKey:key];
        }
        else {
            NSLog(@"键值不匹配");
        }
        //通过KVC取值age打印
        NSLog(@"test的年龄是%@", [test valueForKey:@"age"]);
        
    }
    return 0;
}

所以我们可以通过这些方法在设值时进行验证:
在这里插入图片描述

7.KVC处理集合

KVC 同时还提供了一些函数来帮助我们处理集合的数据,主要有下面这些:

简单集合运算符共有5种:

  • @avg:计算集合中对象的平均值。
  • @count:返回集合中对象的数量。
  • @max:返回集合中的最大值。
  • @min:返回集合中的最小值。
  • @sum:计算集合中对象的总和。
@interface Book : NSObject
@property (nonatomic, copy)  NSString* name;
@property (nonatomic, assign)  CGFloat price;
@end

@implementation Book
@end


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        Book *book1 = [Book new];
        book1.name = @"The Great Gastby";
        book1.price = 10;
        Book *book2 = [Book new];
        book2.name = @"Time History";
        book2.price = 20;
        Book *book3 = [Book new];
        book3.name = @"Wrong Hole";
        book3.price = 30;
        
        Book *book4 = [Book new];
        book4.name = @"Wrong Hole";
        book4.price = 40;
        
        NSArray* arrBooks = @[book1,book2,book3,book4];
        NSNumber* sum = [arrBooks valueForKeyPath:@"@sum.price"];
        NSLog(@"sum:%f",sum.floatValue);
        NSNumber* avg = [arrBooks valueForKeyPath:@"@avg.price"];
        NSLog(@"avg:%f",avg.floatValue);
        NSNumber* count = [arrBooks valueForKeyPath:@"@count"];
        NSLog(@"count:%f",count.floatValue);
        NSNumber* min = [arrBooks valueForKeyPath:@"@min.price"];
        NSLog(@"min:%f",min.floatValue);
        NSNumber* max = [arrBooks valueForKeyPath:@"@max.price"];
        NSLog(@"max:%f",max.floatValue);
        
    }
    return 0;
}

我们通过这些函数可以快速得到结果:

在这里插入图片描述

数组和集合操作符

  • @distinctUnionOfObjects:返回一个数组,包含被观察对象集合中唯一对象的列表。
  • @unionOfObjects:返回一个数组,包含所有观察对象的集合。
@interface Book : NSObject
@property (nonatomic, copy)  NSString* name;
@property (nonatomic, assign)  CGFloat price;
@end

@implementation Book
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        Book *book1 = [Book new];
        book1.name = @"The Great Gastby";
        book1.price = 40;
        Book *book2 = [Book new];
        book2.name = @"Time History";
        book2.price = 40;
        
        NSArray* arrBooks = @[book1,book2];
        
        NSLog(@"distinctUnionOfObjects");
        NSArray* arrDistinct = [arrBooks valueForKeyPath:@"@distinctUnionOfObjects.price"];
        for (NSNumber *price in arrDistinct) {
            NSLog(@"%f",price.floatValue);
        }
        NSLog(@"unionOfObjects");
        NSArray* arrUnion = [arrBooks valueForKeyPath:@"@unionOfObjects.price"];
        for (NSNumber *price in arrUnion) {
            NSLog(@"%f",price.floatValue);
        }
        
    }
    return 0;
}

运行的结果如下:

在这里插入图片描述

嵌套集合操作符

  • @distinctUnionOfArrays:对数组的数组进行扁平化处理,并返回一个包含唯一对象的数组。
  • @unionOfArrays:对数组的数组进行扁平化处理,返回一个包含所有对象的数组。

8.KVC处理字典

KVC里面还有两个关于NSDictionary的方法:

//用于从对象中获取一组属性的值,并将这些值以字典的形式返回。
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;

//允许同时设置多个属性的值,通过传递一个字典来实现。
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;

下面是一个例子:

@interface Address : NSObject
@end

@interface Address()
@property (nonatomic, copy)NSString* country;
@property (nonatomic, copy)NSString* province;
@property (nonatomic, copy)NSString* city;
@property (nonatomic, copy)NSString* district;
@end

@implementation Address
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //模型转字典
        Address* add = [Address new];
        add.country = @"China";
        add.province = @"Guang Dong";
        add.city = @"Shen Zhen";
        add.district = @"Nan Shan";
        NSArray* arr = @[@"country",@"province",@"city",@"district"];
        NSDictionary* dict = [add dictionaryWithValuesForKeys:arr]; //把对应key所有的属性全部取出来
        NSLog(@"%@",dict);
        
        //字典转模型
        NSDictionary* modifyDict = @{@"country":@"USA",@"province":@"california",@"city":@"Los angle"};
        [add setValuesForKeysWithDictionary:modifyDict];            //用key Value来修改Model的属性
        NSLog(@"country:%@  province:%@ city:%@",add.country,add.province,add.city);    
    }
    return 0;
}

运行结果如下:

在这里插入图片描述

二、KVO

1.定义

  • KVO的全称是Key-Value Observing,俗称“键值观察/监听”,是苹果提供的一套事件通知机制,允许一个对象观察/监听另一个对象指定属性值的改变。当被观察对象属性值发生改变时,会触发KVO的监听方法来通知观察者。KVO是在MVC应用程序中的各层之间进行通信的一种特别有用的技术。
  • KVO和NSNotification都是iOS中观察者模式的一种实现。
  • KVO可以监听单个属性的变化,也可以监听集合对象的变化。监听集合对象变化时,需要通过KVC的mutableArrayValueForKey:等可变代理方法获得集合代理对象,并使用代理对象进行操作,当代理对象的内部对象发生改变时,会触发KVO的监听方法。集合对象包含NSArray和NSSet。

2.基本使用

KVO使用三部曲:添加/注册KVO监听、实现监听方法以接收属性改变通知、 移除KVO监听。

  1. 调用方法 addObserver:forKeyPath:options:context: 给被观察对象添加观察者;
  2. 在观察者类中实现 observeValueForKeyPath:ofObject:change:context: 方法以接收属性改变的通知消息;
  3. 当观察者不需要再监听时,调用 removeObserver:forKeyPath: 方法将观察者移除。需要注意的是,至少需要在观察者销毁之前,调用此方法,否则可能会导致Crash。

a.注册监听

addObserver:forKeyPath:options:context: 是 Objective-C 中 NSKeyValueObserving 协议的一部分,用于为对象的属性添加观察者。这是实现观察者模式的关键方法,允许对象监视另一个对象的属性变化。

- (void)addObserver:(NSObject *)observer 
          forKeyPath:(NSString *)keyPath
             options:(NSKeyValueObservingOptions)options 
             context:(nullable void *)context;

observer:观察者对象,当被观察的属性值发生变化时会接收到通知。
keyPath:要观察的属性的键路径。
options:观察选项,决定观察者接收到的通知包含哪些信息。
				/*NSKeyValueObservingOptionNew:观察新值
        	NSKeyValueObservingOptionOld:观察旧值
        	NSKeyValueObservingOptionInitial:观察初始值,如果想在注册观察者后,立即接收一次回调,可以加入该枚举值
        	NSKeyValueObservingOptionPrior:分别在值改变前后触发方法(即一次修改有两次触发*/
context:可以传入任意数据(任意类型的对象或者C指针),在监听方法中可以接收到这个数据,是KVO中的一种传值方式,
					如果传的是一个对象,必须在移除观察之前持有它的强引用,否则在监听方法中访问context就可能导致Crash

b.监听方法

如果对象被注册成为观察者,则该对象必须能响应以下监听方法,即该对象所属类中必须实现监听方法。当被观察对象属性发生改变时就会调用监听方法。如果没有实现就会导致Crash。

- (void)observeValueForKeyPath:(NSString *)keyPath 
                      ofObject:(id)object 
                        change:(NSDictionary<NSKeyValueChangeKey, id> *)change 
                       context:(void *)context;
keyPath:被观察对象的属性的关键路径
object: 被观察对象
context:注册方法中传入的context
change: 字典 NSDictionary<NSKeyValueChangeKey, id>,属性值更改的详细信息,根据注册方法中options参数传入的枚举来返回
             /*key为 NSKeyValueChangeKey 枚举类型
             {
                 1.NSKeyValueChangeKindKey:存储本次改变的信息(change字典中默认包含这个key)
                 {
                     对应枚举类型 NSKeyValueChange
                     typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
                         NSKeyValueChangeSetting     = 1,
                         NSKeyValueChangeInsertion   = 2,
                         NSKeyValueChangeRemoval     = 3,
                         NSKeyValueChangeReplacement = 4,
                     };
                     如果是对被观察对象属性(包括集合)进行赋值操作,kind 字段的值为 NSKeyValueChangeSetting
                     如果被观察的是集合对象,且进行的是(插入、删除、替换)操作,则会根据集合对象的操作方式来设置 kind 字段的值
                         插入:NSKeyValueChangeInsertion
                         删除:NSKeyValueChangeRemoval
                         替换:NSKeyValueChangeReplacement
                 }    
                 2.NSKeyValueChangeNewKey:存储新值(如果options中传入NSKeyValueObservingOptionNew,change字典中就会包含这个key)
                 3.NSKeyValueChangeOldKey:存储旧值(如果options中传入NSKeyValueObservingOptionOld,change字典中就会包含这个key)
                 4.NSKeyValueChangeIndexesKey:如果被观察的是集合对象,且进行的是(插入、删除、替换)操作,则change字典中就会包含这个key,
                     这个key的value是一个NSIndexSet对象,包含更改关系中的索引
                 5.NSKeyValueChangeNotificationIsPriorKey:如果options中传入NSKeyValueObservingOptionPrior,则在改变前通知的change字典中会包含这个key。
                     这个key对应的value是NSNumber包装的YES,我们可以这样来判断是不是在改变前的通知[change[NSKeyValueChangeNotificationIsPriorKey] boolValue] == YES]
             }*/

c.移除方法

在调用注册方法后,KVO并不会对观察者进行强引用,所以需要注意观察者的生命周期。至少需要在观察者销毁之前,调用以下方法移除观察者,否则如果在观察者被释放后,再次触发KVO监听方法就会导致Crash。

- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context;

observer:要移除的观察者对象。
keyPath:之前注册观察者时用的键路径。
context:之前注册观察者时提供的上下文,仅在使用 removeObserver:forKeyPath:context: 时需要。

d.使用示例

下面这个示例展示了如何使用 KVO 来监听和响应 Objective-C 对象属性的变化。通过这种方式,可以构建响应式的应用程序,其中对象可以响应其它对象状态的变化。

@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
@end

@implementation Person
@end

//-------------------------------------------------------------------------------------------------------
@interface Observer : NSObject
@end

@implementation Observer
- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary<NSKeyValueChangeKey, id> *)change
                       context:(void *)context {
    if ([keyPath isEqualToString:@"name"]) {
        NSLog(@"Observer :Name changed to %@", change[NSKeyValueChangeNewKey]);
    }
}
@end

//-------------------------------------------------------------------------------------------------------

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        Observer *observer = [[Observer alloc] init];
        
        // 添加观察者
        [person addObserver:observer
                 forKeyPath:@"name"
                    options:NSKeyValueObservingOptionNew
                    context:NULL];
        
        // 改变属性
        person.name = @"Alice";
        
        // 移除观察者
        [person removeObserver:observer forKeyPath:@"name"];
    }
    return 0;
}

运行结果如下:
在这里插入图片描述

3.触发监听的方式

KVO 触发分为自动触发和手动触发两种方式。

a.自动触发

(1)如果是监听对象特定属性值的改变,通过以下方式改变属性值会触发KVO:

  • 使用点语法
  • 使用setter方法
  • 使用KVC的 setValue:forKey:方法
  • 使用KVC的 setValue:forKeyPath:方法

(2)如果是监听集合对象的改变,需要通过KVC的 mutableArrayValueForKey: 等方法获得代理对象,并使用代理对象进行操作,当代理对象的内部对象发生改变时,会触发KVO。集合对象包含 NSArray 和 NSSet。

b.手动触发

(1)普通对象属性或是成员变量使用:

- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;

(2)NSArray对象使用:

- (void)willChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;
- (void)didChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;

(3)NSSet对象使用:

- (void)willChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;
- (void)didChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;

4.进阶使用

a.observationInfo 属性

  • 在 Objective-C 的 Key-Value Observing (KVO) 机制中,observationInfo 属性是 NSKeyValueObserving.h 文件中系统通过 Category给 NSObject 添加的属性,所有继承于 NSObject 的对象都含有该属性,用于获取或设置与对象相关的观察者信息。这个属性提供了一个低级的方式来访问对象的观察者详情;
  • 可以通过observationInfo属性查看被观察对象的全部观察信息,包括observer、keyPath、options、context等。
@property (nullable, nonatomic) void *observationInfo;

//类型:void *,这意味着它是一个指向任意类型的指针,具体类型取决于 KVO 的内部实现。
//访问级别:可读可写。你可以获取这个属性来查看当前的观察者信息,也可以设置它来自定义或替换观察者信息

我们用上面的例子来看看这个属性里面的内容:

在这里插入图片描述

b.context 的使用

context作用:标签-区分,可以更精确的确定被观察对象属性,用于继承、 多监听;也可以用来传值。

KVO只有一个监听回调方法observeValueForKeyPath:ofObject:change:context:,我们通常情况下可以在注册方法中指定context为NULL,并在监听方法中通过 object 和 keyPath 来判断触发KVO的来源。

苹果的推荐用法:

用 context 来精确的确定被观察对象属性,使用唯一命名的静态变量的地址作为 context 的值。可以为整个类设置一个 context,然后在监听方法中通过 object 和 keyPath 来确定被观察属性,这样存在继承的情况就可以通过 context 来判断;也可以为每个被观察对象属性设置不同的 context,这样使用 context 就可以精确的确定被观察对象属性。

当一个对象观察多个属性时,context 可以帮助区分是哪一个属性发生了变化。下面是一个例子:

static void *PersonAccountBalanceContext = &PersonAccountBalanceContext;
static void *PersonAccountInterestRateContext = &PersonAccountInterestRateContext;

- (void)registerAsObserverForAccount:(Account*)account {
    [account addObserver:self
              forKeyPath:@"balance"
                 options:(NSKeyValueObservingOptionNew |
                          NSKeyValueObservingOptionOld)
                 context:PersonAccountBalanceContext];
 
    [account addObserver:self
              forKeyPath:@"interestRate"
                 options:(NSKeyValueObservingOptionNew |
                          NSKeyValueObservingOptionOld)
                  context:PersonAccountInterestRateContext];
}

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context {
 
    if (context == PersonAccountBalanceContext) {
        // Do something with the balance…
 
    } else if (context == PersonAccountInterestRateContext) {
        // Do something with the interest rate…
 
    } else {
        // Any unrecognized context must belong to super
        [super observeValueForKeyPath:keyPath
                             ofObject:object
                               change:change
                               context:context];
    }
}

context优点:嵌套少、性能高、更安全、扩展性强。

context注意点:

  • 如果传的是一个对象,必须在移除观察之前持有它的强引用,否则在监听方法中访问context就可能导致Crash;
  • 空传 NULL 而不应该传 nil 。

c.KVO 监听集合对象

mutableArrayValueForKey: 是 Objective-C 中 NSKeyValueCoding 协议的一个方法,它用于获取一个可以被观察的可变数组代理。这个代理数组允许开发者使用标准的 NSMutableArray 方法来修改数组,同时自动触发 Key-Value Observing (KVO) 通知,从而使观察者能够响应这些改变。

例如我们通过 mutableArrayValueForKey: 获取数组的一个可观察代理 friendsProxy。任何通过 friendsProxy 做的修改都会触发 KVO 通知。

@interface Person : NSObject
@property (nonatomic, strong) NSMutableArray *friends;
@end

@implementation Person
- (instancetype)init {
    if (self = [super init]) {
        _friends = [NSMutableArray array];
    }
    return self;
}
@end

//----------------------------------------------------------------------------------------------------

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        NSMutableArray *friendsProxy = [person mutableArrayValueForKey:@"friends"];
        
        [friendsProxy addObject:@"Alice"];
        [friendsProxy addObject:@"Bob"];
        
        NSLog(@"Friends: %@", person.friends);
    }
    return 0;
}

运行结果如下:
在这里插入图片描述

d.KVO 的自动触发控制

  • automaticallyNotifiesObserversForKey: 是 Objective-C 中 NSObject 类的一个类方法,用于确定当特定属性的值改变时,是否应该自动发送 Key-Value Observing (KVO) 通知。这个方法提供了一种机制,允许开发者控制特定属性的 KVO 通知的自动化。
  • 默认情况下,NSObject 的实现总是返回 YES,这意味着对于大多数属性,任何更改都会自动触发 KVO 通知。如果属性是通过 @synthesize 自动生成的访问器来实现的,那么这些访问器会自动包含 KVO 通知的发送。
  • 如果你想要手动控制属性的 KVO 通知(例如,当你需要在一个操作中更新多个属性并且只想发送一次通知时),你可以重写这个方法,并在你的类中对特定的键返回 NO。然后,你需要在属性变化前后手动调用 willChangeValueForKey: 和 didChangeValueForKey: 来发送通知。

下面是一个示例,展示如何为一个名为 age 的属性禁用自动通知,并在更改值时手动发送通知:

@interface Person : NSObject
@property (nonatomic, assign) NSInteger age;
@end

@implementation Person

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    if ([key isEqualToString:@"age"]) {
        return NO;  // 禁用自动通知
    }
    return [super automaticallyNotifiesObserversForKey:key];
}

- (void)setAge:(NSInteger)age {
    [self willChangeValueForKey:@"age"];
    _age = age;
    [self didChangeValueForKey:@"age"];
}

@end

e.KVO 的手动触发控制

在 Objective-C 中使用 Key-Value Observing (KVO) 时,通常情况下,属性的更改会自动触发观察者的通知。然而,有时候为了更细粒度的控制或优化性能,我们可能需要手动控制这些通知的触发。这可以通过重写 automaticallyNotifiesObserversForKey: 方法并使用 willChangeValueForKey: 和 didChangeValueForKey: 方法来实现。

假设有一个 Account 类,其中有一个 balance 属性,我们希望手动控制其 KVO 通知:

@interface Account : NSObject
@property (nonatomic, assign) double balance;
@end

@implementation Account

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    if ([key isEqualToString:@"balance"]) {
        return NO;  // 禁用自动通知
    }
    return [super automaticallyNotifiesObserversForKey:key];
}

- (void)setBalance:(double)newBalance {
    [self willChangeValueForKey:@"balance"];
    _balance = newBalance;
    [self didChangeValueForKey:@"balance"];
}

@end

在这个例子中,我们首先禁用了 balance 属性的自动通知。在 setBalance: 方法中,我们在实际更改值之前调用了 willChangeValueForKey:,并在更改后调用了 didChangeValueForKey:。这样,每次 balance 属性被更改时,都会手动触发 KVO 通知。

f.KVO 新旧值相等时不触发

  • 有时候我们可能会有这样的需求,KVO监听的属性值修改前后相等的时候,不触发KVO的监听方法,可以结合KVO的自动触发控制和手动触发来实现。
  • 例如:对 person 对象的 name 属性注册了KVO监听,我们希望在对 name 属性赋值时做一个判断,如果新值和旧值相等,则不触发KVO,可以在 Person 类中如下这样实现,将 name 属性值改变的KVO触发方式由自动触发改为手动触发。
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
    BOOL automatic = YES;
    if ([key isEqualToString:@"name"]) {
        automatic = NO;
    } else {
        automatic = [super automaticallyNotifiesObserversForKey:key];
    }
    return automatic;
}

- (void)setName:(NSString *)name
{
    if (![_name isEqualToString:name]) {
        [self willChangeValueForKey:@"name"];
        _name = name;
        [self didChangeValueForKey:@"name"];
    } 
}

5.使用注意

a.移除观察者的注意点

  • 在调用KVO注册方法后,KVO并不会对观察者进行强引用,所以需要注意观察者的生命周期。至少需要在观察者销毁之前,调用KVO移除方法移除观察者,否则如果在观察者被释放后,再次触发KVO监听方法就会导致Crash。
  • KVO的注册方法和移除方法应该是成对的,如果重复调用移除方法,就会抛出异常NSRangeException并导致程序Crash。
  • 苹果官方推荐的方式是,在观察者初始化期间(init 或者 viewDidLoad 的时候)注册为观察者,在释放过程中(dealloc时)调用移除方法,这样可以保证它们是成对出现的,是一种比较理想的使用方式。

b.防止多次注册和移除相同的 KVO

  • 有时候我们难以避免多次注册和移除相同的KVO,或者移除了一个未注册的观察者,从而产生可能会导致Crash的风险。三种解决方案:黑科技防止多次添加删除KVO出现的问题

c.其它注意点

  • 如果对象被注册成为观察者,则该对象必须能响应监听方法,即该对象所属类中必须实现监听方法。当被观察对象属性发生改变时就会调用监听方法。如果没有实现就会导致Crash。所以KVO三部曲缺一不可。
  • 如果注册方法中context传的是一个对象,必须在移除观察之前持有它的强引用,否则在监听方法中访问context就可能导致Crash。
  • 如果是监听集合对象的改变,需要通过 KVC的mutableArrayValueForKey: 等方法获得代理对象,并使用代理对象进行操作,当代理对象的内部对象发生改变时,会触发KVO。如果直接对集合对象进行操作改变,不会触发KVO。
  • 在观察者类的监听方法中,应该为无法识别的context或者object、keyPath 调用父类的实现[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值