【iOS】KVO相关总结


1. 什么是KVO?

KVO的全称是Key-Value Observing,即键值监听或键值观察,用于监听某个对象属性值的改变

KVO是苹果提供的一套事件通知机制(其声明全部在Foundation框架中的NSKeyValueObserving.h里),允许一个对象监听另一个对象指定属性值的改变。当被观察对象属性值发生改变时,会触发KVO的监听方法来通知观察者

因为KVO接口声明是@interface NSObject(NSKeyValueObserving),即给NSObject添加的分类Category,所以大多数对象都可以键值观察或键值监听

2. KVO的基本使用

KVO使用主要有以下三步:

  1. 添加/注册KVO监听:调用addObserver:forKeyPath:options:context:给被观察对象添加观察者

    - (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
    
    • target:方法调用者为被观察对象
    • observer:观察者对象
    • keyPath:被观察者对象的关键路径,不能为nil
    • options:配置观察内容的枚举选项,请添加图片描述
    • context:可以传入任意数据(任意类型的对象或者C指针),在监听方法中可以接收到这个数据,是KVO中的一种传值方式。如果传的是一个对象,必须在移除观察之前持有这个对象的强引用,否则在监听方法中访问context就可能导致Crash
  2. 实现监听方法来接收属性改变通知:监听方法为observeValueForKeyPath:ofObject:change:context:

    - (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context {
    
    }
    
    • keyPath:被观察对象属性的关键路径
    • object:被观察对象
    • change:字典NSDictionary<NSKeyValueChangeKey, id>,属性值更改的详细信息,根据注册方法中options参数传入的枚举来返回,key为NSKeyValueChangeKey枚举类型
      1. NSKeyValueChangeKindKey:存储本次改变的信息(change字典中默认包含这个key)

        对应枚举类型NSKeyValueChange

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

        如果是对被观察对象属性(包括集合)进行赋值操作,kind字段的值为NSKeyValueChangeSetting
        如果被观察的是集合对象,且进行的是(插入、删除、替换)操作,则会根据集合对象的操作方式来设置kind字段的值:NSKeyValueChangeInsertion插入、NSKeyValueChangeRemoval删除、NSKeyValueChangeReplacement替换

      2. NSKeyValueChangeNewKey:存储新值(如果options中传入NSKeyValueObservingOptionNew,change字典中就会包含这个key)
      3. NSKeyValueChangeOldKey:存储旧值(如果options中传入NSKeyValueObservingOptionOld,change字典中就会包含这个key)
      4. NSKeyValueChangeIndexesKey:如果被观察的是集合对象,且进行的是(插入、删除、替换)操作,则change字典中就会包含这个key,这个key的value是一个NSIndexSet对象,包含更改关系中的索引
      5. NSKeyValueChangeNotificationIsPriorKey:如果options中传入NSKeyValueObservingOptionPrior,则在改变前通知的change字典中会包含这个key。
        这个key对应的value是NSNumber包装的YES,我们可以这样来判断是不是在改变前的通知[change[NSKeyValueChangeNotificationIsPriorKey] boolValue] == YES
    • context:注册方法中传入的context
  3. 移除KVO监听:当观察者不需要再监听时,调用removeObserver:forKeyPath:方法将观察者移除。需要注意的是,至少需要在观察者销毁之前,调用此方法,否则再次触发KVO监听方法就可能会导致Crash

    - (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
    - (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context;
    

以下为KVO使用示例:

@interface ViewController ()
@property (nonatomic, strong)Person* person;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.person = [[Person alloc] init];
    
    //  注册观察者
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.person addObserver: self forKeyPath: @"age" options: options context: NULL];
}

//  改变属性值
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    // [self.person setAge: 12];
    self.person.age = 12;
}

//  监听方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
}

//  移除监听
- (void)dealloc {
    [self.person removeObserver:self forKeyPath:@"age"];
}

@end

使用KVO为person对象添加观察者为当前viewController,监听person对象的name属性值的改变,当点击当前页面使name值改变时,触发KVO的监听方法:

请添加图片描述

3. KVO的进阶使用

observationInfo属性

observationInfo属性是NSKeyValueObserving.h文件中系统通过分类给NSObject添加的属性,所以所有继承于NSObject的对象都含有该属性

