前言

一、KVO基础使用总结

二、KVO的底层原理探索

三、模拟系统自定义KVO

四、优化KVO实现


KVO算是IOS领域响应式编程思想的鼻祖了。它全称是:“Key-Value Observing”,主要用来监听对象属性值的改变。

基于响应式思想的框架,还有ReactiveCocoa、Rx系列以及Swift的Combine等等。这些框架在日后会一个一个来分析总结。

官网KVO的具体介绍:官网指南


一、KVO基础使用总结

1、KVO和KVC一样,同样是NSObjct的非正式协议:NSKeyValueObserving、NSKeyValueObserverRegistration、NSKeyValueObserverNotification、NSKeyValueObservingCustomization等。

简单的KVO实现如下:

@interface MyKVOObject : NSObject
@property (nonatomic, copy) NSString *kvoName;
@end
......

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    self.kvoObjc = [MyKVOObject new];
    //1.添加观察者
    [self.kvoObjc addObserver:self forKeyPath:@"kvoName" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL];
}

//2.观察属性监听
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"old = %@",change[NSKeyValueChangeOldKey]);
    NSLog(@"new = %@",change[NSKeyValueChangeNewKey]);
}

- (void)dealloc {
    //3.释放观察者
    [self.kvoObjc removeObserver:self forKeyPath:@"kvoName"];
}
@end

2、context(上下文)的作用总结代码如下:

static void *kvoObjctContext = &kvoObjctContext;
static void *kvoInfoContext = &kvoInfoContext;

@interface ViewController ()

@property (nonatomic, strong) MyKVOObject *kvoObjc;
@property (nonatomic, strong) MyKVOInfo *kvoInfo;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    self.kvoObjc = [MyKVOObject new];
    self.kvoInfo = [MyKVOInfo new];
    
    //1.添加观察者
    [self.kvoObjc addObserver:self forKeyPath:@"kvoName" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:kvoObjctContext];
    [self.kvoInfo addObserver:self forKeyPath:@"kvoName" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:kvoInfoContext];
}

//2.观察属性监听
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if (context == kvoObjctContext) {
        NSLog(@"kvoObjc -> old = %@",change[NSKeyValueChangeOldKey]);
        NSLog(@"kvoObjc ->  new = %@",change[NSKeyValueChangeNewKey]);
    } else if (context == kvoInfoContext) {
        NSLog(@"kvoInfo ->  old = %@",change[NSKeyValueChangeOldKey]);
        NSLog(@"kvoInfo -> new = %@",change[NSKeyValueChangeNewKey]);
    }
}

- (void)dealloc {
    //3.释放观察者
    [self.kvoObjc removeObserver:self forKeyPath:@"kvoName"];
    [self.kvoInfo removeObserver:self forKeyPath:@"kvoName"];
}

增加了一个kvoInfo对象,MyKVOObject和MyKVOInfo类具有相同名称的属性,此时我们在监听方法中就可以通过context来区分不同上下文对应的属性值变化。当然我们也可以使用object去判断,但是使用context的优势就是:(1)代码可读性增加 (2)当有多个对象多个属性的时候,可以更方便的去做归类的判断。

3、移除观察者

每次创建完监听之后,在dealloc方法中需要移除观察者。如果不移除,"可能"会造成野指针的问题(EXC_BAD_ACCESS )。
比如一个单利对像添加了一个观察者A,在离开页面时没有移除它。当其他地方再发消息通知所有添加的观察者,因为观察者A已经被释放了,所以再给观察者A发消息的时候就会造成野指针访问。(如果由于循环引用的问题,造成观察A没有被释放的话,那不会造成野指针访问。这就是为什么有时候KVO莫名其妙的就闪退,有时候又没有问题)。

4 、手动添加观察

@implementation MyKVOObject

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    return false;
}

- (void)setKvoName:(NSString *)kvoName {
    [self willChangeValueForKey:@"kvoName"];
    _kvoName = kvoName;
    [self didChangeValueForKey:@"kvoName"];
}

@end

(1)通过+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key来控制手动和自动观察。
(2)如果是手动观察,需要在set方法里面去执行willChangeValueForKey和didChangeValueForKey方法。

5、合并监听

@interface MyKVOObject : NSObject

@property (nonatomic, copy) NSString *kvoName;

@property (nonatomic, copy) NSString *kvoSubName1;
@property (nonatomic, copy) NSString *kvoSubName2;

@end
.......
@implementation MyKVOObject

