KVO的原理初探及应用

1. 实现原理

关于KVO的实现原理,苹果有如下说明:

Key-Value Observing Implementation Details
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.

从官方的说明来看:

  • 采用了isa-swizzling技术,当有属性被观察则会将对象的isa指向一个中间类而不是本身的类;这样做的好处就是不会影响到实例对象的原来的类
  • 同时我们不应该依赖于isa去判断类的继承关系,应该使用class方法去判断;这里从侧面说明class返回的还是原来的类

接下来通过例子来探究一下系统的实现

1.1 isa-swizzling

isa-swizzling简单理解为:通过修改isa的指向,是isa指向另一个类来达到对对象的一些行为的修改

下面通过例子来看一下:
被观察对象TestKVOObject

@interface TestKVOObject : NSObject

@property (nonatomic, copy) NSString *testString;

@end

@implementation TestKVOObject

@end

添加观察者

- (void)testSystemKVO {
    TestKVOObject *test = [TestKVOObject new];
    [test addObserver:self forKeyPath:@"testString" options:NSKeyValueObservingOptionNew context:nil];
    test.testString = @"testString";
}

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

在添加观察者前打上断点:


图片.png

在添加观察者后打上断点:


图片.png

发现实例对象的isa的指向变了,变成了NSKVONotifying_TestKVOObject

用lldb打印一下看新创建一个TestKVOObject的实例,看看它的isa是否有变化,可以看到系统的kvo的isa-swizzling只对被观察的对象实例产生了影响,并且class方法返回的还是原来的类;
原类在实例化一个对象出来它的isa指向的还是原来的类对象。

(lldb) p test->isa
(Class) $2 = NSKVONotifying_TestKVOObject
(lldb) p [TestKVOObject new]->isa
(__unsafe_unretained Class) $3 = TestKVOObject
(lldb) p [test class]
(Class) $4 = TestKVOObject
(lldb) 
1.2 NSKVONotifying_XXX

接下来我们看看被观察对象的isa指向的一个新类NSKVONotifying_XXX是怎样设计的;通过
_shortMethodDescription命令在lldb打印出该类的方法及实例

(lldb) po [NSKVONotifying_TestKVOObject _shortMethodDescription]
     <NSKVONotifying_TestKVOObject: 0x600001e558c0>:
     in NSKVONotifying_TestKVOObject:
         Instance Methods:
             - (void) setTestString:(id)arg1; (0x7fff207b5b57)
             - (Class) class; (0x7fff207b4662)
             - (void) dealloc; (0x7fff207b440b)
             - (BOOL) _isKVOA; (0x7fff207b4403)
     in TestKVOObject:
         Properties:
             @property (copy, nonatomic) NSString* testString;  (@synthesize testString = _testString;)
         Instance Methods:
             - (id) testString; (0x1071074f0)
             - (void) setTestString:(id)arg1; (0x107107540)
             - (void) dealloc; (0x107107490)
             - (void) .cxx_destruct; (0x1071075a0)
     (NSObject ...)

可以看到新的类中有4个实例方法

- (void) setTestString:(id)arg1; (0x7fff207b5b57)
- (Class) class; (0x7fff207b4662)
- (void) dealloc; (0x7fff207b440b)
- (BOOL) _isKVOA; (0x7fff207b4403)

接下来一个个看这些方法是干了啥

1.2.1 set方法
通过lldb打印一下

