自定义实现 KVO

1.KVO (Key-Value Observing)是什么?

观察者模式,指定一个被观察对象,当被观察对象某个属性发生改变时,观察者会获得通知,并作出相应处理。

2.KVO 实现原理

  • 当使用观察者模式观察一个对象时,KVO机制会在运行期动态创建一个对象当前类的子类,如果当前类为Yinker,动态创建的子类就是NSKVONotifying_Yinker
  • 这个新的子类重写了被观察属性 keyPath 的 setter 方法,在 setter 方法内调用了 NSObject 的两个方法:willChangeValueForKey:didChangevlueForKey:,被观察属性发生改变之前,willChangeValueForKey:被调用,通知系统该 keyPath 的属性值即将变更;当改变发生后,didChangeValueForKey:被调用,通知系统该 keyPath 的属性值已经变更;之后,observeValueForKey:ofObject:change:context:会被调用。
  • 被观察对象的isa指针会被修改成指向新创建的子类,被观察对象也就成了新创建的子类的实例。
  • Apple 重写了class方法,隐藏新创建的子类,通过class方法获取的还是原来的类。

接下来进行一个简单的验证:

实例.jpeg

在三个断点处获取 [yinker class]object_getClass(yinker)的输出内容:

(lldb) po [yinker class]
Yinker
(lldb) po object_getClass(yinker)
Yinker
(lldb) po [yinker class]
Yinker
(lldb) po object_getClass(yinker)
NSKVONotifying_Yinker
(lldb) po [yinker class]
Yinker
(lldb) po object_getClass(yinker)
Yinker

从上面可以看出来,在添加观察者之后,对象 isa 指向了NSKVONotifying_Yinker,证明了确实新生成了一个新的子类。但是通过class获取的还是Yinker,这就验证了上面说的 Apple 重写了class方法,隐藏新创建的子类。而在移除观察者之后,又变回了原来的样子。

3.KVO 不好用的地方

  • 如果我想要观察几个不同的属性,就只能在-observeValueForKeyPath:ofObject:change:context:keyPath做判断,一堆代码摞在一起。。。
  • 我只能重写-observeValueForKeyPath:ofObject:change:context:方法来获得属性的变化,并不能使用自己想要自定义使用的方式。
  • 如果父类同样监听同一个对象的同一个属性,但是我并不想父类也做出相应,这个时候就需要使用context来进行区分,在-addObserver:forKeyPath:options:context:传进去一个父类不知道的context就成实现,虽然使用context这个参数可以干这个,但是总感觉这个使用方式有些繁琐。

所以,我们就自定义一个我们自己用起来方便的 KVO,啊哈哈哈。

4.自定义实现 KVO

首先创建NSObjectcategory,添加两个自定义方法,分别是添加观察者和移除观察者,详情如下:

#import 

/**
 属性变化后执行的block

 @param observedObject 需要被观察的对象
 @param observedKey 观察的属性
 @param oldValue 属性旧值
 @param newValue 属性新值
 */
typedef void(^CJObservingBlock)(id observedObject, NSString * observedKey, id oldValue, id newValue);

@interface NSObject (CJKVO)

/**
 添加观察者

 @param observer 需要添加的观察者
 @param key 观察的属性
 @param block 属性变化后执行的block
 */
- (void)CJ_addObserver:(NSObject *)observer
                forKey:(NSString *)key
             withBlock:(CJObservingBlock)block;

/**
 移除观察者

 @param observer 需要移除的观察者
 @param key 观察的属性
 */
- (void)CJ_removeObserver:(NSObject *)observer forKey:(NSString *)key;

@end

然后我们的主要思路就是在CJ_addObserver:forKey:withBlock:方法的实现当中:

  1. 根据key得到setter方法,判断对象的类有没有相应的setter方法,如果没有则返回。
  2. 获取当前类的name,如果当前类不是kvo子类,那么就去生成 kvo子类,然后让 isa 指向kvo子类
  3. 如果kvo子类没有对应的setter方法,则添加自定义的setter方法。(同一个key可能会被添加多次)。
  4. 添加观察者集合,并关联观察者集合的数组,存储所有的观察者集合。