+ (NSSet <NSString *>*)keyPathsForValuesAffectingValueForKey:(NSString *)key {
    NSSet <NSString *>*keypaths = [super keyPathsForValuesAffectingValueForKey:key];
    if ([key isEqualToString:@"kvoName"]) {
        NSArray *keys = @[@"kvoSubName1",@"kvoSubName2"];
        keypaths = [keypaths setByAddingObjectsFromArray:keys];
    }
    return keypaths;
}

- (NSString *)kvoName {
    return [NSString stringWithFormat:@"%@ - %@",_kvoSubName1,_kvoSubName2];
}

@end

通过keyPathsForValuesAffectingValueForKey方法,判断当key等于kvoName时,设置keypaths为kvoSubName1和kvoSubName2。当改变kvoSubName1或者kvoSubName2的值时,也会收到通知消息。

6、数组监听

- (void)viewDidLoad {
    [super viewDidLoad];

 //1.添加观察者
    [self.kvoObjc addObserver:self forKeyPath:@"dataArray" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL];
    //设置dataArray
    [self.kvoObjc setValue:[[NSMutableArray alloc] init] forKey:@"dataArray"];
}

//2.观察属性监听
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
   
    NSString *kind = change[NSKeyValueChangeKindKey];
    switch ([kind intValue]) {
        case NSKeyValueChangeSetting:
            NSLog(@"设置 - %@",change);
            break;
        case NSKeyValueChangeInsertion:
            NSLog(@"添加 - %@",change);
            break;
        case NSKeyValueChangeRemoval:
            NSLog(@"移除 - %@",change);
            break;
        case NSKeyValueChangeReplacement:
            NSLog(@"替换 - %@",change);
            break;
    }
}

- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [[self.kvoObjc mutableArrayValueForKey:@"dataArray"] addObject:@"1"];
    [[self.kvoObjc mutableArrayValueForKey:@"dataArray"] addObject:@"2"];
    [[self.kvoObjc mutableArrayValueForKey:@"dataArray"] removeObject:@"1"];
    [[self.kvoObjc mutableArrayValueForKey:@"dataArray"] replaceObjectAtIndex:0 withObject:@"3"];
}
..........

简单的说,KVO是建立在KVC基础之上的。监听数组时,必须使用mutableArrayValueForKey方法获取到数组然后执行操作。另外通过以上代码可以看到,NSKeyValueChangeKindKey可以来判断是设置、添加、移除、替换的操作。

二、KVO的底层原理探索

1、首先明确一点,KVO只能观察属性,不能观察成员变量。

2、获取一个类的所有子类的方法:

- (void)printSubClass:(Class)curClass {
    
    //获取注册类的总数count (注意不包括元类,因为如果知道一个类,通isa就能找到元类)
    int count = objc_getClassList(NULL, 0);
    //获取所有已注册的类
    Class *allClass = (Class *)malloc(sizeof(Class) * count);
    objc_getClassList(allClass, count);
    //遍历所有类,将curClass的子类加入数组
    NSMutableArray *mArray = [[NSMutableArray alloc] init];
    for (int i = 0; i < count; i++) {
        Class cls = allClass[i];
        if (curClass == class_getSuperclass(cls)) {
            [mArray addObject:cls];
        }
    }
    free(allClass);
    NSLog(@" ==== %@",mArray);   
}
//底层:
int
objc_getClassList(Class *buffer, int bufferLen)
{
    mutex_locker_t lock(runtimeLock);
    
    realizeAllClasses();
    
    return objc_getRealizedClassList_nolock(buffer, bufferLen);
}

static int
objc_getRealizedClassList_nolock(Class *buffer, int bufferLen)
{
    int count = 0;
    //buffer存在就进行赋值
    if (buffer) {
        int c = 0;
        foreach_realized_class([=, &count, &c](Class cls) {
            count++;
            if (c < bufferLen) {
                buffer[c++] = cls;
            }
            return true;
        });
    } else {
         //buffer不存在就进行count++
        foreach_realized_class([&count](Class cls) {
            count++;
            return true;
        });
    }
    
    return count;
}

3、通过上面获取子类的方法,我们可以断点跟踪一下" [self.kvoObjc addObserver:self forKeyPath:@"kvoName" options:NSKeyValueObservingOptionNew context:NULL];"前后有什么不同。


如图所示,经过调用addObserver:forKeyPath:options:context 方法之后,MyKVOObject多了一个子类"NSKVONotifying_MyKVOObject"。
我们通过x/4gx 打印kvoObjc:

或者 通过object_getClassName获取当前的类

