018*:kvo:(context、kvo合规【kvc基础上】)(isa-swizzling kvo派生类 NSKVONotifying_本类名)(重写setter、class、dealloc、_isKVOA)(重写set【手动开启通知、修改isa】、class、malloc【修改isa】)

问题

 

目录

 

预备

1:测试代码:(监听person对象的name属性的新值

// HTPerson
@interface HTPerson : NSObject
@property (nonatomic, copy) NSString *name;
@end

@implementation HTPerson
@end

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

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    self.person = [HTPerson new];
    self.person.name = @"ht";
    
    // 1. 添加
    [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context: NULL];
}

// 2. 监听
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString: @"name"]) {
        NSLog(@"新值:%@", change[NSKeyValueChangeNewKey]);
    }
}

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
   self.person.name = [NSString stringWithFormat:@"%@ +",self.person.name];
}

-(void)dealloc {
    // 3. 移除
    [self.person removeObserver:self forKeyPath:@"name" context: NULL];
}

@end

正文

一.KVO 是什么?

1:KVO,全称为Key-Value observing,中文名为键值观察

2:键值观察是一种机制,允许对象其他对象指定属性发生更改得到通知

  在Key-Value Observing Programming Guide官方文档中,又这么一句话:理解KVO之前,必须先理解KVC(即KVO是基于KVC基础之上)

3:KVC是键值编码,在对象创建完成后,可以动态的给对象属性赋值

  而KVO是键值观察,提供了一种监听机制,当指定的对象的属性被修改后,则对象会收到通知,所以可以看出KVO是基于KVC的基础上对属性动态变化的监听

4:在iOS日常开发中,经常使用KVO来监听对象属性的变化,并及时做出响应,即当指定的被观察的对象的属性被修改后,KVO会自动通知相应的观察者,那么KVONSNotificatioCenter有什么区别呢?

  • 相同点
    • 1、两者的实现原理都是观察者模式,都是用于监听

    • 2、都能实现一对多的操作

  • 不同点
    • 1、KVO只能用于监听对象属性的变化,并且属性名都是通过NSString来查找,编译器不会帮你检测对错和补全,纯手敲会比较容易出错

    • 2、NSNotification发送监听(post)的操作我们可以控制,kvo系统控制。

    • 3、KVO可以记录新旧值变化

二:KVO和 NSNotificationCenter的相同点和不同点

KVO和 NSNotificationCenter一样,都是 iOS观察者模式的一种实现。不同的是 KVO一对一的,且一般情况下对被监听的对象无侵入性,不需要被监听对象修改代码。
  • 以 KVO为模型慢慢衍生出来响应式编程的思想。
  • KVO 可以监听普通类型属性,也可以监听集合类型( NSArrayNSSet)的属性。
  • KVO默认自动开启监听,也可以手动开启。
  • 注册与移除监听需要成对儿出现
    1. 如果重复移除监听,会报 NSRangeException 异常。
    2. 如果不移除,观察者释放后,再次发送 KVO 消息给它时,会报野指针的异常。
  • 苹果官方推荐的方式是,在init的时候进行addObserver,在deallocremoveObserver,这样可以保证addremove是成对出现的,是一种比较理想的使用方式。

三:KVO 怎么用?

1:KVO 使用三部曲

1.1:init 中添加监听,  addObserver:forKeyPath:options:context:, keyPath 为被监听对象的属性(不能是成员变量),观察者可以收到属性变化的通知。

[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];

1.2:实现KVO回调,observeValueForKeyPath:ofObject:change:context:,当被监听的属性改变时,会回调这个方法。

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    if ([keyPath isEqualToString:@"name"]) {
        NSLog(@"%@",change);
    }
}

1.3:delloc中移除观察者,removeObserver:forKeyPath:

-(void)dealloc {
    [self.person removeObserver:self forKeyPath:@"name" context: NULL];
}

2: 下面是一般情况下使用KVO的示例:

static void *PersonNameContext = &PersonNameContext;

