iOS KVO详解

KVO 使用

基础使用(三步)

1、添加观察者事件

objc 复制代码
[self.person addObserver:self forKeyPath:@"isImageLoadFinshed" options:(NSKeyValueObservingOptionNew) context:NULL];

context 是标签,用于区分,比如继承关系(student、person),当 keyPath 都相同时,只能通过标签区分,否则就传NULL

2、实现观察者响应方法

objc 复制代码
//object是被监听的对象
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    if ([keyPath isEqualToString:@"isImageLoadFinshed"]) {
        NSLog(@"change = %@", change);
    }
}
  1. dealloc中移除监听,不移除会引发崩溃
objectivec 复制代码
- (void)dealloc{
    [self.person removeObserver:self forKeyPath:@"isImageLoadFinshed"];    
}

高级用法

监听集合变化

在监听集合时比较特殊,在增加或减少数组元素时,以正常的方式addObjectremoveObject无法成功监听,要使用下面这种方式

objc 复制代码
[[self.person mutableArrayValueForKey:@"dataArray"] addObject:@"Hello"];

为什么呢,看 KVO 的回调方法

objc 复制代码
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object 
change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context

其中的 change 参数是传入的变化量,通过在注册时用 options 参数进行的配置,会包含不同的内容,其中NSKeyValueChangeKey其实是NSKeyValueChange枚举,

objectivec 复制代码
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
    NSKeyValueChangeSetting = 1,//属性的值被重新设置
    NSKeyValueChangeInsertion = 2,//表示更改的是集合属性,代表插入操作。
    NSKeyValueChangeRemoval = 3,//表示更改的是集合属性,代表删除操作。
    NSKeyValueChangeReplacement = 4,//表示更改的是集合属性,代表替换操作。
};

我们大部分数据类型可以通过setter方法监听变化,即值变化的类型是NSKeyValueChangeSetting,而数组等集合类型不是这样的,addObject并不会调用setter方法,因此需要通过 KVC 的mutableArrayValueForKey:获取到数组的代理对象,再进行增删改,才可以监听到数组内容的变化。

猜测mutableArrayValueForKey底层是先获取到数组,进行增删改,然后再把修改完的数组 set 回去,触发的 KVO 监听。

联动观察

只要监听一个属性值,就可以联动观察其他属性值的变化

场景一:

比如有个Student类如下:

less 复制代码
@interface Student : NSObject

@property (nonatomic , assign) NSInteger englishScore; // 英语分数
@property (nonatomic , assign) NSInteger mathScore; // 数学分数
@property (nonatomic , assign) NSInteger chineseScore; // 语文分数

@end

一个学生的总分=英语分数+数学分数+语文分数,三门学科中任意一个分数发生变化总分都会跟着变。如果有个界面只需要展示总分,我们可以通过KVO监听englishScoremathScorechineseScore这三个属性的变化来实现我们的需求。那如果有 10 门学科,那就得监听 10 门学科分数的变化,这样太麻烦了,那可不可以只监听totalScore这一关属性就达到相同的效果呢?当然是可以的,这里就需要用到联动观察

首先给Student类增加个totalScore属性,重写keyPathsForValuesAffectingValueForKey方法来设置与这个属性相关联的属性(也就是哪些属性值的改变会影响totalScore的改变),为了方便使用,这里也重写它的getter方法,代码如下

objc 复制代码
#import "Student.h"

@implementation Student

// 重写getter方法,也就是如何通过其他属性来计算totalScore的值
- (NSInteger)totalScore{
    return _mathScore + _englishScore + _chineseScore;
}

// 设置关联属性(重写keyPathsForValuesAffectingValueForKey方法)
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{

    NSMutableSet *set = [[NSMutableSet alloc] initWithSet:[super keyPathsForValuesAffectingValueForKey:key]];
    if ([@"totalScore" isEqualToString:key]) {
        // affectKeys是所有关联属性的属性名
        NSArray *affectKeys = @[@"englishScore",@"mathScore",@"chineseScore"];
        [set addObjectsFromArray:affectKeys];
    }
    return set;
}

@end

场景二:

Stedent类有nameage两个属性,School类有个Student类型的属性,现在要实现Stedent类的任意一个属性的改变都会触发KVO,那我们可以通过KVO同时监听Student实例对象的2个属性变化,这和上面一样,当Student类的属性有很多时就会比较麻烦,因为要同时监听很多属性的变化。其实可以和上面一样,设置好相关的 keypath 后,我们只需要监听School类中的student这一个属性就可以了。代码如下:

objc 复制代码
// Student.h
@interface Student : NSObject

@property (nonatomic , copy) NSString *name; // 姓名
@property (nonatomic , assign) NSInteger age; // 年龄

@end

// Student.m
#import "Student.h"
@implementation Student