@property (nullable) void *observationInfo NS_RETURNS_INNER_POINTER;

可以通过observationInfo属性查看被观察对象的全部观察信息,包括observer、keyPath、options、context等

//  这里的 person 对象已被 KVO 监听
NSLog(@"%@", person.observationInfo);

请添加图片描述

context 的使用

注册方法addObserver:forKeyPath:options:context:中的context可以传入任意数据,并且可以在监听方法中接收到这个数据

context作用:标签-区分,可以更精确的确定被观察对象属性,用于继承、 多监听;也可以用来传值
KVO只有一个监听回调方法observeValueForKeyPath:ofObject:change:context:,我们通常情况下可以在注册方法中指定context为NULL,并在监听方法中通过objectkeyPath来判断触发KVO的来源
但是如果存在继承的情况,比如现在有 Person 类和它的两个子类 Teacher 类和 Student 类,person、teacher 和 student 实例对象都对 account 对象的 balance 属性进行观察(Observer)。问题:

  • 当 balance 发生改变时,应该由谁来处理呢?
  • 如果都由 person 来处理,那么在 Person 类的监听方法中又该怎么判断是自己的事务还是子类对象的事务呢?

这时候通过使用context就可以很好地解决这个问题,在注册方法中为context设置一个独一无二的值,然后在监听方法中对context值进行检验即可

苹果官方的推荐用法:用context来精确的确定被观察对象属性,使用唯一命名的静态变量的地址作为context的值

  • 可以为整个类设置一个context,然后在监听方法中通过object和keyPath来确定被观察属性,这样存在继承的情况就可以通过context来判断
  • 也可以为每个被观察对象属性设置不同的context,这样使用context就可以精确的确定被观察对象属性
static void* PersonAccountBalanceContext = &PersonAccountBalanceContext;
static void* PersonAccountInterestRateContext = &PersonAccountInterestRateContext;
- (void)registerAsObserverForAccount:(Account*)account {
    [account addObserver: self forKeyPath: @"balance" options: (NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context: PersonAccountBalanceContext];
 
    [account addObserver: self forKeyPath: @"interestRate" options: (NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context: PersonAccountInterestRateContext];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
 
    if (context == PersonAccountBalanceContext) {
        // Do something with the balance…
    } else if (context == PersonAccountInterestRateContext) {
        // Do something with the interest rate…
    } else {
        // Any unrecognized context must belong to super
        [super observeValueForKeyPath: keyPath ofObject: object change: change context: context];
    }
}

context优点:嵌套少、性能高、更安全、扩展性强

context注意点:

  • 如果传的是一个对象,必须在移除观察之前持有它的强引用,否则在监听方法中访问context就可能导致Crash;
  • 空传NULL而不应该传nil

KVO触发监听方法的方式

自动触发

默认使用以下方式改变被监听属性的值会自动触发 KVO 方法:

  • 点语法
  • setter 方法
  • KVC 的setValue:forKey:setValue:forKey:方法

如果是监听集合对象的改变,需要通过KVC的mutableArrayValueForKey:等方法获得代理对象,并使用代理对象进行操作,当代理对象的内部对象发生改变时,会触发KVO。集合对象包含NSArrayNSSet

手动触发

要想手动触发KVO,需要修改类方法automaticallyNotifiesObserversForKey:,下面的逻辑让我们精准施策,选择对哪些属性是自动,哪些属性是手动

//  默认返回触发返回YES,即如果不手动调用合适的方法的话,就不会触发KVO
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    BOOL automatic = NO;
    if ([key isEqualToString: @"age"]) {
        automatic = NO;
    } else {
        automatic = [super automaticallyNotifiesObserversForKey: key];
    }
    return automatic;
}

也可以实现遵循命名规则为+ (BOOL)automaticallyNotifiesObserversOf<Key>的方法来单一控制属性的KVO自动触发,<Key>为属性名(首字母大写):

+ (BOOL)automaticallyNotifiesObserversOfAge {
    return NO;
}

且该方法的优先级高于上面的方法;options指定的NSKeyValueObservingOptionInitial触发的KVO通知,是无法被automaticallyNotifiesObserversForKey:阻止的

普通对象属性或是成员变量使用:

- (void)setAge:(int)age {
    if (_age != age) {
        [self willChangeValueForKey: @"age"];
        _age = age;
        [self didChangeValueForKey: @"age"];
    }
}

对于集合对象,必须指定更改的类型和所涉及对象的索引:

- (void)removeBooksAtIndexes:(NSIndexSet *)indexes {
    [self willChange: NSKeyValueChangeRemoval
        valuesAtIndexes: indexes forKey: @"books"];
 
    // Remove the book objects at the specified indexes.
 
    [self didChange: NSKeyValueChangeRemoval
        valuesAtIndexes:indexes forKey: @"books"];
}

更改的类型是NSKeyValueChange

请添加图片描述

NSKeyValueObservingOptionPrior(分别在值改变前后触发方法,即一次修改有两次触发)的两次触发分别在willChangeValueForKey:和didChangeValueForKey:的时候进行的。如果注册方法中options传入NSKeyValueObservingOptionPrior,那么可以通过只调用willChangeValueForKey:来触发改变前的那次KVO,可以用于在属性值即将更改前做一些操作

下面以观察数组为例。
关键方法:

- (void)willChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;
- (void)didChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;

需要注意的是,根据KVC的NSMutableArray搜索模式:
【iOS】KVC相关总结

  • 至少要实现一个插入和一个删除方法,否则不会触发KVO。如
    插入方法:insertObject:in<Key>AtIndex:或insert<Key>:atIndexes:
    删除方法:removeObjectFrom<Key>AtIndex:或remove<Key>AtIndexes:
  • 可以不实现替换方法,但是如果不实现替换方法,执行替换操作时,KVO会把它当成先删除后添加,即会触发两次KVO。第一次触发的KVO中change字典的old键的值为替换前的元素,第二次触发的KVO中change字典的new键的值为替换后的元素,前提条件是注册方法中的options传入对应的枚举值
  • 如果实现替换方法,则执行替换操作只会触发一次KVO,并且change字典会同时包含newold,前提条件是注册方法中的options传入对应的枚举值
    替换方法:replaceObjectIn<Key>AtIndex:withObject:replace<Key>AtIndexes:with<Key>:
  • 建议实现替换方法以提高性能

示例代码:

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    BOOL automatic = NO;
    if ([key isEqualToString:@"mArray"]) {
        automatic = NO;
    } else {
        automatic = [super automaticallyNotifiesObserversForKey:key];
    }
    return automatic;
}

