Key-Value Observing(kvo)一:原理分析

2021-08-02  本文已影响0人  HotPotCat

一、kvo简介

Key-Value Observing Programming Guide
对于kvo使用分为3步:

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

observer:要添加的监听者对象,当监听的属性发生改变时会通知该对象,必须实现- observeValueForKeyPath:ofObject:change:context:方法,否则程序会抛出异常。
keyPath:监听的属性,不能传nil
options:指明通知发出的时机以及change中的键值。
context:是一个可选的参数,可以传任何数据。

⚠️添加监听的方法addObserver:forKeyPath:options:context:并不会对监听和被监听的对象以及context做强引用,必须自己保证他们在监听过程中不被释放。

- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

1.1 options

typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
    NSKeyValueObservingOptionNew = 0x01,//更改前的值
    NSKeyValueObservingOptionOld = 0x02,//更改后的值
    NSKeyValueObservingOptionInitial = 0x04,//观察最初的值(在注册观察服务时会调用一次触发方法)
    NSKeyValueObservingOptionPrior  = 0x08 //分别在值修改前后触发方法(即一次修改有两次触发)
};

可以看到NSKeyValueObservingOptions4个枚举值,测试代码如下:

self.obj = [HPObject alloc];
self.obj.name = @"hp1";
[self.obj addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
self.obj.name = @"hp2";

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"change:%@",change);
}

修改options参数输出如下:

//NSKeyValueObservingOptionNew
change:{
    kind = 1;
    new = hp2;
}
//NSKeyValueObservingOptionOld
change:{
    kind = 1;
    old = hp1;
}
//NSKeyValueObservingOptionInitial
change:{
    kind = 1;
}
change:{
    kind = 1;
}
//NSKeyValueObservingOptionPrior
change:{
    kind = 1;
    notificationIsPrior = 1;
}
 change:{
    kind = 1;
}
//NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld | NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionPrior
change:{
    kind = 1;
    new = hp1;
}
change:{
    kind = 1;
    notificationIsPrior = 1;
    old = hp1;
}
change:{
    kind = 1;
    new = hp2;
    old = hp1;
}
// 0
change:{
    kind = 1;
}

NSKeyValueChangeKey定义如下:

typedef NSString * NSKeyValueChangeKey NS_STRING_ENUM;

FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeKindKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeNewKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeOldKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeIndexesKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeNotificationIsPriorKey;
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
    NSKeyValueChangeSetting = 1,//普通类型设置
    NSKeyValueChangeInsertion = 2,//集合元素插入
    NSKeyValueChangeRemoval = 3,//集合元素移除
    NSKeyValueChangeReplacement = 4,//集合元素替换
};

综上:

1.2 context

这个参数最后会被传递到监听者的响应方法中,可以用来区分不同通知,也可以用来传值。
对于多个keyPath的观察,需要在observeValueForKeyPath同时判断objectkeyPath,可以声明一个静态变量传递给context用来区分不同的通知提高代码的可读性:

static void *PersonAccountBalanceContext = &PersonAccountBalanceContext;

当然如果子类和父类都实现了对同一对象的同一属性的观察,并且父类和子类都可能对其进行设值,那么这个时候用context区分就很有用了。

1.3 移除观察者

官方文档说了在观察者dealloc的时候被观察者不会自动移除观察者,还是会继续给观察者发送消息。需要自己保证移除。
比如某个页面监听了一个对象的属性,这个对象是从前一个页面传递进来的(本质上是对象不被释放)。在不移除观察的情况下,多次进入这个页面在属性变化的时候就发生了crash

image.png
根本原因是之前进入页面的时候观察者没有移除,导致发送消息的时候之前的observer不存在。

kvo的使用三步曲要完整

当然如果页面是个单例则不会崩溃,如果addObserver每次都调用则会进行多次回调。

二、kvo初探

2.1 kvo手动自动通知

在被观察者中实现automaticallyNotifiesObserversForKey可以控制kvo是否自动通知:

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    NSLog(@"%s key:%@",__func__,key);
    return NO;
}

willChangeValueForKey & didChangeValueForKey手动通知

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

2.2 嵌套层次的监听