此时kvoObjc的isa指向了NSKVONotifying_MyKVOObject。
也就证明了kvo会动态的创建一个子类。

4、探索中间子类

(1)首先看下NSKVONotifying_MyKVOObject中有什么方法:

......
 [self printClassAllMethod:objc_getClass("NSKVONotifying_MyKVOObject")];
//获取类中所有方法
 - (void)printClassAllMethod:(Class)cls {
    unsigned int count = 0;
    Method *methodList = class_copyMethodList(cls, &count);
    for (int i = 0; i < count; i++) {
        Method method = methodList[i];
        SEL sel = method_getName(method);
        IMP imp = class_getMethodImplementation(cls, sel);
        NSLog(@"%@-%p",NSStringFromSelector(sel),imp);
    }
}

打印结果:

通过打印的方法可以猜测,是在这个子类中进行了set方法的监听。
(2)那么问题来了,这些方法在子类中是继承还是重写呢?
答案肯定是重写了,因为class_copyMethodList是获取当前类中的方法,

Method *
class_copyMethodList(Class cls, unsigned int *outCount)
{
    unsigned int count = 0;
    Method *result = nil;
    
    if (!cls) {
        if (outCount) *outCount = 0;
        return nil;
    }
    
    mutex_locker_t lock(runtimeLock);
    const auto methods = cls->data()->methods();
    
    ASSERT(cls->isRealized());
    
    count = methods.count();
    
    if (count > 0) {
        result = (Method *)malloc((count + 1) * sizeof(Method));
        
        count = 0;
        for (auto& meth : methods) {
            result[count++] = &meth;
        }
        result[count] = nil;
    }
    
    if (outCount) *outCount = count;
    return result;
}

(3)核心就在于子类重写的那个set方法是如何实现监听值的。那么在研究核心问题之前,先来研究一下“取消监听之后,子类是否移除,isa是否会指回原来的类?”
这个问题其实很好验证:

在dealloc打个断点,当在执行removeObserver之前,打印self.kvoObjc依然指向NSKVONotifying_MyKVOObject;removeObserver之后,self.kvoObjc的isa重新指向了MyKVOObject。此时继续打印MyKVOObject的子类,发现NSKVONotifying_MyKVOObject依然存在,并没有从内存中移除。
(4)言归正传,继续来看子类的那个set方法实现了什么。
这里直接进行汇编探索,通过lldb的命令 watchpoint set variable 来设置观察断点:

然后当kvoName值改变的时候,继续看汇编:
在设置kvoName值的前后,分别出现了NSKeyValueWillChange和NSKeyValueDidChange。所以这里可以简单的写下伪代码:

- (void)setKvoName:(NSString *)kvoName {
    ....
   willChange
    [super setKvoName:kvoName];
   didChange
   ....
}

以上就是对KVO底层实现的整体分析。


三、模拟系统自定义KVO

KVO的原理分析完之后,我们先尝试模拟系统的实现来自定义一个KVO

创建NSObject+MyKVO的分类,声明方法:

@interface NSObject (MyKVO)

- (void)my_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(MyKeyValueObservingOptions)options context:(nullable void *)context;

- (void)my_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

- (void)my_observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;

@end

核心的实现部分如下:

1、验证是否存在setter方法

#pragma mark - 验证是否存在setter方法

- (void)verifySetterMethodFromKeyPath:(NSString *)keyPath {
    //获取当前的类
    Class curClass = object_getClass(self);
    //通过keyPath获取sel
    SEL setterSEL = NSSelectorFromString(setterForGetter(keyPath));
    //判断当前类中是否有setter方法
    Method setterMethod = class_getInstanceMethod(curClass, setterSEL);
    if (!setterMethod) {
        @throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"can not find %@ property",keyPath] userInfo:nil];
    }
}

#pragma mark - get/set 名称获取

//从get方法中获取setter方法的名称
static NSString *setterForGetter(NSString *getter) {
    if ([getter length] <= 0) {
        return nil;
    }
    //第一个字母大写
    NSString *nGetter = [getter capitalizedString];
    return [NSString stringWithFormat:@"set%@:",nGetter];
    
}

//从set方法中获取getter方法的名称
static NSString *getterForSetter(NSString *setter) {
    if (setter.length <= 0 || ![setter hasPrefix:@"set"] || ![setter hasSuffix:@":"]) { return nil;}
    
    NSRange range = NSMakeRange(3, setter.length-4);
    NSString *getter = [setter substringWithRange:range];
    NSString *firstString = [[getter substringToIndex:1] lowercaseString];
    return  [getter stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:firstString];
}

