KVO

官方文档地址:

https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/KeyValueObserving/KeyValueObserving.html

基本用法:

1. [self.student addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL]

** context作用,用来打标签在回调方法中做区分,应用场景是在如果父类和子类有同一个属性名,回调判断会复杂,如果是用context来区分会很方便,在内存中只能惟一的写一个,相当于一个静态值。而且查找更优越,如果判断类和属性列表的话需要查找他们的缓存列表,所以context的优点是嵌套少,性能好。

2. 改变值 self.person.name = @"person";

3. 响应回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{

NSLog(@"%@",change);
NSLog(@"downloadProgress == %@",self.person.downloadProgress);
}

4. 析构:

- (void)dealloc{

[self.student removeObserver:self forKeyPath:@"name"];

}

** 如果没有在dealloc中移除观察者,并且被观察对象是一个单例的换,再次打开界面重新给name复制时会闪退或者调用两次等异常。(所以一定要移除,至于是什么导致的崩溃,闭源源码无法看到,猜测是不断添加注册观察者,当系统需要给观察者复制是,原页面已经销毁,找不到原地址,导致野指针崩溃。),下面是官方文档:

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {

    if([key isEqualToString:@"name"]){

        return YES;

    }

    return NO;

}

返回NO,则之前添加的观察者,不再回调,也可以选择观察一些特定的key.

如果这里返回NO,也可以使用在set方法里面添加手动观察方法的方式实现添加观察者:

- (void)setName:(NSString *)name{

    [self willChangeValueForKey:@"name"];

    _name = name;

    [self didChangeValueForKey:@"name"];

}

KVO的一个简单应用(为downloadProgress添加观察者,影响下载进度的影响因素包括总的大小和已经写入的大小):

如果是传统的做法需要同时监控两个值,但是使用下面的方法就更便捷一些:

+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{

 

    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];

    if ([key isEqualToString:@"downloadProgress"]) {

        NSArray *affectingKeys = @[@"totalData", @"writtenData"];

        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];

    }

    return keyPaths;

}

上面的代码的主要作用是,当观察的值为downloadProgress时,添加连个影响因子totalData,writtenData,这样当这两个值改变的时候也会回调观察者方法。(联动观察 )

或者是把对象当做观察对象时,对象的属性改变也会调用观察者回调方法。

集合类观察:

如果采用上面的方式观察数组时,如果给数组添加元素,会发现不会调用观察者回调。如果要了解这个原因,就需要了解KVC的内容。(因为集合和简单变量的取值和赋值过程是不一样的)

[[self.student mutableArrayValueForKey:@"dataArray"] addObject:@"hello"];(满足KVC的设置流程)


这样就可以了,必须先初始化数组,接下来分析下原因:

 

根据之前KVC的官方文档中介绍。self.student是有可能获取不到值的,因为有_key,isKey,key这种变量的存在,赋值的时候不一定会复制到key中,有可能是_key中,所以self.student中不一定有值,所以获取不到。这样的话就需要直接避免这种情况,直接用KVC获取dataArray,这样可以规避KVC查找值导致的问题。

 

 

KVO底层原理:

首先验证下是否是观察的set方法:

为类添加一个属性(属性会自动生成set方法)和成员变量

------------------------------------------

#import "KVOPerson.h"

 

NS_ASSUME_NONNULL_BEGIN

 

@interface KVOStudent : KVOPerson{

    @public

    NSString *age;

}

@property (nonatomic, strong)   NSString *name;

@property (nonatomic, assign) int progress;

@property (nonatomic, assign) int total;

@property (nonatomic, assign) int download;

@property (nonatomic, strong) NSMutableArray *dataArray;

@end

------------------------------------------

NS_ASSUME_NONNULL_END

 

[self.student addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:NULL];

[self.student addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];

self.student.name = @"fanxing";

self.student->age = @"age";

------------------------------------------

只打印了name,没有打印age,证明确实是KVO默认在set方法里处理的相关逻辑。下面是官方文档:

 

翻译:自动化KVO是由一种叫做isa-swizzing(isa改变类的指向)的技术实现的 :

首先使用objc_getClass()方法获取:(打印发现并没有中间类生成)

原因是应该查看对象的class而不是类的class,而且类对象的isa指向原类. 

说明类是没有变化的,变化的是当前的对象的isa(原来是指向KVOStudent,现在指向 NSKVONotifying_KVOStudent )动态类

 这个类是什么时候生成的呢?和原来的类KVOStudent是什么关系?

打印类的子类父类

#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<count; i++) {

        if (cls == class_getSuperclass(classes[i])) {

            [mArray addObject:classes[i]];

        }

    }

    free(classes);

    NSLog(@"classes = %@", mArray);

}

 

发现多了一个类,并且NSKVONotifying_KVOStudent是KVOStudent的子类,继承于KVOStudent。

接下来看下NSKVONotifying_KVOStudent 这个类的其他东西有没有不一样的地方,查看methodList

#pragma mark - 遍历方法-ivar-property

- (void)printClassAllMethod:(Class)cls{

    unsigned int count = 0;

    Method *methodList = class_copyMethodList(cls, &count);

    for (int i = 0; i<count; i++) {

        Method method = methodList[i];

        SEL sel = method_getName(method);

        IMP imp = class_getMethodImplementation(cls, sel);

        NSLog(@"%@-%p",NSStringFromSelector(sel),imp);

    }

    free(methodList);

}

可以看到如果重写父类的方法,IMP实现的指针内存地址是不一样的 

总结KVO做了哪些事情:

1.. 修改了isa的指向。

2. 从打印的内容来看,因为NSKVONotifying_KVOStudent是student的子类,并且setName的imp实现指针发生了变化,所以可以肯定的是,NSKVONotifying_KVOStudent类重写了setName方法。

3. 添加了一个class方法。

4. 重写了dealooc析构函数。

5. 添加了一个标记 _isKVO标识,标记是KVO的类。 

 

原文地址:https://www.cnblogs.com/coolcold/p/12059193.html