- (void)insertMArray:(NSArray *)array atIndexes:(NSIndexSet *)indexes {
    [self willChange:NSKeyValueChangeInsertion valuesAtIndexes:indexes forKey:@"mArray"];
    [self.mArray insertObjects:array atIndexes:indexes];
    [self didChange:NSKeyValueChangeInsertion valuesAtIndexes:indexes forKey:@"mArray"];
}

- (void)removeMArrayAtIndexes:(NSIndexSet *)indexes {
    [self willChange:NSKeyValueChangeRemoval valuesAtIndexes:indexes forKey:@"mArray"];
    [self.mArray removeObjectsAtIndexes:indexes];
    [self didChange:NSKeyValueChangeRemoval valuesAtIndexes:indexes forKey:@"mArray"];
}

- (void)replaceMArrayAtIndexes:(NSIndexSet *)indexes withMArray:(NSArray *)array {
    [self willChange:NSKeyValueChangeReplacement valuesAtIndexes:indexes forKey:@"mArray"];
    [self.mArray replaceObjectsAtIndexes:indexes withObjects:array];
    [self didChange:NSKeyValueChangeReplacement valuesAtIndexes:indexes forKey:@"mArray"];
}

实际自动触发调用的就是这些函数,手动触发不会动态生成子类,下面的KVO 本质分析会提到

KVO新旧值相等时不触发

被 KVO 监听的属性修改前后值相等时,也会触发监听方法:

self.person.age = 12;
self.person.age = 12;
/*
 change: {
  kind = 1;
  new = 12;
  old = 12;
 }
*/

有时会认为这样的值没必要监听,就可通过重写automaticallyNotifiesObserversForKey:setter方法,当属性被修改前后值相等时,不触发 KVO:

//  首先关闭手动触发 KVO
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    BOOL isOpen = YES;
    if ([key isEqualToString: @"age"]) {
        isOpen = NO;
    }
    return isOpen;
}