/* 1: 注册观察者
self.person: 被观察对象
Observer: 观察者(self)
KeyPath: 被观察对象的属性(name)、不能是成员变量
options: 观察的值的类型(NSKeyValueObservingOptionNew)
context:上下文,用来区分当KeyPath相同时,哪个keyPath是我们要观察的。当不需要区分时,传NULL,需要区分时,传入已定义的context, 例如:PersonNameContext
*/
/**
options 取值: 
•    NSKeyValueObservingOptionNew : 只接收新值
•    NSKeyValueObservingOptionOld :只接收旧值
•    NSKeyValueObservingOptionInitial : 在注册观察者后,立即接收一次回调
•    使用 |  将值相连,表示接收多种值。
*/
- (instancetype)init
{
    self = [super init];
    if (self) {
        [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context: PersonNameContext];
    }
    return self;
}

/*
2. 重写监听方法
change.new  表示新值
change.old  表示旧值
change.kind 表示值改变的方式的类型
•     NSKeyValueChangeSetting = 1,            // 普通类型属性的赋值
•     NSKeyValueChangeInsertion = 2,          // 集合类型属性的操作方式 - 添加
•     NSKeyValueChangeRemoval = 3,            // 集合类型属性的操作方式 - 移除
•     NSKeyValueChangeReplacement = 4,        // 集合类型属性的操作方式 - 替换
*/ 
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    // 改变的相关值
   if (context == PersonNameContext) {
        NSLog(@"%@",change);
    }
}

/// 3. 在观察者销毁前,移除监听
- (void)dealloc{
    [self.person removeObserver:self forKeyPath:@"name"];
}

/// 4. 手动修改值
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.person->name = [NSString stringWithFormat:@"%@+",self.person->name];
}

2.1:添加观察者

  • addObserver 添加操作中,addObserver是监听对象KeyPath是监听路径option是监听类型,是一个枚举,包含:
typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
    NSKeyValueObservingOptionNew     // 新值
    NSKeyValueObservingOptionOld     // 旧值
    NSKeyValueObservingOptionInitial // 初始值 
    NSKeyValueObservingOptionPrior   // 变化前
};

2.2:context的使用

大致含义就是:addObserver:forKeyPath:options:context:方法中的上下文context指针包含任意数据,这些数据将在相应的更改通知中传递回观察者。可以通过指定context为NULL,从而依靠keyPath键路径字符串传来确定更改通知的来源,但是这种方法可能会导致对象的父类由于不同的原因也观察到相同的键路径而导致问题。所以可以为每个观察到的keyPath创建一个不同的context,从而完全不需要进行字符串比较,从而可以更有效地进行通知解析

通俗的讲,context上下文主要是用于区分不同对象的同名属性,从而在KVO回调方法中可以直接使用context进行区分,可以大大提升性能,以及代码的可读性

  • 不使用context,使用keyPath区分通知来源
//context的类型是 nullable void *,应该是NULL,而不是nil
[self.person addObserver:self forKeyPath:@"nick" options:NSKeyValueObservingOptionNew context:NULL];
  • 使用context区分通知来源
//定义context
static void *PersonNickContext = &PersonNickContext;
static void *PersonNameContext = &PersonNameContext;

//注册观察者
[self.person addObserver:self forKeyPath:@"nick" options:NSKeyValueObservingOptionNew context:PersonNickContext];
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:PersonNameContext];
    
    
//KVO回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    if (context == PersonNickContext) {
        NSLog(@"%@",change);
    }else if (context == PersonNameContext){
        NSLog(@"%@",change);
    }
}

2.3、移除KVO通知的必要性