接下来,就开始上代码和详细注释:

/*
  CJ_addObserver:forKey:withBlock:方法的实现
  添加观察者
*/
- (void)CJ_addObserver:(NSObject *)observer forKey:(NSString *)key withBlock:(CJObservingBlock)block {
    
    //1.检查对象的类有没有相应的 setter 方法
    SEL setterSelector = NSSelectorFromString([self setter:key]);
    // 因为重写了 class,所以[self class]获取的一直是父类
    Method setterMethod = class_getInstanceMethod([self class], setterSelector);
    if (!setterMethod) {
        NSLog(@"key 没有相应的 setter 方法");
        return;
    }
    
    // 获取当前类的 name
    Class clazz = object_getClass(self);
    NSString * clazzName = NSStringFromClass(clazz);
    
    // 如果当前类不是 kvo子类。(如果添加了多次观察者,kvo子类在第一次添加观察者的时候就创建了)
    if (![clazzName hasPrefix:kCJKVOClassPrefix]) {
        // 生成 kvo子类
        clazz = [self setKVOClassWithOriginalClassName:clazzName];
        // 让 isa 指向 kvo子类
        object_setClass(self, clazz);
    }
    
    // 如果 kvo子类 没有对应的 setter 方法,则添加。(同一个 key 可能会被添加多次)
    if (![self hasSelector:setterSelector]) {
        const char * types = method_getTypeEncoding(setterMethod);
        class_addMethod(clazz, setterSelector, (IMP)kvo_setter, types);
    }
    
    // 创建观察者组合
    CJObservation * observation = [[CJObservation alloc] initWithObserver:observer key:key block:block];
    // 获取所有观察者组合
    NSMutableArray * observations = objc_getAssociatedObject(self, (__bridge const void *)(kCJKVOObservations));
    if (!observations) {
        observations = [NSMutableArray array];
        // 添加关联所有观察者组合
        objc_setAssociatedObject(self, (__bridge const void *)(kCJKVOObservations), observations, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    [observations addObject:observation];
}
详细解析:

1.首先我们根据传进来的key的首字符大写,然后在前面拼上set,也就变成了setKey:的样子,然后再用class_getInstanceMethod去获得setKey:的实现(Method),如果没有就返回,具体代码如下:

//获取 setter 方法
- (NSString *)setter:(NSString *)key {
    if (key.length <= 0) {
        return nil;
    }
    
    // key 第一个大写
    NSString * firstStr = [[key substringToIndex:1] uppercaseString];
    // 截取 key 第二到最后
    NSString * remainingStr = [key substringFromIndex:1];
    // 拼接成 setter
    NSString * setter = [NSString stringWithFormat:@"set%@%@:", firstStr, remainingStr];
    
    return setter;
}

2.获取当前类的name,如果当前类不是kvo子类,那么就去生成kvo子类,然后通过object_setClass()isa指向kvo子类。动态创建子类具体代码以及详细注释如下:

// 生成 kvo子类
- (Class)setKVOClassWithOriginalClassName:(NSString *)originalClazzName {
    
    //1.拼接 kvo 子类并生成
    NSString * kvoClazzName = [NSString stringWithFormat:@"%@%@",kCJKVOClassPrefix,originalClazzName];
    Class kvoClazz =NSClassFromString(kvoClazzName);
    
    //2.如果已经存在则返回
    if (kvoClazz) {
        return kvoClazz;
    }
    
    //3.如果不存在,则传一个父类,类名,然后额外的空间(通常为 0),它返回给你一个子类。
    Class originalClazz = object_getClass(self);
    kvoClazz = objc_allocateClassPair(originalClazz, kvoClazzName.UTF8String, 0);
    
    //4.重写了 class 方法,隐藏这个新的子类
    Method clazzMethod = class_getInstanceMethod(originalClazz, @selector(class));
    const char * types = method_getTypeEncoding(clazzMethod);
    class_addMethod(kvoClazz, @selector(class), (IMP)kvo_class, types);
    
    //5.注册到 runtime 告诉 runtime 这个类的存在
    objc_registerClassPair(kvoClazz);
    
    return kvoClazz;
}

// 获取当前类的父类
static Class kvo_class(id self, SEL _cmd) {
    return class_getSuperclass(object_getClass(self));
}

3.通过hasSelector判断kvo子类有没有对应的setter方法,如果没有,则添加自定义的setter方法。加这一步判断的原因是因为如果同一个key可能会被添加多次,那么再添加完第一次之后它的setter方法就会存在了,不需要重新添加。使用class_addMethod()动态添加setter方法 ,并自定义完成这个方法的实现kvo_setter,具体代码和详细注释如下:

// 是否包含 selector 方法
- (BOOL)hasSelector:(SEL)selector {
    Class clazz = object_getClass(self);
    unsigned int methodCount = 0;
    // 获取方法列表
    Method* methodList = class_copyMethodList(clazz, &methodCount);
    for (unsigned int i = 0; i < methodCount; i++) {
        SEL thisSelector = method_getName(methodList[i]);
        if (thisSelector == selector) {
            free(methodList);
            return YES;
        }
    }
    free(methodList);
    return NO;
}

// 实现 setter 方法
static void kvo_setter(id self, SEL _cmd, id newValue) {
    
    // 根据 setter 获取 getter,_cmd 代表本方法的名称
    NSString * setterName = NSStringFromSelector(_cmd);
    NSString * getterName = [self getter:setterName];
    if (!getterName) {
        NSLog(@"key 没有相应的 getter 方法");
        return;
    }
    
    // 根据 key 获取对应的旧值
    id oldValue = [self valueForKey:getterName];
    
    // 构造 objc_super 的结构体
    struct objc_super superclazz = {
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self)),
    };
    
    // 对 objc_msgSendSuper 进行类型转换,解决编译器报错的问题
    void (* objc_msgSendSuperCasted)(void *, SEL, id) = (void *)objc_msgSendSuper;
    
    // id objc_msgSendSuper(struct objc_super *super, SEL op, ...) ,传入结构体、方法名称,和参数等
    objc_msgSendSuperCasted(&superclazz, _cmd, newValue);
    
    // 调用之前传入的 block
    NSMutableArray * observations = objc_getAssociatedObject(self, (__bridge const void *)(kCJKVOObservations));
    for (CJObservation * observation in observations) {
        if ([observation.key isEqualToString:getterName]) {
            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                // 此处是处于子线程,如果在 block 内需要处理 UI 的话,记得回到主线程
                observation.block(self, getterName, oldValue, newValue);
            });
        }
    }
}