//  修改前后值不相等,不去触发 KVO
- (void)setAge:(int)age {
    //手动设置KVO
    if (_age != age) {
        [self willChangeValueForKey: @"age"];
        _age = age;
        [self didChangeValueForKey: @"age"];
    }
}

KVO的从属关系

一对一关系

有时,一个属性的改变依赖于其他的一个或多个属性的改变
比如,对 Download 类中的 downloadProgress 属性进行 KVO 监听,该属性的改变依赖于wirteDatatotalData属性的改变

- (NSString *)downloadProgress {
    return [NSString stringWithFormat: @"%@ %@", self.writeData, self.totalData];
}

法一: 重写类方法keyPathsForValuesAffectingValueForKey,来返回一个集合

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

这里需要先对父类发送keyPathsForValuesAffectingValueForKey消息,以免干扰父类中对此方法的重写

法二: 实现一个遵循命名规则为keyPathsForValuesAffecting<Key>的类方法,<Key>是依赖于其他值的属性名(首字母大写),针对某个属性:

+ (NSSet<NSString *> *)keyPathsForValuesAffectingDownloadProgress {
    return [NSSet setWithObjects: @"writeData", @"totalData", nil];
}

以上两个方法可以同时存在,且都会调用,但是最终结果会以keyPathsForValuesAffectingValueForKey:为准

一对多关系

以上方法在观察集合属性时就不管用了。例如,假如你有一个 Department 类,它有一个装有 Employee 类的实例对象的数组,Employee 类有 salary 属性
你希望 Department 类有一个totalSalary属性来计算所有员工的薪水,也就是在这个关系中 Department 的 totalSalary 依赖于所有 Employee 实例对象的 salary 属性。以下有两种方法可以解决这个问题:

法一: 用KVO将 Department 作为所有 employee 相关属性的观察者,在observeValueForKeyPath:ofObject:change:context:方法中我们可以针对被依赖项的变更来更新依赖项的值:

#import "Department.h"

static void *totalSalaryContext = &totalSalaryContext;

@interface Department ()
@property (nonatomic,strong)NSArray <Employee *>* employees;
@property (nonatomic,strong)NSNumber* totalSalary;
@end


@implementation Department

- (instancetype)initWithEmployees:(NSArray *)employees {
    self = [super init];
    if (self) {
        self.employees = [employees copy];
        //  核心代码
        for (Employee* em in self.employees) {
            [em addObserver: self forKeyPath: @"salary" options: NSKeyValueObservingOptionNew context: totalSalaryContext];
        }
    }
    return self;
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if (context == totalSalaryContext) {
        self.totalSalary = [self valueForKeyPath: @"employees.@sum.salary"];
    } else {
        [super observeValueForKeyPath: keyPath ofObject: object change: change context: context];
    }
}
 
- (void)setTotalSalary:(NSNumber *)totalSalary {
    if (_totalSalary != totalSalary) {
        [self willChangeValueForKey:@"totalSalary"];
        _totalSalary = totalSalary;
        [self didChangeValueForKey:@"totalSalary"];
    }
}

- (void)dealloc {
    for (Employee *em in self.employees) {
        [em removeObserver:self forKeyPath:@"salary" context:totalSalaryContext];
    }
}

@end

法二: 使用通知中心NSNotification

4. KVO使用注意

  • 至少需要在观察者销毁之前,调用KVO移除方法移除观察者,否则如果在观察者被释放后,再次触发KVO监听方法就会导致Crash

  • 我们在注册观察者的时候,要求传入的keyPath是字符串类型,如果我们拼写错误的话,编译器是不能帮我们检查出来的,所有最佳实践应该是使NSStringFromSelector(SEL aSelector),比如我们要观察tableView的contentSize属性,我们可以这样使用:

    NSStringFromSelector(@selector(contentSize))
    

    将 getter 方法 SEL 转换成字符串,在编译阶段进行检验

  • 有时候我们难以避免多次注册和移除相同的KVO,或者移除了一个未注册的观察者,从而产生可能会导致Crash的风险
    三种解决方案:黑科技防止多次添加删除KVO出现的问题

    • 利用 @try @catch(只能针对删除多次KVO的情况下)
      给NSObject增加一个分类,然后利用Runtime API交换系统的removeObserver方法,在里面添加@try @catch
    • 利用 模型数组 进行存储记录
    • 利用 observationInfo 里私有属性
  • 观察者对象所属类中必须实现监听方法,否则会 Crash

  • 如果是监听集合对象的改变,需要通过KVC的mutableArrayValueForKey:等方法获得代理对象,并使用代理对象进行操作,当代理对象的内部对象发生改变时,会触发KVO。如果直接对集合对象进行操作改变,不会触发KVO

  • 在观察者类的监听方法中,应该为无法识别的context、object或keyPath调用父类的实现[super observeValueForKeyPath: keyPath ofObject: object change: change context: context];

