【iOS】KVO相关总结

目录

    • 1. 什么是KVO?
    • 2. KVO的基本使用
    • 3. KVO的进阶使用
      • observationInfo属性
      • context 的使用
      • KVO触发监听方法的方式
        • 自动触发
        • 手动触发
      • KVO新旧值相等时不触发
      • KVO的从属关系
        • 一对一关系
        • 一对多关系
    • 4. KVO使用注意
    • 5. 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: NSKeyValueChangeRemovalvaluesAtIndexes: indexes forKey: @"books"];// Remove the book objects at the specified indexes.[self didChange: NSKeyValueChangeRemovalvaluesAtIndexes: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 {//手动设置KVOif (_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.isaobject_getClass(self.person2)); // self.person2.isaNSLog(@"元类对象 - %p %p",object_getClass(object_getClass(self.person1)), // self.person1.isa.isaobject_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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/852733.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

小白都能看懂的 “栈”

什么是栈&#xff1f;首先引用维基百科的解释&#xff1a; 栈&#xff08;stack&#xff09;是计算机科学中的一种抽象资料类型&#xff0c;只允许在有序的线性资料集合的一端&#xff08;称为堆栈顶端&#xff0c;top&#xff09;进行加入数据&#xff08;push&#xff09;和移…

Go语言结构体内嵌接口

前言 在golang中&#xff0c;结构体内嵌结构体&#xff0c;接口内嵌接口都很常见&#xff0c;但是结构体内嵌接口很少见。它是做什么用的呢&#xff1f; 当我们需要重写实现了某个接口的结构体的(该接口)的部分方法&#xff0c;可以使用结构体内嵌接口。 作用 继承赋值给接口…

信号与系统实验MATLAB-实验1-信号的MATLAB表示及信号运算

实验1-信号的MATLAB表示及信号运算 一、实验目的 1、掌握MATLAB的使用&#xff1b; 2、掌握MATLAB生成信号波形&#xff1b; 3、掌握MATLAB分析常用连续信号&#xff1b; 4、掌握信号运算的MATLAB实现。 二、实验内容 编写程序实现下列常用函数&#xff0c;并显示波形。…

PyTorch -- Visdom 快速实践

安装&#xff1a;pip install visdom 注&#xff1a;如果安装后启动报错可能是 visdom 版本选择问题 启动&#xff1a;python -m visdom.server 之后打开出现的链接 http://localhost:8097Checking for scripts. Its Alive! INFO:root:Application Started INFO:root:Working…

数据网格和视图入门

WinForms数据网格&#xff08;GridControl类&#xff09;是一个数据感知控件&#xff0c;可以以各种格式&#xff08;视图&#xff09;显示数据。本主题包含以下部分&#xff0c;这些部分将指导您如何使用网格控件及其视图和列&#xff08;字段&#xff09;。 Grid Control’s…

BUUCTF-Web题目1

目录 [HCTF 2018]admin 1、题目 2、知识点 3、思路 [极客大挑战 2019]BuyFlag 1、题目 2、知识点 3、思路 [HCTF 2018]admin 1、题目 2、知识点 BP暴力破解密码 3、思路 打开题目&#xff0c;查看页面源代码&#xff0c;发现需要admin用户才可以登录 这一台有很多解法…

LeetCode | 20.有效的括号

这道题就是栈这种数据结构的应用&#xff0c;当我们遇到左括号的时候&#xff0c;比如{,(,[&#xff0c;就压栈&#xff0c;当遇到右括号的时候&#xff0c;比如},),]&#xff0c;就把栈顶元素弹出&#xff0c;如果不匹配&#xff0c;则返回False&#xff0c;当遍历完所有元素后…

K8s 卷快照类

卷快照类 卷快照类 这个警告信息通常出现在使用 kubectl 删除 Kubernetes 集群资源时&#xff0c;如果尝试删除的是集群作用域&#xff08;cluster-scoped&#xff09;的资源&#xff0c;但指定了命名空间&#xff08;namespace&#xff09;&#xff0c;就会出现这个警告。 集…

基于PointNet / PointNet++深度学习模型的激光点云语义分割

一、场景要素语义分割部分的文献阅读笔记 1.1 PointNet PointNet网络模型开创性地实现了直接将点云数据作为输入的高效深度学习方法&#xff08;端到端学习&#xff09;。最大池化层、全局信息聚合结构以及联合对齐结构是该网络模型的三大关键模块&#xff0c;最大池化层解决了…

