iOS-KVO原理分析

前言

KVO这个在我们iOS实际项目中经常用到的,今天我们来介绍一下它的原理。

KVO的坑点

首先我们打开KVO官方文档。

context

然后我们看下我们的Demo工程,

[self.person addObserver:self forKeyPath:@"nick" options:NSKeyValueObservingOptionNew context:NULL];

context这里的context我们一般传NULL(这个是C++的void 而不是id)。
那么
context*是什么东西,有什么作用?
我们看下官方文档的解释:
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.
这句话的意思大概是如果我们传了这个参数,会使性能安全,更加直接,这又是为什么呢?

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

这个是KVO的回调函数,如果当有多个监听时,这里的判断就比较复杂一些,先判断keyPath,再判断object。

if (object) {
   if (keyPath == @"key") {
    }
}

代码就是这样的, 嵌套层次比较深。
context就是创建一个唯一标识码。

static void *PersonAccountBalanceContext = &PersonAccountBalanceContext;
static void *PersonAccountInterestRateContext = &PersonAccountInterestRateContext;

比如官方文档的写法,可以通过唯一标识码判断。

移除观察者

Removing an Object as an Observer官方文档上有写,这个观察者还需要移除,那么究竟是否需要呢,我们来验证一下。
我们在dealloc方法里,不调用的removeObserver这个方法,发现这里没有自动移除的,退出这个页面再进来,可以继续监听没有发出问题,如图:

1

官方文档有这样一段话:
An observer does not automatically remove itself when deallocated. The observed object continues to send notifications, oblivious to the state of the observer. However, a change notification, like any other message, sent to a released object, triggers a memory access exception. You therefore ensure that observers remove themselves before disappearing from memory.
在deallocated时,观察者并不会主动移除,还是会续发送notifications时,有可能observed object不存在,如果是在disappearing from memory 时,请保证移除了。
我们来验证一下刚才说的。

我们添加一行这样的代码:

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

这个对象是一个单例,也就是说退出页面,对象不会消失。
接着我们运行项目,第一次进去监听没有任何问题,如图:


2

我们退出来,再进去,去改变值的时候,出现如下图:


3
  1. 我们在RoDetailViewController添加的self.student 对name的观察。
  2. RoDetailViewController释放的时候,我们的self.student对name的观察没有释放。
    3.当我们再次进入RoDetailViewController的是时候,又再次添加self.student对name的观察。我们对当前的RoDetailViewController发送通知没有问题,那么对原来的发送的通知的时候,原来的RoDetailViewController已经释放掉了,回收了,对它发送通知,就闪退了,所以尽量移除KVO的观察。

手动/自动打开观察开关

我们在RoPerson类中加入以下代码:

// 自动开关
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
    return NO;
}

运行项目,如图所示:


4

没有任何打印,KVO的自动观察不起作用了。

一对多的观察

我们举一个例子,下载的进度。
下载进度=已下载/总大小
我们在Controller加入以下代码:

 [self.person addObserver:self forKeyPath:@"downloadProgress" options:NSKeyValueObservingOptionNew context:NULL];

在Controller改变值的地方加入代码:

  self.person.writtenData +=10;
  self.person.totalData +=1;

我们运行下项目,如图:


5

这里为什么可以打印当前的进度呢,我们继续分析。
我们把RoPerson的的代码贴出来:
RoPerson.h代码:

#import 
@class RoStudent;

NS_ASSUME_NONNULL_BEGIN

@interface RoPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nick;
@property (nonatomic, copy) NSString *downloadProgress;
@property (nonatomic, assign) double writtenData;
@property (nonatomic, assign) double totalData;
@property (nonatomic, strong) NSMutableArray *dateArray;
@property (nonatomic, strong) RoStudent *st;

@end

NS_ASSUME_NONNULL_END

RoPerson.m代码:

#import "RoPerson.h"

@implementation RoPerson

// 下载进度 -- writtenData/totalData

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

// 自动开关
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
    return YES;
}

- (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];
}