5. KVO本质原理分析

从上面的代码可以得知,只有对象被观察者监听了,该对象的属性值使用点语法或setter方法赋值时才会触发KVO,没有被监听的对象则不会触发KVO监听方法
这是为什么呢?

我们将person变量名改为person1,再初始化一个person2对象,去改变其age属性值,但不使用KVO为它添加观察者(未被监听),打断点使用lldb打印出两个对象isa所指向的类:

请添加图片描述

可以得知,被KVO监听的对象person1的isa指向的类对象已经不是原本的Person类,而是一个Runtime运行时动态创建的新类NSKVONotifying_Person(且是Person类的子类):

请添加图片描述

对象调用一个方法,实际就是给实例对象(调用者)发送一个消息,根据实例对象的isa指针找到类对象,而后再在类对象里找到方法的实现去调用。
当使用setter方法改变被监听的person1对象的属性值时,会找到NSKVONotifying_Person类下面的setAge:(已被重写)进行调用,而此方法的实现已经不是简单地给实例变量赋值_age = age,而是调用Foundation框架中的C函数_NSSetIntValueAndNotify(),这个函数中会有调用KVO监听方法的代码段

伪代码

因为我们无法知道Foundation框架的源码,所以我们可以从结果推出一些伪代码,探索出KVO大概做了些什么事情:

//  NSKVONotifying_Person.h
#import "Person.h"
@interface NSKVONotifying_Person : Person
@end

//  NSKVONotifying_Person.m
#import "NSKVONotifying_Person.h"

@implementation NSKVONotifying_Person

- (void)setAge:(int)age {
    _NSSetIntValueAndNotify();
}

//  伪代码
void _NSSetIntValueAndNotify(void) {
    [self willChangeValueForKey: @"age"];
    [super setAge: age];  //  _age = age
    [self didChangeValueForKey: @"age"];
}

- (void)didChangeValueForKey:(NSString *)key {
    //  通知观察者(监听器),某某属性值发生了改变
    [observer observeValueForKeyPath: key ofObject: self change: nil context: NULL];
    //  ...
}

@end

person1和person2两个对象的isa指向不同,即类对象不同,就会使找到的对应方法的实现不一样,所以才导致两种不同的结果(被KVO监听的对象指定属性值改变,触发监听方法,未被监听的对象属性值改变,不会触发监听方法)。

现在来验证以上分析是否正确:

保留伪代码下的类并编译运行

现在注释掉伪代码,保留类的声明和实现文件,运行项目,控制台会出现如下提示:

请添加图片描述

意为KVO创建NSKVONotifying_Person类失败,因为自己已经强行手动创建了该类,所以无法在运行时动态生成此类解决方案就是,将此类不纳入待编译文件内:

在这里插入图片描述

在此处将其删掉即可,表示项目中只是存在这些代码,但不参与编译
也从侧面证明了确实有动态生成NSKVONotifying_Person类

对比添加监听前后实例对象的类对象

NSLog(@"person1添加KVO监听之前 - %@ 和person2: %@", object_getClass(self.person1), object_getClass(self.person2));
    
//  给person1对象添加KVO监听
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver: self forKeyPath: @"age" options: options context: NULL];
    
NSLog(@"person1添加KVO监听之后 - %@ 和person2: %@", object_getClass(self.person1), object_getClass(self.person2));

运行结果表明被监听对象所属的类的确不是原来的类:
请添加图片描述

对比添加监听前后实例对象的方法实现