//获取 getter 方法字符串
- (NSString *)getter:(NSString *)setter {
    if (setter.length <=0 || ![setter hasPrefix:@"set"] || ![setter hasSuffix:@":"]) {
        return nil;
    }
    // 先截掉 set,获取后面属性字符
    NSRange range = NSMakeRange(3, setter.length - 4);
    NSString * key = [setter substringWithRange:range];
    
    // 把第一个字符换成小写
    NSString * firstStr = [[key substringToIndex:1] lowercaseString];
    key = [key stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:firstStr];
    
    return key;
}

4.最后,我们创建观察者集合,并关联观察者集合的数组,存储所有的观察者集合。这个所谓的观察者集合就是指存储了CJ_addObserver:forKey:withBlock:方法的三个参数的对象,方便我们管理。详细内容如下:

@interface CJObservation : NSObject

// 观察者
@property (nonatomic, weak) NSObject *observer;
// 属性key
@property (nonatomic, copy) NSString *key;
// 回调block
@property (nonatomic, copy) CJObservingBlock block;

@end

@implementation CJObservation

- (instancetype)initWithObserver:(NSObject *)observer key:(NSString *)key block:(CJObservingBlock)block {
    self = [super init];
    if (self) {
        _observer = observer;
        _key = key;
        _block = block;
    }
    return self;
}

@end