(lldb) dis -s 0x7fff207b5b57
Foundation`_NSSetObjectValueAndNotify:
    0x7fff207b5b57 <+0>:  pushq  %rbp
    0x7fff207b5b58 <+1>:  movq   %rsp, %rbp
    0x7fff207b5b5b <+4>:  pushq  %r15
    0x7fff207b5b5d <+6>:  pushq  %r14
    0x7fff207b5b5f <+8>:  pushq  %r13
    0x7fff207b5b61 <+10>: pushq  %r12
    0x7fff207b5b63 <+12>: pushq  %rbx
    0x7fff207b5b64 <+13>: subq   $0x58, %rsp
    0x7fff207b5b68 <+17>: movq   %rdx, -0x78(%rbp)
    0x7fff207b5b6c <+21>: movq   %rsi, %r15
    0x7fff207b5b6f <+24>: movq   %rdi, %r13

内部实现调用的是_NSSetObjectValueAndNotify,下个符号断点看看是怎么实现的:

图片.png

或者使用hopper查看Foundation.framework的伪代码


图片.png

通过hopper我们还发现对于不同的数据类型都有不同的set方法实现,我们定义的是NSString类型,则调用的是_NSSetObjectValueAndNotify,如果你定义的是BOOL那么就会是_NSSetBoolValueAndNotify

大致的实现就是:

  • willChangeValueForKey
  • 调用原始的set方法
  • didChangeValueForKey

didChangeValueForKey内部实现

void -[NSObject(NSKeyValueObserverNotification) didChangeValueForKey:](int arg0) {
    _NSKeyValueDidChangeWithPerThreadPendingNotifications(arg0, rdx, 0x0, _NSKeyValueDidChangeBySetting, 0x0);
    return;
}

为了看清didChangeValueForKey内部的实现,我在类中重写了一下该方法,你也可以断点调试一下

@implementation TestKVOObject

- (void)didChangeValueForKey:(NSString *)key {
    [super didChangeValueForKey:key];
}

@end

调用堆栈信息:

(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 46.1
  * frame #0: 0x000000010b3fedf8 RuntimeLearning`-[ViewController observeValueForKeyPath:ofObject:change:context:](self=0x0000000000000000, _cmd=<no value available>, keyPath=0x0000000000000000, object=0x0000000000000000, change=0x0000000000000000, context=0x4046000000000000) at ViewController.m:293
    frame #1: 0x00007fff207b96f4 Foundation`NSKeyValueNotifyObserver + 329
    frame #2: 0x00007fff207bce28 Foundation`NSKeyValueDidChange + 439
    frame #3: 0x00007fff207b8bff Foundation`NSKeyValueDidChangeWithPerThreadPendingNotifications + 146
    frame #4: 0x000000010b3fe3e2 RuntimeLearning`-[TestKVOObject didChangeValueForKey:](self=0x0000600003776d00, _cmd="didChangeValueForKey:", key=@"testString") at ViewController.m:72:5
    frame #5: 0x00007fff207b5c09 Foundation`_NSSetObjectValueAndNotify + 178
    frame #6: 0x000000010b402923 RuntimeLearning`-[ViewController testSystemKVO](self=0x00007f976e606f60, _cmd="testSystemKVO") at ViewController.m:1087:10

可以看到didChangeValueForKey的调用堆栈,经过一系列的函数跳转最后执行了
observeValueForKeyPath:ofObject:change:context方法

1.2.2 重写的class方法
我们先通过lldb来打印查看一下被观察者的class、isa、superclass

(lldb) p [test class]
(Class) $6 = TestKVOObject
(lldb) p test->isa
(Class) $7 = NSKVONotifying_TestKVOObject
(lldb) p class_getSuperclass(test->isa)
(Class _Nullable) $8 = TestKVOObject
(lldb) p object_getClass(test)
(Class _Nullable) $9 = NSKVONotifying_TestKVOObject
(lldb) 

class指向的是原来的类
isa、object_getClass都返回的是新的类
superclass指向的是原来的类

这里可以看到KVO被观察者是派生出了一个子类来实现的,而且将派生的类的class返回的是原来的类

 - (Class)class {
     return object_getClass(self); // 返回的就是isa
 }
 
Class object_getClass(id obj)
 {
     if (obj) return obj->getIsa();
     else return Nil;
 }

对于为什么要重写class方法,我个人认为:

  • class返回被观察者原来的类,这样上层使用者就不必关注内部的实现细节,对于实例的使用,如果不使用runtime API的话,那么跟原来的类基本一致;比如isKindOfClass、isMemberOfClass等的判断;
  • 类的class方法默认是返回isa的,如果没有重写,那么返回的就是子类了,这样假如上层使用者再通过isMemberOfClass去判断的时候,那就出问题了啊,上层不知道有这个子类的存在的;
  • class方法和object_getClass返回的不一样,也可以作为判断该实例对象有没有被isa-swizzling的判断

