iOS底层原理(20) - KVO原理

154 阅读9分钟

关于KVO的官方文档: 官方文档

一、KVO的一些细节

1、context

官方文档的解释

The context pointer in the addObserver:forKeyPath:options:context: message contains arbitrary data that will be passed back to the observer in the corresponding change notifications. You may specify NULL and rely entirely on the key path string to determine the origin of a change notification, but this approach may cause problems for an object whose superclass is also observing the same key path for different reasons.

A safer and more extensible approach is to use the context to ensure notifications you receive are destined for your observer and not a superclass.

The address of a uniquely named static variable within your class makes a good context. Contexts chosen in a similar manner in the super- or subclass will be unlikely to overlap. You may choose a single context for the entire class and rely on the key path string in the notification message to determine what changed. Alternatively, you may create a distinct context for each observed key path, which bypasses the need for string comparisons entirely, resulting in more efficient notification parsing. 翻译 在addObserver:forKeyPath:options:context:消息中的上下文指针包含了将在相应的更改通知中传递回观察者的任意数据。您可以指定NULL并完全依赖键路径字符串来确定更改通知的来源,但是这种方法可能会导致一个对象的问题,该对象的超类由于不同的原因也在观察相同的键路径。

一种更安全、更可扩展的方法是使用上下文来确保接收到的通知是发送给观察者的,而不是一个超类。

类中唯一命名的静态变量的地址是一个很好的上下文。在超类或子类中以类似方式选择的上下文不太可能重叠。您可以为整个类选择一个上下文,并依赖通知消息中的关键路径字符串来确定更改了哪些内容。或者,您可以为每个观察到的键路径创建一个不同的上下文,这将完全绕过字符串比较的需要,从而产生更有效的通知解析。

代码示例

static void *PersonAccountBalanceContext = &PersonAccountBalanceContext;
static void *PersonAccountInterestRateContext = &PersonAccountInterestRateContext;

- (void)registerAsObserverForAccount:(Account*)account {
    [account addObserver:self
              forKeyPath:@"balance"
                 options:(NSKeyValueObservingOptionNew |
                          NSKeyValueObservingOptionOld)
                 context:PersonAccountBalanceContext];
 
    [account addObserver:self
              forKeyPath:@"interestRate"
                 options:(NSKeyValueObservingOptionNew |
                          NSKeyValueObservingOptionOld)
                  context:PersonAccountInterestRateContext];
}


- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context {
 
    if (context == PersonAccountBalanceContext) {
        // Do something with the balance…
 
    } else if (context == PersonAccountInterestRateContext) {
        // Do something with the interest rate…
 
    } else {
        // Any unrecognized context must belong to super
        [super observeValueForKeyPath:keyPath
                             ofObject:object
                               change:change
                               context:context];
    }
}

小结

通常我们通过keyPath来区分观察的内容,但是这种方式有一个问题,那就是如果父类也做了同样的观察,那么就会出现观察内容一样产生不可预知的问题;我们可以用context来区分观察的内容,这种方式既安全又便于扩展。

2、移除观察者

官方文档解释

After receiving a removeObserver:forKeyPath:context: message, the observing object will no longer receive any observeValueForKeyPath:ofObject:change:context: messages for the specified key path and object.

When removing an observer, keep several points in mind:

Asking to be removed as an observer if not already registered as one results in an NSRangeException. You either call removeObserver:forKeyPath:context: exactly once for the corresponding call to addObserver:forKeyPath:options:context:, or if that is not feasible in your app, place the removeObserver:forKeyPath:context: call inside a try/catch block to process the potential exception. An observer does not automatically remove itself when deallocated. The observed object continues to send notifications, oblivious to the state of the observer. However, a change notification, like any other message, sent to a released object, triggers a memory access exception. You therefore ensure that observers remove themselves before disappearing from memory. 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.

翻译

观察对象收到removeObserver:forKeyPath:context:消息后,将不再接收到指定密钥路径和对象的observeValueForKeyPath:ofObject:change:context:消息。

当移除观察者时,请记住以下几点:

如果没有注册为观察者,请求被移除会导致NSRangeException。你可以调用removeObserver:forKeyPath:context:恰好一次,对应于对addObserver:forKeyPath:options:context:的调用,或者如果在你的应用中不可行,将removeObserver:forKeyPath:context:调用放在try/catch块中处理潜在的异常。

当释放时,观察者不会自动删除自身。被观察对象继续发送通知,与观察者的状态无关。但是,与任何其他消息一样,发送到被释放对象的更改通知会触发内存访问异常。因此,您可以确保观察者在从内存中消失之前删除了自己。