keyPathsForValuesAffectingValueForKey(key - keys)
比如下载文件:
下载进度 = 已下载数据大小 / 总数据大小。总数据大小由于添加文件可能会发生变化。

@interface HPObject : NSObject

@property (nonatomic, copy) NSString *downloadProgress;
@property (nonatomic, assign) double writtenData;
@property (nonatomic, assign) double totalData;

@end

@implementation HPObject

+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
    NSLog(@"%s key:%@",__func__,key);
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    if ([key isEqualToString:@"downloadProgress"]) {
        NSArray *affectingKeys = @[@"totalData", @"writtenData"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}

- (NSString *)downloadProgress {
    if (self.writtenData == 0) {
        self.writtenData = 10;
    }
    if (self.totalData == 0) {
        self.totalData = 100;
    }
    return [[NSString alloc] initWithFormat:@"%f",1.0f*self.writtenData/self.totalData];
}

@end

监听和调用:


- (void)viewDidLoad {
    [super viewDidLoad];
    [self.obj addObserver:self forKeyPath:@"downloadProgress" options:NSKeyValueObservingOptionNew context:NULL];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"change:%@",change);
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    self.obj.writtenData += 10;
    self.obj.totalData += 1;
}

- (void)dealloc {
    [self.obj removeObserver:self forKeyPath:@"downloadProgress"];
    NSLog(@"dealloc");
}
+[HPObject keyPathsForValuesAffectingValueForKey:] key:downloadProgress
+[HPObject keyPathsForValuesAffectingValueForKey:] key:writtenData
+[HPObject keyPathsForValuesAffectingValueForKey:] key:writtenData
+[HPObject keyPathsForValuesAffectingValueForKey:] key:totalData
+[HPObject keyPathsForValuesAffectingValueForKey:] key:totalData

2.3 kvo对可变数组的观察

self.obj.dateArray = [NSMutableArray array];
[self.obj addObserver:self forKeyPath:@"dateArray" options:NSKeyValueObservingOptionNew context:NULL];
[self.obj.dateArray addObject:@(1)];

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"change:%@",change);
}

上面的案例dateArray添加元素并不能触发kvo,需要修改为:

[[self.obj mutableArrayValueForKey:@"dateArray"] addObject:@"1"];
[[self.obj mutableArrayValueForKey:@"dateArray"] addObject:@"2"];
[[self.obj mutableArrayValueForKey:@"dateArray"] removeObject:@"1"];
[self.obj mutableArrayValueForKey:@"dateArray"][0] = @"3";

输出:

change:{
    indexes = "<_NSCachedIndexSet: 0x600000a635a0>[number of indexes: 1 (in 1 ranges), indexes: (0)]";
    kind = 2;
    new =     (
        1
    );
}
change:{
    indexes = "<_NSCachedIndexSet: 0x600000a635c0>[number of indexes: 1 (in 1 ranges), indexes: (1)]";
    kind = 2;
    new =     (
        2
    );
}
change:{
    indexes = "<_NSCachedIndexSet: 0x600000a635a0>[number of indexes: 1 (in 1 ranges), indexes: (0)]";
    kind = 3;
}
change:{
    indexes = "<_NSCachedIndexSet: 0x600000a635a0>[number of indexes: 1 (in 1 ranges), indexes: (0)]";
    kind = 4;
    new =     (
        3
    );
}

⚠️kvo监听集合元素变化,需要用到kvc的原理机制才能监听到变化。由于kvo底层也是由kvc实现的

集合相关API如下:
NSMutableArraymutableArrayValueForKeymutableArrayValueForKeyPath
NSMutableSetmutableSetValueForKeymutableSetValueForKeyPath
NSMutableOrderedSetmutableOrderedSetValueForKeymutableOrderedSetValueForKeyPath
These methods provide the additional benefit of maintaining key-value observing compliance for the objects held in the collection object
说明了集合类型的要特殊处理,具体可以参考kvc的说明:Accessing Collection Properties

2.3.1 可变数组专属API

当然除了上面对于集合类型的赋值通过kvc相关接口还可以通过数组专属API来完成。

@property (nonatomic, strong) NSMutableArray <HPObject *>*array;