1.2.3 重写的dealloc方法

// 0x7fff207b4403位dealloc的地址
(lldb) dis -s 0x7fff207b4403
 Foundation`NSKVODeallocate:

内部调用的是NSKVODeallocate,使用hopper看下伪代码实现:

int _NSKVODeallocate(int arg0, int arg1) {
    r13 = rdi;
    var_-48 = **___stack_chk_guard;
    rax = object_getClass(rdi);
    r12 = __NSKVOUsesBaseClassObservationInfoImplementationForClass(rax);
    rax = object_getIndexedIvars(rax);
    r14 = rax;
    rbx = class_getInstanceMethod(*rax, rsi);
    if (r12 == 0x0) goto loc_7fff207b448e;

loc_7fff207b4461:
    if (**___stack_chk_guard == var_-48) {
            rdi = r13;
            rsi = rbx;
            rax = method_invoke(rdi, rsi);
    }
    else {
            rax = __stack_chk_fail();
    }
    return rax;

loc_7fff207b448e:
    rax = __NSKeyValueRetainedObservationInfoForObject(r13, 0x0);
    *var_-72 = r13;
    *(var_-72 + 0x8) = rax;
    *(var_-72 + 0x10) = 0x0;
    __NSKeyValueAddObservationInfoWatcher(var_-72);
    r12 = __NSKVOObservationInfoOverridenObjectMayThrowOnDealloc(r13);
    method_invoke(r13, rbx);
    if (var_-64 == 0x0) goto loc_7fff207b4570;

loc_7fff207b44d1:
    r15 = dyld_get_program_sdk_version();
    if (r12 != 0x0) {
            r12 = (*_objc_msgSend)(var_-64, *0x7fff86b9d448) ^ 0x1;
    }
    else {
            r12 = 0x0;
    }
    *(int8_t *)var_-73 = 0x0;
    rax = CFPreferencesGetAppBooleanValue(@"NSKVODeallocateCleansUpBeforeThrowing", **_kCFPreferencesCurrentApplication, var_-73);
    rcx = 0x0;
    CMP(r15, 0x7ffff);
    rdx = r12 & 0xff;
    rsi = 0x0;
    asm{ cmova      esi, edx };
    rbx = (var_-73 == rcx ? 0x1 : 0x0) | (rax == 0x0 ? 0x1 : 0x0);
    if (rbx == 0x0) {
            rsi = rdx;
    }
    if (rsi != 0x0) goto loc_7fff207b45b4;

loc_7fff207b4542:
    if ((r15 < 0x80000) || (r12 != 0x0)) {
            _NSLog(@"An instance %p of class %@ was deallocated while key value observers were still registered with it. Observation info was leaked, and may even become mistakenly attached to some other object. Set a breakpoint on NSKVODeallocateBreak to stop here in the debu…", r13, *r14);
            _NSKVODeallocateBreak(r13);
    }
    goto loc_7fff207b4570;

loc_7fff207b4570:
    __NSKeyValueRemoveObservationInfoWatcher(var_-72);
    [var_-64 release];
    if (0x0 == 0x0) {
            rax = *___stack_chk_guard;
            rax = *rax;
            if (rax != var_-48) {
                    rax = __stack_chk_fail();
            }
    }
    else {
            rax = objc_exception_rethrow();
    }
    return rax;

loc_7fff207b45b4:
    r15 = (*_objc_msgSend)(var_-64, *0x7fff86b9a5e8);
    if (rbx == 0x0) {
            __NSKeyValueRemoveObservationInfoForObject(var_-72);
    }
    rax = (*_objc_msgSend)(@class(NSString), *0x7fff86b9a4b8);
    rax = (*_objc_msgSend)(@class(), *0x7fff86b9a700);
    rax = objc_exception_throw(rax);
    return rax;
}

汇编看不懂,可以参照着网上大神根据伪代码实现的逻辑对照看看
KVO实现

void DSKVODeallocate(id object, SEL selector) {
    DSKeyValueObservationInfo *observationInfo = _DSKeyValueRetainedObservationInfoForObject(object, nil);
    
    ObservationInfoWatcher watcher = {object, observationInfo, NULL};
    _DSKeyValueAddObservationInfoWatcher(&watcher);
    
    DSKeyValueNotifyingInfo *notifyInfo = (DSKeyValueNotifyingInfo *)object_getIndexedIvars(object_getClass(object));
    
    Method originDellocMethod = class_getInstanceMethod(notifyInfo->originalClass, selector);
    ((id (*)(id,Method))method_invoke)(object, originDellocMethod);
    
    @try {
        if(watcher.observationInfo) {
            BOOL keyExistsAndHasValidFormat = false;
            BOOL cleansUpBeforeThrowing = false;
            
            cleansUpBeforeThrowing = (BOOL)CFPreferencesGetAppBooleanValue(CFSTR("NSKVODeallocateCleansUpBeforeThrowing"), kCFPreferencesCurrentApplication, (Boolean *)&keyExistsAndHasValidFormat);
            
            cleansUpBeforeThrowing = cleansUpBeforeThrowing && keyExistsAndHasValidFormat;
            
            if (dyld_get_program_sdk_version() > 0x7FFFF || cleansUpBeforeThrowing) {
                if (cleansUpBeforeThrowing) {
                    _DSKeyValueRemoveObservationInfoForObject(object, watcher.observationInfo);
                }
                [NSException raise:NSInternalInconsistencyException format:@"An instance %p of class %@ was deallocated while key value observers were still registered with it. Current observation info: %@", object, notifyInfo->originalClass, watcher.observationInfo];
            }
            else {
                NSLog(@"An instance %p of class %@ was deallocated while key value observers were still registered with it. Observation info was leaked, and may even become mistakenly attached to some other object. Set a breakpoint on NSKVODeallocateBreak to stop here in the debugger. Here's the current observation info:\n%@", object, notifyInfo->originalClass, watcher.observationInfo);
                DSKVODeallocateBreak(object);
            }
        }

    }
    @catch (NSException *exception) {
        [exception raise];
    }
    @finally {
        _DSKeyValueRemoveObservationInfoWatcher(&watcher);
        
        [watcher.observationInfo release];
    }    
}

大致流程:

  • 获取跟该类的observationInfo
  • 获取该类的原始类(父类)的dealloc方法并调用
  • 判断是否有observationInfo,如果有的话就跑出异常

1.2.4 _isKVOA

(lldb) dis -s 0x7fff207b4403
Foundation`NSKVOIsAutonotifying:

内部调用的是NSKVOIsAutonotifying
使用hopper查看伪代码实现:

图片.png

这里返回的是0x1

使用hopper搜索下_isKVOA的实现

图片.png

这里返回的是0x0

一个返回1一个返回0,那么这个猜测就是内部用来做KVO类和非KVO的区分的判断的;但是断点发现在触发kvo的时候并没有调用该方法,一时无法知道它的内部作用是啥;从源码中搜索一下KVOIsAutonotifying

Class _DSKVONotifyingOriginalClassForIsa(Class isa) {
    if(class_getMethodImplementation(isa, ISKVOA_SELECTOR) == (IMP)DSKVOIsAutonotifying) {
        void *ivars = object_getIndexedIvars(isa);
        return ((DSKeyValueNotifyingInfo *)ivars)->originalClass;
    }
    return isa;
}

再去hopper中看下系统的实现:

int __NSKVONotifyingOriginalClassForIsa(int arg0) {
    rbx = arg0;
    if (class_getMethodImplementation(arg0, *0x7fff86b9d430) == _NSKVOIsAutonotifying) {
            rbx = *object_getIndexedIvars(rbx);
    }
    rax = rbx;
    return rax;
}

最后大致得知他的作用:KVO内部去获取原始类的时候,用来判断的,如果_isKVOA的实现是_NSKVOIsAutonotifying那么就去获取它的原始类返回,否则就直接返回传入的类。

