KVO探究

作者: 沉江小鱼 | 来源:发表于2019-11-06 13:45 被阅读0次

1. 介绍

键值观察是一种机制,它允许将其他对象的指定属性的更改通知给对象。

对于MVC中model层和controller层之间的通信很有用。(在OS X中,控制器层绑定技术在很大程度上依赖于键值观察)。controller对象通常观察model对象的属性,而view对象通过controller观察model对象的属性去修改。此外,model对象可以观察其他model对象(通常用于确定从属值何时更改),甚至可以观察自身(再次确定从属值何时更改)。

1.1 下面举一个简单的实例,说明KVO在应用程序中的用处:

假设一个Person对象与一个Account对象交互,表示该人在银行的储蓄账户。Person可能需要了解Account(例如余额或利率)何时发生变化。


kvo_objects_properties.png

如果这些属性是Account的公共属性,则Person可以定期轮询Account来发现更改,但是这样效率低下,并且不切实际。更好的方法就是使用KVO,在Account发生更改的时候Person可以接收到消息。

2. KVO的使用

2.1 使用KVO可以分为三步:
2.1.1 注册观察者

[addObserver:forKeyPath:options:context:]

参数options: 会影响通知中提供的更改字典的内容以及生成通知的方式。
比如,我们经常使用NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew,接收旧值和新值。

NSKeyValueObservingOptionNew:提供更改前的值
NSKeyValueObservingOptionOld:提供更改后的值
NSKeyValueObservingOptionInitial:观察最初的值(在注册观察服务时会调用一次触发方法)
NSKeyValueObservingOptionPrior:分别在值修改前后触发方法(即一次修改有两次触发)

可以自己写例子实验一下。

参数context: 上下文,一般我们会设置为NULL,可以包含任意数据,这些数据会在更改通知中传递回观察者。作用:区分多个观察的标识,防止继承时,观察相同的keypath,在判断时,嵌套少。
比如:

static void *PersonAccountBalanceContext = &PersonAccountBalanceContext;

注意: 键值观察addObserver:forKeyPath:options:context:方法不维护对观察对象、被观察对象或上下文的强引用。

2.1.2 接收观察者回调

[observeValueForKeyPath:ofObject:change:context:]
如果是在子线程中修改被观察者对象的值,那么这个回调也会是在子线程中。
注意:在最后任何无法识别的上下文都需要调用

[super observeValueForKeyPath:keyPath
                             ofObject:object
                               change:change
                               context:context];
2.1.3 移除观察者

[removeObserver:forKeyPath:]

  • 如果移除尚未注册的观察者,会导致NSRangeException
  • 确保观察者从内存中消失之前将观察者移除
  • 一般在init或viewDidLoad注册,dealloc中注销
2.2 手动通知观察者

我们想重写属性的setter,增加一些自定义的操作,但是我们又想使用系统提供的KVO,可以手动通知观察者,比如:

@interface Person : NSObject

@property (nonatomic, assign) NSInteger age;

- (void)config:(NSInteger)age;

@end

@implementation Person

-(void)config:(NSInteger)age{
    [self willChangeValueForKey:@"age"];
    _age = age;
    [self didChangeValueForKey:@"age"];
}

@end

这样,既能使用系统提高工的KVO,也能满足我们的需求。

2.3 automaticallyNotifiesObserversForKey 是否开启自动观察

控制对应key改变,是否自动通知观察者。

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

控制自动通知观察者,可以在这个方法内,对key判断,如果返回为NO,则不会通知观察者对象,也就不会调用那个方法。

比如上面例子中,Person的age,如果age变化时,我们不想自动通知观察者,除了在上面的那个方法中控制之外,还可以通过指定key值的方法实现:

+ (BOOL)automaticallyNotifiesObserversOfAge

当有一个属性名为 propertyName 时,会自动有一个类方法:

+ (BOOL)automaticallyNotifiesObserversOfPropertyName

