iOS进阶笔记(六) KVO

📣 iOS进阶笔记目录


一、KVO定义

KVO,即Key-value observing,是一种允许监听指定属性值改变的通知机制。官方文档解释如下:

Key-value observing is a mechanism that allows objects to be notified of changes to specified properties of other objects.


二、KVO使用

1、使用步骤

  • 通过addObserver:forKeyPath:options:context:方法注册观察者,观察者可以接收keyPath属性的变化事件。

  • 在观察者中实现observeValueForKeyPath:ofObject:change:context:方法,当keyPath属性发生改变后,KVO会回调这个方法来通知观察者。

  • 当观察者不需要监听时,可以调用removeObserver:forKeyPath:方法将KVO移除。

    NOTE:需要注意的是,调用`removeObserver:forKeyPath:`需要在观察者消失之前,否则会导致Crash。

    另外,苹果官方推荐:在init的时候进行addObserver,在deallocremoveObserver,这样可以保证addremove是成对出现的,是一种比较理想的使用方式。

    The protocol offers no way to ask an object if it is an observer or being observed. Construct your code to avoid release related errors. A typical pattern is to register as an observer during the observer’s initialization (for example in init or viewDidLoad) and unregister during deallocation (usually in dealloc), ensuring properly paired and ordered add and remove messages, and that the observer is unregistered before it is freed from memory.


2、KVO触发条件

序号 KVO触发条件
1

直接修改成员属性(调用属性set:方法)

2

通过KVC修改属性值


NOTE:另外,直接修改成员变量(例如:_age)不会触发KVO。


3、KVO使用示例

@interface Animal : NSObject {
@public;
    NSString *_name;
}
@property (nonatomic, assign) int age;
@end

@implementation Animal
@end

  • 【第一步】注册观察者(在init:方法或viewDidLoad方法中均可)
- (void)viewDidLoad {
    [super viewDidLoad];
    self.an = [[Animal alloc] init];
    [self.animal addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];
    [self.animal addObserver:self forKeyPath:@"_name" options:NSKeyValueObservingOptionOld |NSKeyValueObservingOptionNew context:nil];
}

  • 【第二步】实现KVO监听回调方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"age"]) {
        NSLog(@"age old:%@",[change objectForKey:NSKeyValueChangeOldKey]);
        NSLog(@"age new:%@",[change objectForKey:@"new"]);
    }else{
        NSLog(@"_name old:%@",[change objectForKey:NSKeyValueChangeOldKey]);
        NSLog(@"_name new:%@",[change objectForKey:@"new"]);
    }
}

  • 【第三步】移除观察者
- (void)dealloc
{
    [self.animal removeObserver:self forKeyPath:@"age" context:nil];
    [self.an removeObserver:self forKeyPath:@"_name" context:nil];
}

接下来测试验证,


测试1:通过修改属性值,可以正常监听到age值变化

self.animal.age = 2;

测试2:通过修改成员变量值,无法监听到`_name`值变化。(但可以通过手动触发实现)
self.animal->_name = @"泰迪";

测试3:通过KVC,可以正常监听到`_name`值变化
[self.animal setValue:@"边牧" forKey:@"_name"];

4、手动触发KVO

上面的示例是通过KVO自动触发机制完成对属性值的监听,我们也通过将在监听对象内重写+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key方法,并将返回值设置为NO,并且重写对象属性的set方法,即可完成手动监听,以达到等同自动监听目的。

稍加对Animal类进行改动即可:

@interface Animal : NSObject
{
@public;
    int _age;
}
@property (nonatomic, copy) NSString *name;
// 重写属性的set方法
- (void)setName:(NSString *)name;
- (void)setAge:(int)age;
@end

@implementation Animal
/// 去掉name和age属性自动触发KVO
/// @param key <#key description#>
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    if ([key isEqualToString:@"name"] || [key isEqualToString:@"age"]) {
        return NO;
    }else {
       return [super automaticallyNotifiesObserversForKey:key];
    }
}

// 重写set方法,并手动实现KVO
- (void)setAge:(int)age {
    if (_age != age) {
        [self willChangeValueForKey:@"age"];
        NSLog(@"willChangeValueForKey:%d",_age);
        _age = age;
        [self didChangeValueForKey:@"age"];
        NSLog(@"didChangeValueForKey:%d",age);
    }
}