//- (void)setNick:(NSString *)nick{
//    [self willChangeValueForKey:@"nick"];
//    _nick = nick;
//    [self didChangeValueForKey:@"nick"];
//}

- (void)insertObject:(id)object inDateArrayAtIndex:(NSUInteger)index{
    [self.dateArray insertObject:object atIndex:index];
}

-(void)removeObjectFromDateArrayAtIndex:(NSUInteger)index{
    [self.dateArray removeObjectAtIndex:index];
}

keyPathsForValuesAffectingValueForKey在这里把影响进度的两个值写进去就可以了。

对可变数组的观察

我们在viewDidLoad加入以下代码:

 [self.person addObserver:self forKeyPath:@"dateArray" options:NSKeyValueObservingOptionNew context:NULL];
    [self.person.dateArray addObject:@"1"];

发现变没有观察到,这是为什么?
我们来看下官方文档的解释。
In order to understand key-value observing, you must first understand key-value coding
这句话的意思,要想理解KVO就先要理解KVC,也就是说KVO是建立在KVC上的。
在KVC的官文档上有这么一句话:
These methods provide the additional benefit of maintaining key-value observing compliance for the objects held in the collection object (see Key-Value Observing Programming Guide for details.
如果是集合类的型的话

  • [mutableArrayValueForKey:](https://developer.apple.com/documentation/objectivec/nsobject/1416339-mutablearrayvalueforkey) and [mutableArrayValueForKeyPath:](https://developer.apple.com/documentation/objectivec/nsobject/1414937-mutablearrayvalueforkeypath)

    These return a proxy object that behaves like an NSMutableArray object.

  • [mutableSetValueForKey:](https://developer.apple.com/documentation/objectivec/nsobject/1415105-mutablesetvalueforkey) and [mutableSetValueForKeyPath:](https://developer.apple.com/documentation/objectivec/nsobject/1408115-mutablesetvalue)

    These return a proxy object that behaves like an NSMutableSet object.

  • [mutableOrderedSetValueForKey:](https://developer.apple.com/documentation/objectivec/nsobject/1415479-mutableorderedsetvalue) and [mutableOrderedSetValueForKeyPath:](https://developer.apple.com/documentation/objectivec/nsobject/1407188-mutableorderedsetvalue)

    These return a proxy object that behaves like an NSMutableOrderedSet object.
    要这么处理。
    我们再次修改我们的代码:

[self.person addObserver:self forKeyPath:@"dateArray" options:NSKeyValueObservingOptionNew context:NULL];
    self.person.dateArray = [NSMutableArray array];
    [[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"hello"];

运行项目,如图:

6

观察到数据了。
这里看到 第一条打印kind=1,第二条kind=2是为什么呢?
我们看下NSKeyValueChange这个枚举,如下:

typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
    NSKeyValueChangeSetting = 1,
    NSKeyValueChangeInsertion = 2,
    NSKeyValueChangeRemoval = 3,
    NSKeyValueChangeReplacement = 4,
};

NSKeyValueChangeInsertion是添加数据是2。

KVO底层原理分析

  • isa指向的变化
  • 对setter方法的处理
  • 中间产物
  • 中间产物是否消失
  • 中间产物是否销毁
  • setter方法是谁的RoPerson还是中间产物的?
  • RoPerson与中间产物有什么关系
  • 是否有动态的处理呢
  • 是否有setter方法的生成
    针对这些疑问,我们来一步一步的展开分析。

我们在viewDidLoad打个断点,运项目,如图:

5

从这张图可以看出,在观察之前self.person是指向的RoPerson,观察后指向NSKVONotifying_RoPerson这个类。
也就是说在底层动态生成了NSKVONotifying_RoPerson这个类
我们再看下面一张图:
6

观察之前是
NSKVONotifying_RoPerson没有加载,观察后有了NSKVONotifying_RoPerson*这个类。
由此可见NSKVONotifying_RoPerson这个类确实是上底层动态生成的。

那么NSKVONotifying_RoPerson与RoPerson又有什么关系呢?我们分析下。
我们可以通过isa指向分析下,我们通过以下代码进行分析验证

- (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

以上代码就是递归获取所有子类,接着我们运行项目,如图:

7

从上图可以看出,在添加观察之前,RoPerson只有RoStudent一个子类,添加观察后,多了一个NSKVONotifying_RoPerson子类,NSKVONotifying_RoPerson没有子类。
由此可以得出NSKVONotifying_RoPerson是RoPerson的子类,在添加观察后,动态生成了NSKVONotifying_为开头的子类,也就是子类与父类的关系。
NSKVONotifying_RoPerson这个动态生成的中间类,有什么东西呢,我们来分析一下。
对一个类来说,无非就是属性,方法,协议。我们来介绍下方法。
遍历方法的代码如下:

- (void)printClassAllMethod:(Class)cls{
    unsigned int count = 0;
    Method *methodList = class_copyMethodList(cls, &count);
    for (int i = 0; i

接着运行项目,如图:


8
  • class 类型
  • dealloc 是否释放。
  • _isKVOA 用来标识是KVO生成的类。
  • setNickName 观察对象的setter方法。
    以上几个方法都是重写方法。
    添加观察后,isa会指向动态生成的NSKVONotifying_RoPerson这个类,那么这个isa会指回来吗,我们分析下。
    我们在dealloc打个断点,运行项目,如图所示:
    9

    从上图可以看出[self.person removeObserver:self forKeyPath:@"nickName"];执行之前,self.person的isa指向的NSKVONotifying_RoPerson,移除后指向了RoPerson。
    由此说明isa在移除观察后重新指向了原来的类。

那么NSKVONotifying_RoPerson这个类会消失吗?
我们在ViewController打印一下。代码如下:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
    [self printClasses:[RoPerson class]];
}
- (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

接着运行项目,如图:

10

从这可以看出NSKVONotifying_RoPerson并不会销毁。
我们都知道KVO重写了setter方法,那么这个setter方法是谁的呢,NSKVONotifying_RoPerson还是RoPerson,我们来看下,如下图:
11

添加观察后,这里可以看出NSKVONotifying_RoPerson在后台默默的帮我们做事情,前台给我们显示的还是RoPerson这个类。
NSKVONotifying_RoPerson中的class方法重写打了马赛克。
NSKVONotifying_RoPerson中的dealloc销毁的时候把isa指回去。
NSKVONotifying_RoPerson中的setNickName做了什么事情。我们接着看。
KVO是针对setter方法监听,那么成员变量就永远监听不到。
我们先贴下RoPerson的代码:
.h代码:

#import 

NS_ASSUME_NONNULL_BEGIN

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

@end

NS_ASSUME_NONNULL_END

.m代码:

#import "RoPerson.h"

@implementation RoPerson
- (void)setNickName:(NSString *)nickName{
    _nickName = nickName;
}
@end

我们在Controller添加对name的观察,看下能否打印。
如图所示:

12

可以看出nickName有监听到,而name没有监听到,由此证实了KVO是对属性的setter方法监听,无法监听成员变量。
这个setter方法是谁的呢,我们来看下。
我们在dealloct中移除person的观察者断点,运行项目,如图所示:
13

当移除观察者时,这里的self.person的isa已经指向了RoPerson,打印出来了ro值,这里的setter方法是RoPerson的。
如下图:

14

我们再bt查看堆栈:
如下图:

15

*frame #2: 0x0000000100719c58 Foundation-[NSObject(NSKeyValueObservingPrivate) _changeValueForKeys:count:maybeOldValuesDict:maybeNewValuesDict:usingBlock:] + 646 frame #3: 0x000000010071a51a Foundation-[NSObject(NSKeyValueObservingPrivate) _changeValueForKey:key:key:usingBlock:] + 68
frame #4: 0x000000010071314c Foundation`_NSSetObjectValueAndNotify + 269*
在赋值的时候,在这里处理后,才会进入setter方法。

结论

这是个人对KVO的一些理解和看法,欢迎大家来学习和交流。

你可能感兴趣的:(iOS-KVO原理分析)