KVO实现原理

博客链接KVO实现原理

在iOS开发中,我们可以通过KVO机制来监听某个对象的某个属性的变化。

KVO实现步骤

KVO的实现分为三步:

  1. - (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;

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

  3. - (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

KVO实现机制

KVO的实现依赖于RunTime,在Apple的文档中有提到过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.

Apple的文档提到KVO是使用了isa-swizzling的技术。当观察者注册对象的属性时,观察对象的isa指针被修改,指向中间类而不是真正的类。因此,isa指针的值不一定反映实例的实际类。另外还提到我们不应该依赖isa指针来确定类成员资格,而是使用类方法来确定对象实例的类。

isa-swizzling

先用一段代码验证一下KVO的实现是不是进行了isa-swizzling

@interface KVOTestModel : NSObject
    
@property (nonatomic, copy) NSString *name;

- (void)printInfo;

@end

@implementation KVOTestModel

- (void)printInfo {
    NSLog(@"isa:%@, supper class:%@", NSStringFromClass(object_getClass(self)), class_getSuperclass(object_getClass(self)));
    NSLog(@"self:%@, [self superclass]:%@", self, [self superclass]);
    NSLog(@"name setter function pointer:%p", class_getMethodImplementation(object_getClass(self), @selector(setName:)));
    NSLog(@"printInfo function pointer:%p", class_getMethodImplementation(object_getClass(self), @selector(printInfo)));
}

@end

ViewController中使用KVO监听KVOTestMod对象的相关属性:

#pragma mark - Lifceycle

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.kvoTestModel = [[KVOTestModel alloc] init];
    NSLog(@"Before KVO ---------------------------------------");
    [self.kvoTestModel printInfo];
    [self.kvoTestModel addObserver:self
                        forKeyPath:@"name"
                           options:NSKeyValueObservingOptionNew
                           context:nil];
    NSLog(@"After KVO ---------------------------------------");
    [self.kvoTestModel printInfo];
    [self.kvoTestModel removeObserver:self forKeyPath:@"name"];
    NSLog(@"Remove KVO ---------------------------------------");
    [self.kvoTestModel printInfo];
}

#pragma mark - KVO

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

打印一下结果:
isa-swizzling

添加KVO之后,isa已经替换成了NSKVONotifying_Person,而根据class_getSuperclass得到的结果竟然是Person, 然后name是使我们KVO需要观察的属性,它的setter函数指针变了。

这里先直接总结KVO的实现原理:

  • add observer

    1. 通过runtime生成一个以NSKVONotifying_+类名的形式来命名的派生类;
    2. 将被观察的对象的isa指针指向这个派生类;
    3. 重写派生类的setter方法,重写setter方法的本质是在赋值语句之前调用willChangeValueForKey,赋值之后调用didChangeValueForKey,在didChangeValueForKey中调用observeValueForKeyPath:ofObject:change:context:方法。
  • remove observer

    将其的isa指针指向原来的类对象中

KVO派生类

在使用了KVO添加了观察者以后,runtime会生成一个NSKVONotifying_开头的派生类,那这个类做了些什么呢?验证代码如下:

// KVOTestModel类

@interface KVOTestModel : NSObject

@property (nonatomic, copy) NSString *name;

@end

@implementation KVOTestModel

@end

// 打印类的方法列表
void printClassMethod(Class aClass) {
    unsigned int count;
    Method *methodList = class_copyMethodList(aClass, &count);
    NSMutableString *methodNames = [NSMutableString string];
    
    for (int i = 0; i < count; i++) {
        Method aMethod =methodList[i];
        NSString *aMethodString = NSStringFromSelector(method_getName(aMethod));
        [methodNames appendString:[NSString stringWithFormat:@"%@, ", aMethodString]];
    }
    
    free(methodList);
    
    NSLog(@"%@", methodNames);
}

//调用代码
- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.kvoTestModel = [[KVOTestModel alloc] init];
    self.kvoTestModel2 = [[KVOTestModel alloc] init];
    
    [self.kvoTestModel addObserver:self
                        forKeyPath:@"name"
                           options:NSKeyValueObservingOptionNew
                           context:nil];
    
    printClassMethod(object_getClass(self.kvoTestModel));
    printClassMethod(object_getClass(self.kvoTestModel2));
}

执行结果如下:
kvo派生类

从上面的结果可以得出,派生类重写了四个方法分别是:

  1. 重写Setter方法;
  2. 重写Class方法,苹果官方并不想让我们知道KVO的具体实现,所以重写了Class方法以返回其原来的类,这也证实了使用了KVO的实例对象为什么调用class方法和object_getClass方法但是结果不一样;
  3. 实现了dealloc方法,用来实现KVO的一些收尾工作;
  4. 实现了_isKVOA方法,应该是KVO内部的一个私有方法;