- (void)setName:(NSString *)name {
    if (_name != name) {
        [self willChangeValueForKey:name];
        NSLog(@"willChangeValueForKey:%@",_name);
        _name = name;
        [self didChangeValueForKey:name];
        NSLog(@"didChangeValueForKey:%@",name);
    }
}
@end

接下来测试验证,

测试1:通过set方法,可监听到属性age及变量_name值变化

[self.an setAge:1];
[self.an setName:@"边牧"];

测试2:通过KVC,可监听到属性age及变量_name值变化

[self.an setValue:@(2) forKey:@"age"];
[self.an setValue:@"边牧" forKey:@"_name"];

根据上面示例我们可以发现,在willChangeValueForKey:didChangeValueForKey:两个方法之间修改值后,可以触发KVO监听方法(即执行observeValueForKeyPath: ofObject: change: context:回调方法)。


5、KVO一些应用场景

1)跨线程监听

@interface Animal : NSObject
@property (nonatomic, copy) NSString *name;
@end

@implementation Animal
- (void)setName:(NSString *)name {
    @synchronized (self) {// 保证多线程写安全
        _name = name;
    }
}
@end

@interface ViewController ()
@property (nonatomic, strong) Animal *animal;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    dispatch_queue_t queue = dispatch_queue_create("com.ryan.queue", DISPATCH_QUEUE_CONCURRENT);
    self.animal = [Animal new];
    
    self.animal.name = @"null";
    dispatch_async(queue, ^{
        NSLog(@"%@",[NSThread currentThread]);
        [self.animal addObserver:self
        forKeyPath:@"name"
           options:NSKeyValueObservingOptionNew
           context:nil];
    });
    
    dispatch_async(queue, ^{
        NSLog(@"%@ name:泰迪",[NSThread currentThread]);
        sleep(1);
        self.animal.name = @"泰迪";
    });
    
    dispatch_async(queue, ^{
        NSLog(@"%@ name:边牧",[NSThread currentThread]);
        self.animal.name = @"边牧";
    });
}

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

测试结果如下,可以看出KVO可以监听不同线程对name属性值的修改。

2)监听可变集合的变化

1、监听一个数组元素的改变,无非是监听内部单个或多个元素的增、删、改操作。
因此我们可以这样设计API:

  • 单个元素的增、删、改操作的接口;
  • 子数组的增、删、改接口;
  • 监听改变的回调接口。

NOTE:由于要监听子数组整体修改操作,而不必要监听字子数组每一个元素的改变。因此要确保只触发一次KVO监听即可。


API接口如下:

@interface GGMutableArrayKVOModel : NSObject
- (instancetype)init;

/// 可以所在类中监听该可变数组,防止外部修改,设置为只读属性
@property (nonatomic, copy, readonly) NSMutableArray <id>*ggMutableArray;

- (void)gg_addObject:(id)object;
- (void)gg_removeObject:(id)object;
- (void)gg_replaceObjectAtIndex:(NSUInteger)index withObject:(id)object;

/// 对子数组操作,只会触发一次KVO
- (void)gg_addObjects:(NSArray *)objects;
- (void)gg_removeObjectsAtIndexSetRange:(NSRange)range;
- (void)gg_replaceObjectsAtIndexSetRange:(NSRange)range withObjects:(NSArray *)objects;
- (void)gg_removeAllObjects;


///  设置KVO监听
/// @param callback 注意获取change中改变的结果时,有可能是object或objects
- (void)setKVOObserverForChangeCallback:(void(^)(NSDictionary<NSKeyValueChangeKey,id> * _Nonnull change))callback;

@end

具体实现如下:

#import "GGMutableArrayKVOModel.h"
static void *GGKVO_Context = &GGKVO_Context;

static void(^ChangeCallback)(NSDictionary<NSKeyValueChangeKey,id> *change);

BOOL p_isSubRange(NSRange originRange, NSRange subRange)
{
    return (subRange.location >= originRange.location && (subRange.location + subRange.length <= originRange.length));
}

@implementation GGMutableArrayKVOModel
// 注册监听者
- (instancetype)init
{
    if (self == [super init]) {
        [self addObserver:self forKeyPath:NSStringFromSelector(@selector(ggMutableArray)) options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:GGKVO_Context];
    }
    return self;
}
// 移除监听者
- (void)dealloc
{
    [self removeObserver:self forKeyPath:NSStringFromSelector(@selector(ggMutableArray)) context:GGKVO_Context];
}