可以在这个方法中,返回NO,控制这个属性值改变时,是否自动通知观察者对象。

如果,我们通过上面的方法设置了age变化时,不会自动通知观察者,那么我们想要手动通知观察者了需要怎么弄呢?
可以重写age的setter方法,使用上面提到过的两个方法:

-(void)setAge:(NSInteger)age{
    [self willChangeValueForKey:@"age"];
    _age = age;
    [self didChangeValueForKey:@"age"];
}
2.4 +(NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key 路径集合观察

举个例子吧,Person类,里面有学校名称、城市名称、学校详细地址,三个属性,学校详细地址=城市名称+学校名称,所以学校名称和城市名称改变的时候,学校的详细地址也会改变,此时我们添加对学校的详细地址的观察,应该怎么实现呢?
添加观察依赖,当学校名称和城市名称两个任意一个发生变化时,详细地址都发生变化。
Person类:

@interface Person : NSObject

@property (nonatomic, copy) NSString *schoolName;
@property (nonatomic, copy) NSString *cityName;

@property (nonatomic, copy) NSString *detailAddress;

@end


@implementation Person

// 重写getter
-(NSString *)detailAddress{
    return [NSString stringWithFormat:@"%@%@",self.schoolName,self.cityName];
}

// 添加依赖,当schoolName cityName 发生变化的时候,detailAddress也会变化
+(NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
    if ([key isEqualToString:@"detailAddress"]) {
        return [NSSet setWithArray:@[@"schoolName",@"cityName"]];
    }
    return [super keyPathsForValuesAffectingValueForKey:key];
}

@end

ViewController中添加观察者:

@interface ViewController () 

@property (nonatomic, strong) Person *person;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person = [[Person alloc] init];
    [self.person addObserver:self forKeyPath:@"detailAddress" options:NSKeyValueObservingOptionNew context:NULL];
}

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

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.person.schoolName = @"111111111111";
    self.person.cityName = @"2222222222222";
}

@end

当点击屏幕的时候,我们给schoolName cityName 赋值,此时观察回调会打印两次。为啥呢?
因为两者之中任意一个变化,都会触发detailAddress的变化。

如果Person中有一个自定义类Company(公司)的实例,当CompanyName 和 CompanyAddress变化时,收到通知,还是上面的那种方法,只不过关键方法换成了:

// 添加依赖,当schoolName cityName 发生变化的时候,detailAddress也会变化
+(NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
    if ([key isEqualToString:@"detailAddress"]) {
        return [NSSet setWithArray:@[@"company.companyName",@"company.companyAddress"]];
    }
    return [super keyPathsForValuesAffectingValueForKey:key];
}
2.5 集合类的观察 mutableArrayValueForKey:

对象的属性可以是1对1的,也可以是1对多的,一对多的属性要么是有序的(数组),要么是无序的(集合)。

不可变的有序容器属性(NSArray)和无序容器属性(NSSet)一般可以使用valueForKey:来获取。比如有一个角items的NSArray属性,你可以使用valueForKey:@"items"来获取这个属性。
而当对象的属性是可变的容器时,对于有序的容器,可以用下面的方法:

- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;