删除观察者时,请记住以下几点:

  • 要求被移除为观察者(如果尚未注册为观察者)会导致NSRangeException。您可以对removeObserver:forKeyPath:context:进行一次调用,以对应对addObserver:forKeyPath:options:context:的调用,或者,如果在您的应用中不可行,则将removeObserver:forKeyPath:context:调用在try / catch块内处理潜在的异常。

  • 释放后,观察者不会自动将其自身移除。被观察对象继续发送通知,而忽略了观察者的状态。但是,与发送到已释放对象的任何其他消息一样,更改通知会触发内存访问异常。因此,您可以确保观察者在从内存中消失之前将自己删除

  • 该协议无法询问对象是观察者还是被观察者。构造代码以避免发布相关的错误。一种典型的模式是在观察者初始化期间(例如,在init或viewDidLoad中)注册为观察者,并在释放过程中(通常在dealloc中)注销,以确保成对和有序地添加和删除消息,并确保观察者在注册之前被取消注册,从内存中释放出来

所以,总的来说,KVO注册观察者 和移除观察者是需要成对出现的,如果只注册,不移除,会出现类似野指针的崩溃,如下图所示

崩溃的原因是,由于第一次注册KVO观察者后没有移除,再次进入界面,会导致第二次注册KVO观察者,导致KVO观察的重复注册,而且第一次的通知对象还在内存中,没有进行释放,此时接收到属性值变化的通知,会出现找不到原有的通知对象,只能找到现有的通知对象,即第二次KVO注册的观察者,所以导致了类似野指针的崩溃,即一直保持着一个野通知,且一直在监听

注:这里的崩溃案例是通过单例对象实现(崩溃有很大的几率,不是每次必现),因为单例对象在内存是常驻的,针对一般的类对象,貌似不移除也是可以的,但是为了防止线上意外,建议还是移除比较好

2.4:KVO的自动触发与手动触发
KVO观察的开启和关闭有两种方式,自动手动

使用手动开关的好处就是你想监听就监听,不想监听关闭即可,比自动触发更方便灵活

注意点:

  • 注册完KVO的监听后,监听是默认自动开启的。
  • 当我们有控制某些监听生效,某些监听不生效时的需求时,便可使用手动的方式控制监听是否开启
  • 手动控制属性监听的方法,需要更改被观察的类中的代码, 观察者不需要更改
    • 方法1:关闭自动开关, 重写 setNick 方法,手动触发值改变的监听
    • 方法2:在自动开启监听的开关位置处增加判断,指定某些属性可以触发监听

1. 方法1: 关闭自动开关, 重写 setNick 方法,手动触发值改变的监听

@interface Person : NSObject
@property (nonatomic, copy) NSString *nick;
@end

@implementation Person

/// 方法 1 第一步:关闭自动开关
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
    return NO;
}
/// 方法 1 第二步:重写setNick方法,手动触发值改变的监听
- (void)setNick:(NSString *)nick{
    [self willChangeValueForKey:@"nick"];
    _nick = nick;
    // 底层会触发值改变的回调
    [self didChangeValueForKey:@"nick"];
}
@end

2. 方法2:在自动开启监听的开关位置处增加判断,指定某些属性可以触发监听

@interface Person : NSObject
@property (nonatomic, copy) NSString *nick;
@end

@implementation Person

/// 方法 2 :自动开关设置处,增加属性判断
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
    if ([key isEqualToString:@"nick"]) {
        return NO;
    }
    return YES;
}
@end

四:不能监听的情况 

KVO合规 

为了让指定的属性的符合KVO,类必须确保以下各项:

  • 类的属性必须是遵循KVC的。KVO支持的数据类型与KVC一样。
  • 该类为该属性发出KVO更改通知。
  • 依赖的键值已正确注册

1: 不能监听成员变量

注意:KVO只能监听属性,不能监听成员变量,会不能走值改变的回调

1. 定义一个 Person类,包含一个 name的成员变量。

@interface Person : NSObject {
    // 默认成员变量是protect的
    @public
    NSString *name;
}
@end

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

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    self.person  = [Person new];
    
    [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL];
}

// ... 监听、移除代码、及点击触发值的改变方法均与上面"一般情况下使用KVO"代码一样,就省略了

@end

实测结果:值改变的时候,不能走回调方法!!!

2 :数组类型属性的监听