- (NSMutableArray *)myArray
{
    // KVC可变代理方法mutableArrayValueForKey,内部手动实现了KVO,因此其内部对象发生改变时,会触发KVO的监听方法。
    return [self mutableArrayValueForKeyPath:NSStringFromSelector(@selector(ggMutableArray))];
}

// 单个元素增加
- (void)gg_addObject:(id)object
{
    @synchronized (self) {
        [[self myArray] addObject:object];
    }
}

// 单个元素删除
- (void)gg_removeObject:(id)object
{
    @synchronized (self) {
        [[self myArray] removeObject:object];
    }
}
// 单个元素替换
- (void)gg_replaceObjectAtIndex:(NSUInteger)index withObject:(id)object
{
    @synchronized (self) {
        [[self myArray] replaceObjectAtIndex:index withObject:object];
    }
}

/// 增加子数组(尾插法)
- (void)gg_addObjects:(NSArray *)objects
{
    @synchronized (self) {
        NSIndexSet *indexSet = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(self.ggMutableArray.count , objects.count)];
        // 子数组整体作为对象,操作时,只会触发一次KVO
        [[self myArray] insertObjects:objects atIndexes:indexSet];
    }
}

// 删除子数组
- (void)gg_removeObjectsAtIndexSetRange:(NSRange)range
{
    @synchronized (self) {
        NSRange originRange = NSMakeRange(0, self.ggMutableArray.count);
        if (p_isSubRange(originRange, range)) {
            NSIndexSet *indexSet = [NSIndexSet indexSetWithIndexesInRange:range];
            [[self myArray] removeObjectsAtIndexes:indexSet];
        }else{
            @throw [NSException exceptionWithName:[NSString stringWithFormat:@"Error at %s",__func__] reason:[NSString stringWithFormat:@"rang out of the ggMutableArray's range\nrange: %@\nggMutableArray range: %@",NSStringFromRange(range),NSStringFromRange(originRange)] userInfo:nil];
        }
    }
}
// 删除数组所有元素
- (void)gg_removeAllObjects
{
    @synchronized (self) {
        [[self myArray] removeAllObjects];
    }
}
// 替换子数组元素
- (void)gg_replaceObjectsAtIndexSetRange:(NSRange)range withObjects:(NSArray *)objects
{
    @synchronized (self) {
        NSRange originRange = NSMakeRange(0, self.ggMutableArray.count);
        if (p_isSubRange(originRange, range)) {
            NSIndexSet *indexSet = [NSIndexSet indexSetWithIndexesInRange:range];
            [[self myArray] replaceObjectsAtIndexes:indexSet withObjects:objects];
        }else{
            @throw [NSException exceptionWithName:[NSString stringWithFormat:@"Error at %s",__func__] reason:[NSString stringWithFormat:@"rang out of the ggMutableArray's range\nrange: %@\nggMutableArray range: %@",NSStringFromRange(range),NSStringFromRange(originRange)] userInfo:nil];
        }
    }
}
// 设置监听代理
- (void)setKVOObserverForChangeCallback:(void(^)(NSDictionary<NSKeyValueChangeKey,id> * _Nonnull change))callback
{
    if (callback) {
        ChangeCallback = callback;
    }
}

// 监听回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    if (context == GGKVO_Context) {
        if ([keyPath isEqualToString:NSStringFromSelector(@selector(ggMutableArray))]) {
            if (ChangeCallback) {
                ChangeCallback(change);
            }
        }
    }else{
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

测试代码:

#import "ViewController.h"
#import "GGMutableArrayKVOModel.h"

@interface Animal : NSObject
@property (nonatomic, copy) NSString *name;
@end

@implementation Animal
@end

@interface ViewController ()
@property (nonatomic, strong) GGMutableArrayKVOModel *kvoModel;
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    self.kvoModel = [[GGMutableArrayKVOModel alloc] init];
    __weak typeof(self) weak_self = self;
    // 监听可数组变化
    [self.kvoModel setKVOObserverForChangeCallback:^(NSDictionary<NSKeyValueChangeKey,id> * _Nonnull change) {
        __strong typeof(self) strong_self = weak_self;
        id new = change[NSKeyValueChangeNewKey];
        for (Animal *a in new) {
            NSLog(@"name:%@",a.name);
        }
        NSLog(@"----%@",strong_self.kvoModel.ggMutableArray);
    }];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    Animal *a1 = [[Animal alloc] init];
    a1.name = [NSString stringWithFormat:@"name%ld",random()%10];
    Animal *a2 = [[Animal alloc] init];
    a2.name = [NSString stringWithFormat:@"name%ld",random()%10];
    Animal *a3 = [[Animal alloc] init];
    a3.name = [NSString stringWithFormat:@"name%ld",random()%10];
    
    // 添加子数组
    [self.kvoModel gg_addObjects:@[a1,a2]];

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        // 替换数组第一个元素
        [self.kvoModel gg_replaceObjectAtIndex:0 withObject:a3];
        
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            Animal *a4 = [[Animal alloc] init];
            a4.name = [NSString stringWithFormat:@"name%ld",random()%10];
            Animal *a5 = [[Animal alloc] init];
            a5.name = [NSString stringWithFormat:@"name%ld",random()%10];
            // 整体替换一个子数组
            [self.kvoModel gg_replaceObjectsAtIndexSetRange:NSMakeRange(0, 2) withObjects:@[a4,a5]];
        });
    });
}

