Objective-C 之 KVO 原理

2020-04-11  本文已影响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. 具体参考苹果文档

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

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 方案

上一篇下一篇

猜你喜欢

热点阅读