@end

// School.h
#import "Student.h"

@interface School : NSObject

@property (nonatomic , strong) Student *student;

@end

// School.m
#import "School.h"

@implementation School
// 关键就是这里设置相关的keyPath
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
    NSMutableSet *set = [[NSMutableSet alloc] initWithSet:[super keyPathsForValuesAffectingValueForKey:key]];
    if ([@"student" isEqualToString:key]) {
        NSArray *affectKeys = @[@"_student.name",@"_student.age"];
        [set addObjectsFromArray:affectKeys];
    }
    return set;
}

@end

禁止监听

禁止目标类所有属性的监听,就把automaticallyNotifiesObserversOfSteps返回NO

objc 复制代码
@implementation LGPerson

+ (BOOL)automaticallyNotifiesObserversOfSteps {
    return NO;
}

@end

禁止目标类某个属性的监听,下面代码意思是禁止外部对 name 属性的监听,不影响其他属性的监听

objc 复制代码
@implementation LGPerson

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    if ([key isEqualToString:@"name"]) {
        return NO;
    }
    return YES;
}

@end

手动观察

手动观察的前提是属性被禁止监听,需要用到两个方法willChangeValueForKey:didChangeValueForKey:

objectivec 复制代码
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    if ([key isEqualToString:@"name"]) {
        return NO;
    }
    return YES;
}

- (void)setName:(NSString *)name{
    [self willChangeValueForKey:@"name"];
    _name = name;
    [self didChangeValueForKey:@"name"];
}

KVO底层原理

KVO 的底层实现大致可以描述如下:

  1. 动态创建子类 NSKVONotifying_XXX,
  2. 创建 class 方法,class 指向原类,为了隐藏这个子类
  3. 创建 setter 方法,内部把消息转发给父类,并触发回调 observeValueForKeyPath
  4. 创建 dealloc 方法
  5. 创建 isKVOA
  6. 处理 removeObserver 操作
    1. isa 指回父类
    2. 不会移除这个子类,即把这个子类缓存下来,为防止下次监听时再次创建耗费性能,这里用空间换时间

FBKVOController源码解析

FBKVOController优点

  1. 写法简洁:增加了 block 回调、自定义回调或NSKeyValueObserving回调通知
  2. 不需要手动 removeObserver(系统 KVO 如果 remove 2 次会 crash),会在观察者被释放时自动释放

架构设计

  • FBKVOController 持有 NSMapTable,以 objectkey 得到相对应的 NSMutableSetNSMutableSet 中存储了不同的 _FBKVOInfo。这套数据结构的主要作用是防止开发人员重复添加相同的 KVO。当检查到其中已存在相同的 _FBKVOInfo 对象时,不再执行后面的代码。
  • _FBKVOSharedController 持有 NSHashTableNSHashTable 以弱引用的方式持有不同的 _FBKVOInfo。此处实际执行 KVO 代码。_FBKVOInfo 有一个重要的成员变量 _FBKVOInfoState,根据这个枚举值(_FBKVOInfoStateInitial_FBKVOInfoStateObserving_FBKVOInfoStateNotObserving) 来决定新增或者删除 KVO

被监听对象和属性的关系

使用 NSMapTable 保存了被监听对象 object 和 FBKVOInfo 对象集合的关系。

源码学习收获

1 中介者模式

中介者模式(Mediator Pattern)是用来降低多个对象和类之间的通信复杂性。这种模式提供了一个中介类,该类通常处理不同类之间的通信,并支持松耦合,使代码易于维护。

比如我们平时买房、租房都会找中介,通过中介可以更快更高效的找到合适的房子,很多事情中介帮我们去做了,不用我们自己去找房源,如图

中介者模式 4 种角色:

  • 抽象中介者(Mediator)角色: 抽象中介者角色定义统一的接口,以及一个或者多个事件方法,用于各同事角色之间的通信。
  • 具体中介者(ConcreteMediator)角色: 实现了抽象中介者所声明的事件方法,协调各同事类之间的行为,持有所有同事类对象的引用。
  • 抽象同事(Colleague)角色: 定义了抽象同事类,持有抽象中介者对象的引用。
  • 具体同事(ConcreteColleague)角色: 继承抽象同事类,实现自己业务,通过中介者跟其他同事类进行通信。

举个例子:有 ViewController1、ViewController2 要监听 Person1(name、age)、Person2(name、age),使用FBKVOController后,各自扮演的角色

  • FBKVOControllerFBKVOSharedController是中介类,可以理解前者为抽象中介者,定义了所有的接口方法(observe、unObserve...);后者为具体中介者,实现了抽象中介者声明的具体方法,有序的交给系统 KVO 处理监听任务;
  • ViewController1ViewController2是同事类,它们通过中介者FBKVOController监听某对象的某属性,由系统 KVO 处理完后,再经过中介者把回调传递给自己。