注意点:

  • [self.person.dateArray addObject:@(1)]  使用这个方法,直接改变数组里面的内容,是不能触发监听事件的回调的。
  • 需要使用 mutableArrayValueForKey:,来获取代理对象,代理对象的内容发生改变时,可以触发回调。KVO的底层也使用了KVC,这与KVO中修改数组属性时, 需要使用mutableArrayValueForKey:类似(TODO!!!!)

2.1. 在 Person类中,增加一个数组类型的 dateArray属性

@interface Person : NSObject
@property (nonatomic, strong) NSMutableArray *dateArray;
@end

2.2. 在 ViewController 中,改变观察的数组内容

// ... 注册、监听、移除代码均与上面"一般情况下使用KVO"代码一样,就省略了

/// 4. 手动修改值
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    /* 向数组里面增加值
    不能使用[self.person.dateArray addObject:@(1)];
    */
    [[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"1"];
}

2.3 关联监听多个属性

注意点:

  • 假如有下载进度、已下载数量、总数量 3 个属性。因为下载进度 = 已下载数量 / 总数量,所以当想观察下载进度属性的时候,其实也是需要关联观察影响下载进度的已下载数量及总数量的。
  • 需要在被监听的类中实现这个方法 keyPathsForValuesAffectingValueForKey: 来关联监听影响下载进度的多个属性. 当关联属性改变时,会触发底层调用观察属性的getter方法,通过在被监听的类中重写属性的getter 方法, 来实现下载进度 = 已下载数量 / 总数量的逻辑。

2.3.1. 在 Person类中,增加 downloadProgress、 writtenData、totalData三个属性

@interface Person : NSObject
@property (nonatomic, copy) NSString *downloadProgress;
@property (nonatomic, assign) double writtenData;
@property (nonatomic, assign) double totalData;
@end

@implementation Person
// 下载进度 -- writtenData/totalData
// 因为"totalData", @"writtenData"的值的改变,会影响到下载进度,通过这个方法,可以关联监听这两个值
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    if ([key isEqualToString:@"downloadProgress"]) {
        NSArray *affectingKeys = @[@"totalData", @"writtenData"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}

// 重写 downloadProgress 的 getter 方法
- (NSString *)downloadProgress{
    if (self.writtenData == 0) {
        self.writtenData = 10;
    }
    if (self.totalData == 0) {
        self.totalData = 100;
    }
    // 下载进度 = 已下载数量 / 总数量
    return [[NSString alloc] initWithFormat:@"%f",1.0f*self.writtenData/self.totalData];
}
@end

2.3.2. 在 ViewController 中,点击事件改变writtenData、totalData

// ... 注册、监听、移除代码均与上面"一般情况下使用KVO"代码一样,就省略了

/// 4. 手动修改值
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    // 修改 writtenData、totalData 来改变downloadProgress
    self.person.writtenData += 10;
    self.person.totalData  += 1;
}

五:KVO 底层是怎么做到的?kvo派生类

 官方文档:

  • 1.KVO是使用一种isa-swizzling的技术实现的。(isa交换 派生类)
  • 2.isa指针,顾名思义是指向维护分配表的对象的类,这个分配表本质上包含了指向类实现的方法和其它数据的指针。
  • 3.当观察者为对象的属性注册时,被观察者的isa指针将被修改指向中间类而不是真正的类。因此isa指针的值不一定反应实例的实际类。
  • 4.所以你不应该依赖isa指针来确定成员关系,相反,你应该使用类方法来确定实例对象的类。
  • 5.释放的时候修改isa指向,指向原来的类。

通过上面的文档我们知道KVO是通过isa-swizzling来实现的,而在对对象属性注册时会生成中间类,并将isa指针指向中间类

1.注册时做了什么?

我们在注册监听代码的前后,添加断点:

打印观察者 self.person的注册前后的isa指向的类型名字。

发现在注册监听后,类型的名字变成了NSKVONotifying_Person

