KVO使用及分析

2020-04-30  本文已影响0人  哦小小树

0x01 用途

键值观察是一种机制:

对于MVCmodel层和controller层之间的通信很有用。

  1. API要求使用KVO

  2. 为其他人设计API

  3. 想获取私有变量并修改


简单使用

使用分为三步:

  1. 注册观察者
[obj addObserver:forKeyPath:options:context:]
  1. 设置观察者接收回调
- (void)observeValueForKeyPath:ofObject:change:context:
  1. 移除观察者
[obj removeObserver:forKeyPath:]

自定义使用

  1. 是否开启自动发送通知
// Person.m 
// 方案一
+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
    if(key isEqualToString:@"name"){
        return NO;      // name将不会被自动触发,如果需要发出通知需要手动触发
    }
    return YES;
}

// 方案二
// + (BOOL)automaticallyNotifiesObserversOfPropertyName 会根据属性名,生成下面方法
+ (BOOL)automaticallyNotifiesObserversOfName {
return NO;  // 取消自动发送
}
  1. 如果需要手动发出通知
- (void)willChangeValueForKey:(NSString *)key
- (void)didChangeValueForKey:(NSString *)key

示例

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

可变容器的监听

我们知道KVO监听的事属性的内存地址的更改,对于非集合类型,我们调整其值就可以做到修改监听改变。

但是对于属性是集合来说,我们修改的是属性内部元素,属性的内存地址并未改变。

所以我们可以通过,重新生成一个集合,然后再赋值的方式触发监听。

// Person.h
@property (nonatomic, strong) NSMutableArray *banks;
// vc.m
NSMutableArray *tmpArr = self.person.banks;
[tmpArr addObject:[NSString stringWithFormat:@"中国银行:%d",arc4random()%4]];
self.person.banks = [tmpArr mutableCopy];       // 新赋值一个对象

如果每次都这样写一下然后再赋值也很麻烦,有没有便捷的方式呢?

有,KVC中有方法,可以实现这个功能。

- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
- (NSMutableSet *)mutableSetValueForKey:(NSString *)key;

// 功能相似,都是从对象中根据Key获取一个可变的代理对象(可读写)当操作完毕后就会生成一个新的可变对象覆盖原值。所以才会产生新的内存地址,进而触发KVO监听

0x02 原理推导

官方描述

通过查看苹果官方对KVO实现介绍,可以发现是通过isa-swizzling技术实现的。

当对一个对象的属性添加一个观察者时,被观察对象的isa指针被修改,指向了一个中间类,而不是真正的类。

抛出几个问题作为跟踪理解:

  1. 生成的中间类是什么,何时生成?它与我们当前类是什么关系
  2. 我们看到手动出发时需要发送willChangeValueForKey,didChangeValueForKey,那么是不是生成的中间类也做了这些事,在哪里做的。
  3. 不移除监听会怎样?

中间类是什么

// Person

@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) NSString *bank;
@end