当我们要使用KVO监听集合对象变化时,需要通过KVC的可变代理方法(如mutableArrayValueForKey,可变代理方法内部手动实现了KVO)替代getter方法来获取集合代理对象(即runtime动态生成的NSKeyValueArray类型对象,继承自NSArray),然后对代理对象进行操作。当代理对象的内部对象发生改变时,会触发KVO的监听方法。


3)其它场景

  • 监听数据源改变,刷新UI;
  • 页面反向传值;
  • 监听程序中某些状态的变化(如网路状态)等。

由于比较简单就不再举例说明了。


三、KVO原理分析

根据前面,我们知道手动触发KVO有两个关键方法willChangeValueForKey:didChangeValueForKey:。那么在非手动触发KVO情况下,修改属性会触发KVO,相必也是在属性set方法内部实现了这两个方法。

为了实现这一方式,通过runtime作了如下的操作:

  • 利用runtime API动态生成所监听Animal对象的子类,即NSKVONotifying_Animal,通过isa-swizzing,让Animal实例对象的isa指向NSKVONotifying_Animal类,NSKVONotifying_Animal类的isa指向Animal。

验证:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    // 打印当前监听的类及父类
    Class cls = object_getClass(object);
    NSLog(@"%@",cls);// NSKVONotifying_Animal
    NSLog(@"%@",[cls superclass]);// Animal
}

这里需要注意的是[object class]拿到的类对象为Animalobject_getClass(object)并不一样。原因是NSKVONotifying_Animal子类重写了class方法。Apple这里采用里式替换原则也是为了让使用者不要去Care KVO内部到底干了啥。


  • NSKVONotifying_Animal类中包含要监听Animal类对象属性的set方法。

验证:我们利用runtime API 打印NSKVONotifying_Animal类的方法列表。

// 方法列表
NSArray* getMethodList(Class cls)
{
    unsigned int count;
    Method *methodList = class_copyMethodList(cls, &count);
    NSMutableArray *selArray = [NSMutableArray array];
    for (NSInteger i = 0; i < count; i++) {
        Method method = methodList[i];
        SEL method_sel = method_getName(method);
        [selArray addObject:NSStringFromSelector(method_sel)];
    }
    free(methodList);
    return [selArray copy];
}

方法列表为(子类重写父类的方法):

(
    "setAge:",
    class,
    dealloc,
    "_isKVOA"
)

顺便再打印下成员变量列表和属性列表,发现均为空。

