使用Runtime实现KVO的两种姿势

KVO

Key-Value Observing,KVO 是一种观察者模式的实现:当被观察对象的某个属性发生更改时,观察者对象会获得通知。

KVO使用很简单:

- (void)viewDidLoad {
    [super viewDidLoad];
    self.text = @"1";
    // 添加observer
    [self addObserver:self forKeyPath:@"text" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];
}
// key值改变
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSString *text = [NSString stringWithFormat:@"%i",arc4random()%100];
    NSLog(@"text change to %@",text);
    self.text = text;
}
// 监听
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSString *oldValue = [change objectForKey:NSKeyValueChangeOldKey];
    NSString *newValue = [change objectForKey:NSKeyValueChangeNewKey];
    NSLog(@"observer oldValue:%@=====>newValue:%@",oldValue,newValue);
}
2018-11-11 21:52:31.832483+0800 FWCustomKVO[863:22943] origin setter:1
2018-11-11 21:52:43.410054+0800 FWCustomKVO[863:22943] text change to 8
2018-11-11 21:52:43.410308+0800 FWCustomKVO[863:22943] origin setter:8
2018-11-11 21:52:43.410476+0800 FWCustomKVO[863:22943] observer oldValue:1=====>newValue:8

可以看到,不需要给被观察的对象添加任何额外代码,只是简单的-observeValueForKeyPath:ofObject:change:context:就能使用 KVO 。这是怎么做到的呢?

KVO 实现原理

KVO 的实现使用了 Runtime:当你观察一个对象时,一个新的类会被动态创建。这个类继承自该对象的原本的类,并重写了key属性的 setter 方法。通过重写 setter 方法会在调用原 setter 方法之前和之后,通知所有观察对象值的更改。最后把这个对象的 isa 指针指向这个新创建的子类,对象就变成了原本类的子类的实例。这个动态创建的子类还重写了 - class 方法返回了原本类的class,我们使用- class方法查看被观察到对象类信息时,并不能发现它其实已经改变了。

自己实现KVO

虽然,我们调用2句代码就能使用KVO,但系统的KVO有时并不满足我们的需求。比如,你只能通过重写
-observeValueForKeyPath:ofObject:change:context:方法来获得通知。我们既不能使用自定义的 selector ,也不能使用 block 。而且父类同样监听同一个对象的同一个属性时还要处理父类的情况。
既然我们已经知道了KVO的实现原理,那为何不自己使用Runtime实现KVO呢?我们现在就来实现一个block回调的KVO.
这里有两种实现方式(当然都是使用Runtime),我们先模仿系统KVO的实现:

  • 姿势1
    新建NSObject分类,实现类似系统提供的两个方法addObserver和removeObserver:
@interface NSObject (KVO)
- (void)fw_addObserver:(NSObject *)observer
                forKey:(NSString *)key
             callBack:(FWObserverBlock)block;

- (void)fw_removeObserver:(NSObject *)observer forKey:(NSString *)key;

@end
static const void * kFWKVOAssociateKey = "FWKVOAssociateKey";
static NSString * kFWKVOClassPrefix = @"FWKVOClassPrefix_";

@implementation NSObject (KVO)

- (void)fw_addObserver:(NSObject *)observer forKey:(NSString *)key callBack:(FWObserverBlock)block {
    SEL setterSel = NSSelectorFromString(setterForKey(key));
    Class cls = object_getClass(self);
    NSString *className = NSStringFromClass(cls);
    // 类没有更改过
    if (![className hasPrefix:kFWKVOClassPrefix]) {
        // 创建自定义KVO类
        Class cls_kvo = [self makeKVOClassWithOriginClassName:className];
        object_setClass(self, cls_kvo); // isa指向创建的子类
    }
    
    // 更改创建的KVO类的setter方法
    if (![self hasSelector:setterSel]) {
        Method setterMethod = class_getInstanceMethod([self class], setterSel);
        class_addMethod(object_getClass(self), setterSel, (IMP)fw_setter, method_getTypeEncoding(setterMethod));
    }
    
    // 关联对象
    NSMutableArray *observers = objc_getAssociatedObject(self, kFWKVOAssociateKey);
    if (!observers) { // 没有关联
        observers = [NSMutableArray array];
        objc_setAssociatedObject(self, kFWKVOAssociateKey, observers, OBJC_ASSOCIATION_RETAIN);
    }
    FWObserverModel *model = [[FWObserverModel alloc] initWithObserver:observer key:key block:block];
    [observers addObject:model];
}

- (Class)makeKVOClassWithOriginClassName:(NSString *)className {
    NSString *KVOClassName = [kFWKVOClassPrefix stringByAppendingString:className];
    Class cls_kvo = NSClassFromString(KVOClassName);
    if (cls_kvo) {
        return cls_kvo;
    }
    Class cls_base = object_getClass(self);
    // 创建子类
    cls_kvo = objc_allocateClassPair(cls_base, KVOClassName.UTF8String, 0);
    
    // 更改class方法 调用父类的 (迷惑作用)
    Method classMethod = class_getInstanceMethod(cls_base, @selector(class));
    class_addMethod(cls_kvo, @selector(class), method_getImplementation(classMethod), method_getTypeEncoding(classMethod));
    objc_registerClassPair(cls_kvo);
    
    return cls_kvo;
}