// VC.m
{
    self.person = [[Person alloc] init];
    
    NSLog(@"\n指针:%p-当前类名:%s-父类:%@",self.person,object_getClassName(self.person),class_getSuperclass(object_getClass(self.person)));
    
    [self.person addObserver:self forKeyPath:@"name"    options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
    NSLog(@"\n指针:%p-当前类名:%s-父类:%@",self.person,object_getClassName(self.person),class_getSuperclass(object_getClass(self.person)));
}

// 打印:
指针:0x600002f9e4c0-当前类名:Person-父类:NSObject
指针:0x600002f9e4c0-当前类名:NSKVONotifying_Person-父类:Person

通过上述打印可以发现:

  1. 当添加观察者时就会生成一个新的类,前缀为NSKVONotifying_的一个中间类。
  2. NSKVONotifying_PersonPerson的一个子类。

中间类做了什么

我们可以使用以下代码打印下中间类实现了哪些方法

unsigned int count;
Method *methods = class_copyMethodList(object_getClass(self.person), &count);
for (int i = 0; i< count; i++) {
Method m = methods[i];
printf("methodName:%s - %s\n",method_getName(m), method_getTypeEncoding(m));
}

// 输出
methodName:setName: - v24@0:8@16
methodName:class - #16@0:8
methodName:dealloc - v16@0:8
methodName:_isKVOA - B16@0:8

理解输出:v16@0:8

v16@0:8
返回值(v)偏移量(16) 参数1(@)偏移量(0) 参数2(:)偏移量(8)
返回值为void类型
第一个参数为OC对象类型
第二个参数类型为SEL类型

查看更多Type-Encoding

通过上述分析可以发现,中间类 NSKVONotifying_Person一共生成了4个方法

接下来我们验证中间类:setName:做了什么

我们在setName处打上断点,精简下调用栈可以发现:
当我们调用self.person.name = @"222"会先走KVO的系列方法,然后才会去调用setName

这就说明在中间类中重写了setter方法,优先处理了通知机制,然后调用父类的[super setName:]

* thread #1, -[Person setName:](self=0x0000600002e61000, _cmd="setName:", name=@"测试158") at Person.m:14:2
    frame #1: -[NSObject(NSKeyValueObservingPrivate) _changeValueForKeys:count:maybeOldValuesDict:maybeNewValuesDict:usingBlock:] + 646
    frame #2: -[NSObject(NSKeyValueObservingPrivate) _changeValueForKey:key:key:usingBlock:] + 68
    frame #3: _NSSetObjectValueAndNotify + 269
    frame #4: -[TmpViewController touchesEnded:withEvent:](self=0x00007ff599413740, _cmd="touchesEnded:withEvent:", touches=1 element, event=0x0000600001b35e00) at TmpViewController.m:71:14

kvo不移除监听会怎样

如果同一个对象有AB两个监听者,当发现改变时,KVO会通知这两个监听者。如果有监听者被释放,就会出现访问坏内存错误。

KVO_IS_RETAINING_ALL_OBSERVERS_OF_THIS_OBJECT_IF_IT_CRASHES_AN_OBSERVER_WAS_OVERRELEASED_OR_SMASHED

怎么探究:暂且放放


使用注意点

注意代码逻辑的划分,避免所有逻辑处理都写在同一个函数中

  1. 对于自己定义可以在外部访问的变量使用NSSTringFromSelector(@selector(xxx))来实现。

  2. 对于无法获取到的,只能使用key的方式,这个无法避免。

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context
{
    if (context == CURRENT_POINTER && object == xxx && [keyPath isEqualToString:@"contentSize"]) {  // 判断类型
    } else {    // 回归父类调用,避免父类无法调用
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

0x03 KVO使用注意点

但是更多的时候我们想要监听的事系统对我们自定的view做出了调整。
这个时候监听frame就会是无效的,需要通过监听center来实现。
为什么监听frame是无效的?

Note: Although the classes of the UIKit framework generally do not support KVO, 
you can still implement it in the custom objects of your application, including custom views.
# 尽管UIKit框架的类通常不支持KVO,但是您仍然可以在应用程序的自定义对象(包括自定义视图)中实现它。

我们可以发现通常KVO是不支持UIKIt框架中的类,它本身是为NSObject类实现的功能,当然你也可以自定义一些操作来实现这些功能。

一个示例效果(KVO监听frame无效,只能监听center)

# 写了个物理仿真器,创建一些小视图从顶部掉下来,要求超出屏幕范围就移除掉
self.animator = [[UIDynamicAnimator alloc] initWithReferenceView:self.view];
self.gravity = [[UIGravityBehavior alloc] initWithItems:tmpArr];
self.gravity.magnitude = 0.1;
[self.animator addBehavior:self.gravity];

[NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
        MyTmpView *tmpV = [[MyTmpView alloc] initWithFrame:CGRectMake(arc4random()%(int)UIScreen.mainScreen.bounds.size.width, 0, 2, 2)];
        tmpV.backgroundColor = UIColor.redColor;
        [tmpV addObserver:self forKeyPath:@"center" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
        [self.view addSubview:tmpV];
        [self.gravity addItem:tmpV];
}];
// 超出屏幕范围就移掉
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"center"]) {
        UIView *tmp = (UIView *)object;
        if (!CGRectContainsRect(self.view.frame, tmp.frame)) {
            [self.gravity removeItem:tmp];
            [tmp removeFromSuperview];
        }
    }
}

0x04总结

KVO内部的实现细节还有很深,这次主要分析了KVO的逻辑操作设计原理和使用注意点。

针对于KVO Crash原因也只是理论上理解,尚未脚踏实步的探究下,后续会再完善。


参考:
苹果官网描述KVO

stackOverflow问题解答

上一篇 下一篇

猜你喜欢

热点阅读