iOS-KVO

一、VKO 简述

KVO 全称 Key Value Observing,俗称“键值监听”;可以监听对象某个属性值的变化

1. KVO 是已什么方式实现的?(底层原理是什么?)

答:当对一个对象添加监听(addObserver:forKeyPath: ... ),iOS会修改该对象的 isa (isa默认指向对象所所属的类)。改为指向一个通过Runtime动态创建的子类,子类拥重写 set 方法,并且 set 方法内部会顺序调用 willChangeValueForKey, 原来的set方法,即:[super set...], didChangeValueForKey。并且会在 didChangeValueForKey 中调用KVO的回调方法:observeValueForKeyPath:ofObject:change:context:

2. 如何手动触发KVO?

答:已添加监听的属性,在值发生变化时,系统会自动触发回调。如果想要手动触发,则需我们自己调用 willChangeValueFor 和 didChallengeValueForKey方法,这两个方法缺一不可。

二、KVO 实现原理探索

1. 话不多说,上代码:
- (void)useSystemKVOTest {
    // 1. 创建测试对象
    self.p1 = [Person new];
    self.p2 = [Person new];
    self.p1.age = 1;
    self.p2.age = 2;

    // 2. 打印监听前p1、p2 所属类、setter 方法实现地址
    NSLog(@"监听前 p1 class is : %@, p2 class is : %@", object_getClass(self.p1), object_getClass(self.p2));
    // 输出结果:监听前 p1 class is : Person, p2 class is : Person
    NSLog(@"监听前 p1-setAage: address is : = %p, p2-setAage: address is : %p", [self.p1 methodForSelector:@selector(setAge:)], [self.p2 methodForSelector:@selector(setAge:)]);
    // 输出结果:监听前 p1-setAage: address is : = 0x102f98ea8, p2-setAage: address is : 0x102f98ea8

    // 3. 添加监听,
    [self.p1 addObserver:self forKeyPath:NSStringFromSelector(@selector(age)) options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:NULL];

    // 4. 打印监听后p1、p2 所属类、setter 方法实现地址
    NSLog(@"监听后 p1 class is : %@, p2 class is : %@", object_getClass(self.p1), object_getClass(self.p2));
    // 输出结果:监听后 p1 class is : NSKVONotifying_Person, p2 class is : Person
    NSLog(@"监听后 p1-setAage: address is : = %p, p2-setAage: address is : %p", [self.p1 methodForSelector:@selector(setAge:)], [self.p2 methodForSelector:@selector(setAge:)]);
    // 输出结果:监听后 p1-setAage: address is : = 0x194c61d54, p2-setAage: address is : 0x102f98ea8
    
    // 5. 改变值
    self.p1.age = 10;
    self.p2.age = 20;

    // 6.移除 p1.age 的监听者
    [self.p1 removeObserver:self forKeyPath:NSStringFromSelector(@selector(age))];
}

// kvo 回调方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"监听到 %@ 的 %@ 改变了 %@", [object isEqual:self.p1]?@"p1":@"p2", keyPath, change);
    /* 输出结果:
     监听到 p1 的 age 改变了 {
         kind = 1;
         new = 10;
         old = 1;
     }
     */
}
输出结果:001
2. 有以上输出结果,我们发现:
  • 在添加监听后,p1 的 isa 指向了 NSKVONotifying_Person
  • NSKVONotifyin_Person其实是Person的子类,那么也就是说其superclass指针是指向Person类对象的
  • NSKVONotifyin_Person 是 runtime 在运行时生成的。那么 p1 对象在调用 setage 方法的时候,肯定会根据 p1 的 isa 找到NSKVONotifyin_Person,在 NSKVONotifyin_Person 中找 setage 的方法及实现。
  • p1 的 setAge 方法的实现由 Person 类方法中的 setAge 方法转换为了C语言的 Foundation 框架的 _NSsetIntValueAndNotify 函数。
3. NSKVONotifyin_Person 的内部结构:

首先我们知道,NSKVONotifyin_Person作为Person的子类,其superclass指针指向Person类,并且NSKVONotifyin_Person内部一定对setAge方法做了单独的实现,那么NSKVONotifyin_Person同Person类的差别可能就在于其内存储的对象方法及实现不同。
我们通过runtime分别打印Person类对象和NSKVONotifyin_Person类对象内存储的对象方法

