浅谈 KVO 的实现原理

KVO 全称 KeyValueObservingObjective-C 对观察者模式(Observer Pattern)的实现;KVO 提供一种机制,当指定的对象的属性被修改后,则对象就会接受收到通知。简单的说就是每次指定的被观察的对象的属性被修改后,KVO 就会自动通知相应的观察者了。

KVO 实现机制

使用KVO分为三个步骤:

  • 通过 addObserver:forKeyPath:options:context: 方法注册观察者,观察者可以接收 keyPath 属性的变化事件。
  • 在观察者中实现 observeValueForKeyPath:ofObject:change:context: 方法,当 keyPath 属性发生改变后,KVO 会回调这个方法来通知观察者。
  • 当观察者不需要监听时,可以调用 removeObserver:forKeyPath: 方法将 KVO 移除。需要注意的是,调用 removeObserver 需要在观察者消失之前,否则会导致 Crash

KVO 实现原理:

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 ...

KVO 是通过 isa-swizzling 技术实现的,当你观察一个对象时,一个新的类会动态被创建。这个类继承自该对象的原本的类,并重写了被观察属性的 setter 方法。重写的 setter 方法会在调用原 setter 方法之前和之后,通知所有观察对象值的更改。最后把这个对象的 isa 指针指向这个新创建的类,对象变成了新创建的类的实例,而不是原来真正的类。

KVO 缺陷

使用 KVO 时,既需要进行注册成为某个对象属性的观察者,还要在合适的时间点将自己移除,在观察者中实现 observeValueForKeyPath:ofObject:change:context: 方法,并在方法里判断这次是不是自己要观测的属性发生了变化,并且很重要的一点就是 KVO 如果使用不当很容易导致 Crash
例如,重复 addObserverremoveObserver 导致的 CrashObserver 被释放导致的崩溃,keyPath 传错导致的崩溃等。

@interface TestKVOViewController ()

@property (nonatomic, strong) Message *message;

@end

@implementation TestKVOViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.view.backgroundColor = [UIColor whiteColor];
    
    Message *message = [[Message alloc] init];
    [message addObserver:self forKeyPath:@"text" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
    
    message.text = @"hello object-c";
    self.message = message;
}

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

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