重写Setter方法

上面提到必须是派生类重写setter方法,如果是直接对属性进行赋值的话,是不会触发KVO的。虽然Apple并没有开源KVO的代码,但是我们可以通过验证的方式进行推导。

KVOTestModel文件中添加以下代码:

#pragma mark - Override

- (void)willChangeValueForKey:(NSString *)key {
    NSLog(@"willChangeValueForKey beigin");
    
    [super willChangeValueForKey:key];
    
    NSLog(@"willChangeValueForKey end");
}

- (void)didChangeValueForKey:(NSString *)key {
    NSLog(@"didChangeValueForKey beigin");
    
    [super didChangeValueForKey:key];
    
    NSLog(@"didChangeValueForKey end");
}

#pragma mark - Setter

- (void)setName:(NSString *)name {
    _name = name;
    
    NSLog(@"%s", __func__);
}

打印结果:
setter

从上面的结果我们可以知道KVO在重写setter方法后大概分成三个步骤:

  1. 先调用willChangeValueForKey
  2. 调用父类的setter方法;
  3. 调用didChangeValueForKeydidChangeValueForKey中会调用KVO的相关代理方法来通知观察者。

keyPath

keyPath使用我们用来监听的属性,它的实质是什么?先看一段代码:

//Person.h
@interface Person : NSObject

@property (nonatomic, copy) NSString *nick;

@end

//Person.m
@synthesize nick = realNick;

- (void)setNick:(NSString *)nick {
    realNick = nick;
}

- (NSString *)nick {
    return realNick;
}

//ViewController
- (void)_testKeyPath {
    self.person = [[Person alloc] init];
    [self.person addObserver:self
                  forKeyPath:@"nick"//"realNick"
                     options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
                     context:nil];
    self.person.nick = @"Nero";
    [self.person removeObserver:self forKeyPath:@"nick"];
}

实际结果是,使用nick能够监听到对应变化,而使用真正的实例变量realNick时,无法监听到值。keyPath指向的并不是真正的实例变量,而是对于setter方法的关联,KVO会使用keypath作为后缀去寻找原类的setter方法的方法签名,和实际存取对象和属性名称没有关系。所以这也是为什么我们重命名了setter方法之后,没有办法再去使用KVO或KVC了,需要手动调用一次willChangeValue方法。

手动触发一个KVO

代码如下:

//Person
@interface Person : NSObject {
@public NSInteger age;
}

@end

//ViewController
- (void)_testImplementKVOManually {
    self.person = [[Person alloc] init];
    [self.person addObserver:self
                  forKeyPath:@"age"
                     options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
                     context:nil];
    [self.person willChangeValueForKey:@"age"];//获取旧值
    self.person->age = 26;
    [self.person didChangeValueForKey:@"age"];//获取新值
    [self.person removeObserver:self forKeyPath:@"age"];
}

KVO的跨线程监听

我们知道使用Notification时,跨线程发送通知是无法被接受到的,但是KVO是可以跨线程监听的。

- (void)_testKVOinGCD {
    dispatch_queue_t queue = dispatch_queue_create("test.concurrent.queue", DISPATCH_QUEUE_CONCURRENT);
    self.person = [Person new];
    dispatch_async(queue, ^{
        NSLog(@"%@", [NSDate date]);
        self.person.name = @"Nero";
    });
    
    dispatch_async(queue, ^{
        NSLog(@"%@", [NSDate date]);
        sleep(1);
        [self.person addObserver:self
                      forKeyPath:@"name"
                         options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
                         context:nil];
        NSLog(@"%@", [NSDate date]);
    });
    
    dispatch_barrier_async(queue, ^{
        NSLog(@"%@", [NSDate date]);
        self.person.name = @"NeroXie";
    });
}

可以看到在两个不同的线程里创建的Observer和Target,观察变化也是能够生效的。
这里使用了dispatch_barrier_async是确保第三个task在前两个task运行后再执行,而且使用的队列必须是自定义的并发队列,如果使用全局队列,栅栏就不想起作用了,因为dispatch_barrier_async相当于dispatch_asysc

KVO拾遗

  • 子类继承父类的一个属性(无论是否被暴露),当这个属性被改变时,KVO都能观察到。

因为继承的关系Father <- Son <- KVOSon,当我监听一个父类属性的keyPath的时候,Son实例同样可以通过消息查找找到父类的setter方法,再将该方法加入到KVOSon类当中去。

  • 子类继承父类属性并重写了它的setter方法,当这个属性被改变时,KVO能观察到。