2、动态生成子类 (申请类、注册、添加方法)

#pragma mark - 动态生成子类
- (Class)createChildClassWithKeyPath:(NSString *)keyPath {
    //类名称
    NSString *oldClassName = NSStringFromClass(self.class);
    NSString *newClassName = [NSString stringWithFormat:@"%@%@",kMyKVOPrefix,oldClassName];
    //防止重复创建生成新类
    Class newClass = NSClassFromString(newClassName);
    if (newClass) {
        return newClass;
    }
    /**
     * 如果内存不存在,创建生成
     * 参数一: 父类
     * 参数二: 新类的名字
     * 参数三: 新类的开辟的额外空间
     */
    //申请类
    newClass = objc_allocateClassPair(self.class, newClassName.UTF8String, 0);
    //注册类
    objc_registerClassPair(newClass);
    //添加class,这里-(Class)class; 的底层是object_getClass(self);这里相当于重写了class实例方法。
    SEL classSEL = NSSelectorFromString(@"class");
    Method classMethod = class_getInstanceMethod([self class], classSEL);
    const char *classTypes = method_getTypeEncoding(classMethod);
    class_addMethod(newClass, classSEL, (IMP)my_class, classTypes);
    return newClass;
}

Class my_class(id self,SEL _cmd){
    return class_getSuperclass(object_getClass(self));
}

其中需要注意3个地方:

(1)在创建子类之后添加了-(Class)class方法。
原因是:-(Class)class的底层实现是object_getClass(self),是获取当前对象isa指向的class。由于不应该改变原有类的逻辑,所以这里重写了-(Class)class,使得外界获取class的时候,还是原来的类。

(2)假如添加成员变量,那么一定要在objc_allocateClassPair和objc_registerClassPair之间,因为在注册类之后,ro(clean memery)是不能进行修改的。
class_addIvar的核心源码如下:

   // class allocated but not yet registered
   #define RW_CONSTRUCTING       (1<<26)
BOOL 
class_addIvar(Class cls, const char *name, size_t size, 
              uint8_t alignment, const char *type)
{
 .....
    // No class variables 
    if (cls->isMetaClass()) {
        return NO;
    }
    // Can only add ivars to in-construction classes.
    // 这里会判断类是否注册
    if (!(cls->data()->flags & RW_CONSTRUCTING)) {
        return NO;
    }
..........
    // fixme allocate less memory here
    ivar_list_t *oldlist, *newlist;
    //判断类中是否有成员变量
    if ((oldlist = (ivar_list_t *)cls->data()->ro()->ivars)) {
        size_t oldsize = oldlist->byteSize();
        newlist = (ivar_list_t *)calloc(oldsize + oldlist->entsize(), 1);
        memcpy(newlist, oldlist, oldsize);
        free(oldlist);
    } else {
        newlist = (ivar_list_t *)calloc(ivar_list_t::byteSize(sizeof(ivar_t), 1), 1);
        newlist->entsizeAndFlags = (uint32_t)sizeof(ivar_t);
    }
    ..........
    //重新设置ivars
    ro_w->ivars = newlist;
    //重新计算空间大小
    cls->setInstanceSize((uint32_t)(offset + size));
    return YES;
}

(3)重点说一下class_addProperty方法:

    objc_property_attribute_t type = { "T", "@\"NSString\"" };//T:类型
    objc_property_attribute_t ownership = { "C", "" }; // C:copy
    objc_property_attribute_t nonatomic = { "N", "" }; // N:nonatomic
    objc_property_attribute_t backingivar  = { "V", "_myVar" };//V: 实例变量
    objc_property_attribute_t attributs[] = { type, ownership,nonatomic, backingivar };

    class_addProperty(newClass,
                      "myVar",
                      attributs,4);

如上面代码,动态创建了一个myVar的属性,但是此时类中的属性和成员变量是什么情况呢?
继续来打印一下类中属性和成员变量。

//打印属性
    unsigned int count;
    objc_property_t *propertyList = class_copyPropertyList(object_getClass(self), &count);
    for (unsigned int i = 0; i< count; i++) {
        const char *name = property_getName(propertyList[i]);
        NSLog(@" == %@",[NSString stringWithUTF8String:name]);
        objc_property_t property = propertyList[i];
        const char *a = property_getAttributes(property);
        NSLog(@"属性信息 == %@",[NSString stringWithUTF8String:a]);
    }
    //打印成员变量
    unsigned int iCount;
    Ivar *ivars = class_copyIvarList(object_getClass(self), &iCount);
    for (int i = 0; i< iCount; i++) {
        Ivar var = ivars[i];
        const char *name = ivar_getName(var);
        printf("成员变量 == %s",name);
    }