这里举个例子理解一下:
假设传入的是NSKVONotifying_TestKVOObject此时获取它的_isKVOA的实现就是_NSKVOIsAutonotifying这时候就需要去获取到它的原始类的class返回,如果传入的是TestKVOObject那么它的_isKVOA的实现就不是_NSKVOIsAutonotifying,而是-[NSObject(NSKeyValueObserverNotifying) _isKVOA],那么就直接返回了

至此KVO派生出的类中的几个方法的作用及大概实现已经看的差不多了;然而这里只是系统KVO实现的冰山一角,还有好多细节需要去探索

2.问答环节

看了上面的一大段讲解,通过下面问题来回顾一下
如何触发KVO
前提是添加了观察者

  • 1.通过set方法设置属性
  • 2.使用kvc的方式设置值
    kvc会找是否有set方法去调用,对于属性通过kvc的方式去触发kvo是很容易理解的,测试发现定义的实例变量ivar,通过kvc也是可以触发kvo的,你知道为什么吗
  • 3.手动调用willChangeValueForKeydidChangeValueForKey
    看上面的实现,didChangeValueForKey内部最后调用到observer回调方法,那么只调用didChangeValueForKey会触发kvo吗?

如何禁止KVO

  • 实现+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key,返回NO
  • 自己实现一个NSKVONotifying_XXX类
@interface NSKVONotifying_TestKVOObject : TestKVOObject

@end

@implementation NSKVONotifying_TestKVOObject

@end

当添加观察者的时候就会报一下日志,同时KVO也失效了:

RuntimeLearning[36847:2908186] [general] KVO failed to allocate class pair for name NSKVONotifying_TestKVOObject, automatic key-value observing will not work for this class

如何hook某个实例对象

假如我只想hook某个类的某个实例的行为,对于类本身不产生影响--新创建的实例的行为还保持以前的逻辑

一般我们hook一个实例的方法,是通过交换方法的imp来达到目的,而这种实现则是对class中的方法的imp的交换,不符合我们上述的场景,当然你也可以在交换的imp中去判断是否是hook的实例来判断是否走hook之后的实现,还是以前的实现;这样也可以达到目的,但不太优雅

在看了KVO的实现之后,我们大概有了思路去hook某个类的某个实例对象的行为,而不去影响原类本身;那就是isa-swizzling技术。

大致思路:

动态派生一个子类
将子类的isa指向新创建的类
将子类的class方法hook掉返回原类方法
将需要hook的方法实现重写

我自己简易实现了一下KVO的逻辑,大致代码如下:
这个实现很简陋,但也大概实现了如何hook一个instance


#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface HCSwizzleInstance : NSObject

void HCSwizzleHookInstance(id instance);
void HCSwizzleUnhookInstance(id instance);
// test
void HCObserveValueForKey(id instance, NSString *key);
void HCRemoveObserveValueForKey(id instance, NSString *key);

@end
//
//  HCSwizzleInstance.m
//  RuntimeLearning
//
//  Created by 贺超 on 2020/5/22.
//  Copyright © 2020 hechao. All rights reserved.
//

#import "HCSwizzleInstance.h"
#import <objc/runtime.h>
#import <objc/message.h>
#import <UIKit/UIKit.h>
#import <libffi-iOS/ffi.h>

#define kHCHookPrefix   @"HC_HOOK_"

@interface HCKVOSetter : NSObject

- (void)testSetter:(id)obj;

@end

@implementation HCSwizzleInstance


void HCSwizzleHookInstance(id instance) {
    _HCSwizzleHookInstance(instance, true);
}