- (void)printMethods {
    [self printMehtodsOfClass:object_getClass(self.p1)];
    [self printMehtodsOfClass:object_getClass(self.p2)];
}

- (void)printMehtodsOfClass:(Class)cls {
    unsigned int count = 0;
    Method * methods = class_copyMethodList(cls, &count);    
    NSMutableString *methodNames = @"".mutableCopy;
    [methodNames appendFormat:@"%@ - ", cls];
    
    for (int i = 0; i < count; i++) {
        Method method = methods[i];
        NSString * methodName = NSStringFromSelector(method_getName(method));
        [methodNames appendString:methodName];
        [methodNames appendString:@"  "];
    }    
    NSLog(@"%@", methodNames);
    free(methods);
}
输出结果002

通过上述代码我们发现NSKVONotifyin_Person中有4个对象方法。分别为setAge: class dealloc _isKVOA,那么至此我们可以画出NSKVONotifyin_Person的内存结构以及方法调用顺序。

image.png

这里NSKVONotifyin_Person重写class方法是为了隐藏NSKVONotifyin_Person。不被外界所看到。我们在p1添加过KVO监听之后,分别打印p1和p2对象的class可以发现他们都返回Person。

NSLog(@"%@, %@", [self.p1 class],  [self.p2 class]);
// 打印结果 Person, Person

三. 自定义 KVO 实现监听

1. ViewController 调用实现:
#import "ViewController.h"
#import "Person.h"
#import "NSObject+YJKVO.h"

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

@implementation ViewController
#pragma mark - Life Cycle
- (void)viewDidLoad {
    [super viewDidLoad];
    [self useCustomKVOTest];
}

#pragma mark - 使用自定义kvo
- (void)useCustomKVOTest {
    self.p = [[Person alloc] init];
    [self.p yj_addObserver:self forKeyPath:NSStringFromSelector(@selector(name))];
    self.p.name = @"张三";
}

#pragma mark - 自定义kvo,回调
- (void)yj_observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object newValue:(id)newValue {
    NSLog(@"newValue = %@", newValue);
}
2. Person 类
  • Person.h
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN
@interface Person : NSObject
@property (nonatomic, copy) NSString * name;
@end
  • Person.m
#import "Person.h"

@implementation Person
- (void)setName:(NSString *)name {
    _name = name;
    NSLog(@"调用了");
}
@end
3. 定义一个 NSObject 的分类 NSObject+YJKVO,实现KVO监听
  • NSObject+YJKVO.h
@interface NSObject (YJKVO)
/// 添加观察者
/// @param observer 观察者
/// @param keyPath keyPath
- (void)yj_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

/// 移除观察者
/// @param observer 观察者
/// @param keyPath keyPath
- (void)yj_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

/// kvo 回调方法 (由观察者实现)
/// @param keyPath keyPath
/// @param object 被观察对象
/// @param newValue 新值
- (void)yj_observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object newValue:(id)newValue;
@end
  • NSObject+YJKVO.m
#import "NSObject+YJKVO.h"
#import <objc/message.h>

// 通过 Runtime 动态成子类的前缀
static NSString *const YJKVOPrefix = @"YJKVO_";
// 关联 观察者
static NSString *const YJKVOAssociatedOberverKey = @"YJKVOAssociatedOberverKey";

@implementation NSObject (YJKVO)