说明在注册的同时,创建了一个 "NSKVONotifying_" + "本类名"新的类,并将本类的 isa指针指向了新的类。本类名前面加前缀,很有可能是本类的子类,接下来我们打印出 Person的子类列表,来验证一下。

1.1 验证NSKVONotifying_Person是否为派生类

查找Person 类的子类:

- (void)printSubClasses:(Class)cls{
    
    // 注册类的总数
    int count = objc_getClassList(NULL, 0);
    // 创建一个数组, 其中包含给定对象
    NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls];
    // 获取所有已注册的类
    Class* classes = (Class*)malloc(sizeof(Class)*count);
    objc_getClassList(classes, count);
    for (int i = 0; i<count; i++) {
        // 查找父类为当前类的
        if (cls == class_getSuperclass(classes[i])) {
            [mArray addObject:classes[i]];
        }
    }
    free(classes);
    NSLog(@"classes = %@", mArray);
}

打印结果如下:

 从结果中可以说明NSKVONotifying_LGPersonLGPerson的子类

1.2 查看派生类中的所有方法

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self printClassAllMethod:objc_getClass("NSKVONotifying_Person")];
    [self printClassAllMethod:[Student class]];
}

- (void)printSubClasses:(Class)cls{
    
    // 注册类的总数
    int count = objc_getClassList(NULL, 0);
    // 创建一个数组, 其中包含给定对象
    NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls];
    // 获取所有已注册的类
    Class* classes = (Class*)malloc(sizeof(Class)*count);
    objc_getClassList(classes, count);
    for (int i = 0; i<count; i++) {
        // 查找父类为当前类的
        if (cls == class_getSuperclass(classes[i])) {
            [mArray addObject:classes[i]];
        }
    }
    free(classes);
    NSLog(@"classes = %@", mArray);
}

 从结果中可以看出有四个方法,分别是setNickName 、 class 、 dealloc 、 _isKVOA,这些方法是继承还是重写

之前我们自定义的 Student 一个方法都没有,从打印结果上看, Student的确一个方法都没有。而NSKVONotifying_Person有4个方法 。说明这四个方法均是 NSKVONotifying_Person 自己的,不是从父类继承过来的。

  • setNickName、class、dealloc是重写父类的方法
  • _isKVOA:是标识是否为KVO,属于派生类自有的方法

2. 值改变时底层做了什么?

2.1 setNickName:

我们下符号断点setNickName:,看汇编堆栈信息,在改变 nickName的时候,断点会停在父类的setNickName:方法中:

再点击看下堆栈信息的第2行:

 

从堆栈信息中可以看出,在值改变的时候,系统的调用顺序如下:

  • 先调用了NSKeyValueWillChange
  • 然后调用了父类的setter方法。调用了[HTPerson setName]方法,完成了给父类HTPersonname属性赋值。(此时的willChange和didChange方法是继承自NSObject的)
  • 然后调用NSKeyValueDidChange。

这里的NSKeyValueWillChangeNSKeyValueDidChange正好与前面手动开启KVO监听的方法相呼应:

[self willChangeValueForKey:@"nick"];
_nick = nick;
[self didChangeValueForKey:@"nick"];    

可以猜测,系统底层是通过NSKeyValueWillChangeNSKeyValueDidChange来向观察者发起消息通知。

3. 移除监听时做了什么?

3.1 在移除监听的时候,isa是否指回本类?

从打印结果中,可以看出移除监听之前,还是NSKVONotifying_Person类型,移除后,对象的isa指针指回了Person。

3.2 系统生成的派生类NSKVONotifying_Person是否被销毁?

我们再打印一下当前对象的所有子类:

可见NSKVONotifying_Person还是存在的,并没有因为被移除监听而销毁。

所以派生类一旦被注册到内存中,便会一直存在。

  • 发现NSKVONotifying_HTPerson在外部removeObserver时,完成的移除操作。isa指回了原类

  • 但是我们在removeObserver移除操作之后,打印HTPerosn类和子类的信息,发现NSKVONotifying_HTPerson派生类并没有移除

        ps: 页面销毁之后再打印HTPerosn类和子类,也一样存在NSKVONotifying_HTPerson派生类。

  • KVO派生类只要生成,就会一直存在,这样可以减少频繁的添加操作。考虑到重用问题(KVO再次注册),中间一旦注册到内存中去,就不会销毁