在上一条中知道,其实子类监听父类属性,并不依赖继承,而是通过ISA指针在消息转发的时候能够获取到父类方法就足够。所以当我们重写父类setter方法,相当于在子类定义了该setter函数,在我们去用sel找方法签名时,直接在子类中就拿到了,甚至都不需要去到父类里。所以理解了KVO监听父类属性和继承没有直接联系这一点,就不再纠结set方法是否重写这个问题了。

补充:以Block的形式实现KVO

通过上面的API我们知道KVO的调用相对来说是有点繁琐的,所以我用Category实现了KVO的Block形式的调用

// 声明
@interface NSObject(KVOBlock)

- (NSString *)nn_addObserverForKeyPath:(NSString *)keyPath
                               options:(NSKeyValueObservingOptions)options
                            usingBlock:(void (^)(id obj, NSString *keyPath, NSDictionary *change))block;

- (void)nn_removeBlockObserverWithIdentifier:(NSString *)identifier;
- (void)nn_removeAllBlockObservers;

@end

// 实现
#pragma mark - NSObject+KVOBlock

static void *NNObserverBlockContext = &NNObserverBlockContext;

typedef void (^NNObserverBlock) (id obj, NSString *keyPath, NSDictionary<NSKeyValueChangeKey,id> *change);

#pragma mark - Private Class

@interface _NNInternalObserver : NSObject

@property (nonatomic, assign) BOOL isObserving;
@property (nonatomic, weak) id observed;
@property (nonatomic, copy) NSString *keyPath;
@property (nonatomic, copy) NNObserverBlock observerBlock;

@end

@implementation _NNInternalObserver

#pragma mark - Init

- (instancetype)initWithObserved:(id)observed
                         keyPath:(NSString *)keyPath
                   observerBlock:(NNObserverBlock)observerBlock {
    if ((self = [super init])) {
        self.isObserving = NO;
        self.observed = observed;
        self.keyPath = keyPath;
        self.observerBlock = [observerBlock copy];
    }
    
    return self;
}

- (void)dealloc {
    if (self.keyPath) [self stopObserving];
}

#pragma mark - Helper

- (void)startObservingWithOptions:(NSKeyValueObservingOptions)options {
    @synchronized(self) {
        if (self.isObserving) return;
        
        [self.observed addObserver:self forKeyPath:self.keyPath options:options context:NNObserverBlockContext];
        
        self.isObserving = YES;
    }
}

- (void)stopObserving {
    NSParameterAssert(self.keyPath);
    
    @synchronized (self) {
        if (!self.isObserving) return;
        if (!self.observed) return;
        
        [self.observed removeObserver:self forKeyPath:self.keyPath context:NNObserverBlockContext];
        
        self.observed = nil;
        self.keyPath = nil;
        self.observerBlock = nil;
    }
}

#pragma mark - KVO

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if (context != NNObserverBlockContext) return;
    
    @synchronized (self) {
        self.observerBlock(object, keyPath, change);
    }
}

@end

@interface NSObject()

/** 保存所有的block */
@property (nonatomic, strong, setter=nn_setObserverBlockMap:) NSMutableDictionary *nn_observerBlockMap;

@end

@implementation NSObject(KVOBlock)

/** 所有修改过dealloc方法的类 */
+ (NSMutableSet *)nn_observedClasses {
    static dispatch_once_t onceToken;
    static NSMutableSet *classes = nil;
    dispatch_once(&onceToken, ^{
        classes = [[NSMutableSet alloc] init];
    });
    
    return classes;
}

#pragma mark - Public

- (NSString *)nn_addObserverForKeyPath:(NSString *)keyPath
                               options:(NSKeyValueObservingOptions)options
                                  usingBlock:(void (^)(id obj, NSString *keyPath, NSDictionary *change))block {
    NSString *identifier = [NSProcessInfo processInfo].globallyUniqueString;
    [self nn_addObserverForKeyPath:keyPath identifier:identifier options:options block:block];
    
    return identifier;
}

- (void)nn_removeBlockObserverWithIdentifier:(NSString *)identifier {
    NSParameterAssert(identifier.length);
    
    NSMutableDictionary *dict;
    
    @synchronized (self) {
        dict = self.nn_observerBlockMap;
        if (!dict) return;
    }
    
    _NNInternalObserver *observer = dict[identifier];
    [observer stopObserving];
    [dict removeObjectForKey:identifier];
    if (dict.count == 0) self.nn_observerBlockMap = nil;
}


