Runtime窥探 (五)| KVO底层实现

前言

怎么看待励志的书籍?
看再多,那都是别人的人生

踏实走自己的路

一、KVO介绍

KVO(键值监听 Key-Value Observing),是OC观察者设计模式的一种具体实现。

作用:当指定的对象的属性被修改后,观察者就会接受到监听通知的消息,开发者可以根据收到消息来进行自定义处理。

二、KVO使用

KVO的使用步骤大概分为三步:

  • 1.添加观察者,实施监听
  • 2.监听方法中处理属性发生的变化
  • 3.移除观察者

具体例子如下:

#pragma mark - ------------runtime在KVO中的使用--------------
- (void)objc_KVO{
    self.num = 0;
    self.p = [[Person alloc] init];
    self.p.name = @"firstName";
    //添加观察者
    [self.p addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionInitial context:NULL];
//    [self.p test_addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
}

//监听方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"%@-%@-%@", keyPath, change,self.p);
}

//移除监听
- (void)dealloc{
    [self.p removeObserver:self forKeyPath:@"name"];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.num++;
    self.p.name = [NSString stringWithFormat:@"name%ld",self.num];
}

三、KVO的底层原理

Apple官方文档

Apple 的文档有简单提到过 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 指针会指向一个中间类,而不是原来真正的类。Apple并没有说明具体的实现细节。那我们就来尝试猜测KVO的底层实现原理。

验证原理

1.生成中间类NSKVONotifying_XXX

对上面的例子加4个断点:分别在添加观察者之前、之后、以及监听方法的地方。

断点

根据上面的断点处,每个断点打印出被观察的类以及类的isa指针,结果如下:

断掉打印

可以看出在我们添加观察者之后,被观察的类的isa变成了NSKVONotifying_Person,也就是说我们的类person变成了另外一个类。其实这个NSKVONotifying_XXX类是我们被观察者的子类,是动态创建的。
那什么时候我们的类会变成原始的类呢?那肯定是我们调用removeObserver之后,就会变成原始的类。

这也验证了Apple官方文档中,生成了一个中间类:NSKVONotifying_XXX

2.KVO只监听setter方法

我们正常定义一个属性如下,系统会自动生成一个setNamegetName的方法,

@property (nonatomic, strong) NSString *name;

现在我们自定义set方法

@property (nonatomic, strong, setter=setname:) NSString *name;

//实现
- (void)setname:(NSString *)name{
    _name = name;
}

注意这里的方法setname不是标准命名的set方法,当我们点击屏幕的时候修改name的时候,监听方法并没有执行。而当我们是正常的setName方法,监听方法就会执行。同样的方法验证getter方法,发现跟getter方法没有关系。

也就是说KVO监听过程是通过setter方法来操作的。也就是说如果的setter方法命名不标准(set首字母大写)或者没有实现,那么KVO监听则无效。

3.KVO重写了class方法

上面的断点中,添加观察者之前是person类,添加观察者之后是NSKVONotifying_Person类。但是我们打印self.p却发现还是打印出来person而不是NSKVONotifying_Person,其中的isa是中间类。这个是苹果没法没有隐藏的,才是真正的类。name只是苹果重写了class方法,把class中结构体的name指定为先前的类名而已,为了隐藏中间的类,返回还是真正的中间类。当我们removeobserve后,isa指针又会指向我们原始的类。把中间类销毁了。

KVO的原理说明

当我们添加KVO监听对象A时,KVO机制动态创建一个对象A所属类的子类(NSKVONotifying_A),并且为这个子类动态添加一个被观察属性keyPath的setter方法,然后把A所属类的isa指针指向新建子类,所以当我们调用A所属类的setter方法其实调用的是新建子类的setter方法,setter方法随后负责通知观察对象属性的改变状况以及调用A所属类的setter方法。

自定义模拟KVO底层实现

不关注KVO方法中后两个参数,只是一些if和else填充的回调参数,这些就是一些细节考虑,这里只是关注和模拟kvo的流程实现,不考虑一些容错处理,只关注原理实现,学习这种思想,知道kvo底层是如何运转起来的就可以了,我们没必要重写一个kvo机制,因为苹果把这个封装的很好了。当然有兴趣也可以尝试实现一个完整的kvo。下面代码基本上可以把整个流程写清楚了,看懂下面就差不多了。伪代码我就不写了。

#import "NSObject+KVO.h"
#import <objc/message.h>

NSString *const kJYKVOClassPrefix = @"JYKVOClassPrefix_";//自定义类前缀
NSString *const kJYKVOAssociatedParameDict = @"JYKVOAssociatedParameDict";//参数key
NSString *const kJYKVOObservers = @"JYKVOObservers";  //观察者key
NSString *const kJYKVOOldValue = @"JYKVOOldValue";   //旧值key
NSString *const kJYKVOKeyPath = @"JYKVOKeyPath";     //KeyPathkey
NSString *const kJYKVOSetter = @"JYKVOSetter";      //setter方法key

@implementation NSObject (KVO)