从打印结果来看,只添加了属性,并没有添加成员变量。
在从源码来分析一下:

static bool 
_class_addProperty(Class cls, const char *name, 
                   const objc_property_attribute_t *attrs, unsigned int count,
                   bool replace)
{
    //参数校验 
    if (!cls) return NO;
    if (!name) return NO;
    //获取对应的属性
    property_t *prop = class_getProperty(cls, name);
    //1.如果属性存在并且不需要替换时,直接返回
    if (prop  &&  !replace) {
        // already exists, refuse to replace
        return NO;
    }
    else if (prop) {
         //2.如果属性存在,替换属性设置
        // replace existing
        mutex_locker_t lock(runtimeLock);
        try_free(prop->attributes);
        prop->attributes = copyPropertyAttributeString(attrs, count);
        return YES;
    }
    else {
         //2.如果属性不存在,进行添加
        mutex_locker_t lock(runtimeLock);
        //创建rwe
        auto rwe = cls->data()->extAllocIfNeeded();
        
        ASSERT(cls->isRealized());
        
        property_list_t *proplist = (property_list_t *)
        malloc(property_list_t::byteSize(sizeof(property_t), 1));
        proplist->count = 1;
        proplist->entsizeAndFlags = sizeof(property_t);
        proplist->begin()->name = strdupIfMutable(name);
        proplist->begin()->attributes = copyPropertyAttributeString(attrs, count);
        //添加到rwe的proplist中
        rwe->properties.attachLists(&proplist, 1);
        return YES;
    }
}

class_addProperty并不会添加ivar和get/set方法。
那么如果要实现真正的属性添加,我测试了2个方法可以实现 :
<1>在创建类之后,注册类之前添加成员变量,同时在属性添加完之后添加get/set方法;
<2>不添加成员变量的情况下,使用一个全局字典来保存“get/set方法维护的变量”。

3、添加setter方法:


- (void)addSetterMethod:(Class)newClass keyPath:(NSString *)keyPath {
    SEL setterSEL = NSSelectorFromString(setterForGetter(keyPath));
    Method setterMethod = class_getInstanceMethod([self class], setterSEL);
    const char *setterTypes = method_getTypeEncoding(setterMethod);
    class_addMethod(newClass, setterSEL, (IMP)my_setter, setterTypes);
}

static void my_setter(id self,SEL _cmd,id newValue) {
    
    //获取到keypath,也是get方法
    NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
    //拿到旧值
    id oldValue = [self valueForKey:keyPath];
    //获取父类 class_getSuperclass(object_getClass(self)) 或者 直接[self class]
    Class superClass = [self class];
    //方法1: 获取父类方法的imp调用
//    void (*superIMP)(id,SEL,id);
//    superIMP = class_getMethodImplementation(superClass, _cmd);
//    superIMP(self,_cmd,newValue);
    
    //方法2:通过objc_msgSend
    //记录当前的cls
//    Class curClass = object_getClass(self);
    //将self的isa指向父类
//    object_setClass(self, superClass);
    //发送消息
//    objc_msgSend(self, _cmd,newValue);
    //完成后再将isa指回来
//    object_setClass(self, curClass);
    
    //方法3:通过objc_msgSendSuper消息发送
    void(*my_msgSendSuper)(void *,SEL,id) = (void*)objc_msgSendSuper;
    struct objc_super superStruct = {
        .receiver = self,
        .super_class = superClass
    };
    my_msgSendSuper(&superStruct,_cmd,newValue);
  
    /*
     my_addObserver的时候通过关联对象进行信息保存
    //4.保存信息
     MyKVOInfo *info = [[MyKVOInfo alloc] initWitObserver:observer forKeyPath:keyPath options:options];
     //这里简单的用数组实现,当然用字典也可以更好的优化
     NSMutableArray *infoArray = objc_getAssociatedObject(self, &kMyKVOAssiociateKey);
     if (!infoArray) {
         infoArray = [NSMutableArray arrayWithCapacity:1];
         objc_setAssociatedObject(self, &kMyKVOAssiociateKey, infoArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
     }
     [infoArray addObject:info];
     */
    //拿到观察者
    NSMutableArray *infoArray = objc_getAssociatedObject(self, &kMyKVOAssiociateKey);
    for (MyKVOInfo *info in infoArray) {
        if ([info.keyPath isEqualToString:keyPath]) {
            NSMutableDictionary<NSKeyValueChangeKey,id> *change = [NSMutableDictionary dictionaryWithCapacity:1];
            //对新旧值处理
            if (info.options & MyKeyValueObservingOptionNew) {
                [change setObject:newValue forKey:NSKeyValueChangeNewKey];
            }
            
            if (info.options & MyKeyValueObservingOptionOld) {
                if (oldValue) {
                    [change setObject:oldValue forKey:NSKeyValueChangeOldKey];
                } else {
                    [change setObject:@"" forKey:NSKeyValueChangeOldKey];
                }
            }
            //消息发送给观察者
            SEL observerSEL = @selector(my_observeValueForKeyPath:ofObject:change:context:);
            
            objc_msgSend(info.oberver,observerSEL,keyPath,self,change,NULL);
        }  
    }   
}