协议没有提供询问对象是观察者还是正在被观察的方法。构建代码以避免发布相关错误。一个典型的模式是在观察者的初始化期间注册为观察者(例如在init或viewDidLoad中),在释放期间注销(通常在dealloc中),确保正确配对和有序的添加和删除消息,并且在从内存中释放观察者之前,它是未注册的。

- (void)unregisterAsObserverForAccount:(Account*)account {
    [account removeObserver:self
                 forKeyPath:@"balance"
                    context:PersonAccountBalanceContext];
 
    [account removeObserver:self
                 forKeyPath:@"interestRate"
                    context:PersonAccountInterestRateContext];
}

小结

确保添加观察者和移除观察者是成对出现的,否则会报出异常错误。

3、手动改变通知

官方文档解释

In some cases, you may want control of the notification process, for example, to minimize triggering notifications that are unnecessary for application specific reasons, or to group a number of changes into a single notification. Manual change notification provides means to do this.

Manual and automatic notifications are not mutually exclusive. You are free to issue manual notifications in addition to the automatic ones already in place. More typically, you may want to completely take control of the notifications for a particular property. In this case, you override the NSObject implementation of automaticallyNotifiesObserversForKey:. For properties whose automatic notifications you want to preclude, the subclass implementation of automaticallyNotifiesObserversForKey: should return NO. A subclass implementation should invoke super for any unrecognized keys. The example in Listing 2 enables manual notification for the balance property, allowing the superclass to determine the notification for all other keys.

翻译

在某些情况下,您可能希望控制通知流程,例如,最小化由于应用程序特定原因而不必要的触发通知,或者将许多更改分组到单个通知中。手动更改通知提供了这样做的方法。

手动和自动通知不是互斥的。除了已经存在的自动通知外,您还可以自由地发出手动通知。更典型的情况是,您可能希望完全控制特定属性的通知。在这个例子中,你覆盖了NSObject实现的automallynotifiesobserversforkey:。对于那些你想要排除自动通知的属性,automcallynotifiesobserversforkey:的子类实现应该返回NO。对于任何无法识别的键,子类实现都应该调用super。清单2中的示例启用了balance属性的手动通知,允许超类确定所有其他键的通知。

代码示例

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
 
    BOOL automatic = NO;
    //这里加判断,是为了控制某些属性是手动的,其他的属性还是按照之前的自动控制。
    if ([theKey isEqualToString:@"balance"]) {
        automatic = NO;
    }
    else {
        automatic = [super automaticallyNotifiesObserversForKey:theKey];
    }
    return automatic;
}

- (void)setBalance:(double)theBalance {
//这里做判断,是为了避免发送不必要的消息
    if (theBalance != _balance) {
        [self willChangeValueForKey:@"balance"];
        _balance = theBalance;
        [self didChangeValueForKey:@"balance"];
    }
}

小结

当然了,上面无论是手动还是自动,都需要添加观察者(addObserver:forKeyPath:options:context:)这一不可获取的步骤。这里通过手动通知的方式,可以更加方便地控制属性观察的开关。

4、注册依赖键

在许多情况下,一个属性的值依赖于另一个对象中的一个或多个其他属性的值。如果一个属性的值发生更改,则派生属性的值也应标记为更改。如何确保为这些依赖属性发布键值观察通知取决于关系的基数。

代码示例

- (NSString *)fullName {
    return [NSString stringWithFormat:@"%@ %@",firstName, lastName];
}