void _HCSwizzleHookInstance(id instance, bool hookMethods) {
    Class originalClass = object_getClass(instance);
    if ([instance class] != originalClass) {
        // 已经hook过了
        return;
    }
    Class hookClass = objc_allocateClassPair(originalClass, HCHookClassName(originalClass), 0);
    if (!hookClass) {
        // The new class, or Nil if the class could not be created (for example, the desired name is already in use).
        hookClass = objc_getClass(HCHookClassName(originalClass));
        if (hookClass) {
            object_setClass(instance, hookClass);
            return;
        }
    }
    if (hookMethods) {
        // 这里如果需要对该实例的所有方法都做hook的话,比如用来记录一些执行的信息;那么就可以将方便列表遍历进行hook,但是需要一个统一的跳板来处理不同方法的不同参数及参数个数
        unsigned int count;
        Method *mList = class_copyMethodList(originalClass, &count);
        for (unsigned int i = 0; i < count; i++) {
            Method method = mList[i];
            SEL selector = method_getName(method);
            if ([NSStringFromSelector(selector) hasPrefix:kHCHookPrefix]) {
                continue;
            }
            const char *mType = method_getTypeEncoding(method);
            IMP originImp = method_getImplementation(method);
            class_addMethod(hookClass, selector, originImp, mType);
            class_addMethod(hookClass, selector, imp_implementationWithBlock(^(void){
                return originImp;
            }), mType);
            // TODO:trampoline 需要一个通用的跳板来hook所有的方法
        }
        free(mList);
    }
    for (Class class in @[hookClass, object_getClass(hookClass)]) {
        SEL classSEL = @selector(class);
        Method oldMethod = class_getInstanceMethod(class, classSEL);
        // 由于类的class的内部实现直接返回的类(self);实例对象的class内部实现是调用的object_getClass(self)
        // 我们修改实例的isa的话,如果不做处理,就会在需要unhook的时候无法知道原class
        // 所以我们这里将类以及实例的class方法hook掉返回原始的类,此时isa的值是修改之后的值
        class_replaceMethod(class, classSEL, imp_implementationWithBlock(^(void){
            return originalClass;
        }), method_getTypeEncoding(oldMethod));
    }
    objc_registerClassPair(hookClass);
    object_setClass(instance, hookClass);
}

void HCSwizzleUnhookInstance(id instance) {
    Class hookClass = object_getClass(instance);
    if ([instance class] != hookClass) {
        object_setClass(instance, [instance class]);
        //const char *name = class_getName(hookClass);
        //objc_duplicateClass(hookClass, name, 0);
    }
}

#pragma mark - Test KVO

void HCObserveValueForKey(id instance, NSString *key) {
    if (!key || key.length == 0) {
        return;
    }
    _HCSwizzleHookInstance(instance, false);
    NSString *firstCharacter = [key substringToIndex:1];
    NSString *tmpKey = [key stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:firstCharacter.uppercaseString];
    SEL setSelector = NSSelectorFromString([NSString stringWithFormat:@"set%@:", tmpKey]);
    Method oldMethod = class_getInstanceMethod([instance class], setSelector);
    if (!oldMethod) {
        return;
    }
    const char *mType = method_getTypeEncoding(oldMethod);
    /*
    IMP originImp = method_getImplementation(oldMethod);
    class_replaceMethod([instance class], setSelector, imp_implementationWithBlock(^(void){
        [instance willChangeValueForKey:key];
        ((void(*)(id, SEL, id))originImp)(instance, setSelector, @"1111");
        [instance didChangeValueForKey:key];
    }), mType);
    */
    Method hookImpMethod = class_getInstanceMethod(HCKVOSetter.class, @selector(testSetter:));
    IMP hookImp = method_getImplementation(hookImpMethod);
    if (class_addMethod(object_getClass(instance), setSelector, hookImp, mType) == NO) {
        class_replaceMethod(object_getClass(instance), setSelector, hookImp, mType);
    }
}

void HCRemoveObserveValueForKey(id instance, NSString *key) {
    // 这里应该将hook的setter方法的实现修改回去
    if (!key || key.length == 0) {
        return;
    }
    NSString *firstCharacter = [key substringToIndex:1];
    NSString *tmpKey = [key stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:firstCharacter.uppercaseString];
    SEL setSelector = NSSelectorFromString([NSString stringWithFormat:@"set%@:", tmpKey]);
    Method oldMethod = class_getInstanceMethod([instance class], setSelector); // 拿到父类中的方法实现,也就是set方法
    Method hookedMethod = class_getInstanceMethod(object_getClass(instance), setSelector); // 拿到kvo类的方法
    if (!oldMethod) {
        return;
    }
    method_setImplementation(hookedMethod, method_getImplementation(oldMethod)); // 将kvo类的set方法的实现修改成之前的实现
}

