自定义实现 KVO

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

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

2.KVO 实现原理

  • 当使用观察者模式观察一个对象时,KVO机制会在运行期动态创建一个对象当前类的子类,如果当前类为Yinker,动态创建的子类就是NSKVONotifying_Yinker
  • 这个新的子类重写了被观察属性 keyPath 的 setter 方法,setter 方法的实现其实就是 Foundation 框架中的 _NSSetXxxxValueAndNotify(Xxxx为属性类型),方法内部实现:
void _NSSetXxxxValueAndNotify() {
    [self willChangeValueForKey:@"key"];
    [super setKey:key];
    [self didChangevlueForKey:@"key"];
}
1. 被观察属性发生改变之前,'willChangeValueForKey:'被调用,通知系统该 keyPath 的属性值即将变更;
2. 调用父类的'setKey:'方法;
3. 当改变发生后,'didChangeValueForKey:'被调用,通知系统该 keyPath 的属性值已经变更;
4. 最后'didChangeValueForKey:'会调用'observeValueForKey:ofObject:change:context:'。
  • 被观察对象的isa指针会被修改成指向新创建的子类,被观察对象也就成了新创建的子类的实例。
  • Apple 重写了class方法,隐藏新创建的子类,通过class方法获取的还是原来的类。
  • Apple 重写了dealloc方法。
  • Apple 重写了_isKVOA方法。

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

实例.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方法,隐藏新创建的子类。而在移除观察者之后,又变回了原来的样子。

2.1 如果手动去触发KVO?
  • 当前对象手动调用 willChangeValueForKey:didChangeValueForKey: 2个方法;
  • 虽然是 didChangeValueForKey: 内部调用的 observeValueForKey:ofObject:change:context:willChangeValueForKey:也必须要调用,因为 didChangeValueForKey: 内部有判断 willChangeValueForKey: 是否被调用。

3.KVO 不好用的地方

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

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

4.自定义实现 KVO

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

#import <Foundation/Foundation.h>

/**
 属性变化后执行的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] <NSThread: 0x60400027a6c0>{number = 3, name = (null)},<Yinker: 0x60000000e490>,name,HH,CJ

效果图.gif

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

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

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

最后编辑于
?著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,029评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,238评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,576评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,214评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,324评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,392评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,416评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,196评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,631评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,919评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,090评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,767评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,410评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,090评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,328评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,952评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,979评论 2 351

推荐阅读更多精彩内容

  • 本篇会对KVO的实现进行探究,不涉及太多KVO的使用方法,但是会有一些使用时的思考。 一、使用上的疑问 1.key...
    奋拓达阅读 504评论 0 2
  • 本文是 Objective-C Runtime 系列文章的第三篇。如果你对 Objective-C Runtime...
    克鲁德李阅读 351评论 0 2
  • 上半年有段时间做了一个项目,项目中聊天界面用到了音频播放,涉及到进度条,当时做android时候处理的不太好,由于...
    DaZenD阅读 3,015评论 0 26
  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,694评论 0 9
  • 很久没有画画了,都忘记了静心完成一幅画的感觉,每天被繁琐的事情牵绊,要挤出时间,给自己。
    佑茗幸阅读 177评论 0 1