美文网首页
KVO使用及分析

KVO使用及分析

作者: 哦小小树 | 来源:发表于2020-04-30 13:10 被阅读0次

0x01 用途

键值观察是一种机制:

  • 对于观察的属性为NSObject类型:它允许将其他对象的属性的更改【属性内存地址的更改】通知给观察者
  • 对于观察的属性为基本类型如整型,结构体什么的:当值改变就会通知给观察者

对于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:: 重写父类Person的方法
  • class:用来返回类型,当我们直接打印self.person返回的就是Person,而不是NSKVONotifying_Person
  • dealloc:销毁方法,移除监听的时候调用
  • _isKVOA: 返回是否为一个KVO

接下来我们验证中间类: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

怎么探究:暂且放放


使用注意点

  • 回调函数只有一个,不允许自定义

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

  • NSString类型keyPath容易传错
  1. 对于自己定义可以在外部访问的变量使用NSSTringFromSelector(@selector(xxx))来实现。

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

  • 子类会拦截父类实现,需要使用Context进行区分
- (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];
    }
}
  • 重复remove或者不remove会导致crash

0x03 KVO使用注意点

  • 为什么有时监听frame无效,需要使用center才行
    我们有时会需要通过监听自定义viewFrame的变化去做某些事情,这是如果是我们动态调整了自定义viewframe会很明显的触发。

但是更多的时候我们想要监听的事系统对我们自定的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问题解答

相关文章

  • KVO使用及分析

    0x01 用途 键值观察是一种机制: 对于观察的属性为NSObject类型:它允许将其他对象的属性的更改【属性内存...

  • RxSwift学习(7)KVO知识补充

    网上好多关于OC的KVO介绍。在这里就不详细赘述了。参考KVO原理分析及使用进阶.在RxSwift中使用todoN...

  • iOS-KVO

    一.kvo使用 kvo可以监听一个对象属性的变化,下面为简单使用. 二.使用runtime分析kvo 我写了个简单...

  • KVO原理分析及使用进阶

    该文章属于<简书 — 刘小壮>原创,转载请注明: <简书 — 刘小壮> https://www.jianshu.c...

  • KVO原理分析及使用进阶

    1、概念 KVO(Key-Value-Oberver)观察者模式,是苹果提供的一套事件通知机制,允许对象监听另一个...

  • iOS高级进阶之KVO

    KVO的原理 分析原理 使用 手动调用 自己实现KVO NSObject+KVOBlock.h NSObject+...

  • KVO使用分析

    KVO(Key-valueObserve):是OC的一套通知机制,用对象监听该对象的属性改变,不能对于成员变量进行...

  • KVC/KVO 的使用及原理分析

    KVC/KVO 概念 KVC : 即 Key-Value-Coding,用于键值编码。作为 cocoa 的一个标准...

  • KVO和KVC的使用及原理解析

    一 KVO基本使用 二 KVO本质原理讲解及代码验证 三 KVC基本使用 四 KVC设值原理 五 KVC取值原理 ...

  • KVO使用及实现原理

    KVO使用及实现原理 KVO使用 对属性进行监听 对属性的属性进行监听 容器监听 触发(手动触发,kvc赋值) 添...

网友评论

      本文标题:KVO使用及分析

      本文链接:https://www.haomeiwen.com/subject/eejzwhtx.html