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太强大啦

推荐阅读更多精彩内容

  • 一、概述 KVO,即:Key-Value Observing,它提供一种机制,当指定的对象的属性被修改后,则其观察...
    DeerRun阅读 9,432评论 11 33
  • 上半年有段时间做了一个项目,项目中聊天界面用到了音频播放,涉及到进度条,当时做android时候处理的不太好,由于...
    DaZenD阅读 2,620评论 0 26
  • 前言 Key-Value-Observer,它来源于观察者模式, 其基本思想(copy于某度)是一个目标对象管理所...
    CholMay阅读 2,775评论 6 16
  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,035评论 0 9
  • 我怎么如此幸运【重中之重/成果论】(第1个月第8天):从自己的工作生活开始落地体验 幸运者:余俊娟 地点:海南省海...
    余俊娟阅读 42评论 0 0
  • 惊讶于你会发那篇文章给我 我开始自恋的想,或许,或多或少,你也开始想一些我们的未来了 不要那么多,只要一点点 你对...
    hniyanzi阅读 148评论 0 0
  • 曾经听说过这么一个故事。两个人在一起去做卖酒生意。一个人很富有,因此他愿意出费买酿酒的大米。而另一个人比较贫穷,所...
    不易悲秋阅读 93评论 0 0
  • 思考,是有着极大魅力的…… 思考,如果按我自己的想法解词的话,我觉得就是用思维来考验自己。思维这个东西,是...
    雪源不懂悲伤阅读 94评论 0 1
  • 晚上,妈妈下班回家开始工作。 小明想:爸爸今天加班,等妈妈工作做完了,我来给她洗洗脚。 不一会,妈妈的工作做完了。...
    池风晓阅读 277评论 2 7