<1> 通过_cmd获取keypath
<2>通过kvc获取旧值
<3>获取父类
<4>通过3种方法(1、获取父类方法imp直接调用 2、通过objc_msgSend发送消息 3、通过objc_msgSendSuper发送消息)调用父类set方法。
<5>拿到观察者信息,给观察者发送消息。

4、设置对象的isa指向新创建的类

object_setClass(self, newClass);

5、添加观察者的方法总结为:

- (void)my_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(MyKeyValueObservingOptions)options context:(nullable void *)context {
    //1.验证是否有setter方法
    [self verifySetterMethodFromKeyPath:keyPath];
    //2.动态生成子类
    Class newClass = [self createChildClassWithKeyPath:keyPath];
    //3.添加setter方法
    [self addSetterMethod:newClass keyPath:keyPath];
    //4.设置isa的指向
    object_setClass(self, newClass);
    //5.保存信息
    MyKVOInfo *info = [[MyKVOInfo alloc] initWitObserver:observer forKeyPath:keyPath options:options];
    //这里简单的用数组实现,当然用字典也可以更好的优化
    NSMutableArray *infoArray = objc_getAssociatedObject(self, &kMyKVOAssiociateKey);
    if (!infoArray) {
        infoArray = [NSMutableArray arrayWithCapacity:1];
        objc_setAssociatedObject(self, &kMyKVOAssiociateKey, infoArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    [infoArray addObject:info];
}

6、移除观察者

- (void)my_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath {
    //遍历判断keyPath,然后移除info
    NSMutableArray *infoArray = objc_getAssociatedObject(self, &kMyKVOAssiociateKey);
    if (infoArray.count <= 0) {
        return;
    }
    for (MyKVOInfo *info in infoArray) {
        if ([info.keyPath isEqualToString:keyPath] && info.oberver == observer) {
            [infoArray removeObject:info];
            break;
        }
    }
    if (infoArray.count <= 0) {
        //isa指回父类
        Class class = [self class];
        object_setClass(self, class);
    }
}

7、MyKVOInfo补充:

typedef NS_OPTIONS(NSUInteger, MyKeyValueObservingOptions) {
    MyKeyValueObservingOptionNew = 0x01,
    MyKeyValueObservingOptionOld = 0x02,
};

@interface MyKVOInfo : NSObject
////此处使用unsafe_unretained,因为在dealloc去遍历移除的时候,如果用weak,底层会因为多线程的原因,observer已经置为nil了。(但是如果不及时remove的话,会造成也指针访问)
@property (nonatomic, unsafe_unretained) id oberver;
@property (nonatomic, copy) NSString *keyPath;
@property (nonatomic, assign) MyKeyValueObservingOptions options;

- (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(MyKeyValueObservingOptions)options;

@end
.................
@implementation MyKVOInfo

- (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(MyKeyValueObservingOptions)options {
    self = [super init];
    if (self) {
        self.oberver = observer;
        self.keyPath = keyPath;
        self.options = options;
    }
    return self;
}
@end

以上就是模拟系统自定义KVO的全部过程。
在使用KVO的过程中有2个地方使用起来比较麻烦:
(1)每次添加完观察者,都必须手动移除掉。
(2)不支持函数式。
另外以上自定义KVO,不支持基本数据类型的监听。
接下来,对自定义KVO进行优化。

四、优化KVO实现

1、支持函数式

首先修改声明的方法

- (void)my_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath callBack:(LLKVOBlock)callBack;

改成block回调的方式通知属性值改变。

然后改造MyKVOInfo的保存信息

typedef void(^LLKVOBlock)(NSString *keyPath,id oldValue,id newValue);

@interface LLKVOInfo : NSObject

//此处使用unsafe_unretained,因为在dealloc去遍历移除的时候,如果用weak,底层会因为多线程的原因,observer已经置为nil了。(但是如果不及时remove的话,会造成也指针访问)
@property (nonatomic, unsafe_unretained) id oberver;
@property (nonatomic, copy) NSString *keyPath;
@property (nonatomic, copy) LLKVOBlock callBack;

- (instancetype)initWithObserver:(id)observer KeyPath:(NSString *)keyPath handleBlock:(LLKVOBlock)callBack;

@end

相当于每次监听都会多保存一个block。

最后改造setter方法:

//setter方法
void my_setter(id self,SEL _cmd,id newValue) {
    //获取keyPath
    NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
    //kvc->获取旧值
    id oldValue = [self valueForKey:keyPath];
    //获取父类
    Class superClass = [self class];
    //给父类发消息
    void (*my_msgSendSuper)(void *,SEL,id) = (void*)objc_msgSendSuper;
    struct objc_super my_objc_super = {
        .receiver = self,
        .super_class = superClass
    };
    my_msgSendSuper(&my_objc_super,_cmd,newValue);
    //信息数据回调
    NSMutableArray *infoArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kLLKVOAssiociateKey));
    for (LLKVOInfo *info in infoArray) {
        if ([info.keyPath isEqualToString:keyPath]) {
            info.callBack(keyPath, oldValue, newValue);
        }
    }
}

这里也可以进一步的优化保存信息:把infoArray改成infoDic(key = keyPath,value = info数组)。这样的话,可以在信息回调时,根据keyPath找到所有监听keyPath的block,然后遍历发送。

2、支持基本数据类型监听

这个功能可以参考DIS_KVC_KVO的源码来实现:

void _DSKVONotifyingEnableForInfoAndKey(DSKeyValueNotifyingInfo *info, NSString *key) {
  ..........
    DSKeyValueSetter * setter = _DSKeyValueSetterForClassAndKey(info->originalClass, key, info->originalClass);
    if([setter isKindOfClass: [DSKeyValueMethodSetter class]]) {
        Method setMethod = [(DSKeyValueMethodSetter *)setter method];
        const char *encoding = method_getTypeEncoding(setMethod);
        if (*encoding == 'v') {
            char *argType = method_copyArgumentType(setMethod, 2);
            IMP imp = NULL;
            switch (*argType) {
                case 'c': {
                    imp = (IMP)_DSSetCharValueAndNotify;
                } break;
                case 'd': {
                    imp = (IMP)_DSSetDoubleValueAndNotify;
                } break;
                case 'f': {
                    imp = (IMP)_DSSetFloatValueAndNotify;
                } break;
                case 'i': {
                    imp = (IMP)_DSSetIntValueAndNotify;
                } break;
                case 'l': {
                    imp = (IMP)_DSSetLongValueAndNotify;
                } break;
                case 'q': {
                    imp = (IMP)_DSSetLongLongValueAndNotify;
                } break;
                case 's': {
                    imp = (IMP)_DSSetShortValueAndNotify;
                } break;
                case 'S': {
                    imp = (IMP)_DSSetUnsignedShortValueAndNotify;
                } break;
                case '{': {
                    if(strcmp(argType, @encode(CGPoint)) ==0) {
                        imp = (IMP)_DSSetPointValueAndNotify;
                    }
#if TARGET_OS_OSX
                    else if (strcmp(argType, @encode(NSPoint)) == 0) {
                        imp = (IMP)_DSSetPointValueAndNotify;
                    }
#endif
                    else if (strcmp (argType, @encode(NSRange)) == 0) {
                        imp = (IMP)_DSSetRangeValueAndNotify;
                    }
                    else if (strcmp(argType,@encode(CGRect)) == 0) {
                        imp = (IMP)_DSSetRectValueAndNotify;
                    }
#if TARGET_OS_OSX
                    else if (strcmp(argType,@encode(NSRect)) == 0) {
                        imp = (IMP)_DSSetRectValueAndNotify;
                    }
#endif
                    else if(strcmp(argType, @encode(CGSize)) == 0) {
                        imp = (IMP)_DSSetSizeValueAndNotify;
                    }
#if TARGET_OS_OSX
                    else if (strcmp(argType, @encode(NSSize)) == 0) {
                        imp = (IMP)_DSSetSizeValueAndNotify;
                    }
#endif
                    else {
                        imp = (IMP)_CF_forwarding_prep_0;
                    }
                } break;
                case 'B': {
                    imp = (IMP)_DSSetBoolValueAndNotify;
                } break;
                case 'C': {
                    imp = (IMP)_DSSetUnsignedCharValueAndNotify;
                } break;
                case 'I': {
                    imp = (IMP)_DSSetUnsignedIntValueAndNotify;
                } break;
                case 'L': {
                    imp = (IMP)_DSSetUnsignedLongValueAndNotify;
                } break;
                case 'Q': {
                    imp = (IMP)_DSSetUnsignedLongLongValueAndNotify;
                } break;
                case '#':
                case '@':{
                    imp = (IMP)_DSSetObjectValueAndNotify;
                } break;
                default: {
                    //
                } break;
            }
            ............

核心原理是:
<1> 根据方法签名判断返回值是是void类型
<2> 根据method_copyArgumentType(setMethod, 2) 获取参数类型。
<3> 根据参数类型实现不同的setter方法。

3、自动销毁机制实现

在实现自动销毁机制之前,先探索几个问题:

(1)我们可以在分类里面重写dealloc吗?

很明显,分类肯定不能重写dealloc方法,重写的话那么不就把系统的dealloc方法给覆盖了吗?那还怎么去释放对象呢?

如果是在NSObject的分类中去重写dealloc,确实会覆盖掉系统的dealloc方法。

如果是在非NSObject的类中去重写dealloc,通过汇编跟踪,在分类的dealloc执行完之后,会进入NSObject的dealloc。这里应该是llvm中一层代码注入。

void DeallocInCategoryCheck::registerMatchers(MatchFinder *Finder) {
  // Non-NSObject/NSProxy-derived objects may not have -dealloc as a special
  // method. However, it seems highly unrealistic to expect many false-positives
  // by warning on -dealloc in categories on classes without one of those
  // base classes.
  Finder->addMatcher(
      objcMethodDecl(isInstanceMethod(), hasName("dealloc"),
                     hasDeclContext(objcCategoryImplDecl().bind("impl")))
          .bind("dealloc"),
      this);
}

通过llvm源码分析,证明了这一点。
所以在非NSObject分类中重写dealloc方法,不会影响系统去释放对象。但是会覆盖主类的dealloc。

(2)我们通过method swizzling交换dealloc方法:

//error!!!!!
- (void)my_addKeyPath:(NSString *)keyPath callBack:(LLKVOBlock)callBack {
  ..........
    static dispatch_once_t predicate;
    dispatch_once(&predicate, ^{
        Method m1 = class_getInstanceMethod([self class], NSSelectorFromString(@"dealloc"));
        Method m2 = class_getInstanceMethod([self class], NSSelectorFromString(@"myDealloc"));
        method_exchangeImplementations(m1, m2);
    });
}
- (void)myDealloc {
    [self myDealloc];
     NSLog(@"====");
}

以上的交换方法存在2个严重问题:

<1> 如果“[self class]”(也就是原来的类)中没有实现dealloc方法的话,那么就会交换NSObject中的dealloc方法。会造成所有类的释放都会走myDealloc方法。

<2>当第一个问题成立之后,一旦在myDealloc方法中做NSLog打印,就会造成无限递归。因为NSLog打印结束也会释放对象。当然除了NSLog,还会有其他的情况造成无限递归,这里就不具体分析了。

显然如果用方法交换,会存在很多问题。

通过以上问题的探索,我们应该怎么正确的实现自动销毁机制呢?

其实不难想到,我们只需要在创建子类之后,再添加一个dealloc方法就ok了

    SEL deallocSEL = NSSelectorFromString(@"dealloc");
    Method deallocMethod = class_getInstanceMethod([self class], deallocSEL);
    class_addMethod(newClass, deallocSEL, (IMP)my_dealloc, method_getTypeEncoding(deallocMethod));
..........
void my_dealloc(id self, SEL _cmd) {
    NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kLLKVOAssiociateKey));
    if (observerArr.count <= 0) {
        return;
    }
    
    for (LLKVOInfo *info in observerArr) {
        [observerArr removeObject:info];
    }
    
    if (observerArr.count <= 0) {
        // 指回给父类
        Class superClass = [self class];
        object_setClass(self, superClass);
    }
}

补充:对于单利对象来说,添加一个dealloc方法显然不起作用。

所以单利对象仍然要手动调用my_removeObserver方法移除观察。或者监听observer释放来移除。

本文主要是基本原理分析,这里就不在具体的去优化实现了。