系列文章目录
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 方法。
使用场景
- 动态数据模型:在需要根据外部数据动态访问或修改对象属性时,KVC 是一种有效的工具。
- 绑定和模型层操作:在 MVC 模式中,KVC 可以用来简化视图和模型之间的数据绑定。
- 批量操作:KVC 可以与 Key-Value Observing (KVO) 结合使用,对对象属性的变化进行监听和响应,这在处理数据模型的批量更新时非常有用。
2.底层调用逻辑
a.赋值
我们先给出结论,再来验证整个流程,使用KVC给一个对象赋值时, 会有以下方法和属性的调用顺序:
- 查看setKey:方法是否存在, 如果存在直接调用, 如果不存在进入下一步
- 查看_setKey:方法是否存在, 如果存在直接调用, 如果不存在进入下一步
- 查看+ (BOOL)accessInstanceVariablesDirectly方法的返回值, 默认返回YES
- YES: 可以访问成员变量, 进入下一步
- NO: 不可以访问成员变量, 同时调用- (void)setValue:(id)value forUndefinedKey:(NSString *)key方法, 如果方法不存在会抛出异常
- 调用成员变量:_key, _isKey, key, isKey,调用顺序, 从左到右, 只有发现存在成员变量, 就不会在调用后续变量
- 如果没有成员变量, 会调用- (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取值时, 方法和成员变量的调用顺序如下:
- 判断是否有这几个方法: getKey, key, isKey, _key,从左到右, 如果有方法, 直接调用,取值结束
- 如果没有进入下一步,调用+ (BOOL)accessInstanceVariablesDirectly查看是否可以访问成员变量,默认返回YES
- YES: 可以访问成员变量,进入下一步
- NO: 不可以访问成员变量,判断是否实现- (id)valueForUndefinedKey:(NSString *)key方法,实现时调用,未实现报错
- 判断是否有这几个成员变量: _key,_isKey, key, isKey,从左到右,如果有成员变量,直接访问,取值结束
- 如果没有这几个成员变量, 进入下一步,判断是否实现- (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监听。
- 调用方法 addObserver:forKeyPath:options:context: 给被观察对象添加观察者;
- 在观察者类中实现 observeValueForKeyPath:ofObject:change:context: 方法以接收属性改变的通知消息;
- 当观察者不需要再监听时,调用 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];。