KVO分析

2021-09-05  本文已影响0人  浅墨入画

KVO细节分析上

KVO官方文档

image.png

关于context,苹果官方文档解释

### Context

The context pointer in the `addObserver:forKeyPath:options:context:` message contains arbitrary data that will be passed back to the observer in the corresponding change notifications. You may specify `NULL` and rely entirely on the key path string to determine the origin of a change notification, but this approach may cause problems for an object whose superclass is also observing the same key path for different reasons.

A safer and more extensible approach is to use the context to ensure notifications you receive are destined for your observer and not a superclass.

The address of a uniquely named static variable within your class makes a good context. Contexts chosen in a similar manner in the super- or subclass will be unlikely to overlap. You may choose a single context for the entire class and rely on the key path string in the notification message to determine what changed. Alternatively, you may create a distinct context for each observed key path, which bypasses the need for string comparisons entirely, resulting in more efficient notification parsing. Listing 1 shows example contexts for the `balance` and `interestRate` properties chosen this way.

现在编写代码如下

// 可以定义context标识符来区分监听的属性
static void *PersonNameContext = &PersonNameContext;

@interface LGViewController ()
@property (nonatomic, strong) LGPerson  *person;
@end

@implementation LGViewController

- (void)viewDidLoad {
    [super viewDidLoad];
     self.person  = [LGPerson new];
    [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:PersonNameContext];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{    
    self.person.name = @"test";
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
   // 这里使用context来区分监听的属性
   if(context == PersonNameContext){
        NSLog(@"%@",change);
    }
}

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

// 运行工程,点击页面打印如下
2021-09-04 12:07:04.887849+0800 001---KVO初探[10193:13690743] {
    kind = 1;
    new = test;
}

现在创建单例[LGStudent shareInstance];,页面销毁时候不移除属性监听,代码如下

@implementation LGDetailViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor orangeColor];
    // 单例
    self.student = [LGStudent shareInstance];
    [self.student addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.student.name = @"hello word";
}
#pragma mark - KVO回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    NSLog(@"LGDetailViewController :%@",change);
}

- (void)dealloc{
}

运行单例工程,点击页面发生崩溃

image.png

进入一个VC里面有一个单例持有观察VC的属性,哪怕VC被释放 单例没释放继续观察,再次进入VC的时候观察属性发生变化,之前已经被释放的VC会继续接收到消息,因为已经被释放所以野指针崩溃

小结: 使用KVC,一定要注意添加观察者移除观察者一一对应

KVO细节分析下

@implementation LGPerson
// 自动开关
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
    return NO;
}
- (void)setName:(NSString *)name
{
    [self willChangeValueForKey:@"name"];
    _name = name;
    [self didChangeValueForKey:@"name"];
}

//监听name这个属性
[self.student addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
//改变属性值
self.person.nick = [NSString stringWithFormat:@"%@+",self.person.nick];
@implementation LGPerson
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)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];
}

//监听downloadProgress这个属性
[self.person addObserver:self forKeyPath:@"downloadProgress" options:(NSKeyValueObservingOptionNew) context:NULL];
//改变这两个属性
self.person.writtenData += 10;
self.person.totalData  += 1;
image.png
[self.person addObserver:self forKeyPath:@"dateArray" options:(NSKeyValueObservingOptionNew) context:NULL];
self.person.dateArray = [NSMutableArray array];
[[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"nb"];

2021-09-04 12:53:15.899991+0800 001---KVO初探[10500:13724184] {
    kind = 1;
    new =     (
    );
}
2021-09-04 12:53:15.900726+0800 001---KVO初探[10500:13724184] {
    indexes = "<_NSCachedIndexSet: 0x6000022c0c00>[number of indexes: 1 (in 1 ranges), indexes: (0)]";
    kind = 2;
    new =     (
        nb
    );
}

typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
    NSKeyValueChangeSetting = 1,
    NSKeyValueChangeInsertion = 2,
    NSKeyValueChangeRemoval = 3,
    NSKeyValueChangeReplacement = 4,
};
kind=2的类型是insert

KVO原理上

KVO的原理描述

KVO原理分析

image.png

添加KVO监听的时候动态生成了NSKVONotifying_LGPerson

image.png image.png

遍历类以及子类,查看LGPersonNSKVONotifying_LGPerson是什么关系?

#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);
}
image.png

得出结论 LGPersonNSKVONotifying_LGPerson的父类

KVO原理下

#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);
}

// LGViewController.m文件
@implementation LGViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    self.person = [[LGPerson alloc] init];
    [self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
    [self printClassAllMethod:objc_getClass("NSKVONotifying_LGPerson")];
}

// 运行工程打印如下
// 重写setNickName方法
2021-09-04 22:13:31.476760+0800 002---KVO原理探讨[11980:13924110] setNickName:-0x7fff207bab57
2021-09-04 22:13:31.477058+0800 002---KVO原理探讨[11980:13924110] class-0x7fff207b9662
2021-09-04 22:13:31.477224+0800 002---KVO原理探讨[11980:13924110] dealloc-0x7fff207b940b
2021-09-04 22:13:31.477352+0800 002---KVO原理探讨[11980:13924110] _isKVOA-0x7fff207b9403

可以看到自动生成的类NSKVONotifying_Person中,有四个方法,分别是setNickName,class,dealloc,_isKVOA:

  1. setNickName 观察对象的setter方法
  2. class 类型
  3. dealloc 是否释放(该dealloc执行时,将isa重新指向Person)
  4. _isKVOA判断是否是KVO生成的一个辨识码

NSKVONotifying_Person中的方法是重写了父类的方法

在注册成为观察者之前,实例对象person的isa指向LGPerson,在注册成为观察者之后,实例对象person的isa指向NSKVONotifying_Person;,移除观察者之后实例对象person的isa又指向LGPerson

KVO移除之后,生成的NSKVONotifying_LGPerson并不会销毁。

@interface LGPerson : NSObject{
    @public
    NSString *name;
}
@property (nonatomic, copy) NSString *nickName;
@end

// 分别为name和nickName都添加KVO监听
[self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
image.png

KVO只能监听属性,不能对成员变量进行监听;而属性和成员变量的区别在于属性比成员变量多一个setter方法,而KVO监听的就是setter方法

image.png

可以看到在移除观察者时,isa已经指向了LGPerson,而且nickName的值也改变了,那么此时的setter方法是LGPerson的

下面我们通过观察变量值改变来验证一下

image.png

继续运行项目,触发监听

image.png

bt打印堆栈信息


image.png

所以最终调用的setterLGPersonsetNickName方法

小结

1.isa->LGPerson->NSKVONotifying_LGPerson-消失
1.1 动态生成 NSKVONotifying_LGPerson
1.2 LGPerson VS NSKVONotifying_LGPerson 父子关系
1.3 NSKVONotifying_LGPerson有哪些方法
setNickName //重写set方法
class
dealloc
_isKVOA
1.4 isa指回来 //通过这个方法_isKVOA
1.5 NSKVONotifying_LGPerson并没有销毁
2.setter方KVO实例法(setter)/class
2.1 set方法的意义是能够区分成员变量属性变量
2.2 修改LGPerson属性setNickName方法

上一篇下一篇

猜你喜欢

热点阅读