//添加观察者
//下面采用面向过程编程方式,所以下面这个函数有点长,只是为了让大家看清整个流程,
- (void)test_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context{
    
    //获取self所属的类
    Class originalClass = object_getClass(self);
    NSString *originalClassStr = NSStringFromClass(originalClass);
    
    //获取setter方法字符串
    NSString *setter = [NSString stringWithFormat:@"set%@:",[keyPath capitalizedString]];
    
    //1.判断被观察者(self)对应的keyPath有没有setter方法,没有则返回,添加观察者失败
    BOOL haveSetter = NO;
    unsigned int methodCount = 0;
    Method *methodList = class_copyMethodList(originalClass, &methodCount);
    unsigned int i;
    for(i = 0; i < methodCount; i++) {
        NSString *methodName = NSStringFromSelector(method_getName(methodList[i]));
        if ([methodName isEqualToString:setter]) {
            haveSetter = YES;
            break;
        }
    }
    free(methodList);
    if (!haveSetter) {
        return;
    }
    
    //2.动态生成一个前缀为JYKVOClassPrefix_的类,这个新类是self的子类
    NSString *newClassStr = [NSString stringWithFormat:@"%@%@",kJYKVOClassPrefix,originalClassStr];
    Class  newClass = objc_allocateClassPair(originalClass, newClassStr.UTF8String, 0);
    
    //3.为新类添加setter方法
    class_addMethod(newClass, NSSelectorFromString(setter), (IMP)newClassSetterMethod, "v@:@");
    //方法属性添加完成后注册这个类才算创建成功可以使用
    objc_registerClassPair(newClass);
    
    //4.修改被观察者(self)的isa指针,让isa指针指向新建的子类。也就是说被观察者(self)现在所属于的类是新建的子类,
    object_setClass(self, newClass);
    
    //5.使用关联值保存信息
    NSMutableDictionary *parameDict = objc_getAssociatedObject(self, (__bridge const void *)(kJYKVOAssociatedParameDict));
    if (!parameDict) {
        parameDict = [NSMutableDictionary dictionary];
    }
    if (keyPath) {
        [parameDict setValue:setter forKey:kJYKVOSetter];
        [parameDict setValue:keyPath forKey:kJYKVOKeyPath];
    }
    //kvc获取旧值
    id oldValue = [self valueForKeyPath:keyPath];
    if (oldValue) {
        [parameDict setValue:oldValue forKey:kJYKVOOldValue];
    }

    //观察者数组
    if ([parameDict objectForKey:kJYKVOObservers] != nil) {
        NSMutableArray *observers = [parameDict objectForKey:kJYKVOObservers];
        [observers addObject:observer];
        [parameDict setValue:observers forKey:kJYKVOObservers];
    }else{
        NSMutableArray *observers = [NSMutableArray array];
        [observers addObject:observer];
        [parameDict setValue:observers forKey:kJYKVOObservers];
    }
    //关联
    objc_setAssociatedObject(self, (__bridge const void *)kJYKVOAssociatedParameDict, parameDict, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

}


//新类setter方法的实现
static void newClassSetterMethod(id self, SEL _cmd, id newValue){
    
    //1.设置当前子类指向父类(原始的类)
    //获取当前类,这个class就是我们新建的子类
    Class newClass = object_getClass(self);
    
    //获取当前类的父类,也就是我们最原始的类
    Class superClass = class_getSuperclass(newClass);
    
    //把当前子类设置为父类,也就是设置成我们原始的类,让我们原始的类来调用setter方法,这样就正常的调用我们原始的setter方法
    object_setClass(self, superClass);
    
    
    //2.调用父类的setter方法
    //获取关联的参数
    NSDictionary *parameDict = objc_getAssociatedObject(self, (__bridge const void *)(kJYKVOAssociatedParameDict));
    NSString *setter = [parameDict objectForKey:kJYKVOSetter];
    //消息发送调用setter方法
    objc_msgSend(self, NSSelectorFromString(setter), newValue);
    
    NSMutableArray *observers = [parameDict objectForKey:kJYKVOObservers];
    NSString *keyPath = [parameDict objectForKey:kJYKVOKeyPath];
    NSMutableDictionary<NSKeyValueChangeKey, id> *change = [NSMutableDictionary dictionary];
    
    change[NSKeyValueChangeNewKey] = newValue;
    change[NSKeyValueChangeOldKey] = [parameDict objectForKey:kJYKVOOldValue] != nil?[parameDict objectForKey:kJYKVOOldValue]:NULL;

    for (NSObject *observer in observers) {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            objc_msgSend(observer, @selector(observeValueForKeyPath:ofObject:change:context:),keyPath, self, change, NULL);
        });
    }
    
    object_setClass(self, newClass);
    [parameDict setValue:newValue forKey:kJYKVOOldValue];
    objc_setAssociatedObject(self, (__bridge const void *)kJYKVOAssociatedParameDict, parameDict, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    
}

//+ (Class)class{
//
//    Class cls = object_getClass(self);
//    
//    cls->name = "截取原始类名";//这里苹果不允许我们修改,runtime源码中可以查看和修改
//    return self;
//}
@end

四、KVO的总结

通过上面我们知道KVO的底层原理,同时这种设计思想也值得我们学习,通过中间类来伪装很是巧妙。希望根据这种思想来解决我们实际项目中的问题。后面要讲解的AOP编程的开源库Aspect也是使用这种思想。期待你们能模仿一些思想写出优秀的开源库。。只能说Runtime太强大啦

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

推荐阅读更多精彩内容

  • 一、概述 KVO,即:Key-Value Observing,它提供一种机制,当指定的对象的属性被修改后,则其观察...
    DeerRun阅读 10,038评论 11 33
  • 本篇会对KVO的实现进行探究,不涉及太多KVO的使用方法,但是会有一些使用时的思考。 一、使用上的疑问 1.key...
    奋拓达阅读 500评论 0 2
  • 上半年有段时间做了一个项目,项目中聊天界面用到了音频播放,涉及到进度条,当时做android时候处理的不太好,由于...
    DaZenD阅读 3,012评论 0 26
  • 前言 Key-Value-Observer,它来源于观察者模式, 其基本思想(copy于某度)是一个目标对象管理所...
    CholMay阅读 3,378评论 6 18
  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,679评论 0 9