总结

至此,我们已经知道KVO是创建派生类实现了键值观察

  • 实例对象在注册KVO观察者之后,isa指针由原有类更改为指向派生类
  • 中间类重写了观察属性的setter方法、classdealloc_isKVOA方法,(此时开始,所有调用本类的方法,都是调用的派生类派生类没有的方法,就会沿着继承链查询到本类

    重写了被监听属性的setter方法,在派生类setter方法触发时:在willChange之后,didChange之前,调用父类属性settter方法,完成父类属性的赋值`。

  • dealloc方法中,移除KVO观察者之后,实例对象isa指向由中间类更改为原有类,中间类从创建后,就一直存在内存中,不会被销毁
  • 假象: 之所以外部打印class永远看不到派生类,是因为派生类class方法重写了,故意不让外界看到。

六:KVO的不足

KVO很强大,没错。知道它内部实现,或许能帮助更好地使用它,或在它出错时更方便调试。

但是不足的地方也有,比如你只能通过重写 -observeValueForKeyPath:ofObject:change:context:方法来获得通知。想要提供自定义的 selector,不行;想要传一个 block,门都没有。而且你还要处理父类的情况 - 父类同样监听同一个对象的同一个属性。但有时候,你不知道父类是不是对这个消息有兴趣。虽然 context这个参数就是干这个的,也可以解决这个问题 - 在 -addObserver:forKeyPath:options:context:传进去一个父类不知道的 context。但总觉得框在这个 API 的设计下,代码写的很别扭。至少至少,也应该支持 block 吧。 

有不少人都觉得官方 KVO 不好使的。Mike Ash 的 Key-Value Observing Done Right,以及获得不少分享讨论的 KVO Considered Harmful都把 KVO拿出来吊打了一番。所以在实际开发中 KVO使用的情景并不多,更多时候还是用 Delegate或 NotificationCenter

七:自定义kvo

知道了原理,我们可以尝试自定义一个KVO,同时我们还能自定义的KVO里做一些优化。现在注册监听、值改变的回调、移除监听是分开的三段代码,在开发中,代码分离不太符合高内聚的编程思想。我们可以:

  • 将注册、值改变的回调代码写在一起
  • 同时在观察者销毁时自动移除监听。

1. 自定义KVO思路

 

1:注册观察者

1、判断当前观察值keyPath的setter方法是否存在

#pragma mark - 验证是否存在setter方法
- (void)judgeSetterMethodFromKeyPath:(NSString *)keyPath
{
    Class superClass = object_getClass(self);
    SEL setterSelector = NSSelectorFromString(setterForGetter(keyPath));
    Method setterMethod = class_getInstanceMethod(superClass, setterSelector);
    if (!setterMethod) {
        @throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"CJLKVO - 没有当前%@的setter方法", keyPath] userInfo:nil];
    }
    
}

2:动态生成子类,将需要重写的class方法添加到中间类中

#pragma mark - 动态生成子类
- (Class)createChildClassWithKeyPath:(NSString *)keyPath
{
    //获取原本的类名
    NSString  *oldClassName = NSStringFromClass([self class]);
    //拼接新的类名
    NSString *newClassName = [NSString stringWithFormat:@"%@%@",kCJLKVOPrefix,oldClassName];
    //获取新类
    Class newClass = NSClassFromString(newClassName);
    //如果子类存在,则直接返回
    if (newClass) return newClass;
    //2.1 申请类
    newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
    //2.2 注册
    objc_registerClassPair(newClass);
    //2.3 添加方法
    
    SEL classSel = @selector(class);
    Method classMethod = class_getInstanceMethod([self class], classSel);
    const char *classType = method_getTypeEncoding(classMethod);
    class_addMethod(newClass, classSel, (IMP)cjl_class, classType);

    return newClass;
}

//*********class方法*********
#pragma mark - 重写class方法,为了与系统类对外保持一致
Class cjl_class(id self, SEL _cmd){
    //在外界调用class返回CJLPerson类
    return class_getSuperclass(object_getClass(self));//通过[self class]获取会造成死循环
}

3:isa指向由原有类,改为指向中间类

object_setClass(self, newClass);

4:保存信息:这里用的数组,也可以使用map,需要创建信息的model模型类

//*********KVO信息的模型类/*********
#pragma mark 信息model类
@interface CJLKVOInfo : NSObject

@property(nonatomic, weak) NSObject *observer;
@property(nonatomic, copy) NSString *keyPath;
@property(nonatomic, copy) LGKVOBlock handleBlock;

- (instancetype)initWithObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath handleBlock:(LGKVOBlock)block;

@end
@implementation CJLKVOInfo

- (instancetype)initWithObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath handleBlock:(LGKVOBlock)block{
    if (self = [super init]) {
        _observer = observer;
        _keyPath = keyPath;
        _handleBlock = block;
    }
    return self;  
}
@end

//*********保存信息*********
//- 保存多个信息
CJLKVOInfo *info = [[CJLKVOInfo alloc] initWithObserver:observer forKeyPath:keyPath handleBlock:block];
//使用数组存储 -- 也可以使用map
NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kCJLKVOAssociateKey));
if (!mArray) {//如果mArray不存在,则重新创建
    mArray = [NSMutableArray arrayWithCapacity:1];
    objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kCJLKVOAssociateKey), mArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
[mArray addObject:info];

完整的注册观察者代码如下

#pragma mark - 注册观察者 - 函数式编程
- (void)cjl_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath handleBlock:(LGKVOBlock)block{
    
    //1、验证是否存在setter方法
    [self judgeSetterMethodFromKeyPath:keyPath];
    
    //保存信息
    //- 保存多个信息
    CJLKVOInfo *info = [[CJLKVOInfo alloc] initWithObserver:observer forKeyPath:keyPath handleBlock:block];
    //使用数组存储 -- 也可以使用map
    NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kCJLKVOAssociateKey));
    if (!mArray) {//如果mArray不存在,则重新创建
        mArray = [NSMutableArray arrayWithCapacity:1];
        objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kCJLKVOAssociateKey), mArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    [mArray addObject:info];
    
    //判断automaticallyNotifiesObserversForKey方法返回的布尔值
    BOOL isAutomatically = [self cjl_performSelectorWithMethodName:@"automaticallyNotifiesObserversForKey:" keyPath:keyPath];
    if (!isAutomatically) return;
    
    //2、动态生成子类、
    /*
        2.1 申请类
        2.2 注册
        2.3 添加方法
     */
    Class newClass = [self createChildClassWithKeyPath:keyPath];
    //3、isa指向
    object_setClass(self, newClass);
    
    //获取sel
    SEL setterSel = NSSelectorFromString(setterForGetter(keyPath));
    //获取setter实例方法
    Method method = class_getInstanceMethod([self class], setterSel);
    //方法签名
    const char *type = method_getTypeEncoding(method);
    //添加一个setter方法
    class_addMethod(newClass, setterSel, (IMP)cjl_setter, type); 
}

class方法必须重写,其目的是为了与系统一样,对外的类保持一致

2:KVO响应

5、将setter方法重写添加到子类中(主要是在注册观察者方法中添加)

static void cjl_setter(id self, SEL _cmd, id newValue){
    NSLog(@"来了:%@",newValue);
    
    //此时应该有willChange的代码
    
    //往父类LGPerson发消息 - 通过objc_msgSendSuper
    //通过系统强制类型转换自定义objc_msgSendSuper
    void (*cjl_msgSendSuper)(void *, SEL, id) = (void *)objc_msgSendSuper;
    //定义一个结构体
    struct objc_super superStruct = {
        .receiver = self, //消息接收者 为 当前的self
        .super_class = class_getSuperclass(object_getClass(self)), //第一次快捷查找的类 为 父类
    };
    //调用自定义的发送消息函数
    cjl_msgSendSuper(&superStruct, _cmd, newValue);
    
    //此时应该有didChange的代码
    
    //让vc去响应
    /*---函数式编程*/
    NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
    id oldValue = [self valueForKey:keyPath];
    NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kCJLKVOAssociateKey));
    for (CJLKVOInfo *info in mArray) {
        NSMutableDictionary<NSKeyValueChangeKey, id> *change = [NSMutableDictionary dictionaryWithCapacity:1];
        if ([info.keyPath isEqualToString:keyPath] && info.handleBlock) {
            
           info.handleBlock(info.observer, keyPath, oldValue, newValue);
        }
    }
}

3:移除观察者

为了避免在外界不断的调用removeObserver方法,在自定义KVO中实现自动移除观察者

- (void)cjl_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath{
    
    //清空数组
    NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kCJLKVOAssociateKey));
    if (mArray.count <= 0) {
        return;
    }
    
    for (CJLKVOInfo *info in mArray) {
        if ([info.keyPath isEqualToString:keyPath]) {
            [mArray removeObject:info];
            objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kCJLKVOAssociateKey), mArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        }
    }
    
    if (mArray.count <= 0) {
        //isa指回父类
        Class superClass = [self class];
        object_setClass(self, superClass);
    }
}

实现cjl_removeObserver:forKeyPath:方法,主要是清空数组,以及isa指向更改

4:在子类中重写dealloc方法,当子类销毁时,会自动调用dealloc方法(在动态生成子类的方法中添加)

#pragma mark - 动态生成子类
- (Class)createChildClassWithKeyPath:(NSString *)keyPath
{
    //...
    
    //添加dealloc 方法
    SEL deallocSel = NSSelectorFromString(@"dealloc");
    Method deallocMethod = class_getInstanceMethod([self class], deallocSel);
    const char *deallocType = method_getTypeEncoding(deallocMethod);
    class_addMethod(newClass, deallocSel, (IMP)cjl_dealloc, deallocType);
    
    return newClass;
}

//************重写dealloc方法*************
void cjl_dealloc(id self, SEL _cmd){
    NSLog(@"来了");
    Class superClass = [self class];
    object_setClass(self, superClass);
}
其原理主要是:CJLPerson发送消息释放即dealloc了,就会自动走到重写的cjl_dealloc方法中(原因是因为person对象的isa指向变了,指向中间类,但是实例对象的地址是不变的,所以子类的释放,相当于释放了外界的person,而重写的cjl_dealloc相当于是重写了CJLPerson的dealloc方法,所以会走到cjl_dealloc方法中),达到自动移除观察者的目的

综上所述,自定义KVO大致分为以下几步

  • 注册观察者 & 响应
    • 1、验证是否存在setter方法

    • 2、保存信息

    • 3、动态生成子类,需要重写classsetter方法

    • 4、在子类的setter方法中向父类发消息,即自定义消息发送

    • 5、让观察者响应

  • 移除观察者
    • 1、更改isa指向为原有类

    • 2、重写子类的dealloc方法

拓展

以上自定义的逻辑并不完善,只是阐述了KVO底层原来实现的大致逻辑,具体的可以参考facebook的KVO三方框架KVOController

自定义KVO的完整代码见Github-CustomKVC_KVO

注意

 

引用

1:iOS-底层原理 23:KVO 底层原理

2:OC底层原理二十三:KVO原理

3:OC基础知识点之-KVO(键值观察)-上

4:十七、KVO原理分析

5:十一、iOS底层原理 - KVO

6:Objective-C KVO总结

7:KVO

8:KVO使用与实现机制分析(上) 

posted on 2020-12-02 23:55  风zk  阅读(168)  评论(0编辑  收藏  举报

导航