2 NSMapTable和NSHashTable

NSMapTable

NSMapTableNSMutableDictionary的高阶类型,可以给 key 和 value 设置NSPointerFunctionsOptions,传统字典就相当于把它的 keyOptions 设成这样:

objc 复制代码
NSPointerFunctionsStrongMemory  | 
NSPointerFunctionsObjectPersonality|
NSPointerFunctionsCopyIn;

需要注意的是NSPointerFunctionsCopyIn,老字典会对 key 进行 copy,value 不会,但是如果大家平日里都使用NSString作为 key,那大可不必考虑 copy 的性能损耗(因为只是浅拷贝)。但如果使用的是NSMutableString或者一些进行深拷贝的类型,那就另当别论了。

再把它的 valueOptions 设成这样:

objc 复制代码
NSPointerFunctionsStrongMemory | NSPointerFunctionsObjectPersonality

NSMapTable与老字典的性能不能一概而论,因为他们的主要性能差别也是来自于NSPointerFunctionsCopyInNSPointerFunctionsWeakMemory。后者会带来一定的性能损耗,而前者要看 key 的NSCopying协议是如何实现的。

NSHashTable

NSHashTableNSMutableSet的高阶类型,与NSPointerArrayNSMapTable一样,可以指定NSPointerFunctionsOptions,只有可变版本,允许对添加到容器中的对象是弱引用的持有关系, 当NSHashTable 中的对象销毁时,该对象也会从容器中移除。

但是添加对象的时候前者耗费的时间是后者 2 倍,所以平时能用NSSet就用NSSet

3 锁

4 == 和 isEqual 的关系

  • ==对于基本类型,比较的是值;对于对象类型, 比较的是对象的地址(即是否为同一对象)。
  • isEqual比较的是两个对象是否相同,对于系统对象比如 UIColor,系统内部已经实现了 isEqual方法,举例如下:
objc 复制代码
UIColor *color1 = [UIColor colorWithRed:0.5 green:0.5 blue:0.5 alpha:1.0];
UIColor *color2 = [UIColor colorWithRed:0.5 green:0.5 blue:0.5 alpha:1.0];
NSLog(@"color1 == color2 = %@", color1 == color2 ? @"YES" : @"NO");
NSLog(@"[color1 isEqual:color2] = %@", [color1 isEqual:color2] ? @"YES" : @"NO");

结果如下

ini 复制代码
color1 == color2 = NO
[color1 isEqual:color2] = YES

这个例子表示,UIColor 对象的isEqual方法实现的是两个对象的 color 是否相同,实际上这是 2 个不同的对象,从==结果可以看出来。

5 isEqual和hash关系

1、FBKVOInfo 为什么重写 isEqual 方法和 hash?

这个自定义对象会在 FBKVOSharedController 中被添加到 NSHashTable,hash table 会根据 hash 值优化索引效率,基于 hash 值索引的 Hash Table(NSSet、NSDictionry 也是) 查找某个成员的过程是:

  1. 通过 hash 值直接找到查找目标的位置
  2. 如果目标位置上有多个相同 hash 值得成员,此时再按照数组方式遍历判等(isEqual)

2、hash 什么时候会被调用?

NSSet 添加新成员时, 需要根据 hash 值来快速查找成员, 以保证集合中是否已经存在该成员,NSDictionary 在查找 key 时, 也利用了 key 的 hash 值来提高查找的效率。

3、如何重写 hash 方法?

对关键属性的位或运算作为 hash 值,参考如下:

objc 复制代码
- (NSUInteger)hash {
    return [self.name hash] ^ [self.age hash];
}
相关推荐
B.-4 小时前
Flutter 应用在真机上调试的流程
android·flutter·ios·xcode·android-studio
iFlyCai14 小时前
Xcode 16 pod init失败的解决方案
ios·xcode·swift
郝晨妤1 天前
HarmonyOS和OpenHarmony区别是什么?鸿蒙和安卓IOS的区别是什么?
android·ios·harmonyos·鸿蒙
Hgc558886661 天前
iOS 18.1,未公开的新功能
ios
CocoaKier1 天前
苹果商店下载链接如何获取
ios·apple
zhlx28351 天前
【免越狱】iOS砸壳 可下载AppStore任意版本 旧版本IPA下载
macos·ios·cocoa
XZHOUMIN2 天前
网易博客旧文----编译用于IOS的zlib版本
ios
爱吃香菇的小白菜2 天前
H5跳转App 判断App是否安装
前端·ios
二流小码农2 天前
鸿蒙开发:ForEach中为什么键值生成函数很重要
android·ios·harmonyos
hxx2212 天前
iOS swift开发--- 加载PDF文件并显示内容
ios·pdf·swift