#pragma mark - -- public methods
/// 添加观察者
/// @param observer 观察者
/// @param keyPath keyPath
- (void)yj_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath {    
    // 1. 检查时候有 set 方法
    NSString *setterMethodName = setterForGetter(keyPath);
    SEL setterSel = NSSelectorFromString(setterMethodName);
    // method
    Method method = class_getInstanceMethod(self.class, setterSel);
    if (!method) {
        @throw [[NSException alloc] initWithName:NSExtensionItemAttachmentsKey reason:@"没有setter方法" userInfo:nil];
    }
    
    // 2. 动态生成子类
    Class sub_Class = [self registerSubClassWithKeyPath:keyPath];
    if (!sub_Class) {
        @throw [[NSException alloc] initWithName:NSExtensionItemAttachmentsKey reason:@"子类创建失败" userInfo:nil];
    }
    
    // 3. 消息转发
    // 关联 observer
    objc_setAssociatedObject(self, (__bridge void const * _Nonnull)YJKVOAssociatedOberverKey, observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

/// 移除观察者
/// @param observer 观察者
/// @param keyPath keyPath
- (void)yj_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath {
    objc_removeAssociatedObjects(observer);
}

/// kvo 回调方法 (由观察者实现)
- (void)yj_observeValueForKeyPath:(NSString *)keyPath ofObject:(nonnull id)object newValue:(nonnull id)newValue { }


#pragma mark - -- private methods
#pragma mark - 通过 getter 方法名,获取 setter 方法名;例如:age ==> setAge:
static NSString * setterForGetter(NSString *getter) {
    if (getter.length < 1) {
        return nil;
    }
    // 获取第一个字符,变成打下
    NSString *firstString = [[getter substringToIndex:1] uppercaseString]; // substringToIndex:从最前头一直截取到Index
    NSString *otherString = [getter substringFromIndex:1]; // substringFromIndex:从Index开始截取到最后
    // 拼接 age == > setAag:
    return [NSString stringWithFormat:@"set%@%@:", firstString, otherString];
}

#pragma mark - 通过 setter 方法名,获取 getter 方法名;例如:setAge: ==> age
static NSString * getterForSetter(NSString *setter) {
    if (setter.length < 1 || ![setter hasPrefix:@"set"] || ![setter hasSuffix:@":"]) {
        return nil;
    }
    NSString *getter = [setter substringFromIndex:3];
    getter = [getter substringToIndex:getter.length-1];
    NSString *firstString = [[getter substringToIndex:1] lowercaseString];
    return [getter stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:firstString];
}


#pragma mark - 动态生成子类
/// 运行时动态创建子类
/// @param keyPath keyPath
- (Class)registerSubClassWithKeyPath:(NSString *)keyPath {
    // 子类名
    NSString *subClsName = [NSString stringWithFormat:@"%@%@", YJKVOPrefix, self.class];
    // 子类,一个 NSObject 默认分贝 16 个字节
    Class subCls = objc_allocateClassPair(self.class, subClsName.UTF8String, 16);
    // 注册
    objc_registerClassPair(subCls);
    
    // 给子类动态添加 setter、class 实现
    Method class_method = class_getClassMethod(self.class, @selector(class));
    Method setter_method = class_getClassMethod(self.class, NSSelectorFromString(setterForGetter(keyPath)));
    class_addMethod(subCls, @selector(class), (IMP)yj_class, method_getTypeEncoding(class_method));
        class_addMethod(subCls,  NSSelectorFromString(setterForGetter(keyPath)), (IMP)yj_setter, method_getTypeEncoding(setter_method));
    
    // 将父类的 isa 指向子类
    object_setClass(self, subCls);
    // 返回
    return subCls;
}

#pragma mark - 重写 class 方法
static Class yj_class(id self, SEL _cmd) {
    return class_getSuperclass(object_getClass(self));
}

#pragma mark - 重写 setter 方法
/// 重写 setter 方法
/// @param newValue 新值
static void yj_setter(id self, SEL _cmd, id newValue) {    
    // 1. 调用 super setter 方法
    struct objc_super super_cls = {
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self))
    };
    // 调用父类 setter 方法 设置新值
    ((void(*) (id, SEL, id)) (void *)objc_msgSendSuper)((__bridge id)(&super_cls), _cmd, newValue);
            
    // 2. 取出观察者,调用kvo 回调方法
    id observer = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(YJKVOAssociatedOberverKey));
    //
    SEL handleSel = @selector(yj_observeValueForKeyPath:ofObject:newValue:);
    NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
    
    // Runtime 调用回到方法
    // objc_msgSend() 默认的情况下,不支持添加参数。
    // 解决方案一: Build Setting –> 搜索: Enable Strict Checking of objc_msgSend Calls 改为 NO (我自己试了下,无效 Xcode12.1)
    // 解决方案二: 这里通过(void *)送入5个参数,你可以根据自己参数类型强转原本是void()的函数方法
    ((void (*) (id, SEL, NSString*, id, id)) (void*)objc_msgSend)(observer, handleSel, keyPath, self, newValue);
}
@end
输出结果003
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,992评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,212评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,535评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,197评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,310评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,383评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,409评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,191评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,621评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,910评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,084评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,763评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,403评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,083评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,318评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,946评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,967评论 2 351