相信读者对KVO的使用应该已经很熟练了,本文主要讲KVO的一些注意点和原理,对详细的使用不做过多的展示。
日常使用注意点
context 参数
1.context填NULL还是nil?先看源代码:
[self addObserver:self.person forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:<#(nullable void *)#>]
下面context应该填什么?日常我们一般不会用到context
这个参数,一般会填nil或者NULL;那么到底是应该填nil,还是NULL;答案是:NULL;
why? 看看在context的placehold上显示的是context:<#(nullable void *)#>
,是一个可空void *指针,既然不是oc对象,那么就应该填NULL。我们再看看KVO文档,文档中有这么一点段话
2.这个context的作用?show U code
######Person
@interface Person : NSObject
@property (nonatomic,copy) NSString *name;
@property (nonatomic,copy) NSString *nickName;
@end
######Animal
@interface Animal : NSObject
@property (nonatomic,copy) NSString *name;
@end
######ViewController
#import "ViewController.h"
#import "Person.h"
#import "Animal.h"
static void *PersonNameContext = &PersonNameContext;
static void *PersonNickNameContext = &PersonNickNameContext;
static void *AnimalNameContext = &AnimalNameContext;
@interface ViewController ()
@property (nonatomic,strong) Person *person;
@property (nonatomic,strong) Animal *animal;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor redColor];
self.person = [Person new];
self.animal = [Animal new];
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:PersonNameContext];
[self.animal addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:AnimalNameContext];
[self.person addObserver:self forKeyPath:@"nickName" options:NSKeyValueObservingOptionNew context:PersonNickNameContext];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
//常规做法
// if ([object isEqual:self.person]) {
// if ([keyPath isEqualToString:@"name"]) {
// <#statements#>
// } else if ([keyPath isEqualToString:@"nickName"]) {
// <#statements#>
// }
// } else if ([object isEqual:self.animal]) {
// if ([keyPath isEqualToString:@"name"]) {
// <#statements#>
// }
// }
//context做法
if (context == PersonNameContext) {
if ([keyPath isEqualToString:@"name"]) {
NSLog(@"name=%@",self.person.name);
}
} else if (context == AnimalNameContext ) {
if ([keyPath isEqualToString:@"name"]) {
}
} else if (context == PersonNickNameContext) {
if ([keyPath isEqualToString:@"nickName"]) {
NSLog(@"nickName=%@",self.person.nickName);
}
}
}
static int a = 1;
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
a ++;
self.person.name = [NSString stringWithFormat:@"name+%d",a];
self.person.nickName = [NSString stringWithFormat:@"nickName+%d",a];
}
-(void)dealloc {
[self.person removeObserver:self forKeyPath:@"name"];
[self.person removeObserver:self forKeyPath:@"nickName"];
[self.animal removeObserver:self forKeyPath:@"name"];
}
@end
所以:使用context
参数会更加的便捷高效安全
观察者要移除
日常开发中,一定要写移除观察者的代码,如果没有移除,会有造成野指针,成为崩溃隐患。
多次修改代码高效设置
eg:假设在上面context 参数内容段中,因为需求Person
类中的name
属性昨天是需要观察的,而今天一上班,产品经理说需求又改了,又不需要再观察了这个那么属性。通常遇到这种情况的时候,我们会删掉(注销)之前写好的代码,然后又过了几天产品经理要求改回来;遇到这个情况估计你的内心会有一万匹草泥马跑过。因为KVO代码量分散且并不少,这种操作其实让人很烦;这个时候你可以在Person类中重写这个方法automaticallyNotifiesObserversForKey:
(是否自动观察属性)这样操作:只会观察nickName
,而不会观察name
#import "Person.h"
@implementation Person
// 自动开关
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
if ([key isEqualToString:@"name"]) {
return NO;
}
return YES;
}
@end
使用automaticallyNotifiesObserversForKey:
根据key
去判断,可以让程序更加健壮;
多个因素影响
当被观察的对象受到其他多个因素影响时;
eg:下载进度受当前下载量和总下载量的影响,但是我们需要观察的是进度,可以使用keyPathsForValuesAffectingValueForKey:
#####DownLoadManager
@interface DownLoadManager : NSObject
@property (nonatomic, copy) NSString *downloadProgress;
@property (nonatomic, assign) double writtenData;
@property (nonatomic, assign) double totalData;
@end
#import "DownLoadManager.h"
@implementation DownLoadManager
// 下载进度 -- writtenData/totalData
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"downloadProgress"]) {
NSArray *affectingKeys = @[@"totalData", @"writtenData"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
- (NSString *)downloadProgress{
return [NSString stringWithFormat:@"downloadProgress =%.02f%%",self.writtenData/self.totalData*100];
}
@end
######ViewController
#import "ViewController.h"
#import "DownLoadManager.h"
@interface ViewController ()
@property (nonatomic,strong) DownLoadManager *downLoadManager;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.downLoadManager = [DownLoadManager new];
self.downLoadManager.writtenData = 10;
self.downLoadManager.totalData = 100;
// 多个因素影响 - 下载进度 = 当前下载量 / 总量
[self.downLoadManager addObserver:self forKeyPath:@"downloadProgress" options:(NSKeyValueObservingOptionNew) context:NULL];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
if ([keyPath isEqualToString:@"downloadProgress"]) {
NSLog(@"downloadProgress = %@",self.downLoadManager.downloadProgress);
}
}
//点击屏幕
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
self.downLoadManager.writtenData += 10;
self.downLoadManager.totalData += 5;
}
-(void)dealloc {
[self.downLoadManager removeObserver:self forKeyPath:@"downloadProgress"];
}
@end
点击屏幕打印结果:
2020-02-21 11:08:38.002797+0800 KTest[5745:107244] downloadProgress = downloadProgress =20.00%
2020-02-21 11:08:38.002912+0800 KTest[5745:107244] downloadProgress = downloadProgress =19.05%
2020-02-21 11:08:39.408895+0800 KTest[5745:107244] downloadProgress = downloadProgress =28.57%
2020-02-21 11:08:39.409002+0800 KTest[5745:107244] downloadProgress = downloadProgress =27.27%
2020-02-21 11:08:40.105935+0800 KTest[5745:107244] downloadProgress = downloadProgress =36.36%
2020-02-21 11:08:40.106029+0800 KTest[5745:107244] downloadProgress = downloadProgress =34.78%
可变数组
观察可变数组的增删改查时,不要直接使用addObject:
或者removeObject:
直接使用会崩溃,需要先通过mutableArrayValueForKey:
获得数组对象,才能进一步操作;原因:iOS默认不支持对数组的KVO,KVO是通过KVC实现的,普通方式监听的对象的地址的变化,而数组地址不变,而是里面的值发生了改变;
eg:
######Person
@interface Person : NSObject
@property (nonatomic,copy) NSMutableArray *studentNameArray;
@end
######ViewController
#import "ViewController.h"
#import "Person.h"
#import "Animal.h"
#import "DownLoadManager.h"
static void *PersonNameContext = &PersonNameContext;
static void *PersonNickNameContext = &PersonNickNameContext;
static void *AnimalNameContext = &AnimalNameContext;
@interface ViewController ()
@property (nonatomic,strong) Person *person;
@property (nonatomic,strong) Animal *animal;
@property (nonatomic,strong) DownLoadManager *downLoadManager;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [Person new];
self.animal = [Animal new];
self.person.studentNameArray = [NSMutableArray arrayWithCapacity:1];
[self.person addObserver:self forKeyPath:@"studentNameArray" options:NSKeyValueObservingOptionNew context:NULL];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
if ([keyPath isEqualToString:@"studentNameArray"]) {
NSLog(@"studentNameArray = %@",self.person.studentNameArray);
}
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
// KVO 建立在 KVC上
// [self.person.studentNameArray addObject:@"lee"];不要这么做,会崩溃
//使用mutableArrayValueForKey获取数组对象
[[self.person mutableArrayValueForKey:@"studentNameArray"] addObject:@"lee"];
[[self.person mutableArrayValueForKey:@"studentNameArray"] removeObject:@"lee"];
}
-(void)dealloc {
[self.person removeObserver:self forKeyPath:@"studentNameArray"];
}
@end
KVO原理
Automatic key-value observing is implemented using a technique called isa-swizzling.
这段话来自官方文档:KVO是通过isa-swizzling
实现的;
那具体是如何isa-swizzling
的呢?
1.动态的生成子类:NSKVONotifying_XXX
验证:
######Person
@interface Person : NSObject
@property (nonatomic,copy) NSString *nickName;
@end
######KVOIMPViewController
#import "KVOIMPViewController.h"
#import
#import "Person.h"
@interface KVOIMPViewController ()
@property (nonatomic,strong) Person *person;
@end
@implementation KVOIMPViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor greenColor];
self.person = [[Person alloc] init];
[self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
int a ;
}
在[self.person addObserver:self forKey...
和int a
两个地方分别打上断点;使用LLDB调试:
(lldb) po object_getClassName(self.person)
"Person"
(lldb) po object_getClassName(self.person)
"NSKVONotifying_Person"
可以看到在运行了addObserver:(NSObject *)observer forKeyPath:...
之后,当前Person
类变成了NSKVONotifying_Person
。
上面只能说明生成了NSKVONotifying_Person
类但不能说明是Person
类的子类;继续验证
- (void)viewDidLoad {
[super viewDidLoad];
[self printClassAllMethod:NSClassFromString(@"NSKVONotifying_Person")];
[self printClasses:[Person class]];
}
#pragma mark - 遍历类以及子类
- (void)printClasses:(Class)cls{
// 注册类的总数
int count = objc_getClassList(NULL, 0);
// 创建一个数组, 其中包含给定对象
NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls];
// 获取所有已注册的类
Class* classes = (Class*)malloc(sizeof(Class)*count);
objc_getClassList(classes, count);
for (int i = 0; i
打印结果
KTest[10265:237411] classes = (
Person,
"NSKVONotifying_Person"
)
可以看到Person
类下面确实还有子类NSKVONotifying_Person
内部关系图:
2.动态子类生重写了很多方法
打印观察前后,类方法的变化
######Person
@interface Person : NSObject
//@property (nonatomic,copy) NSString *name;
@property (nonatomic,copy) NSString *nickName;
//@property (nonatomic,copy) NSMutableArray *studentNameArray;
@end
######KVOIMPViewController
#import "KVOIMPViewController.h"
#import
#import "Person.h"
@interface KVOIMPViewController ()
@property (nonatomic,strong) Person *person;
@end
@implementation KVOIMPViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [[Person alloc] init];
[self printClassAllMethod:NSClassFromString(@"Person")];
[self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
[self printClassAllMethod:NSClassFromString(@"NSKVONotifying_Person")];
}
#pragma mark - 遍历方法-ivar-property
- (void)printClassAllMethod:(Class)cls{
NSLog(@"*********************%@类的方法list",NSStringFromClass(cls));
unsigned int count = 0;
Method *methodList = class_copyMethodList(cls, &count);
for (int i = 0; i
打印结果:
KTest[9724:222902] *********************Person类的方法list
KTest[9724:222902] .cxx_destruct-0x106ace0d0
KTest[9724:222902] nickName-0x106ace060
KTest[9724:222902] setNickName:-0x106ace090
KTest[9724:222902] *********************NSKVONotifying_Person类的方法list
KTest[9724:222902] setNickName:-0x7fff25721c7a
KTest[9724:222902] class-0x7fff2572073d
KTest[9724:222902] dealloc-0x7fff257204a2
KTest[9724:222902] _isKVOA-0x7fff2572049a
从打印可以看到:
NSKVONotifying_Person重写了setNickName class dealloc _isKVOA
方法;
1.setNickName:
重写的setNickName:方法内部大概这么实现的
@implementation Person
- (void)setNickName:(NSString *)nickName {
[self willChangeValueForKey:@"nickName"];
_nickName = nickName;
[self didChangeValueForKey:@"nickName"];
}
@end
2.class
为何重写class方法:为了让外界感受不到子类NSKVONotifying_Person
的生成
- dealloc
重写这个方法应该是为了在对象销毁的时候做一些操作吧,尚未探究 - _isKVOA
_isKVOA:判断是否是KVO生成的类
3.移除观察之后 isa指针是否指回来?
断点调试:
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor greenColor];
self.person = [[Person alloc] init];
NSLog(@"*******添加观察者之前");
[self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
NSLog(@"*******添加观察者之后");
[self.person removeObserver:self forKeyPath:@"nickName"];
NSLog(@"*******移除观察者之后");
}
打印结果:
KTest[10766:251988] *******添加观察者之前
(lldb) po object_getClassName(self.person)
"Person"
KTest[10766:251988] *******添加观察者之后
(lldb) po object_getClassName(self.person)
"NSKVONotifying_Person"
KTest[10766:251988] *******移除观察者之后
(lldb) po object_getClassName(self.person)
"Person"
可以看到,person
对象的类又指回了Person
类
移除观察者后动态子类会被销毁吗?不会。
验证:
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [[Person alloc] init];
NSLog(@"*******添加观察者之前");
[self printClasses:[Person class]];
[self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
NSLog(@"*******添加观察者之后");
[self printClasses:[Person class]];
[self.person removeObserver:self forKeyPath:@"nickName"];
NSLog(@"*******移除观察者之后");
[self printClasses:[Person class]];
}
#pragma mark - 遍历类以及子类
- (void)printClasses:(Class)cls{
// 注册类的总数
int count = objc_getClassList(NULL, 0);
// 创建一个数组, 其中包含给定对象
NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls];
// 获取所有已注册的类
Class* classes = (Class*)malloc(sizeof(Class)*count);
objc_getClassList(classes, count);
for (int i = 0; i
打印结果:
KTest[11026:259919] *******添加观察者之前
KTest[11026:259919] classes = (
Person
)
KTest[11026:259919] *******添加观察者之后
KTest[11026:259919] classes = (
Person,
"NSKVONotifying_Person"
)
KTest[11026:259919] *******移除观察者之后
KTest[11026:259919] classes = (
Person,
"NSKVONotifying_Person"
)
可以看到在移除观察者之后没有移除动态子类NSKVONotifying_Person
总结
日常注意点
1.context
参数的在不使用时推荐填写NULL
,其功能:程序更加便捷、高效、安全
2.观察者要移除
3.多个因素影响时可以使用:keyPathsForValuesAffectingValueForKey:
4.针对经常需要改动的代码可以使用:automaticallyNotifiesObserversForKey:
方法对key进行选择处理
5.观察可变数组时,要使用mutableArrayValueForKey:
原理
1.KVO通过isa-swizzling实现
2.动态的生成子类:NSKVONotifying_XXX
3.主要是观察setXXX
方法;内部重写
4.还重写了setXXX class dealloc _isKVOA
方法;
5.移除观察之后 isa
指针会重新指回来
6.移除观察者后动态子类会被销毁吗?不会。