美文网首页
Objective-C 之 KVO 原理

Objective-C 之 KVO 原理

作者: 正_文 | 来源:发表于2020-04-11 14:30 被阅读0次

    键值观察提供了一种机制,该机制允许将其他对象的特定属性的更改通知给对象。对于应用程序中模型层和控制器层之间的通信特别有用。 (在OS X中,控制器层绑定技术在很大程度上依赖于键值观察。)控制器对象通常观察模型对象的属性,而视图对象通过控制器观察模型对象的属性。但是,此外,模型对象可能会观察其他模型对象(通常是确定从属值何时更改),甚至是自身(再次确定从属值何时更改)。
    您可以观察到一些属性,包括简单属性,一对一关系和一对多关系。一对多关系的观察者被告知所做更改的类型,以及更改涉及哪些对象。

    一、基本使用

    1.1 注册观察者

    /// 注册观察者
    /// @param observer 观察者
    /// @param keyPath 要观察的属性keyPath
    /// @param options 观察者选项。影响通知的生成方式及回调时字典中携带的信息
    /// @param context 上下文
    - (void)addObserver:(NSObject *)observer
             forKeyPath:(NSString *)keyPath
                options:(NSKeyValueObservingOptions)options
                context:(nullable void *)context;
    

    context接收一个void *类型的参数,基本可以传任何类型。假如子类和他的父类由于不同的原因都注册了对同一个属性的观察,在回调中这两种的处理是不同的,那么回调中的keyPath和被观察者对象是无法区分的,此时就可以通过context这个参数来区分。

    1.2 实现回调

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

    1.3 移除观察者

    - (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context;
    - (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
    

    1.4观察集合类型属性

    @interface Animal : NSObject
    
    
    @property (nonatomic,strong) NSMutableArray *friends;
    
    @end
    
    //viewContoller代码
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        
        Animal *animal = [Animal alloc];
        animal.friends = @[].mutableCopy;
        [animal addObserver:self
                 forKeyPath:@"friends"
                    options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
                    context:NULL];
        
        //1、被动触发错误方式:这里无法触发`kvo`回调
        [animal.friends addObject:@"dog"];
        //2、被动触发正确方法
        [[animal mutableArrayValueForKey:@"friends"] addObject:@"dog"];
        //3、手动触发
        [animal willChangeValueForKey:@"friends"];
        [animal.friends addObject:@"dog"];
        [animal didChangeValueForKey:@"friends"];
    
    }
    

    23行代码相当于

    NSMutableArray *tmp = [NSMutableArray arrayWithArray:animal.friends];
    [tmp addObject:@"dog"];
    animal.friends = tmp;
    

    因此触发了kvo21行,因为KVO是给予set方法的,这样不会触发set方法,所以就不会触发KVO通知。

    1.5多属性的关联

    我们需要在被观察者类重写两个方法:

    1. 一个系统方法+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key或者+ (NSSet *)keyPathsForValuesAffecting<xxx>
    2. 一个是被观察属性的getter方法。

    例如:有一个Downloader.h类,有三个属性totalBytescompletedBytes,和百分比进度progress

    // Downloader.h
    @interface Downloader : NSObject
    
    @property (nonatomic) unsigned long long totalBytes;
    
    @property (nonatomic) unsigned long long completedBytes;
    
    @property (nonatomic, copy) NSString *progress;
    
    @end
    

    在UI层我们只关注progress,但进度是受其他两个属性共同影响的,此时需要在Downloader.m实现中重写两个方法:

    @implementation Downloader
    
    + (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
        
        NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
        if ([key isEqualToString:@"progress"]) {
            NSArray *dependKeys = @[@"totalBytes", @"completedBytes"];
            keyPaths = [keyPaths setByAddingObjectsFromArray:dependKeys];
        }
        return keyPaths;
    }
    
    - (NSString *)progress {
        if (0 == self.totalBytes || 0 == self.completedBytes) {
            return @"0";
        }
        
        double progress = (double)self.completedBytes / (double)self.totalBytes * 100;
        
        if (progress > 100) {
            progress = 100;
        }
        
        return [NSString stringWithFormat:@"%d%%", (int)ceil(progress)];
    }
    
    @end
    

    二、KVO实现原理

    Automatic key-value observing is implemented using a technique called isa-swizzling. 具体参考苹果文档

    当一个类的实例第一次注册观察者时,系统会做以下事情:

    • 动态生成一个继承自该类的中间类:NSKVONotifying_xxx
    • 将对象的isa指向这个中间类(isa-swizzling
    • 观察的是setter
    • 子类中重写set<xxx>-class-dealloc方法,添加一个-_isKVOA方法,依然返回原类,而非子类
    • 移除所有的观察后,isa会指回来,但是动态子类不会销毁

    2.1 原理验证

    被观察类Animal添加代码:

    @interface Animal : NSObject{
        @public
        NSString *nickName;
    }
    
    @property (nonatomic, copy) NSString *name;
    @property (nonatomic,strong) NSMutableArray *friends;
    
    @end
    

    viewController添加代码:

    - (void)viewDidLoad {
        [super viewDidLoad];
        
        
        Animal *animal = [Animal alloc];
        [self printClasses:[animal class]];
        [self printMethods:[animal class]];
        
        [animal  addObserver:self
                    forKeyPath:@"nickName"
                       options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
                       context:NULL];
        [animal addObserver:self
                 forKeyPath:@"name"
                    options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
                    context:NULL];
        [animal addObserver:self
                 forKeyPath:@"friends"
                    options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
                    context:NULL];
        
        printf("\n********************************************************\n\n");
        [self printClasses:[animal class]];
        [self printMethods:NSClassFromString(@"NSKVONotifying_Animal")];
        
        
        
        
        animal.name = @"dog";
        animal->nickName = @"cat";
    }
    
    /// 打印出指定类及其子类列表
    - (void)printClasses:(Class)cls {
        int count = objc_getClassList(NULL, 0);
        NSMutableArray *results = [NSMutableArray arrayWithObject:cls];
        Class *classes = (Class *)malloc(sizeof(Class) * count);
        objc_getClassList(classes, count);
        for (int i = 0; i < count; i++) {
            if (cls == class_getSuperclass(classes[i])) {
                [results addObject:classes[i]];
            }
        }
        NSLog(@"\nClasses: %@", results);
        free(classes);
    }
    
    /// 打印出指定类所有的方法
    - (void)printMethods:(Class)cls {
        unsigned int count = 0;
        Method *methodList = class_copyMethodList(cls, &count);
        printf("Methods of class: %s (\n", NSStringFromClass(cls).UTF8String);
        for (int i = 0; i < count; i++) {
            Method method = methodList[i];
            SEL sel = method_getName(method);
            IMP imp = method_getImplementation(method);
            printf("    %s-%p\n", NSStringFromSelector(sel).UTF8String, imp);
        }
        printf(")\n");
        free(methodList);
    }
    
    

    控制台打印结果 :

    2020-04-13 15:08:51.803441+0800 kvo[23854:22631006] 
    Classes: (
        Animal
    )
    Methods of class: Animal (
        .cxx_destruct-0x10f868e50
        name-0x10f868d80
        setName:-0x10f868db0
        friends-0x10f868df0
        setFriends:-0x10f868e10
    )
    
    ********************************************************
    
    2020-04-13 15:08:51.809383+0800 kvo[23854:22631006] 
    Classes: (
        Animal,
        "NSKVONotifying_Animal"
    )
    Methods of class: NSKVONotifying_Animal (
        setFriends:-0x7fff25701c8a
        setName:-0x7fff25701c8a
        class-0x7fff2570074d
        dealloc-0x7fff257004b2
        _isKVOA-0x7fff257004aa
    )
    2020-04-13 15:08:51.809932+0800 kvo[23854:22631006] -------------------{
        kind = 1;
        new = dog;
        old = "<null>";
    }
    
    
    

    通过上面打印结果发现:只有属性发生了回调,实例变量并没有。它们的区别就是有没有setter方法,所以我们得出结果:KVO是通过setter方法进行处理回调的。

    苹果官方推荐尽量使用属性点语法的形式为属性赋值和访问属性,这样其实是在调用setter和getter,如果重写了setter和getter在期中增加了额外代码,可以保证代码执行的正确性。

    viewController中继续添加代码,移除所有的观察者。

    [self performSelector:@selector(removeAllObserver) withObject:nil afterDelay:2];
    
    - (void)removeAllObserver{
        [_animal removeObserver:self forKeyPath:@"nickName"];
        [_animal removeObserver:self forKeyPath:@"name"];
        [_animal removeObserver:self forKeyPath:@"friends"];
        
        printf("\n********************************************************\n\n");
        [self printClasses:[_animal class]];
    }
    

    打印结果:

    2020-04-13 15:20:17.770486+0800 kvo[24356:22641029] 
    Classes: (
        Animal,
        "NSKVONotifying_Animal"
    )
    

    你也可以通过lldb,来探索一下,整个过程中isa指针的指向,object_getClassName(animal)

    2.2 kvc 和 kvo

    苹果文档有介绍,在理解KVO之前,必须先理解KVC。上篇文章我们也讨论了KVC的实现原理,KVC会先查找settergetter进行调用,如果没有查找到,则调用类方法+accessInstanceVariablesDirectly,如果返回YES,再去查找成员变量。
    KVO也有类似的机制,在KVO接口中有这三个接口:

    - (void)willChangeValueForKey:(NSString *)key;
    - (void)didChangeValueForKey:(NSString *)key;
    + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key;
    

    +automaticallyNotifiesObserversForKey:默认返回YES,动态创建的中间类重写了setter,虽然无法看到实现源码,但可以猜测在修改属性前后分别调用了-willChangeValueForKey:-didChangeValueForKey:类似方法,达到通知观察者的目的。
    如果子类中重载了+automaticallyNotifiesObserversForKey:并返回NO,则无法触发自动KVO通知机制,但我们可以通过手动调用-willChangeValueForKey:-didChangeValueForKey:来触发KVO回调。

    三、自定义KVO

    系统kvo使用时存在不方便的地方,根据kvo的原理和基本使用,我们可以简单自定义kvo实现。

    1. 入参检查
    2. 检查是否有属性的setter
    3. 动态创建对象子类BLKVOClass_xxx
    4. isa-swizzling
    5. 重写-class、-dealloc方法
    6. 重写setter
    7. 保存观察者信息,在属性发生变化时回调

    3.1 动态创建对象子类

        Class newClass = NSClassFromString(newClassName);
        
        if (newClass) {
            return newClass;
        }
        
        /**
        * 如果内存不存在,创建生成
        * 参数一: 父类
        * 参数二: 新类的名字
        * 参数三: 新类的开辟的额外空间
        */
       
        // 2.1 : 申请类
        newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
        // 2.2 : 注册类
        objc_registerClassPair(newClass);
    

    3.2 isa-swizzling

    重写class方法

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

    动态子类添加class实现,完成isa-swizzling

    // 2.3.1 : 添加class : class的指向是父类
        SEL classSEL = NSSelectorFromString(@"class");
        Method classMethod = class_getInstanceMethod(newClass, classSEL);
        const char *classTypes = method_getTypeEncoding(classMethod);
        class_addMethod(newClass, classSEL, (IMP)mm_class, classTypes);
    

    3.3 dealloc

    重写delloc,1、安全移除所有observe,2、销毁关联对象,3、isa指回父类,4、调用系统dealloc。

    - (void)mm_dealloc {
    //    Class superClass = [self class];
        Class superCls = class_getSuperclass(object_getClass(self));
        object_setClass(self, superCls);
        
        // Call system -dealloc
        [self mm_dealloc];
    }
    

    四、FBKVOController

    下面简单聊一下FBKVOController,它里面有几个关键类:

    1. _FBKVOSharedController,单利对象,处理、转发KVOViewController传过来的所有观察者事件。
    2. _FBKVOInfo,数据模型,保存一个完整的KVO数据。
    3. KVOViewController,每个观察者都有一个该类的实例对象,这个类用于处理观察者传过来的所有数据,下图是他的主要属性构成。 KVOViewController.png

    下面是一个简单的调用实现代码:

    - (void)viewDidLoad {
        [super viewDidLoad];
        
        Student *student = [[Student alloc] init];
        
        FBKVOController *kvoCtrl = [FBKVOController controllerWithObserver:self];
        
        [kvoCtrl observe:student keyPath:@"nickName" options:NSKeyValueObservingOptionNew block:^(id  _Nullable observer, id  _Nonnull object, NSDictionary<NSString *,id> * _Nonnull change) {
            NSLog(@"****%@****",change);
        }];
        
        student.nickName = @"kkk";
        
    }
    

    observe对应viewControllerstudent对应object。当viewController被释放的时候,会先调用FBKVOControllerdealloc方法,在这里会将_objectInfosMap里所有的被观察者安全得 remove

    拓展:抖音技术团队iOS大解密:玄之又玄的KVOObjective-C & Swift 最轻量级 Hook 方案

    相关文章

      网友评论

          本文标题:Objective-C 之 KVO 原理

          本文链接:https://www.haomeiwen.com/subject/lvnrmhtx.html