该方法返回一个可变有序数组,如果调用该方法,KVC的搜索顺序如下:

  • 搜索insertObject:in<Key>AtIndex: , removeObjectFrom<Key>AtIndex:或者 insert<Key>AdIndexes , remove<Key>AtIndexes 格式的方法
    如果至少找到一个insert方法和一个remove方法,那么同样返回一个可以响应NSMutableArray所有方法代理集合(类名是NSKeyValueFastMutableArray2),那么给这个代理集合发送NSMutableArray的方法,以insertObject:in<Key>AtIndex:, removeObjectFrom<Key>AtIndex:或者 insert<Key>AdIndexes , remove<Key>AtIndexes组合的形式调用。还有两个可选实现的接口:replaceOnjectAtIndex:withObject:,replace<Key>AtIndexes:with<Key>:。
  • 如果上步的方法没有找到,则搜索set<Key>: 格式的方法,如果找到,那么发送给代理集合的NSMutableArray最终都会调用set<Key>:方法。 也就是说,mutableArrayValueForKey:取出的代理集合修改后,用set<Key>:重新赋值回去(也就是为什么这样获取数组改变后,能够触发KVO)。这样做效率会低很多。所以推荐实现上面的方法。
  • 如果上一步的方法还还没有找到,再检查类方法+ (BOOL)accessInstanceVariablesDirectly,如果返回YES(默认行为),会按_<key>,<key>,的顺序搜索成员变量名,如果找到,那么发送的NSMutableArray消息方法直接交给这个成员变量处理。
  • 如果还是找不到,则调用valueForUndefinedKey:
  • 关于mutableArrayValueForKey:的适用场景,我在网上找了很多,发现其一般是用在对NSMutableArray添加Observer上。如果对象属性是个NSMutableArray、NSMutableSet、NSMutableDictionary等集合类型时,你给它添加KVO时,你会发现当你添加或者移除元素时并不能接收到变化。因为KVO的本质是系统监测到某个属性的内存地址或常量改变时,会添加上- (void)willChangeValueForKey:(NSString *)key- (void)didChangeValueForKey:(NSString *)key方法来发送通知,所以一种解决方法是手动调用者两个方法,但是并不推荐,你永远无法像系统一样真正知道这个元素什么时候被改变。另一种便是利用使用mutableArrayValueForKey:了。

Person:

@interface Person : NSObject

@property (nonatomic, strong) NSMutableArray *mutableArr;

@end

@implementation Person

- (instancetype)init
{
    self = [super init];
    if (self) {
        self.mutableArr = [NSMutableArray array];
    }
    return self;
}

@end

ViewController:

@interface ViewController ()

@property (nonatomic, strong) Person *person;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person = [[Person alloc] init];
}

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

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSMutableArray *mArr = [self.person valueForKey:@"mutableArr"];
    [mArr addObject:@"123"];
}

@end

通过上面的例子,我们可以得出:
只是普通的通过[self.person.mutableArr addObject:@"123"]不会触发Observer的回调,通过 NSMutableArray *mArr = [self.person valueForKey:@"mutableArr"];获取到可变数组,再添加或者删除,会触发Observer的回调。

3. KVO的实现原理

Automatic key-value observing is implemented using a technique called isa-swizzling.
The isa pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.

When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.

