【iOS】KVO相关总结

2024-06-15 03:12
文章标签 总结 相关 ios kvo

本文主要是介绍【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

这篇关于【iOS】KVO相关总结的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1062269

相关文章

HarmonyOS学习(七)——UI(五)常用布局总结

自适应布局 1.1、线性布局(LinearLayout) 通过线性容器Row和Column实现线性布局。Column容器内的子组件按照垂直方向排列,Row组件中的子组件按照水平方向排列。 属性说明space通过space参数设置主轴上子组件的间距,达到各子组件在排列上的等间距效果alignItems设置子组件在交叉轴上的对齐方式,且在各类尺寸屏幕上表现一致,其中交叉轴为垂直时,取值为Vert

学习hash总结

2014/1/29/   最近刚开始学hash,名字很陌生,但是hash的思想却很熟悉,以前早就做过此类的题,但是不知道这就是hash思想而已,说白了hash就是一个映射,往往灵活利用数组的下标来实现算法,hash的作用:1、判重;2、统计次数;

sqlite3 相关知识

WAL 模式 VS 回滚模式 特性WAL 模式回滚模式(Rollback Journal)定义使用写前日志来记录变更。使用回滚日志来记录事务的所有修改。特点更高的并发性和性能;支持多读者和单写者。支持安全的事务回滚,但并发性较低。性能写入性能更好,尤其是读多写少的场景。写操作会造成较大的性能开销,尤其是在事务开始时。写入流程数据首先写入 WAL 文件,然后才从 WAL 刷新到主数据库。数据在开始

安卓链接正常显示,ios#符被转义%23导致链接访问404

原因分析: url中含有特殊字符 中文未编码 都有可能导致URL转换失败,所以需要对url编码处理  如下: guard let allowUrl = webUrl.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else {return} 后面发现当url中有#号时,会被误伤转义为%23,导致链接无法访问

git使用的说明总结

Git使用说明 下载安装(下载地址) macOS: Git - Downloading macOS Windows: Git - Downloading Windows Linux/Unix: Git (git-scm.com) 创建新仓库 本地创建新仓库:创建新文件夹,进入文件夹目录,执行指令 git init ,用以创建新的git 克隆仓库 执行指令用以创建一个本地仓库的

二分最大匹配总结

HDU 2444  黑白染色 ,二分图判定 const int maxn = 208 ;vector<int> g[maxn] ;int n ;bool vis[maxn] ;int match[maxn] ;;int color[maxn] ;int setcolor(int u , int c){color[u] = c ;for(vector<int>::iter

整数Hash散列总结

方法:    step1  :线性探测  step2 散列   当 h(k)位置已经存储有元素的时候,依次探查(h(k)+i) mod S, i=1,2,3…,直到找到空的存储单元为止。其中,S为 数组长度。 HDU 1496   a*x1^2+b*x2^2+c*x3^2+d*x4^2=0 。 x在 [-100,100] 解的个数  const int MaxN = 3000

状态dp总结

zoj 3631  N 个数中选若干数和(只能选一次)<=M 的最大值 const int Max_N = 38 ;int a[1<<16] , b[1<<16] , x[Max_N] , e[Max_N] ;void GetNum(int g[] , int n , int s[] , int &m){ int i , j , t ;m = 0 ;for(i = 0 ;

两个月冲刺软考——访问位与修改位的题型(淘汰哪一页);内聚的类型;关于码制的知识点;地址映射的相关内容

1.访问位与修改位的题型(淘汰哪一页) 访问位:为1时表示在内存期间被访问过,为0时表示未被访问;修改位:为1时表示该页面自从被装入内存后被修改过,为0时表示未修改过。 置换页面时,最先置换访问位和修改位为00的,其次是01(没被访问但被修改过)的,之后是10(被访问了但没被修改过),最后是11。 2.内聚的类型 功能内聚:完成一个单一功能,各个部分协同工作,缺一不可。 顺序内聚:

go基础知识归纳总结

无缓冲的 channel 和有缓冲的 channel 的区别? 在 Go 语言中,channel 是用来在 goroutines 之间传递数据的主要机制。它们有两种类型:无缓冲的 channel 和有缓冲的 channel。 无缓冲的 channel 行为:无缓冲的 channel 是一种同步的通信方式,发送和接收必须同时发生。如果一个 goroutine 试图通过无缓冲 channel