iOS重学之KVO详解

KVO的基本使用

基本使用

KVO:Key Value Observing(键值监听),用来监听某个对象属性值的改变。

// Person类
@interface Person : NSObject

@property (nonatomic, assign) int age;

@end

@implementation Person

@end

// KVOViewController
@interface KVOViewController ()

@property (nonatomic, strong) Person *person1;
@property (nonatomic, strong) Person *person2;

@end

@implementation KVOViewController

- (void)viewDidLoad {
    [super viewDidLoad];
  
    self.person1 = [[Person alloc] init];
    self.person1.age = 10;
  
    self.person2 = [[Person alloc] init];
    self.person2.age = 20;
    
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.person1 addObserver:self forKeyPath:@"age" options:options context:nil];
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    self.person1.age = 11;
    self.person2.age = 21;
}

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

// 打印结果:
2022-11-15 20:08:33.563589+0800 OC对象的本质[81675:15955895] 对象的age属性发生了改变:
{
    kind = 1;
    new = 11;
    old = 10;
}

注意:在不需要监听的时候需要移除。

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

4.png

解释
1、KVO是建立在KVC的基础之上的,即是说给成员变量赋值KVO是无法监听其变化的。
2、context意为上下文信息,我们平时用的时候一般传的NULL,但是苹果官方建议的是把这个参数用起来会更安全、扩展性更强。
5.png

KVO其他细节

1、是否打开自动观察的开关

+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key {
    return YES; // 默认是YES
}

返回可能影响监听值的NSSet

// 当writtenData发生改变的时候,downloadProgress就会发生改变
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    if ([key isEqualToString:@"downloadProgress"]) {
        NSArray *affectingKeys = @[@"writtenData"];  // 只要affectingKeys数组里面的属性发生变化 都会触发downloadProgress的KVO
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}

3、对可变数组的监听

self.person1.booksArr = [NSMutableArray array];
[self.person1 addObserver:self forKeyPath:@"booksArr" options:NSKeyValueObservingOptionNew context:NULL];
[[self.person1 mutableArrayValueForKey:@"booksArr"] addObjectsFromArray:@[@"Hello", @"World"]];

// 打印结果:


6.png

KVO的本质分析

从上面的例子咱们发现:两个不同的对象person1person2,为什么person1添加了KVO可以监听到属性值的改变?

看起来self.person1.age = 11self.person2.age = 21都是调用的setAge:方法,为什么person1就可以监听到属性值的改变了呢?我们可以大胆猜测一下person1person2setAge:的具体实现肯定不一样了,也就是说person1isaperson2isa指向发生了变化,下面我们来验证一下我们的猜想。

验证一
person1添加KVO前后分别打印person1person2class对象

NSLog(@"person1添加监听之前:person1:%@ person2:%@",object_getClass(self.person1),object_getClass(self.person2));

NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | > NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:nil];

NSLog(@"person1添加监听之后:person1:%@ person2:%@",object_getClass(self.person1),object_getClass(self.person2));

打印结果:

2022-11-15 20:21:21.161924+0800 OC对象的本质[81836:15966659] person1添加监听之前:person1:Person person2:Person
2022-11-15 20:21:21.162217+0800 OC对象的本质[81836:15966659] person1添加监听之后:person1:NSKVONotifying_Person person2:Person

如上:我们发现在person1添加了KVO之后,person1isa指向的是NSKVONotifying_Person类,person2isa指向的还是Person类。

验证二
person1添加KVO前后分别打印person1person2setAge:方法的函数地址(IMP):

NSLog(@"person1添加监听之前:person1:%p person1:%p",[self.person1 methodForSelector:@selector(setAge:)], [self.person2 methodForSelector:@selector(setAge:)]);

NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:nil];

NSLog(@"person1添加监听之后:person1:%p person1:%p",[self.person1 methodForSelector:@selector(setAge:)], [self.person2 methodForSelector:@selector(setAge:)]);

打印结果:

2022-11-15 20:32:28.980113+0800 OC对象的本质[81977:15975768] person1添加监听之前:person1:0x102539e70 person1:0x102539e70
2022-11-15 20:32:28.980454+0800 OC对象的本质[81977:15975768] person1添加监听之后:person1:0x7fff207b1cfb person1:0x102539e70

如上:我们发现在person1添加了KVO之后,person1IMPperson2IMP不一样。
通过LLDB指令:

// person1
p (IMP) 0x7fff207b1cfb
(IMP) $0 = 0x00007fff207b1cfb (Foundation`_NSSetIntValueAndNotify)

// person2
p (IMP) 0x102539e70
(IMP) $1 = 0x0000000102539e70 (OC对象的本质`-[Person setAge:] at Person.h:12)

如上:我们发现person1setAge:方法其实是调用到了一个C函数:_NSSetIntValueAndNotify

通过上面的分析
1、我们看到person1添加了KVO之后,其isa 指针指向的是一个派生类NSKVONotifying_Person,这个类是Runtime在程序运行的过程中动态创建的一个类,这个类继承自Person
2、在这个派生类里面调用了C函数:_NSSetIntValueAndNotify
3、在_NSSetIntValueAndNotify里面实现如下代码:

// 伪代码
[self willChangeValueForKey:@"age"];
[super setAge:age];
[self didChangeValueForKey:@"age"];

4、在didChangeValueForKey方法里面去通知监听器某个属性值发生了改变。

用一张图来做总结
未添加KVO监听的对象:

1.png

添加了KVO监听的对象:
2.png

注意
通过Runtime中的object_class拿到的class对象才是真正的class对象,通过class拿到的不一定是真正的class对象,比如使用了KVO监听的对象。

如何验证派生类NSKVONotifing_Person重写了哪些方法?

NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:nil];

Class cls = object_getClass(self.person1);
[self printMethodNameOfClass:cls];

- (void)printMethodNameOfClass:(Class)cls {
unsigned int count;
Method *methodList = class_copyMethodList(cls, &count);
 for (int i = 0; i < count; i++) {
Method method = methodList[i];
NSLog(@"%@",NSStringFromSelector(method_getName(method)));
}
free(methodList);
}

打印结果:

2022-11-16 13:36:32.484809+0800 OC对象的本质[26389:16561796] setAge:
2022-11-16 13:36:32.484965+0800 OC对象的本质[26389:16561796] class
2022-11-16 13:36:32.485084+0800 OC对象的本质[26389:16561796] dealloc
2022-11-16 13:36:32.485195+0800 OC对象的本质[26389:16561796] _isKVOA

从上面打印可以看到:NSKVONotifinh_Person类重写了setAge:classdealloc_isKVOA方法。

KVO的触发场景

从上面KVO的本质分析可以看到:只要有setter方法就可以通过KVO来监听值的改变,比如:属性值发生改变、通过KVC赋值。

请看下面场景:

// Person类
@interface Person : NSObject
{
    @public
    int age;
}
@end

@implementation Person

@end
  
// KVOController
@interface KVOViewController ()

@property (nonatomic, strong) Person *person;

@end
  
@implementation KVOViewController

- (void)viewDidLoad {
  [super viewDidLoad];
  self.person = [[Person alloc] init];
  self.person->age = 10;

  NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
  [self.person addObserver:self forKeyPath:@"age" options:options context:nil];
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
  // 不会触发KVO
  // [self.person willChangeValueForKey:@"age"];
  self.person->age = 11;
  // [self.person didChangeValueForKey:@"age"];
}

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

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

@end

如上:
通过self.person->age = 11不会触发KVO,原因相信大家都很清楚了,没有调用setter,可以在self.person->age = 11前后分别添加[self.person willChangeValueForKey:@"age"][self.person didChangeValueForKey:@"age"]来手动触发KVO。

也可以通过KVC赋值[self.person setValue:@11 forKey:@"age"],这样就可以自动触发KVO。

其他补充

如何查看某个方法的函数地址(IMP)?

- (IMP)methodForSelector:(SEL)aSelector;
+ (IMP)instanceMethodForSelector:(SEL)aSelector;

你可能感兴趣的:(iOS重学之KVO详解)