Principio subyacente de iOS 20: análisis KVO

Este es el noveno día de mi participación en el Desafío de actualización de agosto. Para obtener detalles del evento, consulte: Desafío de actualización de agosto

Introducción a KVO

KVOEl nombre completo Key-Value Observingsignifica que 键值观察, KVOes un mecanismo que permite 允许其他对象的指定属性发生变化时,通知对象, si quieres entender 键值观察, primero debes entender 键值编码que es KVC;

KVC键值编码, después de que se crea el objeto, puede 动态的给对象的属性赋值, KVOpero 键值观察se proporciona un conjunto de mecanismos de monitoreo.Cuando se modifica la propiedad especificada del objeto, el objeto recibirá una notificación, para que pueda verse KVO是基于KVC的基础上对对象属性的动态变化进行监听;

Esto puede sonar NSNotificationCentersimilar, entonces, ¿cuál es la diferencia entre ellos?

Diferencia entre KVO y NotificationCenter

  • Mismo punto
    • Ambos son 观察者模式, ambos se usan 监听;
    • operaciones que se pueden lograr 一对多;
  • diferencia
    • KVOSe utiliza para monitorear los cambios en las propiedades de los objetos y los nombres de las propiedades se NSStringbuscan a través de cadenas;
    • NSNotificationCenter监听Es decir, la operación POSTque podemos controlar, KVOpero que es 系统controlada por;
    • KVOcambios que se pueden registrar 新旧值;

Uso de KVO

Uso básico de KVO

  • Registrar un observador
[self.person addObserver:self forKeyPath:@"nickName" options:NSKeyValueObservingOptionNew context:NULL];
复制代码
  • Escuche las devoluciones de llamada de KVO
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if ([keyPath isEqualToString:@"nickName"]) {
        NSLog(@"%@发生变化:%@", keyPath, [change objectForKey:NSKeyValueChangeNewKey]);
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}
复制代码
  • quitar observador
- (void)dealloc {
    [self.person removeObserver:self forKeyPath:@"nickName" context:NULL];
}
复制代码

Uso del contexto

La introducción en el documento oficial de KVO es contextla siguiente

简要来说就是context是一个包含任意数据的指针,这些数据会在相应的更改通知中回传给观察者。我们可以指定contextNULL,从而使用keyPath键路径来确定更改的通知的来源,但是这种方法可能会导致某些对象发生问题,比如该对象的父类也监听了相同的keyPath;所以我们可以为每一个需要观察的keyPath传建一个不同的context,从而完全跳过对keyPath的比较,直接使用context进行更有效的通知解析;context更安全也更具可扩展性,从而大大提升性能和代码的可读性;

我们通过代码来直观的感受一下:

  • 未使用context时代码逻辑:
- (void)viewDidLoad {
    [super viewDidLoad];

    self.person = [[Person alloc] init];
    self.student = [[Student alloc] init];

    [self.person addObserver:self forKeyPath:@"nickName" options:NSKeyValueObservingOptionNew context:NULL];
    [self.student addObserver:self forKeyPath:@"nickName" options:NSKeyValueObservingOptionNew context:NULL];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if ([keyPath isEqualToString:@"nickName"] && object == self.person) {
        NSLog(@"Person:%@发生变化:%@", keyPath, [change objectForKey:NSKeyValueChangeNewKey]);
    } else if ([keyPath isEqualToString:@"nickName"] && object == self.student) {
        NSLog(@"Student:%@发生变化:%@", keyPath, [change objectForKey:NSKeyValueChangeNewKey]);
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

- (void)dealloc {
    [self.person removeObserver:self forKeyPath:@"nickName" context:NULL];
    [self.student removeObserver:self forKeyPath:@"nickName" context:NULL];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    self.person.nickName = @"昵称";
    self.student.nickName = @"王同学";
}
复制代码

为了监听PersonStudent两个类的相同的nickName属性,我们在接收监听回调时,为了区分nickName来自于哪个对象,除了使用keyPath只要,还需要使用object来判断来源对象,才能准确区分是谁的nickName发生了变化;

  • 使用context时代码逻辑:
//定义context
static void *PersonNameNickContext = &PersonNameNickContext;
static void *StudentNameNickContext = &StudentNameNickContext;

- (void)viewDidLoad {
    [super viewDidLoad];

    self.person = [[Person alloc] init];
    self.student = [[Student alloc] init];

    [self.person addObserver:self forKeyPath:@"nickName" options:NSKeyValueObservingOptionNew context:PersonNameNickContext];
    [self.student addObserver:self forKeyPath:@"nickName" options:NSKeyValueObservingOptionNew context:StudentNameNickContext];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if (context == PersonNameNickContext) {
        NSLog(@"Person:%@发生变化:%@", keyPath, [change objectForKey:NSKeyValueChangeNewKey]);
    } else if (context == StudentNameNickContext) {
        NSLog(@"Student:%@发生变化:%@", keyPath, [change objectForKey:NSKeyValueChangeNewKey]);
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

- (void)dealloc {
    [self.person removeObserver:self forKeyPath:@"nickName" context:PersonNameNickContext];
    [self.student removeObserver:self forKeyPath:@"nickName" context:StudentNameNickContext];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    self.person.nickName = @"昵称";
    self.student.nickName = @"王同学";
}
复制代码

上述方法是不会触发KVO的;

仅仅通过context就能判断是哪个对象的nickName发生了变化;

KVO使用细节

移除观察者的必要性

KVO官方文档中关于removeObserver的说明如下:

解释:

通过向被观察者发送removeObserver:forKeyPath:context:消息,指定观察对象键路径context,可以删除一个键值观察者;

在接收到removeObserver:forKeyPath:context:消息后,观察对象将不再接收指定键路径和对象的任何observeValueForKeyPath:ofObject:change:context消息;

当移除观察者时,需要注意以下几点:

  • 如果没有注册观察者,被移除时会导致NSRangeException,可以通过调用一次removeObserver:forKeyPath:context:来应对addObserver:forKeyPath:options:context:;如果在项目中这种方式不可行时,可以把removeObserver:forKeyPath:context: call放在try/catch块中来处理潜在的异常;
  • 当被释放时,观察者不会自动释放自己。被观察的对象继续发送通知,而忽略了其状态。但是,与发送到已释放的对象的其他消息一样,更改通知也会触发内存访问异常。因此,应该确保观察者在从内存中消失之前将自己删除。
  • 该协议无法询问对象是观察者还是被观察者。构造代码以避免与发布相关的错误,一种典型的模式就是在观察者初始化期间(init或者viewDidLoad中)注册为观察者,并在释放过程中(通常是在dealloc中)注销,以确保添加和删除消息是成对的,并确保观察者在注册之前被取消注册,并从内存中释放。

总的来说就是,KVO注册观察者和移除观察者需要是成对出现的,如果只注册而不移除,会出现崩溃

图中的Person采用单例是为了防止释放,演示崩溃现象

崩溃的原因是,由于第一次注册KVO观察者之后没有移除,再次进入界面,会导致第二次注册KVO观察者,由于之前注册的对象并没有释放,导致重复的注册观察者,此时会收到属性值变化的通知,会出现找不到通知对象。

所以,为了防止出现这种情况,建议在dealloc中移除观察者。需要注意的是,如果使用了context,那么移除时也要使用相同的context,否则将会崩溃,抛出名为NSRangeException的异常:

KVO的自动触发和手动触发

  • 自动触发
// 自动开关
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
    return YES;
}
复制代码

方法返回YES时,表示可以监听,返回NO,表示不可以监听;

  • 手动触发
- (void)setNickName:(NSString *)nickName {
    [self willChangeValueForKey:@"nickName"];
    _nickName = nickName;
    [self didChangeValueForKey:@"nickName"];
}
复制代码

自动触发方法返回NO时,我们可以通过这种方式实现手动触发

KVO观察:一对多

KVO观察中的一对多,意思是通过注册一个观察者,可以监听到多个属性的变化;

比如,下载文件的时候,我们经常需要根据总下载量totalData和当前下载量writtenData来计算出下载进度downloadProgress,有两种方法都可以达到目的:

  • 第一种,分别给两个属性添加观察者,当其中任何一个发生变化时,计算当前的下载进度downloadProgress;
  • 第二种,实现keyPathsForValuesAffectingValueForKey方法,将两个观察者合二为一,即观察当前的downloadProgress,当totalDatawrittenData任意一个值发生改变即会发送通知:
@implementation Person

+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    if ([key isEqualToString:@"downloadProgress"]) {
        NSArray *affectingKeys = @[@"totalData", @"writtenData"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}

- (NSString *)downloadProgress{
    if (self.writtenData == 0) {
        self.writtenData = 10;
    }
    if (self.totalData == 0) {
        self.totalData = 100;
    }
    return [[NSString alloc] initWithFormat:@"%f",1.0f*self.writtenData/self.totalData];
}
@end


@implementation SecondViewController

static void *PersondownloadProgressContext = &PersondownloadProgressContext;

- (void)viewDidLoad {
    [super viewDidLoad];
    self.navigationItem.title = NSStringFromClass(self.class);
    self.view.backgroundColor = [UIColor whiteColor];
    
    self.person = [[Person alloc] init];
    
    [self.person addObserver:self forKeyPath:@"downloadProgress" options:NSKeyValueObservingOptionNew context:PersondownloadProgressContext];

}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if (context == PersondownloadProgressContext) {
        NSLog(@"Person:%@发生变化:%@", keyPath, [change objectForKey:NSKeyValueChangeNewKey]);
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

- (void)dealloc {
    [self.person removeObserver:self forKeyPath:@"downloadProgress" context:PersondownloadProgressContext];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    self.person.writtenData += 10;
}

@end
复制代码

KVO观察可变数组

KVO是基于KVC基础之上的,所以可变数组添加元素,直接调用addObject方法是不会触发setter方法的,所以通过addObject这种方法添加元素时无法监听到数组的变化的;

@implementation SecondViewController

static void *PersondateArrayContext = &PersondateArrayContext;

- (void)viewDidLoad {
    [super viewDidLoad];
    self.navigationItem.title = NSStringFromClass(self.class);
    self.view.backgroundColor = [UIColor whiteColor];
    
    self.person = [[Person alloc] init];
    self.person.dataArray = [[NSMutableArray alloc] initWithCapacity:0];
    
    [self.person addObserver:self forKeyPath:@"dataArray" options:NSKeyValueObservingOptionNew context:PersondateArrayContext];

}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    NSLog(@"--->%@", change);
}

- (void)dealloc {
    [self.person removeObserver:self forKeyPath:@"dataArray" context:PersondateArrayContext];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self.person.dataArray addObject:@"1"];
}
复制代码

KVC官方文档中针对可变数组类型进行了说明,需要通过mutableArrayValueForKey方法:

修改代码如下:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [[self.person mutableArrayValueForKey:@"dataArray"] addObject:@"1"];
}
复制代码

运行结果:

数组改变时,可以被监听到;

其中kind表示键值变化的类型,是一个枚举类型:

typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
    NSKeyValueChangeSetting = 1,    // 设置值
    NSKeyValueChangeInsertion = 2,  // 插入
    NSKeyValueChangeRemoval = 3,    // 移除
    NSKeyValueChangeReplacement = 4, // 替换
};
复制代码

所以,一般属性kind1;

KVO原理探索

KVO官方文档中对KVO的原理描述如下:

解析:

  • KVO自动键值观察是使用isa-swizzling技术实现的;
  • isa指针,顾名思义,指向维护调度表的对象的。这个调度表本质上包含指向类实现的方法的指针,以及其他数据
  • 对象属性注册为观察者时,将会修改被观察对象isa指针,指向一个中间类而不是真正的类。因此,isa指针的值不一定反映实例的实际类;
  • 不应该依赖isa指针来决定类的成员。相反,应该使用类方法来确定对象实例的类

KVO代码调试

属性观察

在上文中,我们测试了属性nickName的修改可以被KVO监听到,那么成员变量是否也能监听到呢?

Person类添加名为name成员变量:

@interface Person : NSObject {
    @public
    NSString *name;
}
@property (nonatomic, copy) NSString *nickName;
@end
复制代码

分别为namenickName都添加KVO监听:

[self.person addObserver:self forKeyPath:@"nickName" options:NSKeyValueObservingOptionNew context:PersonNameNickContext];
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:PersonNameContext];
复制代码