// 成员变量列表
NSArray *getVarList(Class cls)
{
    unsigned int count;
    Ivar *varList = class_copyIvarList(cls, &count);
    // Ivar为objc_ivar结构体指针
    // 这里为了避免与系统重名,加上前缀
    typedef struct gg_objc_ivar {
        int32_t *offset;
        char * _Nullable ivar_name;
        char * _Nullable ivar_type;
    } *ggIvar;
    NSMutableArray *varArray = [NSMutableArray array];
    for (NSInteger i = 0; i < count; i++) {
        ggIvar var = (ggIvar)varList[i];
        [varArray addObject:[NSString stringWithFormat:@"name:%s type:%s",var->ivar_name,var->ivar_type]];
    }
    free(varList);
    return [varArray copy];
}
// 获得成员属性列表
NSArray *getPropertyList(Class cls) {
    // objc_property_t为objc_property类型结构体指针
    // 这里为了避免与系统重名,加上前缀
    typedef struct gg_objc_property {
        const char *name;
        const char *attr;
    } *gg_objc_property_t;
    
    unsigned int count;
    gg_objc_property_t *propertyList = (gg_objc_property_t*)class_copyPropertyList(cls, &count);
    NSMutableArray *propertyArray = [NSMutableArray array];
    for (NSInteger i = 0; i < count; i++) {
        gg_objc_property_t property = propertyList[i];
        [propertyArray addObject:[NSString stringWithFormat:@"name:%s attr:%s",property->name,property->attr]];
    }
    free(propertyList);
    return [propertyArray copy];
}

  • 当修改Animal对象的age属性值时,实际执行是NSKVONotifying_Animal的setAge:方法,该setAge:方法.

    验证:通过重写setAge:方法,当修改age值时,发现真正所处的类为NSKVONotifying_Animal

    #import "Animal.h"
    #import <objc/runtime.h>
    @implementation Animal
    - (void)setAge:(int)age {
        Class cls = object_getClass(self);
        NSLog(@"%@",cls);// NSKVONotifying_Animal
        NSLog(@"%@",object_getClass([cls superclass]));// Animal
        _age = age;
    }
    @end
    

    然后,我们在_age = age;处打断点,在通过汇编查看其以后的执行过程。

    1)首先来到NSObject一个名为NSKeyValueObservingPrivate的Extension中。


    2)接着调用父类的setAge:方法,修改值。


    3)函数内部又手动了实现了KVO,即执行了NSKeyValueWillChange(等同于willChangeValueForKey:)和NSKeyValueDidChange(等同于didChangeValueForKey:)两个方法。


    4)NSKeyValueDidChange方法内部执行了NSKeyValueNotifyObserver,即调用了observeValueForKeyPath: ofObject:change: context:方法,以监听属性值的改变


  • 移除观察者过程

    通过下列代码,可以发现,在执行完移除观察者后,animal实例对象的isa又重新指回了Animal。

[self.animal removeObserver:self forKeyPath:@"age" context:nil];
[self.animal removeObserver:self forKeyPath:@"_name" context:nil];
    
// 打印当前监听的类及父类
Class cls = object_getClass(self.animal);
NSLog(@"%@",cls);// Animal
NSLog(@"%@",[cls superclass]);// NSObject

通过下列方法,查看Animal类中仍然保留NSKVONotifying_Animal这个子类。

// 获取子类
NSArray *getSubClassListName(Class superCls)
{
    int count;
    Class *clsList = NULL;
    count = objc_getClassList((Class *)clsList, 0);
    NSMutableArray *subClassArray = [NSMutableArray array];
    if (count >0) {
        clsList = (Class *)malloc(sizeof(Class)*count);
        count = objc_getClassList(clsList, count);
        for (int i =0; i < count; i++) {
            if (class_getSuperclass(clsList[i]) == superCls) {
                [subClassArray addObject:NSStringFromClass(clsList[i])];
            }
        }
        free(clsList);
    }
    return [subClassArray copy];
}

小结:

KVO底层执行过程为:

  • 利用runtime API动态生成类对象的子类,即NSKVONotifying_xxx,通过isa-swizzing,让该实例对象的isa指向NSKVONotifying_xxx类,NSKVONotifying_xxx类的isa指向该类对象。并且NSKVONotifying_xxx类结构中包含原来类对象结构中属性的set方法,即重写父类的set、class、dealloc、_isKVOA方法。

  • 当修改该instance对象的某一属性值时,其内部又手动调用了willChangeValueForKey:和didChangeValueForKey两个方法,并在这两个方法之间通过superclass指针,指向父类这一属性set方法,进而修改其值。,即

    1)调用willChangeValueForKey:

    2)调用[super setAge:]
    通过NSKVONotifying_XXX类中的superclass指针找到其父类(即instance的类对象)中的setAge:方法,真正修改age的值

    3)调用didChangeValueForKey:
    内部调用了 observeValueForKeyPath: ofObject:change: context:方法,告诉Observer属性值的改变。


  • 当观察对象移除所有的监听后,会将观察对象的isa指向原来的类,但并不会注销该子类,这样可以重复利用,避免多次创建影响性能。

以上


----------End------------
posted @ 2021-08-04 20:57  ITRyan  阅读(108)  评论(0编辑  收藏  举报