self.array = [NSMutableArray array];
HPObject *obj1 = [HPObject alloc];
obj1.name = @"obj1";
[self.array addObject:obj1];
HPObject *obj2 = [HPObject alloc];
obj2.name = @"obj2";
[self.array addObject:obj2];

[self.array addObserver:self toObjectsAtIndexes:[NSIndexSet indexSetWithIndex:1] forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
self.array[0].name = @"_obj0";
self.array[1].name = @"_obj1";

输出:

change:{
    kind = 1;
    new = "_obj1";
}

这样当self.array[1].name发生变化的时候就监听到了。这里本质上就相当于是对obj1的监听。后续数组中替换了1位置的数组是监听不到的。

HPObject *obj3 = [HPObject alloc];
obj3.name = @"obj3";
self.array[1] = obj3;
self.array[1].name = @"_obj3";

这样替换后监听不到。

三、kvo原理分析

Key-Value Observing Implementation Details
根据官方文档可以看到使用了isa-swizzling技术。
You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.

3.1 isa-swizzling验证

直接在addObserver的调用处打个断点:

image.png
addObserverobjHPObject变成了NSKVONotifying_HPObject
  1. 那么NSKVONotifying_HPObject是什么时候生成的呢?
    重新运行,在addObserver之前验证NSKVONotifying_HPObject
(lldb) p objc_getClass("NSKVONotifying_HPObject")
(Class _Nullable) $0 = nil

这样就意味着NSKVONotifying_HPObject是在addObserver的时候底层动态生成的。

  1. NSKVONotifying_HPObjectHPObject有什么关系呢?
- (void)printClasses:(Class)cls {
    //注册类总个数
    int count = objc_getClassList(NULL, 0);
    //先将类本身放入数组中
    NSMutableArray *array = [NSMutableArray arrayWithObject:cls];
    //开辟空间
    Class *classes = (Class *)malloc(sizeof(Class)*count);
    //获取已经注册的类
    objc_getClassList(classes, count);
    for (int i = 0; i < count; i++) {
        //获取cls的子类,一层。
        if (cls == class_getSuperclass(classes[i])) {
            [array addObject:classes[i]];
        }
    }
    free(classes);
    NSLog(@"classes = %@",array);
}

上面这段代码是打印类以及它的子类(单层)。
调用:

[self printClasses:[HPObject class]];
[self.obj addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
[self printClasses:[HPObject class]];
[self printClasses:objc_getClass("NSKVONotifying_HPObject")];

输出:

classes = (
    HPObject,
    HPCat
)
classes = (
    HPObject,
    "NSKVONotifying_HPObject",
    HPCat
)
classes = (
    "NSKVONotifying_HPObject"
)

3.2 kvo 生成子类分析

既然NSKVONotifying_HPObjectHPObject的子类,那么它都有什么内容呢?
方法:

- (void)printClassAllProtocol:(Class)cls {
    unsigned int count = 0;
    Protocol * __unsafe_unretained _Nonnull * _Nullable protocolList = class_copyProtocolList(cls, &count);
    for (int i = 0; i < count; i++) {
        Protocol *proto = protocolList[i];
        NSLog(@"%s",protocol_getName(proto));
    }
    free(protocolList);
}

输出:

setName:-0x7fff207bbb57
class-0x7fff207ba662
dealloc-0x7fff207ba40b
_isKVOA-0x7fff207ba403

同理可以获取协议,属性以及成员变量:

- (void)printClassAllProtocol:(Class)cls {
    unsigned int count = 0;
    Protocol * __unsafe_unretained _Nonnull * _Nullable protocolList = class_copyProtocolList(cls, &count);
    for (int i = 0; i < count; i++) {
        Protocol *proto = protocolList[i];
        NSLog(@"%s",protocol_getName(proto));
    }
    free(protocolList);
}

- (void)printClassAllProprerty:(Class)cls {
    unsigned int count = 0;
    objc_property_t *propertyList = class_copyPropertyList(cls, &count);
    for (int i = 0; i < count; i++) {
        objc_property_t property = propertyList[i];
        NSLog(@"%s-%s", property_getName(property), property_getAttributes(property));
    }
    free(propertyList);
}

- (void)printClassAllIvars:(Class)cls {
    unsigned int count = 0;
    Ivar *ivarList = class_copyIvarList(cls, &count);
    for (int i = 0; i < count; i++) {
        Ivar ivar = ivarList[i];
        NSLog(@"%s-%s",ivar_getName(ivar),ivar_getTypeEncoding(ivar));
    }
    free(ivarList);
}

没有输出任何内容。那么核心就在方法了。
_isKVOA很好理解用来判断是否kvo生成的类,class标记类型。setName:是对父类namesetter方法进行了重写。dealloc中进行了isa重新指回。

3.2.1 class

addObserver后调用class输出:

(lldb) p self.obj.class
(Class) $0 = HPObject

那么重写class就是为了返回原来的类的信息。不会返回kvo类自己的class信息。

3.2.2 dealloc

既然NSKVONotifying_HPObject是动态创建的,那么它销毁吗?
deallocremoveObserver前后分别验证:

image.png
可以看到移除后isa指回了原来的类,也就是dealloc中进行了isa的指回。并且NSKVONotifying_HPObject类仍然存在。

3.2.3 setter

既然重写了setName:观察属性,那么成员变量能观察么?增加age成员变量:

[self.obj addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
[self.obj addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:NULL];
self.obj->age = 18;

当对age进行赋值并没有触发回调。那么就说明了对setter方法进行的监听。
deallocremoveObserver后查看name的值:

image.png
那就说明在kvo生成的类中对name的修改影响到了原始类。
name下个内存断点:
(lldb) watchpoint set variable self->_obj->_name
Watchpoint created: Watchpoint 1: addr = 0x60000129b260 size = 8 state = enabled type = w
    watchpoint spec = 'self->_obj->_name'
    new value: 0x0000000000000000

在赋值的时候堆栈如下:

* thread #1, queue = 'com.apple.main-thread', stop reason = watchpoint 1
  * frame #0: 0x00007fff2018b1e2 libobjc.A.dylib`objc_setProperty_nonatomic_copy + 44
    frame #1: 0x000000010afecd70 KVODemo`-[HPObject setName:](self=0x000060000129b250, _cmd="setName:", name=@"HP") at HPObject.h:19:39
    frame #2: 0x00007fff207c2749 Foundation`-[NSObject(NSKeyValueObservingPrivate) _changeValueForKeys:count:maybeOldValuesDict:maybeNewValuesDict:usingBlock:] + 646
    frame #3: 0x00007fff207c300b Foundation`-[NSObject(NSKeyValueObservingPrivate) _changeValueForKey:key:key:usingBlock:] + 68
    frame #4: 0x00007fff207bbc64 Foundation`_NSSetObjectValueAndNotify + 269
    frame #5: 0x000000010afed248 KVODemo`-[HPDetailViewController viewDidLoad](self=0x00007fe291e0d890, _cmd="viewDidLoad") at HPDetailViewController.m:43:14

调用逻辑如下:

-[HPObject setName:]
Foundation`-[NSObject(NSKeyValueObservingPrivate) _changeValueForKeys:count:maybeOldValuesDict:maybeNewValuesDict:usingBlock:]
Foundation`-[NSObject(NSKeyValueObservingPrivate) _changeValueForKey:key:key:usingBlock:]
Foundation`_NSSetObjectValueAndNotify

_NSSetObjectValueAndNotify汇编调用主要如下:

"willChangeValueForKey:"
call   0x7fff2094ff0e 
"didChangeValueForKey:"
"_changeValueForKey:key:key:usingBlock:"

_changeValueForKeys:count:maybeOldValuesDict:maybeNewValuesDict:usingBlock:中有获取observers操作:

_NSKeyValueObservationInfoGetObservances

那么意味着在处理完所有事情后会进行通知。
并且有NSKeyValueWillChangeNSKeyValueDidChange

image.png

继续在observeValueForKeyPath的回调中打个断点:

image.png
确认是在NSKeyValueNotifyObserver通知中进行的回调。

总结(kvo原理)

上一篇 下一篇

猜你喜欢

热点阅读