NSLog(@"person1添加KVO监听之前setAge:方法 - %p 和person2: %p", [self.person1 methodForSelector: @selector(setAge:)], [self.person2 methodForSelector: @selector(setAge:)]);
    
//  给person1对象添加KVO监听
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver: self forKeyPath: @"age" options: options context: NULL];
    
NSLog(@"person1添加KVO监听之后setAge:方法 - %p 和person2: %p", [self.person1 methodForSelector: @selector(setAge:)], [self.person2 methodForSelector: @selector(setAge:)]);

请添加图片描述

打断点,根据地址使用lldb将setAge:方法在控制台打印出来,运行结果表明被监听对象的方法实现跟监听前不一样,也得知了具体的方法声明:

请添加图片描述

这里如果修改的属性时double类型,实际调用的方法应该是_NSSetDoubleValueAndNotify

在这里插入图片描述

当然不同类型的属性会调用不同的C函数:

请添加图片描述

对比添加监听前后实例对象的类和元类

//  给person1对象添加KVO监听
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver: self forKeyPath: @"age" options: options context: NULL]; 
NSLog(@"类对象 - %p %p",
      object_getClass(self.person1),  // self.person1.isa
      object_getClass(self.person2)); // self.person2.isa

NSLog(@"元类对象 - %p %p",
      object_getClass(object_getClass(self.person1)), // self.person1.isa.isa
      object_getClass(object_getClass(self.person2))); // self.person2.isa.isa

运行结果表明,person1的类(NSKVONotifying_Person)虽继承与person2的类(Person),但它们的元类各不相同,每个类对象都有各自的元类对象:请添加图片描述

监听器监听方法的调用时机和顺序

在Person类中重写以下方法:

#import "MJPerson.h"

@implementation MJPerson

- (void)setAge:(int)age {
    _age = age;
    NSLog(@"setAge:");
}

- (void)willChangeValueForKey:(NSString *)key {
    [super willChangeValueForKey: key];
    NSLog(@"willChangeValueForKey");
}

- (void)didChangeValueForKey:(NSString *)key {
    NSLog(@"didChangeValueForKey - begin");
    [super didChangeValueForKey: key];
    NSLog(@"didChangeValueForKey - end");
}

@end

运行结果表明当被监听的对象的属性改变时,会先调用willChangeValueForKey:,再调用setter相关方法,最后是调用didChangeValueForKey,监听方法的确是在didChangeValueForKey:方法里面调用的:
请添加图片描述

动态生成类重写的方法

对象在被KVO监听后,全新生成的NSKVONotifying_Person类里面,除重写了父类Person的setAge:方法之外,还重写了classdeallocisKVOA三个方法

用class方法打印被KVO监听后对象的类对象:

//  给person1对象添加KVO监听
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver: self forKeyPath: @"age" options: options context: NULL];

//  访问isa指针
NSLog(@"%@ %@", object_getClass(self.person1), object_getClass(self.person2));
//  调用class方法
NSLog(@"%@ %@", [self.person1 class], [self.person2 class]);

从打印结果来看,class方法返回的不见得是真实的类对象:
请添加图片描述

可推出重写的class方法可能会是这样:

- (void)setAge:(int)age {
    _NSSetIntVlaueAndNotify();
}

- (Class)class {
    return [Person class];
}

- (void)dealloc {
    //  一些收尾工作
}

- (BOOL)_isKVOA {
    return YES;
}

可猜测这样重写的原因是,从我们开发者角度来看,这个新类NSKVONotifying_Person是需要被隐藏的,官方不希望它被暴露出来,屏蔽了内部实现

如果没有重写class方法,则会根据继承链往上找到NSObject基类的class方法:

//  伪代码
@implementation NSObject
- (Class)class {
    return object_getClass(self);
}
@end

也会打印出真实的类对象,所以不重写class方法,就达不到隐藏新类的目的

打印新类的方法列表名称

写一个方法对指定类的方法名进行打印:

//  打印出某个类的所有方法名
- (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);
}

调用:

[self printMethodNamesOfClass: object_getClass(self.person1)];
[self printMethodNamesOfClass: object_getClass(self.person2)];

请添加图片描述

可以得知动态生成的新类确实也重写了上面提到的方法

参考文章

iOS - 关于 KVO 的一些总结
iOS 底层探索 - KVO

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值