2017-06-21 12:29:03.397434+0800 ImplementKVO[18119:266230] [general] Caught exception during autorelease pool drain NSRangeException: Cannot remove an observer <TestKVOViewController 0x7faf7b502610> for the key path "text" from <Message 0x60400001eef0> because it is not registered as an observer. userInfo: (null)
2017-06-21 12:29:03.402598+0800 ImplementKVO[18119:266230] *** Terminating app due to uncaught exception 'NSRangeException', reason: 'Cannot remove an observer <TestKVOViewController 0x7faf7b502610> for the key path "text" from <Message 0x60400001eef0> because it is not registered as an observer.'
*** First throw call stack:
(
    0   CoreFoundation                      0x000000010efa11e6 __exceptionPreprocess + 294
    1   libobjc.A.dylib                     0x000000010e636031 objc_exception_throw + 48
    2   CoreFoundation                      0x000000010f016975 +[NSException raise:format:] + 197
    3   Foundation                          0x000000010e061e43 -[NSObject(NSKeyValueObserverRegistration) _removeObserver:forProperty:] + 488
    4   Foundation                          0x000000010e061c03 -[NSObject(NSKeyValueObserverRegistration) removeObserver:forKeyPath:] + 84
    5   ImplementKVO                        0x000000010dd2de9c -[TestKVOViewController dealloc] + 172
    6   UIKit                               0x000000010f406f74 __destroy_helper_block_.150 + 80
    7   libsystem_blocks.dylib              0x0000000112a7098a _Block_release + 111
    8   UIKit                               0x000000010f4e01e5 -[UIViewAnimationBlockDelegate .cxx_destruct] + 58
    9   libobjc.A.dylib                     0x000000010e634920 _ZL27object_cxxDestructFromClassP11objc_objectP10objc_class + 127
    10  libobjc.A.dylib                     0x000000010e640502 objc_destructInstance + 124
    11  libobjc.A.dylib                     0x000000010e640539 object_dispose + 22
    12  libobjc.A.dylib                     0x000000010e64aa6e _ZN11objc_object17sidetable_releaseEb + 202
    13  CoreFoundation                      0x000000010ef0464d -[__NSDictionaryI dealloc] + 125
    14  libobjc.A.dylib                     0x000000010e64aa6e _ZN11objc_object17sidetable_releaseEb + 202
    15  libobjc.A.dylib                     0x000000010e64b1b2 _ZN12_GLOBAL__N_119AutoreleasePoolPage3popEPv + 860
    16  CoreFoundation                      0x000000010eeeb136 _CFAutoreleasePoolPop + 22
    17  CoreFoundation                      0x000000010ef27eae __CFRunLoopRun + 2350
    18  CoreFoundation                      0x000000010ef2730b CFRunLoopRunSpecific + 635
    19  GraphicsServices                    0x0000000114119a73 GSEventRunModal + 62
    20  UIKit                               0x000000010f41e057 UIApplicationMain + 159
    21  ImplementKVO                        0x000000010dd2dfef main + 111
    22  libdyld.dylib                       0x00000001129fe955 start + 1
    23  ???                                 0x0000000000000001 0x0 + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException

手动实现 KVO

typedef void(^ObservingBlock)(id observedObject, NSString *observedKey, id oldValue, id newValue);

@interface NSObject (KVO)

- (void)addObserver:(NSObject *)observer
forKey:(NSString *)key
withBlock:(ObservingBlock)block;

- (void)removeObserver:(NSObject *)observer forKey:(NSString *)key;

@end

接下来,实现 addObserver:forKey:withBlock: 方法:

  • 检查对象的类有没有相应的 setter 方法。如果没有抛出异常;
  • 检查对象 isa 指向的类是不是一个 KVO 类。如果不是,新建一个继承原来类的子类,并把 isa 指向这个新建的子类;
  • 检查对象的 KVO 类重写过没有这个 setter 方法。如果没有,添加重写的 setter 方法;
  • 添加这个观察者;
/**
 1. 通过Method判断是否有这个key对应的selector,如果没有则Crash。
 2. 判断当前类是否是KVO子类,如果不是则创建,并设置其isa指针。
 3. 如果没有实现,则添加Key对应的setter方法。
 4. 将调用对象添加到数组中。
 */
- (void)addObserver:(NSObject *)observer
             forKey:(NSString *)key
          withBlock:(ObservingBlock)block {
    
    SEL setterSelector = NSSelectorFromString(setterForGetter(key));
    Method setterMethod = class_getInstanceMethod([self class], setterSelector);
    if (!setterMethod) {
        NSString *reason = [NSString stringWithFormat:@"Object %@ does not have a setter for key %@", self, key];
        @throw [NSException exceptionWithName:NSInvalidArgumentException
                                       reason:reason
                                     userInfo:nil];
        
        return;
    }
    
    Class clazz = object_getClass(self);
    NSString *clazzName = NSStringFromClass(clazz);
    
    // if not an KVO class yet
    if (![clazzName hasPrefix:kKVOClassPrefix]) {
        clazz = [self makeKvoClassWithOriginalClassName:clazzName];
        object_setClass(self, clazz);
    }
    
    // add our kvo setter if this class (not superclasses) doesn't implement the setter?
    if (![self hasSelector:setterSelector]) {
        const char *types = method_getTypeEncoding(setterMethod);
        class_addMethod(clazz, setterSelector, (IMP)kvo_setter, types);
    }
    
    ObservationInfo *info = [[ObservationInfo alloc] initWithObserver:observer Key:key block:block];
    NSMutableArray *observers = objc_getAssociatedObject(self, (__bridge const void *)(kKVOAssociatedObservers));
    if (!observers) {
        observers = [NSMutableArray array];
        objc_setAssociatedObject(self, (__bridge const void *)(kKVOAssociatedObservers), observers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    [observers addObject:info];
}

第一步,先通过 setterForGetter() 方法获得相应的 setter 的名字 (SEL) 。也就是把 key 的首字母大写,然后前面加上 set 后面加上 : ,这样 key 就变成了 setKey: 。然后再用 class_getInstanceMethod 去获得 setKey: 的实现 (Method) 。如果没有,自然要抛出异常。

第二步,我们先看类名有没有我们定义的前缀。如果没有,我们就去创建新的子类,并通过 object_setClass() 修改 isa 指针。

/**
 1. 判断是否存在KVO类,如果存在则返回。
 2. 如果不存在,则创建KVO类。
 3. 重写KVO类的class方法,指向自定义的IMP。
 */
- (Class)makeKvoClassWithOriginalClassName:(NSString *)originalClazzName {
    NSString *kvoClazzName = [kKVOClassPrefix stringByAppendingString:originalClazzName];
    Class clazz = NSClassFromString(kvoClazzName);
    
    if (clazz) {
        return clazz;
    }
    
    // class doesn't exist yet, make it
    Class originalClazz = object_getClass(self);
    Class kvoClazz = objc_allocateClassPair(originalClazz, kvoClazzName.UTF8String, 0);
    
    // grab class method's signature so we can borrow it
    Method clazzMethod = class_getInstanceMethod(originalClazz, @selector(class));
    const char *types = method_getTypeEncoding(clazzMethod);
    class_addMethod(kvoClazz, @selector(class), (IMP)kvo_class, types);
    
    objc_registerClassPair(kvoClazz);
    
    return kvoClazz;
}

当动态创建新的类需要调用 objc/runtime.h 中定义的 objc_allocateClassPair() 函数。传一个父类,类名,然后额外的空间(通常为 0),它返回给你一个类。然后就给这个类添加方法,也可以添加变量。这里,我们只重写了 class 方法。跟 Apple 一样,这时候我们也企图隐藏这个子类的存在。最后 objc_registerClassPair() 告诉 Runtime 这个类的存在。

第三步,重写 setter 方法。新的 setter 在调用原 setter 方法后,通知每个观察者(调用之前传入的 block ):

static void kvo_setter(id self, SEL _cmd, id newValue) {
    NSString *setterName = NSStringFromSelector(_cmd);
    NSString *getterName = getterForSetter(setterName);
    
    if (!getterName) {
        NSString *reason = [NSString stringWithFormat:@"Object %@ does not have setter %@", self, setterName];
        @throw [NSException exceptionWithName:NSInvalidArgumentException
                                       reason:reason
                                     userInfo:nil];
        return;
    }
    
    id oldValue = [self valueForKey:getterName];
    
    struct objc_super superclazz = {
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self))
    };
    
    // cast our pointer so the compiler won't complain
    void (*objc_msgSendSuperCasted)(void *, SEL, id) = (void *)objc_msgSendSuper;
    
    // call super's setter, which is original class's setter method
    objc_msgSendSuperCasted(&superclazz, _cmd, newValue);
    
    // look up observers and call the blocks
    NSMutableArray *observers = objc_getAssociatedObject(self, (__bridge const void *)(kKVOAssociatedObservers));
    for (ObservationInfo *each in observers) {
        if ([each.key isEqualToString:getterName]) {
            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                each.block(self, getterName, oldValue, newValue);
            });
        }
    }
}

