Runtime梳理(二)KVO原理及实现

  • 简单使用
  • 底层原理
  • 简单实现

简单使用

@interface ViewController ()
@property (nonatomic, strong) Person * p1;
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    Person * p1 = [[Person alloc] init];
    p1.name = @"Tom";
    self.p1 = p1;
    [p1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionOld
     |NSKeyValueObservingOptionNew context:nil];
    p1.name = @"Jack";
  // 没有self.p1 = p1; 需要将观察者移除,不然程序会奔溃
  // [p1 removeObserver:self forKeyPath:@"name"];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
                        change:(NSDictionary<NSKeyValueChangeKey,id> *)change
                        context:(void *)context{
    if ([keyPath isEqualToString:@"name"]) {
        NSLog(@"name 发生了变化 ---");
    }
}

- (void)dealloc{
    [self.p1 removeObserver:self forKeyPath:@"name"];
}

通过上面的代码我们就完成了对p1对象的name属性的监听,name的改变都会通知观察者。

底层原理

KVO的全称Key-Value Observing,是我们俗称的观察者模式,官方的文档是这样描述的:

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.

不难看出,KVO的实现基于isa-swizzling,当被观察对象属性发生变化时,原本指向被观察对象类的isa指针,指向了一个中间的类,并不是真正的被观察的类。最后也提到了,不要通过isa来判断类的继承关系,而是通过class方法来判断一个类的实例。

为了探究KVO的实现,我们重写了Person的description方法

-(NSString *)description{
    IMP setName = class_getMethodImplementation(object_getClass(self),
                                                              @selector(setName:));
    Class class = [self class];
    Class getClass = object_getClass(self);
    Class superClass = class_getSuperclass(getClass);
    return [NSString stringWithFormat:@"\n class -> %@ ,\
                                 \n object_getClass -> %@ , \
                                \n class_getSuperclass -> %@ ,\
                                \n setName -> %p",class,getClass,superClass,setName];
}

在添加观察者之前和之后分别做了打印:

NSLog(@"添加之前 :%@",p1);
[p1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionOld
     |NSKeyValueObservingOptionNew context:nil];
NSLog(@"添加之后 :%@",p1);

运行结果如下:

添加之前 :
 class -> Person ,                                          
 object_getClass -> Person ,                                           
 class_getSuperclass -> NSObject ,                                          
 setName -> 0x1048d7340
添加之后 :
 class -> Person ,                                          
 object_getClass -> NSKVONotifying_Person ,                                           
 class_getSuperclass -> Person ,                                          
 setName -> 0x1049dbc3d
name 发生了变化 ---

对比发现:

  • class类型都是Person。
  • 添加KVO之后,p1的真实类型是NSKVONotifying_Person。
  • NSKVONotifying_Person的父类是Person。
  • 添加KVO之后,setName方法实现的地址也发生了改变。

到此,有没有一种"拨开云雾见天明"的感脚,我们可以大胆的猜想一下它的实现流程:

在对p1对象name属性使用KVO后,在程序运行的过程中生成了一个NSKVONotifying_Person的类,这个类继承自Person类,把p1对象的isa指针指向了生成的NSKVONotifying_Person类,并且重写了setName方法,重写了class方法,隐藏了它真实的类型,使我们误以为还是原来的Person类。

官方文档中对KVO的实现细节避之不提,不过这也符合苹果的一贯作风,但是我们可以看到KVO通知观察者的方法有两种:
1.Automatic Change Notification
通过以下几种方法可以自动通知观察者,无需添加额外的代码

[p1 setName:@"Jack"];
[p1 setValue:@"Jack" forKey:@"name"];
[p1 setValue:@"Jack" forKeyPath:@"name"];

2.Manual Change Notification

In this case, you override the NSObject implementation of automaticallyNotifiesObserversForKey:. For properties whose automatic notifications you want to preclude, the subclass implementation of automaticallyNotifiesObserversForKey: should return NO. A subclass implementation should invoke super for any unrecognized keys. The example in Listing 2enables manual notification for the balance property, allowing the superclass to determine the notification for all other keys

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
    BOOL automatic = NO;
    if ([theKey isEqualToString:@"balance"]) {
        automatic = NO;
    }
    else {
        automatic = [super automaticallyNotifiesObserversForKey:theKey];
    }
    return automatic;
}

To implement manual observer notification, you invoke [willChangeValueForKey:] before changing the value, and [didChangeValueForKey:] after changing the value

- (void)setBalance:(double)theBalance {
    [self willChangeValueForKey:@"balance"];
    _balance = theBalance;
    [self didChangeValueForKey:@"balance"];
}

手动调用的话,在automaticallyNotifiesObserversForKey:方法返回NO,并且在set方法中,值改变之前调用[willChangeValueForKey:],值改变之后调用[didChangeValueForKey:]

- (void)setName:(NSString *)name{
    name = name;
}

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

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

打印结果:

willChangeValueForKey: --- begin
willChangeValueForKey: --- end
didChangeValueForKey: --- begin
name 发生了变化 ---
didChangeValueForKey: --- end

不管是Automatic Change Notification也好还是Manual Change Notification也好,最后都调用了[willChangeValueForKey:],和[didChangeValueForKey:] 方法,唯一的区别就是Automatic Change Notification自动帮我们调用了这两个方法。而[didChangeValueForKey:] 方法内部应该是调用了监听者observeValueForKeyPath: ofObject:change:context:的方法,从而在被监听对象属性值发生改变的时候,通知观察者达到监听的目的。

