KVO 的实现以及自定义一个kvo

KVO (Key-Value Observing)

KVO 是 Objective-C 对观察者模式(Observer Pattern)的实现。也是 Cocoa Binding 的基础。当被观察对象的某个属性发生更改时,观察者对象会获得通知。

KVO 基本用法

我们知道key 可以观察对象某个属性发生变化,或者keypath发生的变化,下面是代码实例。

#import <Foundation/Foundation.h>
#import "Man.h"
@interface Person : NSObject
@property (nonatomic,strong) Man *man;
@property (nonatomic,strong) NSString *name;
@end
#import "Person.h"

@implementation Person

@end
#import <Foundation/Foundation.h>
@interface Man : NSObject
@property (nonatomic,strong) NSString  *age;
@end
#import "Man.h"
@implementation Man

@end

我们定义一个Person 和Man 两个类,Man 是Person中的属性。Person 还有个属性name。man有个属性age
测试代码

- (void)viewDidLoad {
    [super viewDidLoad];
    Person * p=[[Person alloc]init];
    
    [p addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
    [p addObserver:self forKeyPath:@"man.age" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
    p.name = @"KVO";
    Man * man = [[Man alloc]init];
    p.man = man;
    man.age = @"18";
    
}

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

测试结果是

2018-04-26 09:21:11.278539+0800 KVO 实现[41309:1427305] name
2018-04-26 09:21:11.278926+0800 KVO 实现[41309:1427305] man.age
2018-04-26 09:21:11.279126+0800 KVO 实现[41309:1427305] man.age

原理探索

KVO用法简单,但是苹果是怎么实现KVO的呢?
苹果官网有这么句话

Automatic key-value observing is implemented using a technique called isa-swizzling... 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 ..
用了isa 交换 。

当我们调用方法的时候

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

苹果把调用者的isa 指向换成了NSKVONotifying_XXX

image.png

从上图我们能看出来Person 的指针p 指向了NSKVONotifying_Person类

那我们看看NSKVONotifying_Person 类到底有什么方法。

新增一个类

#import <Foundation/Foundation.h>

@interface ObjcClassMethodParse : NSObject
- (instancetype)initWithClass:(Class)cls;
-(void)print;

@end

#import "ObjcClassMethodParse.h"
#import <objc/runtime.h>
@interface ObjcClassMethod: NSObject
@property (nonatomic,strong) NSString *name;
@property (nonatomic,strong) NSString *value;

@end

@implementation ObjcClassMethod

@end

@interface ObjcClassMethodParse()
@property (nonatomic,strong) Class cls;
@property (nonatomic,strong) NSMutableArray * methodListArr;
@end
@implementation ObjcClassMethodParse
- (instancetype)initWithClass:(Class)cls
{
    self = [super init];
    if (self) {
        self.cls = cls;
        self.methodListArr = [NSMutableArray array];
        [self parse];
        
    }
    return self;
}
//https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtPropertyIntrospection.html#//apple_ref/doc/uid/TP40008048-CH101-SW1
-(void)parse{
    
    unsigned int count = 0;
    Method *methodList = class_copyMethodList(self.cls, &count);
    for (int i=0; i<count; i++) {
        Method method = methodList[i];
        ObjcClassMethod * m=[[ObjcClassMethod alloc]init];
     const char * name= sel_getName(method_getName(method));
        m.name = [NSString stringWithFormat:@"%s",name];
        [self.methodListArr addObject:m];
    }
    free(methodList);
    
}

-(void)print{
    for (ObjcClassMethod *m in self.methodListArr) {
        NSLog(@"name %@",m.name);
    }
}
@end

我们通过这个类看看到底NSKVONotifying_Person 有啥方法
测试代码

 struct objc_object * obj = (__bridge  struct objc_object *)p;
    Class objClass = obj->isa;
    NSLog(@"%@",objClass);
    ObjcClassMethodParse * methodParse =[[ObjcClassMethodParse alloc]initWithClass:objClass];
    [methodParse print];

测试结果

2018-04-26 09:42:09.377354+0800 KVO 实现[46687:1454265] NSKVONotifying_Person
2018-04-26 09:42:09.377683+0800 KVO 实现[46687:1454265] name setMan:
2018-04-26 09:42:09.377960+0800 KVO 实现[46687:1454265] name setName:
2018-04-26 09:42:09.378149+0800 KVO 实现[46687:1454265] name class
2018-04-26 09:42:09.378497+0800 KVO 实现[46687:1454265] name dealloc
2018-04-26 09:42:09.378720+0800 KVO 实现[46687:1454265] name _isKVOA

NSKVONotifying_Person 类,修改了class方法,dealloc方法,新增_isKVOA 方法,并且重新实现了两个方法,setMan: setName: 。 这里需要注意,我们修改的是man.age。这里修改的是setMan: 方法,这里我们猜想Man类应该也被修改成了NSKVONotifying_Man, NSKVONotifying_Man 有个新增方法setAge:.
证明我们的猜想
测试代码

    NSLog(@"man 赋值给p前函数");
    Man * man = [[Man alloc]init];
     obj = (__bridge  struct objc_object *)man;
     objClass = obj->isa;
    NSLog(@"%@",objClass);
   methodParse =[[ObjcClassMethodParse alloc]initWithClass:objClass];
    [methodParse print];
    NSLog(@"man 赋值给后前函数");
    p.man = man;
    obj = (__bridge  struct objc_object *)man;
    objClass = obj->isa;
    NSLog(@"%@",objClass);
    methodParse =[[ObjcClassMethodParse alloc]initWithClass:objClass];
    [methodParse print];
    NSLog(@"====end");

结果显示如下

2018-04-26 10:03:35.707870+0800 KVO 实现[52216:1485542] man 赋值给p前函数
2018-04-26 10:03:35.708014+0800 KVO 实现[52216:1485542] Man
2018-04-26 10:03:35.708140+0800 KVO 实现[52216:1485542] name .cxx_destruct
2018-04-26 10:03:35.708243+0800 KVO 实现[52216:1485542] name setAge:
2018-04-26 10:03:35.708338+0800 KVO 实现[52216:1485542] name age
2018-04-26 10:03:35.708483+0800 KVO 实现[52216:1485542] man 赋值给后前函数
2018-04-26 10:03:35.709002+0800 KVO 实现[52216:1485542] NSKVONotifying_Man
2018-04-26 10:03:35.709149+0800 KVO 实现[52216:1485542] name setAge:
2018-04-26 10:03:35.709258+0800 KVO 实现[52216:1485542] name class
2018-04-26 10:03:35.709433+0800 KVO 实现[52216:1485542] name dealloc
2018-04-26 10:03:35.709544+0800 KVO 实现[52216:1485542] name _isKVOA
2018-04-26 10:03:35.709783+0800 KVO 实现[52216:1485542] ====end

这里我们就看出来,我们的猜想是正确的。只不过修改成NSKVONotifying_Man 类是发生在赋值的时候。

我知道我们可以正常的给没有增加kvo监听的属性正常赋值,我们看NSKVONotifying_Man 也没有相关方法,这怎么做到的呢?就是继承啦,我继承Man 类,我就有了Man的所有功能了,NSKVONotifying_Man只要我不认识的功能,那么我就直接调用父类就行了。

这里还有注意一点,我们给man.age keyPath 增加了KVO。Person中,新增的方法是setMan: 传入的参数 找到这个方法找到setAge: 这就有点递归的意思了。

这里还有个方法_isKVOA,这个方法就是给KVO实现的类的标记,只要是KVO生成的类,都是返回YES。
测试代码

-(void)test4{
    Person * p=[[Person alloc]init];
    struct objc_object * obj = (__bridge  struct objc_object *)p;
    Class objClass = obj->isa;
    NSLog(@"%@",objClass);
    ObjcClassMethodParse * methodParse =[[ObjcClassMethodParse alloc]initWithClass:objClass];
    [methodParse print];
    [p addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
    
    [p addObserver:self forKeyPath:@"man.age" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
    obj = (__bridge  struct objc_object *)p;
    objClass = obj->isa;
    NSLog(@"%@",objClass);
    void * mm = &objClass;
    SEL sel = NSSelectorFromString(@"_isKVOA");
    bool is= objc_msgSend((__bridge id)mm, sel);
    NSLog(@"%d" ,is);
    methodParse =[[ObjcClassMethodParse alloc]initWithClass:objClass];
    [methodParse print];
    
}

测试结果

2018-04-26 13:54:50.105262+0800 KVO 实现[11082:1745174] Person
2018-04-26 13:54:50.105516+0800 KVO 实现[11082:1745174] name setMan:
2018-04-26 13:54:50.105666+0800 KVO 实现[11082:1745174] name man
2018-04-26 13:54:50.105963+0800 KVO 实现[11082:1745174] name carId
2018-04-26 13:54:50.106227+0800 KVO 实现[11082:1745174] name setCarId:
2018-04-26 13:54:50.106437+0800 KVO 实现[11082:1745174] name name
2018-04-26 13:54:50.106608+0800 KVO 实现[11082:1745174] name .cxx_destruct
2018-04-26 13:54:50.106883+0800 KVO 实现[11082:1745174] name setName:
2018-04-26 13:54:50.107549+0800 KVO 实现[11082:1745174] NSKVONotifying_Person
2018-04-26 13:54:50.107675+0800 KVO 实现[11082:1745174] 1
2018-04-26 13:54:50.107814+0800 KVO 实现[11082:1745174] name setMan:
2018-04-26 13:54:50.107899+0800 KVO 实现[11082:1745174] name setName:
2018-04-26 13:54:50.108013+0800 KVO 实现[11082:1745174] name class
2018-04-26 13:54:50.108136+0800 KVO 实现[11082:1745174] name dealloc
2018-04-26 13:54:50.108287+0800 KVO 实现[11082:1745174] name _isKVOA

返回的是1 ,YES。(不好意思,这里没有对每个方法进行参数获取,我想着应该是基础的部分,大家自己通过method_copyArgumentType获取下参数看看吧)

实现KVO

新增自定义KVO category

#import <Foundation/Foundation.h>

@interface NSObject (CustomKVO)
-(void)CTKVO_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;
@end

import "NSObject+CustomKVO.h"

import <objc/message.h>

@implementation NSObject (CustomKVO)
-(void)CTKVO_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context{

//动态添加一个类 去重
NSString *oldClassName = NSStringFromClass([self class]);
Class myclass = NSClassFromString(oldClassName);
SEL methodSel = NSSelectorFromString(@"_isKVOA");
if ([oldClassName hasPrefix:@"CTKVO_"]) {
    BOOL is= objc_msgSend(self,methodSel);
    if (!is) {
        NSString *newClassName = [@"CTKVO_" stringByAppendingString:oldClassName];
        const char * newName = [newClassName UTF8String];
        myclass = objc_allocateClassPair([self class], newName, 0);
        class_addMethod(myclass, methodSel, (IMP)_isKVOA, "v@:");
        //注册新添加的这个类
        objc_registerClassPair(myclass);
        ///isa 改变
        object_setClass(self, myclass);
    }
}else{
    NSString *newClassName = [@"CTKVO_" stringByAppendingString:oldClassName];
    const char * newName = [newClassName UTF8String];
    myclass = objc_allocateClassPair([self class], newName, 0);
    class_addMethod(myclass, methodSel, (IMP)_isKVOA, "v@:");
    //注册新添加的这个类
    objc_registerClassPair(myclass);
    ///isa 改变
    object_setClass(self, myclass);
}

NSMutableDictionary * keyPathDic= objc_getAssociatedObject(self, (__bridge const void *)@"kvo_keyPath");
if (!keyPathDic) {
    keyPathDic= [NSMutableDictionary dictionary];
   objc_setAssociatedObject(self, (__bridge const void *)@"kvo_keyPath", keyPathDic, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

NSMutableArray * keyPathValueArr = nil;
if (![keyPathDic objectForKey:keyPath]) {
    keyPathValueArr = [NSMutableArray array];
    [keyPathDic setObject:keyPathValueArr forKey:keyPath];
}

BOOL isHave = NO;
for (NSDictionary * observerInfo in keyPathValueArr) {
   NSObject * obs = [observerInfo objectForKey:@"kvo_observer"];
    if (obs && obs == observer) {
        isHave = YES;
    }
}
if (!isHave) {
    NSMutableDictionary  * observerInfo= [NSMutableDictionary dictionary];
    [observerInfo setObject:observer forKey:@"kvo_observer"];
    [observerInfo setObject:@(options) forKey:@"kvo_options"];
    if (context) {
        [observerInfo setObject:(__bridge id)context forKey:@"kvo_context"];
    }
    [keyPathValueArr addObject:observerInfo];
}

///alloc init
NSArray * array= [keyPath componentsSeparatedByString:@"."];
NSString * methodName = nil;
if (array.count>0) {
methodName = array[0];
}
methodName =[methodName capitalizedString];
methodName =[[@"set" stringByAppendingString:methodName]stringByAppendingString:@":"];
methodSel = NSSelectorFromString(methodName);
class_addMethod(myclass, methodSel, (IMP)CustomKVOIMP, "v@:@");

}

//相当于重写父类的方法
void CustomKVOIMP(id self, SEL sel, id param) {

NSMutableDictionary * keyPathDic= objc_getAssociatedObject(self, (__bridge const void *)@"kvo_keyPath");
NSString * selName = NSStringFromSelector(sel);
selName = [selName substringFromIndex:3];
selName = [selName substringToIndex:selName.length-1];
selName = [selName lowercaseString];
NSMutableArray * selPathArr= [NSMutableArray array];
for (NSString * key in keyPathDic.allKeys) {
   NSString * lKey = [key lowercaseString];
    if ([lKey hasPrefix:selName]) {
        [selPathArr addObject:key];
    }
}

for (NSString *keyPath in selPathArr) {
    ///所有观察者
    NSArray * array = [keyPathDic objectForKey:keyPath];
    //保存当前类
    Class myclass = [self class];
    //将self的isa指针指向父类
    object_setClass(self, class_getSuperclass([self class]));
    objc_msgSend(self, sel,param);
    
    NSArray * arrayPath= [keyPath componentsSeparatedByString:@"."];
    NSMutableArray * apaths =[NSMutableArray arrayWithArray:arrayPath];
    [apaths removeObjectAtIndex:0];
    NSString * path = [apaths componentsJoinedByString:@"."];

    SEL getMethod = NSSelectorFromString(selName);
    

    id getOldValue =objc_msgSend(self, getMethod);
    
    ///这里让path 的下面的对象 更改isa
    for (NSDictionary * info in array) {
        id obs = [info objectForKey:@"kvo_observer"];
        NSNumber * op = [info objectForKey:@"kvo_options"];
        void * content = (__bridge void *)([info objectForKey:@"kvo_context"]);
        NSMutableDictionary * dic=[NSMutableDictionary dictionary];
        if (getOldValue) {
            [dic setObject:getOldValue forKey:@"old"];
        }
        [dic setObject:param forKey:@"new"];

        objc_msgSend(obs,@selector(observeValueForKeyPath:ofObject:change:context:),keyPath,self,dic,content);
        
        if (path) {
            [param CTKVO_addObserver:obs forKeyPath:path options:op.intValue context:content];
        }
    }
    object_setClass(self, myclass);
}

}

BOOL _isKVOA(id self, SEL sel){
return YES;
}

@end

调用

-(void)test2{
Person * p=[[Person alloc]init];
[p CTKVO_addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
[p CTKVO_addObserver:self forKeyPath:@"carId" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
[p CTKVO_addObserver:self forKeyPath:@"man.age" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
Man * man = [[Man alloc]init];
p.man = man;
man.age = @"18";
}

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"keyPath=%@ object=%@ change=%@ context=%@",keyPath,object,change,context);
}

测试结果

2018-04-26 13:33:18.648556+0800 KVO 实现[5534:1718809] keyPath=man.age object=<Person: 0x600000222300> change={
new = "<Man: 0x600000010660>";
old = "<Man: 0x600000010660>";
} context=(null)
2018-04-26 13:33:18.648868+0800 KVO 实现[5534:1718809] keyPath=age object=<Man: 0x600000010660> change={
new = 18;
old = 18;
} context=(null)


上面实现自定义KVO 。不是很细,只是让kvo 增加 keyPath支持。
比如
在kvo中增加
> willChangeValueForKey
> didChangeValueForKey
没有关心,这不影响KVO实现的基本流程。

实现流程

> 1.自定义一个类,继承 [self class]的一个子类
> 2.重写父类的需要观察的属性setter方法
> 3.在实现的setting 方法中解析path 路径,进行递归增加路径上的类
> 4.在子类的setting方法中,让observer 调用observeValueForKeyPath:ofObject:change:context:方法。



















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

推荐阅读更多精彩内容