void fw_setter(id self,SEL _cmd,id newValue) {
    NSString *setter = NSStringFromSelector(_cmd);
    NSString *getter = getterForKey(setter);
    id oldValue = [self valueForKey:getter]; // kvc获取更改前的值
    // 获取关联对象   block回调
    NSMutableArray *observers = objc_getAssociatedObject(self, kFWKVOAssociateKey);
    for (FWObserverModel *model  in observers) {
        if ([model.key isEqualToString:model.key]) {
            model.block(self, getter, oldValue, newValue);
        }
    }
    
    // 调用setter原始方法 即自定义KVO类的父类方法
    struct objc_super superClass = {
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self))
    };
    void (*objc_msgSendSuperCasted)(void *, SEL, id) = (void *)objc_msgSendSuper;
    objc_msgSendSuperCasted(&superClass, _cmd, newValue);
}

- (void)fw_removeObserver:(NSObject *)observer forKey:(NSString *)key {
    NSMutableArray *observers = objc_getAssociatedObject(self, kFWKVOAssociateKey);
    FWObserverModel *removeObserver;
    for (FWObserverModel *object in observers) {
        if ([object.key isEqualToString:key] && object.observer == observer) {
            removeObserver = object;
        }
    }
    [observers removeObject:removeObserver];
}

@end

测试一下,自己写的KVO是否生效:

- (void)viewDidLoad {
    [super viewDidLoad];
    self.text = @"1";
    [self fw_addObserver:self forKey:@"text" callBack:^(id observer, NSString *key, id oldValue, id newValue) {
        NSLog(@"observer oldValue:%@=====>newValue:%@",oldValue,newValue);
    }];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSString *text = [NSString stringWithFormat:@"%i",arc4random()%100];
    NSLog(@"text change to %@",text);
    self.text = text;
}

- (void)setText:(NSString *)text {
    _text = text;
    NSLog(@"origin setter:%@",text);
}

完美:

2018-11-11 22:01:40.128786+0800 FWCustomKVO[1202:48617] origin setter:1
2018-11-11 22:01:43.296806+0800 FWCustomKVO[1202:48617] text change to 81
2018-11-11 22:01:43.297037+0800 FWCustomKVO[1202:48617] observer oldValue:1=====>newValue:81
2018-11-11 22:01:43.297148+0800 FWCustomKVO[1202:48617] origin setter:81
  • 姿势2
    上面的方式动态的创建了子类,接下来介绍的方式可以不用创建类而是使用Method Swizzling黑魔法:通过替换被监听类的属性的setter方法,实现我们自己的功能:
    同样,还是需要创建分类,修改fw_addObserver方法实现即可
- (void)fw_addObserver:(NSObject *)observer forKey:(NSString *)key callBack:(FWObserverBlock)block {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        SEL originalSel = NSSelectorFromString(setterForKey(key));
        SEL targetSel = NSSelectorFromString(@"origin_setter:");
        Method originalMethod = class_getInstanceMethod([self class], originalSel);
        Method targetMethod = class_getInstanceMethod([self class], targetSel);
        // addMethod判断  因为当前类(或父类)可能已经有该method了
        BOOL addRet = class_addMethod([self class], targetSel, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
        if (!addRet) {
            method_setImplementation(targetMethod, method_getImplementation(originalMethod));
        }
        // 替换原来setter方法的IMP
        method_setImplementation(originalMethod, (IMP)fw_setter);
    });

    // 关联对象
    NSMutableArray *observers = objc_getAssociatedObject(self, kFWKVOAssociateKey);
    if (!observers) { // 没有关联
        observers = [NSMutableArray array];
        objc_setAssociatedObject(self, kFWKVOAssociateKey, observers, OBJC_ASSOCIATION_RETAIN);
    }
    FWObserverModel *model = [[FWObserverModel alloc] initWithObserver:observer key:key block:block];
    [observers addObject:model];
}

void fw_setter(id self,SEL _cmd,id newValue) {
    NSString *setter = NSStringFromSelector(_cmd);
    NSString *getter = getterForKey(setter);
    id oldValue = [self valueForKey:getter]; // kvc获取更改前的值
    // 获取关联对象   block回调
    NSMutableArray *observers = objc_getAssociatedObject(self, kFWKVOAssociateKey);
    for (FWObserverModel *model  in observers) {
        if ([model.key isEqualToString:model.key]) {
            model.block(self, getter, oldValue, newValue);
        }
    }

    // 调用setter原始方法
    SEL selector = NSSelectorFromString(@"origin_setter:");
    ((void (*)(id,SEL,id))objc_msgSend)(self,selector,newValue);
}

其中SEL targetSel = NSSelectorFromString(@"origin_setter:");这个自定义方法并不需要实现,因为我们只是利用这个来记录原始setter方法的实现而已,之后可以通过调用origin_setter:方法实现调用原始setter方法的目的。

另外最后使用了objc_msgSend发送消息的方式调用方法,其实还有两种方式:

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

推荐阅读更多精彩内容

  • 1.ios高性能编程 (1).内层 最小的内层平均值和峰值(2).耗电量 高效的算法和数据结构(3).初始化时...
    欧辰_OSR阅读 29,360评论 8 265
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,094评论 1 32
  • 设计模式概述 在学习面向对象七大设计原则时需要注意以下几点:a) 高内聚、低耦合和单一职能的“冲突”实际上,这两者...
    彦帧阅读 3,741评论 0 14
  • “咦,他们恋爱的时候你单身,失恋的时候你单身,各自要结婚的时候你还是单身,你可真是从一而终啊!” 越来越不喜欢社交...
    River的小星球阅读 789评论 0 0
  • 今天想说的就两点。 不过题外话就是最近都感觉身体特别累,眼睛也特别的不舒服,老是觉得干,然后也特别容易觉得身体累。...
    蓝桥东顾阅读 146评论 0 0