+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
 
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
 
    if ([key isEqualToString:@"fullName"]) {
        NSArray *affectingKeys = @[@"lastName", @"firstName"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}

实现上面的keyPathsForValuesAffectingValueForKey方法,那么观察fullName属性,当lastName或者firstName任意一个发生改变的时候,都会发送观察的通知消息。

5、可变容器的监听

当属性是可变容器的时候,应该使用下面的方法进行修改,才可以监听到:

// Use mutableArrayValueForKey: to retrieve a relationship proxy object.
Transaction *newTransaction = <#Create a new transaction for the account#>;
NSMutableArray *transactions = [account mutableArrayValueForKey:@"transactions"];
[transactions addObject:newTransaction];

如果使用下面的方式则监听不到:

[account.transactions addObject:newTransaction]

二、KVO的原理

1、先来看看官方文档的解释

Automatic key-value observing is implemented using a technique called isa-swizzling.

The isa pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.

When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.

You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.

翻译

自动键值观察是使用一种称为isa-swizzling的技术实现的。

这个isa指针,顾名思义,指向维护调度表的对象的类。这个调度表实际上包含指向类实现的方法和其他数据的指针。

当观察者为一个对象的属性注册时,被观察对象的isa指针被修改,指向一个中间类,而不是真正的类。因此,isa指针的值不一定反映实例的实际类。

永远不要依赖isa指针来确定类成员。相反,您应该使用类方法来确定对象实例的类。

从官方文档得知一些关键信息:当观察者为一个对象的属性注册时,被观察对象的isa指针被修改,指向一个中间类,而不是真正的类

要想探索KVO的原理,首先要从addObserver:forKeyPath:options:context:这个方法开始:

2、对象属性被观察前后的一些变化

image.png

image.png

由上面打印可知,当对象属性被观察之后,系统动态生成了一个中间类NSKVONotifying_LGPerson,并且self.personisa指针指向了这个中间类。

3、NSKVONotifying_LGPerson

3.1 NSKVONotifying_LGPersonLGPerson的关系

通过以下方法打印LGPerson的子类:

[self printClasses:[LGPerson class]];

- (void)printClasses:(Class)cls{
    // 注册类的总数
    int count = objc_getClassList(NULL, 0);
    // 创建一个数组, 其中包含给定对象
    NSMutableArray *mArray = [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])) {
            [mArray addObject:classes[i]];
        }
    }
    free(classes);
    NSLog(@"classes = %@", mArray);
}

打印结果如下:

classes = (
    LGPerson,
    LGStudent,
    "NSKVONotifying_LGPerson"
)

通过上面的打印可以知道,NSKVONotifying_LGPersonLGPerson的子类。

3.2 NSKVONotifying_LGPerson类的内容

通过以下代码打印其中包含的方法:

[self printClassAllMethod:objc_getClass("NSKVONotifying_LGPerson")];

- (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);
    }
    free(methodList);
}

打印结果如下:

2022-04-20 21:02:30.885401+0800 : setNickName:-0x7fff207b159b
2022-04-20 21:02:30.885497+0800 : class-0x7fff207b00a3
2022-04-20 21:02:30.885599+0800 : dealloc-0x7fff207afe53
2022-04-20 21:02:30.885703+0800 : _isKVOA-0x7fff207afe4b

从打印结果可以得知:NSKVONotifying_LGPerson重写了LGPerson的3个方法,分别是 setNickNameclassdealloc.

4、isa 在 dealloc 中的变化

4.1 isa的修改

image.png

image.png

从上面的打印可以得出:

self.personisa 在 执行 removeObserver:forKeyPath 之前,依旧指向中间类NSKVONotifying_LGPerson,执行之后又指回了LGPerson

NSKVONotifying_LGPerson重写dealloc方法的目的是为了在dealloc中将isa指回原类LGPerson

4.2、NSKVONotifying_LGPerson中间类是否会销毁

当dealloc执行完之后,打印LGPerson类的子类:

classes = (
    LGPerson,
    LGStudent,
    "NSKVONotifying_LGPerson"
)

可以看出,NSKVONotifying_LGPerson类并不会销毁,苹果这样做的目的推测应该是 为了下次使用的时候可以复用,不需要重新创建,提高性能

5、class方法的作用

image.png

从打印可以知道,这里的class依旧是LGPerson,推测应该是苹果防止给程序员造成理解上的困惑,让程序员无感知NSKVONotifying_LGPerson中间类的存在。

6、setNickName方法的作用

settter方法可以用来区分属性和成员变量,因为KVO只能观察属性,不可以观察成员变量。

我们下符号断点,观察 self.person.nickName

lldb添加符号断点的命令:

watchpoint set variable self->_person->_nickName

image.png

运行然后查看堆栈信息: image.png

发现当修改 nickName的时候依次调用了如下方法:

frame #1:-[LGPerson setNickName:]
frame #2:Foundation`-[NSObject(NSKeyValueObservingPrivate) _changeValueForKeys:count:maybeOldValuesDict:maybeNewValuesDict:usingBlock:]
frame #3:Foundation`-[NSObject(NSKeyValueObservingPrivate) _changeValueForKey:key:key:usingBlock:]
frame #4:Foundation`_NSSetObjectValueAndNotify

总结

当某个对象的属性被监听后,会生成一个中间类,中间类是原类的子类,在这个中间类里面会重写父类的setter、class、dealloc方法,并且原类的isa指针指向了中间类;当对象的属性发生变化的时候,会执行中间类的setter方法,在这个方法中会调用原类的setter方法修改原类属性的值,并且发送属性修改的通知,当dealloc的时候,手动移除观察者,这时候中间类的isa又重新指向了原类,并且中间类不会因此消除,以便下次调用的时候复用。