运行结果:

KVO只能监听属性,不能对成员变量进行监听;而属性成员变量的区别在于属性比成员变量多一个setter方法,而KVO监听的就是setter方法;

中间类NSKVONotifying_Xxxx

根据KVO官方文档的描述,在注册观察者之后,观察对象的isa指针发生了变化,接下来通过代码来验证一下:

在注册成为观察者之前,实例对象personisa指向Person;

在注册成为观察者之后,实例对象personisa指向NSKVONotifying_Person;

在注册成功观察者之后,实例对象的isa指向了一个中间类NSKVONotifying_Xxxxisa的指针指向确实发生了变化

NSKVONotifying_Xxxx 研究

NSKVONotifying_Xxxx与观察者的关系

那么NSKVONotifying_Xxxx究竟是什么呢?

通过上述两张图,我们可以确定NSKVONotifying_Person确实是,在person对象注册成为观察者之后,系统在底层自动生成的,那么这个类和Person有什么关系呢?

我们首先来遍历一下Person在注册观察者前后的子类是否有变化:

注册为观察者之前,Person只有一个子类Student,注册观察者之后,Person多了一个名为NSKVONotifying_Person子类,而NSKVONotifying_Person没有子类;

NSKVONotifying_Xxxx的方法列表

那么,NSKVONotifying_Person中都有那些方法呢:

可以看到,自动生成的类NSKVONotifying_Person中,有四个方法,分别是setNickNameclassdealloc_isKVOA

  • setNickName 观察对象的setter方法
  • class 类型
  • dealloc 是否释放(该dealloc执行时,将isa重新指向Person)
  • _isKVOA 判断是否是KVO生成的一个辨识码

那么这些方法是继承来的还是重写了父类的方法呢?

创建一个继承与类Person的类Student

@interface Student : Person

@end

@implementation Student

@end
复制代码

然后,分别打印StudentNSKVONotifying_Person的方法列表:

StudentLa lista de métodos no se imprime (el método integrado no se puede recorrer en la lista de métodos de la subclase), NSKVONotifying_Personel método en la descripción anula el método de la clase principal

Edición de publicación de NSKVONotifying_Xxxx

Dado que el sistema creó automáticamenteNSKVONotifying_Person

Después de la verificación, después de dealloceliminar el observador en , el isapuntero apunta a la Personclase nuevamente, ¿entonces NSKVONotifying_Personse destruye?

PersonComprobemos la situación de la subclase impresa en la interfaz anterior de la interfaz actual :

Se puede ver que incluso si deallocel método se ejecuta y 观察者se elimina, NSKVONotifying_Personaún existe después de regresar a la interfaz de nivel superior;

Una vez generada la clase intermedia, considerando el problema de la reutilización, siempre existirá y no se destruirá;

Problema de propiedad del método Setter

Hemos verificado antes que KVOel método es monitoreado , y la setterclase intermedia NSKVONotifying_Persontambién reescribe el método, entonces, ¿el método setterque finalmente modificamos es verdadero o verdadero?setterNSKVONotifying_PersonPerson

Se puede ver que en 移除观察者时, isase ha apuntado a Person, y nickNameel valor también ha cambiado, entonces el settermétodo en este momento es yes Person;

A continuación, vamos a 观察变量值改变verificarlo por:

Ejecute el proyecto, los puntos de interrupción y observe cómo _nickNamecambia el valor:

Continúe ejecutando el proyecto y active el oyente:

btInformación de la pila de impresión:

Entonces, el método que finalmente se llama setteres ;PersonsetNickName

Supongo que te gusta

Origin juejin.im/post/6994349205632319496
Recomendado
Clasificación