现在对KVO的实现流程应该有了更加清楚地认识了吧?没有的话,请看下面的图:

KVO实现流程图.png

简单实现

首先,创建了一个NSObject的分类,添加了一个监听的方法,在这个方法里主要做了以下几件事情(以Person name为例):

  • 动态的创建一个XSYKVONotifying_Person类,这个类继承自Person。
  • 将被观察对象的isa指针指向这个新创建的类,并且重写这个类的class方法,隐藏它的真实类型。
  • 重写被观察对象的set方法,通知观察者属性值的变化。
@implementation NSObject (KVO)
- (void)xsy_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath 
options:(NSKeyValueObservingOptions)options context:(void *)context{
 
    NSString * firstString = [[keyPath substringToIndex:1] uppercaseString];
    NSString * otherString = [[keyPath substringFromIndex:1] lowercaseString];
    NSString * setKeyPath  = [NSString stringWithFormat:@"set%@%@:",firstString,otherString];
    SEL keySel = NSSelectorFromString(setKeyPath);
    Method keyMethod = class_getInstanceMethod([self class], keySel);
  
    // 获取当前对象的类
    Class oriClass = [self class];
    NSString * oriString = NSStringFromClass(oriClass);
    NSString * kvoString = [NSString stringWithFormat:@"XSYKVONotifying_%@",oriString];
    const char * kvoClassName = [kvoString cStringUsingEncoding:NSUTF8StringEncoding];
    Class kvoClass = objc_getClass(kvoClassName);
    if (!kvoClass) {
        kvoClass = objc_allocateClassPair([oriClass class], kvoClassName, 0);
        objc_registerClassPair(kvoClass);
        object_setClass(self, [kvoClass class]);
    Method method = class_getInstanceMethod([kvoClass class], keySel);
    Method instanceMethod = class_getInstanceMethod([oriClass class], @selector(class));
    Method classMethod = class_getClassMethod([oriClass class], @selector(class));
    
    // 重写set方法
    const char *types = method_getTypeEncoding(class_getInstanceMethod([Person class], @selector(setName:)));
    BOOL result = class_addMethod(student, @selector(setName:), (IMP)(xsy_setter), types);
    
    Method setUndefined = class_getInstanceMethod([oriClass class], @selector(setValue:forUndefinedKey:));
    Method valueUndefined = class_getInstanceMethod([oriClass class], @selector(valueForUndefinedKey:));
    
    BOOL result1 = class_addMethod(kvoClass, @selector(setValue:forUndefinedKey:), (IMP)(xsy_setValueForUndefinedKey), nil);
    if (!result1) {
        method_setImplementation(setUndefined, (IMP)(xsy_setValueForUndefinedKey));
    }
    
    BOOL result2 = class_addMethod(kvoClass, @selector(valueForUndefinedKey:), (IMP)(xsy_valueForUndefinedKey), nil);
    if (!result2) {
        method_setImplementation(valueUndefined, (IMP)(xsy_valueForUndefinedKey));
    }
    
    BOOL result3 = class_addMethod(kvoClass, @selector(class), (IMP)(xsy_class), nil);
    if (!result3) {
       // 重写class方法
        method_setImplementation(classMethod, (IMP)(xsy_class));
    }
   
   // 将观察者和被观察的对象关联
    objc_setAssociatedObject(self, (__bridge const void *)(observerKey), observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

set方法的实现

static void xsy_setter(id self, SEL _cmd, id value){
    NSString * setterName = NSStringFromSelector(_cmd);
    // setName:
    NSString * keyName = [setterName substringFromIndex:3];
    NSString * getterName = [keyName substringWithRange:NSMakeRange(0, keyName.length-1)];
    NSString * firstL = [[getterName substringToIndex:1] lowercaseString];
    NSString * otherL = [getterName substringFromIndex:1];
    getterName = [NSString stringWithFormat:@"%@%@",firstL,otherL];
    
    id oldValue = [self valueForKey:getterName];

    struct objc_super superClass = {
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self))
    };
    
    // 调用父类的方法
    void (*objc_msgSendSuperCasted)(void *, SEL, id) = (void *)objc_msgSendSuper;
    objc_msgSendSuperCasted(&superClass, _cmd, value);
  
   // 通知观察者回调
    id observer = objc_getAssociatedObject(self, (__bridge const void *)(observerKey));
    NSDictionary<NSKeyValueChangeKey,id> * change = oldValue ? @{NSKeyValueChangeNewKey : value, NSKeyValueChangeOldKey : oldValue} : @{NSKeyValueChangeNewKey : value};
    [observer observeValueForKeyPath:getterName ofObject:self change:change context:nil];
}

viewController

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.p = [[Person alloc] init];
    self.p.name = @"小鲨鱼";
  
    [self.p xsy_addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|
NSKeyValueObservingOptionOld context:nil];
}


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

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

这样我们在点击屏幕的时候就会看到控制台的打印,大致实现了基本流程。

总结

KVO的实现机制过程中,Runtime会动态的创建一个类,这个类以NSKVONotifying_类名命名,并且继承自类名,重写了class方法隐藏了真正类型,将被观察对象的isa指针指向了这个新创建的类,重写了被观察对象keyPath的set方法,通知观察者的回调方法,从而实现了keyPath值变化的监听。

参考:
官方文档
iOS底层原理总结 - 探寻KVO本质

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

推荐阅读更多精彩内容