- (void)nn_removeAllBlockObservers {
    NSDictionary *dict;
    
    @synchronized (self) {
        dict = [self.nn_observerBlockMap copy];
        self.nn_observerBlockMap = nil;
    }
    
    [dict.allValues enumerateObjectsUsingBlock:^(_NNInternalObserver *obj, NSUInteger idx, BOOL * _Nonnull stop) {
        [obj stopObserving];
    }];
}

#pragma mark - Core Method

/**
 KVO Block 实现
 
 @param keyPath     被观察的属性
 @param identifier  唯一标识符 用来标记内部观察者
 @param options     观察选项
 @param block       KVO Block
 */
- (void)nn_addObserverForKeyPath:(NSString *)keyPath
                      identifier:(NSString *)identifier
                         options:(NSKeyValueObservingOptions)options
                            block:(NNObserverBlock)block {
    NSParameterAssert(keyPath.length);
    NSParameterAssert(identifier.length);
    NSParameterAssert(block);
    
    Class classToSwizzle = self.class;
    NSMutableSet *classes = self.class.nn_observedClasses;
    @synchronized (classes) {
        NSString *className = NSStringFromClass(classToSwizzle);
        if (![classes containsObject:className]) {
            SEL deallocSelector = sel_registerName("dealloc");
            
            __block void (*originalDealloc)(__unsafe_unretained id, SEL) = NULL;
            
            id newDealloc = ^(__unsafe_unretained id objSelf) {
                [objSelf nn_removeAllBlockObservers];
                
                if (originalDealloc == NULL) {
                    struct objc_super superInfo = {
                        .receiver = objSelf,
                        .super_class = class_getSuperclass(classToSwizzle)
                    };
                    
                    void (*msgSend)(struct objc_super *, SEL) = (__typeof__(msgSend))objc_msgSendSuper;
                    msgSend(&superInfo, deallocSelector);
                } else {
                    originalDealloc(objSelf, deallocSelector);
                }
            };
            
            IMP newDeallocIMP = imp_implementationWithBlock(newDealloc);
            
            if (!class_addMethod(classToSwizzle, deallocSelector, newDeallocIMP, "v@:")) {
                Method deallocMethod = class_getInstanceMethod(classToSwizzle, deallocSelector);
                originalDealloc = (void(*)(__unsafe_unretained id, SEL))method_getImplementation(deallocMethod);
                originalDealloc = (void(*)(__unsafe_unretained id, SEL))method_setImplementation(deallocMethod, newDeallocIMP);
            }
            
            [classes addObject:className];
        }
    }
    
    _NNInternalObserver *observer = [[_NNInternalObserver alloc] initWithObserved:self
                                                                          keyPath:keyPath
                                                                    observerBlock:block];
    [observer startObservingWithOptions:options];
    
    @synchronized (self) {
        if (!self.nn_observerBlockMap) self.nn_observerBlockMap = [NSMutableDictionary dictionary];
    }
    
    self.nn_observerBlockMap[identifier] = observer;
}

#pragma mark - Setter & Getter

- (void)nn_setObserverBlockMap:(NSMutableDictionary *)nn_observerBlockMap {
    objc_setAssociatedObject(self, @selector(nn_observerBlockMap), nn_observerBlockMap, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (NSMutableDictionary *)nn_observerBlockMap {
    return objc_getAssociatedObject(self, @selector(nn_observerBlockMap));
}

@end

// 调用
self.kvoTestModel = [[KVOTestModel alloc] init];

NSString *identifier =
[self.kvoTestModel nn_addObserverForKeyPath:@"name"
                                    options:NSKeyValueObservingOptionNew
                                 usingBlock:^(id obj, NSString *keyPath, NSDictionary *change) {
                                     NSLog(@"%@", change);
                                 }];

self.kvoTestModel.name = @"Nero";

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

推荐阅读更多精彩内容

  • KVC 什么是KVC KVC是Key-Value-Coding 的简称。 KVC是一种可以直接通过字符串的名字ke...
    Jack_deng阅读 1,009评论 0 0
  • 作者:wangzz原文地址:http://blog.csdn.net/wzzvictory/article/det...
    反调唱唱阅读 1,117评论 0 5
  • KVO KVO 是 Key-Value-Observing 的简称。 KVO 是一个观察者模式。观察一个对象的属性...
    louuXinnn阅读 366评论 0 0
  • 前言 Key-Value-Observer,它来源于观察者模式, 其基本思想(copy于某度)是一个目标对象管理所...
    CholMay阅读 3,395评论 6 18
  • 一、概述 KVO,即:Key-Value Observing,它提供一种机制,当指定的对象的属性被修改后,则其观察...
    DeerRun阅读 10,050评论 11 33