72、AndroidStudio 导入项目Connect timed out错误解决

一、背景&#xff1a; 开发过程中难免会 clone 其他的项目&#xff0c;clone 或者下载成功之后。使用 android studio 打开项目时经常遇到 Connect timed out错误如图所示&#xff1a; 二、分析原因&#xff1a; 1、既然链接超时&#xff0c;肯定是 android studio 在运行…

包装类的应用

一.什么是包装类 基本数据类型所对应的引用数据类型 二.集合中不能存储基本数据类型 三.JDK5以后对包装类新增了什么特性&#xff1f; // 自动装箱:把基本数据类型会自动的变成对应的包装类 // 自动拆箱:把包装类自动的变成其对象的基本数据类型 四.我们以后如何获取包…

02-MybatisPlus批量插入性能够吗?

1 前言 “不要用 mybatis-plus 的批量插入&#xff0c;它其实也是遍历插入&#xff0c;性能很差的”。真的吗&#xff1f;他们的立场如下&#xff1a; 遍历插入&#xff0c;反复创建。这是一个重量级操作&#xff0c;所以性能差。这里不用看源码也知道&#xff0c;因为这个和…

数据结构:手撕代码——顺序表

目录 1.线性表 2.顺序表 2.1顺序表的概念 2.2动态顺序表实现 2.2-1 动态顺序表实现思路 2.2-2 动态顺序表的初始化 2.2-3动态顺序表的插入 检查空间 尾插 头插 中间插入 2.2-4 动态顺序表的删除 尾删 头删 中间删除 2.2. 5 动态顺序表查找与打印、销毁 查找 …

mysql导入sql文件失败及解决措施

1.报错找不到表 1.1 原因 表格创建失败&#xff0c;编码问题mysql8相较于mysql5出现了新的编码集 1.2解决办法&#xff1a; 使用vscode打开sql文件ctrlh&#xff0c;批量替换&#xff0c;替换到你所安装mysql支持的编码集。 2.timestmp没有设置默认值 Error occured at:20…

一个公用的数据状态修改组件

灵感来自于一项重复的工作&#xff0c;下图中&#xff0c;这类禁用启用、审核通过不通过、设计成是什么状态否什么状态的场景很多。每一个都需要单独提供接口。重复工作还蛮大的。于是&#xff0c;基于该组件类捕获组件跳转写了这款通用接口。省时省力。 代码如下&#xff1a;…

Linux开机自启/etc/init.d和/etc/rc.d/rc.local

文章目录 /etc/init.d和/etc/rc.d/rc.local的区别/etc/init.dsystemd介绍 /etc/init.d和/etc/rc.d/rc.local的区别 目的不同&#xff1a; /etc/rc.d/rc.local&#xff1a;用于在系统启动后执行用户自定义命令&#xff0c;适合简单的启动任务。 /etc/init.d&#xff1a;用于管理…

实现一个vue js小算法 选择不同的时间段 不交叉

以上图片选择了时间段 现在需要判断 当前选择的时间段 不能够是 有交叉的所以现在需要循环判断 //判断时间段是否重叠交叉 export function areIntervalsNonOverlapping(intervals:any) {// 辅助函数&#xff1a;将时间字符串转换为从当天午夜开始计算的分钟数function conver…

信息系统架构风格-系统架构师(十

1、信息系统架构风格是描述特定应用领域中系统组织方式的惯用模式。架构风格定义了一个系统家族&#xff0c;即一个架构定义&#xff08;&#xff09;。 A一组设计原则 B一组模式 C一个词汇表和一组约束 D一组最佳实践 解析&#xff1a; 信息系统架构风格是描述某一特定 应…

汽车金属管检测新方法,分度盘高速视觉检测机检测效果如何?

汽车金属管是指在汽车制造和维修中广泛使用的金属管道&#xff0c;用于传输流体、气体或其他介质。汽车金属管在汽车中扮演着重要的角色&#xff0c;用于传输液体&#xff08;如燃油、冷却液、润滑油&#xff09;、气体&#xff08;如空气、排气&#xff09;、制动系统、液压系…

详解函数动态调用的作用——call

动态调用的作用 类似于其他语言的反射能够开发框架性代码 Call调用语法 (bool success, bytes data) <address>.call(bytes calldata)call是address的方法call返回值(bool success, bytes data)忽视返回值success&#xff0c;会造成严重问题 calldata的结构 call的…