#pragma mark - Private Method

static const char *HCHookClassName(Class class) {
  return [kHCHookPrefix stringByAppendingString:NSStringFromClass(class)].UTF8String;
}

@end

@implementation HCKVOSetter
/*
 1.KVO监听,系统会动态子类化一个类NSKVONotifying_ClassName,并且重写了class的实现、set方法、delloc、_isKVOA
 2.class内部实现返回的是KVO监听的类,而类的isa则指向的是NSKVONotifying_ClassName,可通过objc_getClass获取到
 3.set方法内部实现,会调用willChangeValueForKey、调用原始的实现、didChangeValueForKey;didChangeValueForKey中会去调用observeValueForKeyPath
 4.dealloc方法内部会做清理工作
 5._isKVOA是做什么的
 */
static  NSString * _Nullable getKey(SEL cmd);

- (void)testSetter:(id)obj {
    NSString *key = getKey(_cmd);
    if (!key) {
        return;
    }
    Class cls = [self class];
    void (*imp)(id, SEL, id);
    Method originMethod = class_getInstanceMethod(cls, _cmd); // 获取原始的实现
    imp = (void(*)(id, SEL, id))method_getImplementation(originMethod); // 拿到原始函数的imp
    if ([cls automaticallyNotifiesObserversForKey:key]) {
        //[self willChangeValueForKey:key]; // 走系统的那一套,找到observer去执行
        id oldValue = [self valueForKey:key];
        NSMutableDictionary *change = [NSMutableDictionary dictionary];
        if (oldValue) {
            [change setObject:oldValue forKey:@"old"];
        }
        if (obj) {
            [change setObject:obj forKey:@"new"];
        }
        imp(self, _cmd, obj); // 得到imp去直接调用
        //[self didChangeValueForKey:key];
        [self observeValueForKeyPath:key ofObject:nil change:change.copy context:nil];
    } else {
        imp(self, _cmd, obj);
    }
}

NSString *getKey(SEL cmd) {
    //const char *selName = sel_getName(cmd);
    NSString *selString = NSStringFromSelector(cmd);
    if (!selString) {
        return nil;
    }
    NSString *lowerSelString = selString.lowercaseString;
    BOOL checkIsVaildSetter = [lowerSelString containsString:@"set"] && [lowerSelString containsString:@":"];
    if (!checkIsVaildSetter) {
        return nil;
    }
    
    NSRange setRange = [lowerSelString rangeOfString:@"set"];
    NSInteger keyStart = setRange.location + setRange.length;
    NSRange colonRange = [lowerSelString rangeOfString:@":"];
    NSInteger keyEnd = colonRange.location;
    if (keyEnd < keyStart) {
        return nil;
    }
    NSString *tmpKeyString = [selString substringWithRange:NSMakeRange(keyStart, keyEnd - keyStart)];
    NSString *firstCharacter = [tmpKeyString substringToIndex:1];
    NSString *key = [tmpKeyString stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:firstCharacter.lowercaseString];
    return key;
}

@end

4. 参考文档

Key-Value Observing Implementation Details
DIS_KVC_KVO

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

推荐阅读更多精彩内容

  • 键值观察提供了一种机制,该机制允许将其他对象的特定属性的更改通知给对象。对于应用程序中模型层和控制器层之间的通信特...
    正_文阅读 249评论 0 1
  • 前言: 本文基本不讲KVC/KVO的用法,只结合网上的资料说说对这种技术的理解。 由于KVO内容较少,而且是以KV...
    土b兰博王阅读 3,050评论 0 33
  • 作者:wangzz原文地址:http://blog.csdn.net/wzzvictory/article/det...
    反调唱唱阅读 1,114评论 0 5
  • kvo全称Key-Value-Observing,俗称“键值监听”,可以用于对对象的某个属性值的监听。kvo是Ob...
    Devbrave阅读 432评论 0 0
  • 1、KVO简介 KVO 即Key-Value Observing,翻译成是中文键值观察,是一种非正式的协议,它定义...
    风紧扯呼阅读 1,371评论 0 6