在这里会对 objc_msgSendSuper 进行类型转换。因为在 Xcode 6 之后, LLVM 会对 objc_msgSendSuper 以及 objc_msgSend 做严格的类型检查,如果不做类型转换。Xcode 会提示有 too many arguments 的错误。(在 WWDC 2014 的视频 What new in LLVM 中有提到过这个问题。)

最后一步,把这个观察的相关信息存在 associatedObject 里。观察的相关信息(观察者,被观察的 key , 和传入的 block )封装在 ObservationInfo 类里。

@interface ObservationInfo : NSObject

@property (nonatomic, weak) NSObject *observer;
@property (nonatomic, copy) NSString *key;
@property (nonatomic, copy) ObservingBlock block;

@end

@implementation ObservationInfo

- (instancetype)initWithObserver:(NSObject *)observer
                             Key:(NSString *)key
                           block:(ObservingBlock)block {
    self = [super init];
    if (self) {
        _observer = observer;
        _key = key;
        _block = block;
    }
    return self;
}

@end

KVO crash 防护方案

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

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,725评论 0 9
  • 1.ios高性能编程 (1).内层 最小的内层平均值和峰值(2).耗电量 高效的算法和数据结构(3).初始化时...
    欧辰_OSR阅读 29,401评论 8 265
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,107评论 1 32
  • KVO概述 键值观察Key-Value-Observer就是观察者模式。 观察者模式的定义:一个目标对象管理所有依...
    小希嘻阅读 628评论 0 0
  • 人们总是说 ,年纪轻轻的,不要有太多的追忆。还有这大把的青春时光,不是用来浪费在过去里的。只是,我偏偏就是那么一个...
    水清浅阅读 248评论 0 0