当然,添加观察者就需要移除它,不然会造成内存泄漏的,而且在所有的观察者全部移除之后,再把对象的isa指针重新指向它原本的类。移除方法CJ_removeObserver:forKey:具体实现如下:

// 移除观察者
- (void)CJ_removeObserver:(NSObject *)observer forKey:(NSString *)key {
    
    // 获取所有观察者组合
    NSMutableArray * observations = objc_getAssociatedObject(self, (__bridge const void *)(kCJKVOObservations));
    
    // 根据 key 移除观察者组合
    CJObservation * observationShouldRemove;
    for (CJObservation * observation in observations) {
        if (observation.observer == observer && [observation.key isEqual:key]) {
            observationShouldRemove = observation;
            break;
        }
    }
    [observations removeObject:observationShouldRemove];

    //在移除所有观察者之后,让对象的 isa 指针重新指向它原本的类
    if (observations && observations.count == 0) {
        // 获取当前类的 name
        Class clazz = object_getClass(self);
        NSString * clazzName = NSStringFromClass(clazz);
        
        // 如果当前类是 kvo子类
        if ([clazzName hasPrefix:kCJKVOClassPrefix]) {
            // 获取对象原本的类
            clazz = NSClassFromString([clazzName substringFromIndex:kCJKVOClassPrefix.length]);
            // 让 isa 指向原本的类
            object_setClass(self, clazz);
        }
    }
}

接下来,我们就来看一下这个自定义的 KVO 好不好用吧。

@interface Yinker : NSObject
@property (nonatomic, copy) NSString * name;
@property (nonatomic, copy) NSString * job;
@end

#import "Yinker.h"
@implementation Yinker
@end

@interface ViewController ()

@property (nonatomic, strong) Yinker * yinker;
@property (weak, nonatomic) IBOutlet UILabel *label;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    Yinker * yinker = [[Yinker alloc] init];
    yinker.name = @"HH";
    
    // 添加观察者
    [yinker CJ_addObserver:self forKey:@"name" withBlock:^(id observedObject, NSString *observedKey, id oldValue, id newValue) {
        NSLog(@"%@,%@,%@,%@,%@",[NSThread currentThread],observedObject,observedKey,oldValue,newValue);
        dispatch_async(dispatch_get_main_queue(), ^{
            self.label.text = newValue;
        });
    }];
    
    _yinker = yinker;
}
- (IBAction)modifyName:(id)sender {
    
    // 修改属性值
    _yinker.name = @"CJ";

    // 移除观察者
    [_yinker CJ_removeObserver:self forKey:@"name"];
}

下图点击按钮之后控制台输出:
2017-11-14 CJKVO[63387:4285714] {number = 3, name = (null)},,name,HH,CJ

效果图.gif

效果已经实现了,如果观察多个属性值的时候,我们就可以在每一个 block内对不同的属性做不同的处理,哈哈,好用吧。

接着我们再像上面一样加断点查看一下对象的类:

自定义实现 KVO_第1张图片
image.png

在每一个断点的输出内容为如下:

(lldb) po [_yinker class]
Yinker
(lldb) po object_getClass(_yinker)
Yinker
(lldb) po [_yinker class]
Yinker
(lldb) po object_getClass(_yinker)
CJKVONotifying_Yinker
(lldb) po [_yinker class]
Yinker
(lldb) po object_getClass(_yinker)
CJKVONotifying_Yinker
(lldb) po [_yinker class]
Yinker
(lldb) po object_getClass(_yinker)
CJKVONotifying_Yinker
(lldb) po [_yinker class]
Yinker
(lldb) po object_getClass(_yinker)
Yinker

和系统的观察者模式类似,也是在添加了观察者模式之后类变成了CJKVONotifying_Yinker,而在移除一个观察的属性之后,对象的类也是和系统的观察者模式一样并没有变化,而在移除所有的观察属性之后,对象的类又变回了原来的类Yinker

完整的demo在这里,希望能对大家有所帮助,水平有限,有错误请指出。

参考:
iOS--KVO的实现原理与具体应用
如何自己动手实现 KVO
Key-Value Observing Done Right

你可能感兴趣的:(自定义实现 KVO)