KVO 底层本质

栏目: IOS · 发布时间: 5年前

内容简介:一、KVO 的一个疑惑上述是一段简单的 KVO 使用代码,但如果仔细一想确实有个很大的疑惑。两个Person对象调用了类似的方法,其中touchesBegan:(NSSet相信有不少开发者多少了解各大概,是通过运行时动态创建子类实现 KVO 机制。但是不得不承认,苹果的这个 KVO 机制设计的非常巧妙,仅仅一行代码便能实现意想不到的结果。为了更深入的了解 KVO 机制,下面会结合相关底层源码分析 KVO 机制。
  • 一、KVO 的一个疑惑

  • 二、KVO 的浅层分析

  • 三、KVO 浅层分析验证

  • 四、KVO 子类内部方法

  • 五、手动触发 KVO

一、KVO 的一个疑惑

- (void)viewDidLoad {
    [super viewDidLoad];
    self.person1 = [[Person alloc] init];
    self.person1.age = 1;
    self.person2 = [[Person alloc] init];
    self.person2.age = 2;
    // 给person1对象添加KVO监听
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
}
- (void)touchesBegan:(NSSet
<uitouch nbsp="">
  *)touches withEvent:(UIEvent *)event{
    self.person1.age = 21;
    self.person2.age = 22;
}
- (void)dealloc {
    [self.person1 removeObserver:self forKeyPath:@"age"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary
 <nskeyvaluechangekey id="">
   *)change context:(void *)context{
    NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
}
@interface Person : NSObject
@property (assign, nonatomic) int age;
@end
@implementation Person
- (void)setAge:(int)age{
    _age = age;
}
@end
 </nskeyvaluechangekey>
</uitouch>

上述是一段简单的 KVO 使用代码,但如果仔细一想确实有个很大的疑惑。两个Person对象调用了类似的方法,其中touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event方法中,两个 person 对象赋值的本质都是调用了Person类中的 - (void)setAge:(int)age 方法。唯一的不同点在于person1添加了KVO监听。person1 会走 KVO 监听回调,person2却不走监听回调。

相信有不少开发者多少了解各大概,是通过运行时动态创建子类实现 KVO 机制。但是不得不承认,苹果的这个 KVO 机制设计的非常巧妙,仅仅一行代码便能实现意想不到的结果。为了更深入的了解 KVO 机制,下面会结合相关底层源码分析 KVO 机制。

二、KVO 的浅层分析

使用 LLDB 在 touchesBegan 方法内断点打印 person1和  person2 的 isa指针,即 p self.person1.isa 和 p self.person2.isa ,会发现打印结果不同。其中person1的 isa 指针指向的类为NSKVONotifying_Person,而 person2 的 isa 指针指向的类为Person。(说明: 实例对象的 isa 指向类,类的 isa 指针指向元类,元类的 isa 指针指向根元类,根元类的 isa 指针指向根元类自身)。

结合上述 LLDB 调试结果,可以进一步分析两个 person 实例对象的内存布局。首先来看,未使用 KVO 监听对象的内存布局,即person2对象。person2 的 isa 指针指向 Person 的 Class 对象, Person 的 Class 对象中包含 isa 指针,superclass指针,以及age属性 对应的的 setAge:和  age 方法。总的来说,person2 对象的内存布局和普通对象的内存布局无任何特殊之处。

KVO 底层本质

未使用 KVO 监听对象的内存布局

再来看看,使用 KVO 监听对象的内存布局,即 person1 对象。

KVO 底层本质

使用 KVO 监听对象的内存布局

通过上述 LLDB 调试可以看出 person1 的 isa 指向 NSKVONotifying_Person, 该类是借助 runtime 动态生成的类, NSKVONotifying_Person实际上是 Person的子类,故 superclass 指向 Person 类。 self.person1.age = 21会调用 NSKVONotifying_Person 类中的 setAge 方法,setAge 方法中会调用 Foundation 框架中的 _NSSetIntValueAndNotify 方法 , _NSSetIntValueAndNotify 方法内部依次调用willChangeValueForKey 、 super setAge 、 didChangeValueForKey 三个方法。 didChangeValueForKey 方法通知监听器属性值发生变化。大概的伪代码形式如下:

@implementation NSKVONotifying_Person
- (void)setAge:(int)age{
    _NSSetIntValueAndNotify();
}
// 伪代码
void _NSSetIntValueAndNotify(){
    [self willChangeValueForKey:@"age"];
    [super setAge:age];
    [self didChangeValueForKey:@"age"];
}
- (void)didChangeValueForKey:(NSString *)key{
    // 通知监听器,某某属性值发生了改变
    [oberser observeValueForKeyPath:key ofObject:self change:nil context:nil];
}
@end

为了证明伪代码 _NSSetIntValueAndNotify 的内部调用方法执行顺序,可重写 Person 类的如下方法,并输出 log 信息。

- (void)willChangeValueForKey:(NSString *)key{
    [super willChangeValueForKey:key];
    NSLog(@"willChangeValueForKey");
}
- (void)setAge:(int)age{
    _age = age;
    NSLog(@"setAge:");
}
- (void)didChangeValueForKey:(NSString *)key{
    NSLog(@"didChangeValueForKey - begin");
    [super didChangeValueForKey:key];
    NSLog(@"didChangeValueForKey - end");
}

如下打印结果,可证明伪代码 _NSSetIntValueAndNotify 的实现步骤。

willChangeValueForKey
setAge:
didChangeValueForKey - begin
监听到
<person: nbsp="" 0x600002a8a900="">
 的age属性值改变了 - {
    kind = 1;
    new = 21;
    old = 1;
} - 123
didChangeValueForKey - end
</person:>

三、KVO 浅层分析验证

3.1 验证一

如果在原有工程中,创建NSKVONotifying_Person类,运行代码会报 KVO failed to allocate class pair for name NSKVONotifying_Person, automatic key-value observing will not work for this class 错误,因为原有工程中已经存在该类,故无法运行时生成该类。

3.2 验证二

在 person 对象调用 addObserver: forKeyPath: options: context: 方法之前和之后添加如下代码,打印结果分别为添加KVO监听之前 - Person Person 和 添加KVO监听之后 - NSKVONotifying_Person Person。

NSLog(@"添加KVO监听之前 - %@ %@",
          object_getClass(self.person1),
          object_getClass(self.person2));//添加KVO监听之前 - MJPerson MJPerson
NSLog(@"添加KVO监听之后 - %@ %@",
          object_getClass(self.person1),
          object_getClass(self.person2));//添加KVO监听之后 - NSKVONotifying_MJPerson MJPerson

3.3 验证三

在 person 对象调用 addObserver: forKeyPath: options: context: 方法之前和之后添加如下代码,打印结果分别为 添加KVO监听之前 - 0x106515c90 0x106515c90 和 添加KVO监听之后 - 0x10686ffc2 0x106515c90 。

NSLog(@"添加KVO监听之前 - %p %p",
          [self.person1 methodForSelector:@selector(setAge:)],
          [self.person2 methodForSelector:@selector(setAge:)]);
          
NSLog(@"添加KVO监听之后 - %p %p",
          [self.person1 methodForSelector:@selector(setAge:)],
          [self.person2 methodForSelector:@selector(setAge:)]);

进一步借助 LLDB 调试 p (IMP)0x106515c90 和 p (IMP) 0x10686ffc2的打印结果如下:

四、KVO 子类内部方法

KVO 底层本质

上图中可以看出子类 NSKVONotifying_Person 除了重写 setAge:方法,还重写了class、dealloc、以及_isKVOA方法。为了证明NSKVONotifying_Person中存在上面提到的四个方法,可借助class_copyMethodList方法打印特定类中的方法。打印结果为SKVONotifying_MJPerson setAge:, class, dealloc, _isKVOA,

- (void)printMethodNamesOfClass:(Class)cls{
    unsigned int count;
    // 获得方法数组
    Method *methodList = class_copyMethodList(cls, &count);
    // 存储方法名
    NSMutableString *methodNames = [NSMutableString string];
    // 遍历所有的方法
    for (int i = 0; i < count; i++) {
        // 获得方法
        Method method = methodList[I];
        // 获得方法名
        NSString *methodName = NSStringFromSelector(method_getName(method));
        // 拼接方法名
        [methodNames appendString:methodName];
        [methodNames appendString:@", "];
    }
    // 释放
    free(methodList);
    // 打印方法名
    NSLog(@"%@ %@", cls, methodNames);
}

dealloc 方法不难理解,主要是做一些收尾工作,解决内存问题。

class 方法内部大概实现是return [Person class]。试想,如果不这样重写class方法,那么 person1 在添加 KVO 监听之后,调用object_getClass(object_getClass(self.person1)) 或 [self.person1 class] 将返回NSKVONotifying_Person, 这显然与实际情况不符合。因此可以猜测class 方法内部大概实现是return [Person class],从而屏蔽了 NSKVONotifying_Person 底层类的存在。

五、手动触发 KVO

KVO 的触发条件一般是修改监听对象属性值,但是如何在不修改被监听属性值的情况下触发 KVO 监听回调。也就是所谓的手动触发 KVO ,通过下面两行代码可手动触发 KVO, 注意必须同时调用下面两行代码。

[self.person1 willChangeValueForKey:@"age"];
[self.person1 didChangeValueForKey:@"age"];

由此也可以知道直接修改成员变量不会触发 KVO 监听方法,因为 KVO 的本质是重写了 set 方法, set 方法内部调用了willChangeValueForKey 和 didChangeValueForKey 方法,直接修改成员变量并不会调用 set 方法。

另外,通过 KVC 修改属性也会触发 KVO 监听,因为 KVC 中setValue:forKey:会按照setKey、_setKey的顺序查找方法,setValue:forKey: 内部调用顺序如下,这里不再展开说明。

KVO 底层本质

作者:ZhengYaWei

链接:https://www.jianshu.com/p/5e3bb16e4d1b


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Foundations of PEAR

Foundations of PEAR

Good, Nathan A./ Kent, Allan / Springer-Verlag New York Inc / 2006-11 / $ 50.84

PEAR, the PHP Extension and Application Repository, is a bountiful resource for any PHP developer. Within its confines lie the tools that you need to do your job more quickly and efficiently. You need......一起来看看 《Foundations of PEAR》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

URL 编码/解码
URL 编码/解码

URL 编码/解码

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具