You should never rely on the isa pointer to determine class membership. Instead, you should use the `[class] method to determine the class of an object instance.

上面是苹果对KVO的实现的介绍,大概就是KVO是通过isa-swizzling技术实现的,当对一个对象的属性添加一个观察者时,被观察对象的isa指针被修改,指向了一个中间类,而不是真正的类。

这里我们有几个问题?

  • 是观察的属性的setter方法吗?
  • 生成的中间类是什么?
  • 中间类是什么时候生成的?
  • 生成的中间类跟原来的类是什么关系?
3.1 KVO是观察的setter方法吗?

这个很好验证,代码如下:

Person类:
@interface Person : NSObject
{
    @public
    NSString *sex;
}

@property (nonatomic, copy) NSString *name;

@end

@implementation Person

@end

ViewController:

@interface ViewController ()

@property (nonatomic, strong) Person *person;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person = [[Person alloc] init];
    
    [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
    [self.person addObserver:self forKeyPath:@"sex" options:NSKeyValueObservingOptionNew context:NULL];
    
    self.person.name = @"小李";
    self.person->sex = @"男";
}

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

@end

运行上面的代码,我们会看到,当我们修改sex的时候,是不会触发observer的回调的,修改name的时候,是会触发observer的回调的。

3.2 生成的中间类是什么?

在官方文档中,我们看到KVO是用了isa-swizzling实现的,说明被观察对象的isa指针可能发生变化了,我们通过打断点验证一下:


屏幕快照 2019-11-05 上午10.47.56.png

我们知道对象的isa本来是指向对象所属的类的,但是现在,我们发现self.person的isa指向的是NSKVONotifying_Person类。这个类是系统动态生成的,目的就是实现KVO。

3.3 中间类和原有的类是什么关系?

我们可以通过获取原有类的方法列表,和中间类的方法列表去验证一下,还是原来的Person类,但是在ViewController中,我们获取Person和NSKVONotifying_Person类的方法,然后输出:

@interface ViewController ()

@property (nonatomic, strong) Person *person;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.person = [[Person alloc] init];
    
    [self logMethodList:[Person class]];
    
    [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
    [self.person addObserver:self forKeyPath:@"sex" options:NSKeyValueObservingOptionNew context:NULL];
    
    NSLog(@"********准备输出*********");
    
    [self logMethodList:NSClassFromString(@"NSKVONotifying_Person")];
    
    self.person.name = @"小李";
    self.person->sex = @"男";
}

- (void)logMethodList:(Class)class{
    unsigned int count = 0;
    Method *list = class_copyMethodList(class, &count);
    for (int i = 0; i < count; i++) {
        Method method = list[I];
        NSLog(@"%@",NSStringFromSelector(method_getName(method)));
    }
}

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

@end
屏幕快照 2019-11-05 上午11.05.41.png

我们可以看到NSKVONotifying_Person中的方法有4个:

  • setName:
  • class
  • dealloc
  • _isKVOA
    我们发现,它有一个setName的方法,所以可以认为NSKVONotifying_Person是继承Person类的,并且系统还重写class dealloc 添加了 _isKVOA方法。子类不重写父类的方法,是不会打印出来的,打印出来的class dealloc方法,说明这个中间类重写了这两个方法。
3.4 中间类是什么时候生成的?

在添加观察者之后,由系统动态的生成这个中间类。可以try一下。

总的流程就是:

  • 当某个类的属性对象第一次被观察时,系统会动态的创建该类的一个子类(NSKVONotifying_xxx)
  • 在这个子类中重写原类中任何被观察属性的setter方法
  • 将被观察对象的isa指针指向这个子类
  • 子类重写观察属性setter方法内,会调用父类的setter方法设置新值,并且实现真正的通知机制
  • 移除观察者的时候,此时会将被观察对象的isa指针指向原类,但是那个子类不会被释放,因为之前已经在内存中注册这个类了,所以这个子类会存在,以后再次观察的时候,不会再次申请控件,注册这个类。

键值观察通知依赖于NSObject的两个方法:willChangeValueForKeydidChangeValueForKey,在一个被观察属性发生改变之前,willChangeValueForKey会被调用,记录旧的值;改变之后,调用didChangeValueForKey,继而observeValueForKey:ofObject:change:context:也会被调用。

KVO的这套实现机制中,苹果还重写了class方法,让开发者误认为还是使用的当前类,从而达到隐藏生成的派生类。

694844-5cd0554e4de2c3fa.png

4. 自定义模拟KVO

我们知晓了KVO的原理,就可以自定义模拟一下KVO,首先我们看一下添加/移除观察者的方法:

@interface NSObject(NSKeyValueObserverRegistration)

- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

@end

这三个方法是通过给NSObject增加NSKeyValueObserverRegistration分类,添加的,所以我们可以也给NSObject添加一个自定义的分类,增加这三个方法,为了不和系统的方法产生冲突,我们在前面加上前缀。

类似的,我们把Observer的回调方法,也添加到我们自己的分类中:

- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;

之后,我们主要就是自己实现这三个方法了,当然自定义KVO还可以有一些优化:

  • 可以不用observeValueForKeyPath...这个方法来回调,可以使用block来回调信息,可
  • 实现自动移除观察者,不用调用removeObserver ...方法

代码主要是两个类文件组成:

  • LGKVOInfo 将KVO的Observer信息和回调block封装成对象,对observer有一个弱引用

  • NSObject+LGKVO 分类,自己实现注册为观察者的方法

  • LGKVOInfo 类的代码:

*****************LGKVOInfo.h*****************

#import <Foundation/Foundation.h>

typedef NS_OPTIONS(NSUInteger, LGKeyValueObservingOptions) {
    LGKeyValueObservingOptionNew = 0x01,
    LGKeyValueObservingOptionOld = 0x02,
};

typedef void(^LGKVOBlock)(NSObject *observer,NSString *keyPath,id oldValue ,id newValue);

// 这个类是封装kvo的observer信息,以便于之后获取
@interface LGKVOInfo : NSObject

// 这里使用weak,防止循环引用
@property (nonatomic, weak) NSObject *observer;
@property (nonatomic, copy) NSString *keyPath;
@property (nonatomic, assign) LGKeyValueObservingOptions options;
@property (nonatomic, copy) LGKVOBlock handBlock;

- (instancetype)initWithObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(LGKeyValueObservingOptions)options handBlock:(LGKVOBlock)handBlock;

@end

*****************LGKVOInfo.m*****************
#import "LGKVOInfo.h"
@implementation LGKVOInfo

- (instancetype)initWithObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(LGKeyValueObservingOptions)options handBlock:(LGKVOBlock)handBlock;
{
    if (self = [super init]) {
        self.observer = observer;
        self.keyPath  = keyPath;
        self.options  = options;
        self.handBlock= handBlock;
    }
    return self;
}

@end

  • NSObject+LGKVO 类的代码:
*****************NSObject+LGKVO.h*****************
#import <Foundation/Foundation.h>
#import "LGKVOInfo.h"

NS_ASSUME_NONNULL_BEGIN

@interface NSObject (LGKVO)

- (void)lg_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(LGKeyValueObservingOptions)options context:(nullable void *)context handBlock:(LGKVOBlock)handBlock;

@end

NS_ASSUME_NONNULL_END

*****************NSObject+LGKVO.m*****************
#import "NSObject+LGKVO.h"
#import <objc/message.h>

static NSString *const kLGKVOPrefix = @"LGKVONotifying_";
static NSString *const kLGKVOAssiociateKey = @"kLGKVO_AssiociateKey";

@implementation NSObject (LGKVO)

// 添加观察者
- (void)lg_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(LGKeyValueObservingOptions)options context:(nullable void *)context handBlock:(LGKVOBlock)handBlock{
    
    // 1: 验证是否存在setter方法 : 不让实例进来
    [self judgeSetterMethodFromKeyPath:keyPath];
    // 2: 动态生成子类
    Class newClass = [self createChildClassWithKeyPath:keyPath];
    // 3: 更改被观察对象isa的指向为LGKVONotifying_LGPerson
    object_setClass(self, newClass);
    // 4: 保存观察者的信息,以便于之后通知观察者,这里使用关联一个数组进行存储,将观察者信息,存储为一个对象
    LGKVOInfo *info = [[LGKVOInfo alloc] initWithObserver:observer forKeyPath:keyPath options:options handBlock:handBlock];
    
    NSMutableArray *infoArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kLGKVOAssiociateKey));
    if (!infoArray) {
        // observer(VC) -> person ISA -> 数组 -> info -/weak/-> self(VC)
        infoArray = [NSMutableArray arrayWithCapacity:1];
        objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kLGKVOAssiociateKey), infoArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    
    [infoArray addObject:info];
}

// 移除观察者
- (void)lg_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath{
    NSMutableArray *infoArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kLGKVOAssiociateKey));
    // self
    [infoArray enumerateObjectsUsingBlock:^(LGKVOInfo *info , NSUInteger idx, BOOL * _Nonnull stop) {
        // 移除一个观察者信息
        if ([info.keyPath isEqualToString:keyPath] && (observer == info.observer)) {
            [infoArray removeObject:info];
            *stop = YES;
        }
    }];
    
    // 如果关联数组没有关联KVO信息 -- 清空
    if (infoArray.count == 0) {
        objc_removeAssociatedObjects(infoArray);
    }
    // 指回给父类
    Class superClass = [self class]; // LGPerson
    object_setClass(self, superClass);

}

#pragma mark - 中间类的方法实现

// dealloc方法
static void lg_dealloc(id self,SEL _cmd){
    // 指回给父类
    Class superClass = [self class]; // LGPerson
    object_setClass(self, superClass);
}

// setter方法
static void lg_setter(id self,SEL _cmd,id newValue){
    
    NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
    id oldValue       = [self valueForKey:keyPath];
    // 4: 消息发送 : 发送给父类,改变父类的值 可以强制类型转换
    void (*lg_msgSendSuper)(void *,SEL , id) = (void *)objc_msgSendSuper;
    // void /* struct objc_super *super, SEL op, ... */
    struct objc_super superStruct = {
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self)),
    };
    //objc_msgSendSuper(&superStruct,_cmd,newValue)
    lg_msgSendSuper(&superStruct,_cmd,newValue);
    
    // 既然观察到了,下一步就是回调 -- 让我们的观察者调用
    // - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
    // 1: 拿到观察者
    // 2: 消息发送给观察者
    // 这里使用了block,函数式
    NSMutableArray *infoArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kLGKVOAssiociateKey));
    
    for (LGKVOInfo *info in infoArray) {
        if ([info.keyPath isEqualToString:keyPath]) {
            dispatch_async(dispatch_get_global_queue(0, 0), ^{
                // 枚举 -- 新值 + 旧值
                NSMutableDictionary<NSKeyValueChangeKey,id> *change = [NSMutableDictionary dictionaryWithCapacity:1];
                
                if (info.options & LGKeyValueObservingOptionNew) {
                    [change setObject:newValue forKey:NSKeyValueChangeNewKey];
                }
                if (info.options & LGKeyValueObservingOptionOld && oldValue) {
                    [change setObject:oldValue forKey:NSKeyValueChangeOldKey];
                }
                
                if (info.handBlock) {
                    info.handBlock(info.observer, keyPath, oldValue, newValue);
                }
            });
        }
    }
    
}

// 中间类实现class方法,返回的还是原来的类
Class lg_class(id self,SEL _cmd){
    return class_getSuperclass(object_getClass(self));
}

#pragma mark - Help

// 动态创建一个中间类
- (Class)createChildClassWithKeyPath:(NSString *)keyPath{
    
    NSString *oldClassName = NSStringFromClass([self class]);
    NSString *newClassName = [NSString stringWithFormat:@"%@%@",kLGKVOPrefix,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.1 : 添加class : class的指向是LGPerson
    SEL classSEL = NSSelectorFromString(@"class");
    Method classMethod = class_getInstanceMethod([self class], classSEL);
    const char *classTypes = method_getTypeEncoding(classMethod);
    class_addMethod(newClass, classSEL, (IMP)lg_class, classTypes);
    // 2.3.2 : 添加setter
    SEL setterSEL = NSSelectorFromString(setterForGetter(keyPath));
    Method setterMethod = class_getInstanceMethod([self class], setterSEL);
    const char *setterTypes = method_getTypeEncoding(setterMethod);
    class_addMethod(newClass, setterSEL, (IMP)lg_setter, setterTypes);
    
    // 2.3.3 : 添加 dealloc -- 为什么系统给KVO添加dealloc?这样可以在对象销毁的同时,更改isa指向为原来的类
    SEL deallocSEL = NSSelectorFromString(@"dealloc");
    Method deallocMethod = class_getInstanceMethod([self class], deallocSEL);
    const char *deallocTypes = method_getTypeEncoding(deallocMethod);
    class_addMethod(newClass, deallocSEL, (IMP)lg_dealloc, deallocTypes);
    return newClass;
}

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

// 从get方法获取set方法的名称 key ===>>> setKey:
static NSString *setterForGetter(NSString *getter){
    
    if (getter.length <= 0) { return nil;}
    
    NSString *firstString = [[getter substringToIndex:1] uppercaseString];
    NSString *leaveString = [getter substringFromIndex:1];
    
    return [NSString stringWithFormat:@"set%@%@:",firstString,leaveString];
}

// 从set方法获取getter方法的名称 set<Key>:===> key
static NSString *getterForSetter(NSString *setter){
    
    if (setter.length <= 0 || ![setter hasPrefix:@"set"] || ![setter hasSuffix:@":"]) { return nil;}
    
    NSRange range = NSMakeRange(3, setter.length-4);
    NSString *getter = [setter substringWithRange:range];
    NSString *firstString = [[getter substringToIndex:1] lowercaseString];
    return  [getter stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:firstString];
}

@end

这样,我们在使用的时候就会很方便了:

[self.person lg_addObserver:self forKeyPath:@"nickName" options:(LGKeyValueObservingOptionNew | LGKeyValueObservingOptionOld) context:NULL handBlock:^(NSObject * _Nonnull observer, NSString * _Nonnull keyPath, id  _Nonnull oldValue, id  _Nonnull newValue) {
        // 功能逻辑
        NSLog(@"实际情况 %@ -- %@",oldValue,newValue);
    }];

也不需要,手动移除观察者了,当然,自定义只能是用以对KVO有一个更深的认识,系统的KVO肯定还有更多其余操作的。

附一张FBKVOController的理解图,可以结合源码去理解:


屏幕快照 2019-11-06 下午2.32.09.png

FBKVOController中有用到了NSHashTable,NSHashTable可以设置NSHashTableOptions,支持对其元素的弱引用(当其中的元素释放时,会从NSHashTable中移除),下面是介绍链接:
https://blog.csdn.net/u010124617/article/details/45745829

相关文章

  • swift中KVO和属性观察器

    开篇提醒:OC中的KVO及其KVO的基础知识可参见:深入runtime探究KVO Swift中,原本没有KVO模式...

  • KVO探究

    KVO原理 KVO是基于runtime机制实现的当某个类的属性对象第一次被观察时,系统就会在运行期动态地创建该类的...

  • KVO探究

    1. 介绍 键值观察是一种机制,它允许将其他对象的指定属性的更改通知给对象。 对于MVC中model层和contr...

  • KVO-KVC的原理探究 - KVO篇

    关于KVO的探究 KVO的基本使用 创建Person类,添加属性age: 在ViewController中添加属性...

  • iOS开发·KVO用法,原理与底层实现: runtime模拟实现

    摘要:这篇文章首先介绍KVO的基本用法,接着探究 KVO (Key-Value Observing) 实现机制,并...

  • KVO进阶——KVO实现探究

    本篇会对KVO的实现进行探究,不涉及太多KVO的使用方法,但是会有一些使用时的思考。 一、使用上的疑问 1.key...

  • iOS开发·KVO用法,原理与底层实现: runtime模拟实现

    本文Demo传送门:CMKVODemo 摘要:这篇文章首先介绍KVO的基本用法,接着探究 KVO (Key-Val...

  • 探究KVO本质

    看了一些资料,对OC更加深入了解,记录总结一下。KVO:key-value-boserver,键-值-监听。主要是...

  • KVO原理探究

    kvo原理:利用运行时,生成对象子类,并生成子类的对象,并替换原来的对象的isa指针(地址不发生变化,变化的是值)...

  • KVO探究(一)

    KVO的调用分为自动调用和手动调用,一般的使用自动调用比较多。下面先说说自动调用。 一、自动调用 准备工作: 1、...

网友评论

      本文标题:KVO探究

      本文链接:https://www.haomeiwen.com/subject/egvgvctx.html