相關(guān)API以及用法
翻開蘋果的觀察者api,實(shí)現(xiàn)很簡潔接口也很少,定義在NSKeyValueObserving.h里面
@interface NSObject(NSKeyValueObserverRegistration)
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
@end
@interface NSObject(NSKeyValueObserving)
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
@end
@interface NSObject(NSKeyValueObserverNotification)
- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;
@end
如上,是通過給NSObject添加分類實(shí)現(xiàn)的:
- NSKeyValueObserverRegistration注冊觀察者
- observeValueForKeyPath觀察者回調(diào)
- NSKeyValueObserverNotification觀察者通知
使用起來也很簡單,我們定義一個Person類,添加三個屬性a、b、c
@interface Person : NSObject
@property (nonatomic, assign) NSInteger a;
@property (nonatomic, assign) NSInteger b;
@property (nonatomic, assign) NSInteger c;
@end
@interface ViewController ()
@property (nonatomic, strong) Person *person;
@end
- (void)viewDidLoad {
[super viewDidLoad];
[self.person addObserver:self
forKeyPath:@"a"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context:nil];
[self.person addObserver:self
forKeyPath:@"b"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context:nil];
[self.person addObserver:self
forKeyPath:@"c"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context:nil];
self.person.a = 10;
self.person.b = 5;
self.person.c = 2;
[self.person removeObserver:self forKeyPath:@"a"];
[self.person removeObserver:self forKeyPath:@"b"];
[self.person removeObserver:self forKeyPath:@"c"];
NSLog(@"person對象觀察者全部移除");
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSString *,id> *)change
context:(void *)context {
NSLog(@"%@屬性變化:%@", keyPath, change);
}
- (Person *)person {
if (!_person) {
_person = [[Person alloc] init];
}
return _person;
}
初始值都是0,控制臺輸出如下
2021-08-10 21:55:30.100992+0800 test[19703:48456267] a屬性變化:{
kind = 1;
new = 10;
old = 0;
}
2021-08-10 21:55:30.101123+0800 test[19703:48456267] b屬性變化:{
kind = 1;
new = 5;
old = 0;
}
2021-08-10 21:55:30.101235+0800 test[19703:48456267] c屬性變化:{
kind = 1;
new = 2;
old = 0;
}
2021-08-10 21:55:30.101336+0800 test[19703:48456267] person對象觀察者全部移除

我們在如上位置打上斷點(diǎn),然后在控制臺打印person的isa指針,輸出如下
(lldb) po self.person->isa
NSKVONotifying_Person
(lldb) po self.person->isa
Person
可以看到,對象的觀察者沒有完全移除的時候isa指向NSKVONotifying_Person,完全移除之后isa指向Person
實(shí)現(xiàn)原理
蘋果的官方文檔有KVO實(shí)現(xiàn)原理的描述,很遺憾KVO的源碼沒有開源,不過通過上面在控制臺的打印結(jié)果,也能側(cè)面印證底層實(shí)現(xiàn)
當(dāng)對象的屬性被添加觀察者時,一個繼承自該對象所屬類的子類被動態(tài)創(chuàng)建,接著修改該對象的isa指針,使其指向該子類,并重寫了被觀察屬性的
setter方法,依次調(diào)用willChangeValueForKey、父類的setter方法、didChangeValueForKey,最后會調(diào)用到該對象的observeValueForKeyPath方法,不僅如此蘋果還修改了class方法的返回值使其返回對象原本的類,目的是隱藏觀察者的底層實(shí)現(xiàn),當(dāng)對象屬性的觀察者被全部移除之后,對象的isa指針會被修正,重新指向原本的類
觀察者相關(guān)的crash
- 添加次數(shù)多于移除次數(shù),當(dāng)監(jiān)聽者釋放后,觸發(fā)observeValueForKeyPath時crash
- 添加次數(shù)少于移除次數(shù)指直接crash
- 觀察者沒有實(shí)現(xiàn)observeValueForKeyPath時直接crash
如上幾個crash蘋果完全有能力避免他們發(fā)生,但是為什么蘋果沒有做這件事呢,因?yàn)樗恢烙脩舻恼嬲鈭D,蘋果期望在調(diào)試階段就暴露可能有問題的邏輯,讓其直接crash,然而事與愿違,通常我們是成對調(diào)用的,但是由于某種原因,導(dǎo)致添加和移除的次數(shù)無法匹配,最終導(dǎo)致線上大量的crash,所以crash防護(hù)需求就誕生了,沒有什么問題是添加一個中間層解決不了的,如果有,那就再添加一層
在添加或移除觀察者之前插入一層數(shù)據(jù)結(jié)構(gòu)用于存儲次數(shù),比如哈希表
添加觀察者時:控制只添加一次
移除觀察者時:控制只移除一次
觀察鍵值改變時:控制消息分發(fā)到觀察者上
為了避免被觀察者提前被釋放后,觸發(fā)observeValueForKeyPath時的crash,需要hook一下NSObject的dealloc方法,在對象dealloc函數(shù)調(diào)用之前,移除相關(guān)觀察者。
還是有點(diǎn)復(fù)雜!有沒有一種方案既可以實(shí)現(xiàn)安全性又不用hook系統(tǒng)方法呢?
實(shí)現(xiàn)安全的觀察者
一、API
干脆用runtime庫自己實(shí)現(xiàn)一個安全的觀察者,根據(jù)其實(shí)現(xiàn)原理,仿照系統(tǒng)api,通過分類的方式添加一個中間層,作者寫了一個工具,下面講述下實(shí)現(xiàn)原理,如下接口類似系統(tǒng)api,只是把回調(diào)函數(shù)寫成了block
/* - (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
*/
typedef void (^SK_ObservedValueChanged) (id object, NSString *keyPath ,id oldValue, id newValue);
@interface NSObject (SafeKVO)
/// 添加安全觀察者
/// @param observer 觀察者
/// @param keyPath 屬性鏈
/// @param change 回調(diào)
- (void)sk_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath observeValueChanged:(SK_ObservedValueChanged)change;
/// 移除觀察者
/// @param observer 觀察者
/// @param keyPath 屬性鏈
- (void)sk_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
@end
同時去掉了context和options參數(shù)
原因是context參數(shù)用于給同一個屬性添加同一個觀察者同時代入上下文,回調(diào)時用于反解參數(shù),基本沒啥場景,options參數(shù)用于描述屬性改變的類型,通常只用new和change,工具已經(jīng)實(shí)現(xiàn)這兩種類型,綜上省略了context和options參數(shù)
二、安全數(shù)據(jù)模型
用于存儲:觀察者、被觀察者、屬性鏈、觀察者回調(diào)到關(guān)聯(lián)對象
@interface SafeKVOModel : NSObject
@property (nonatomic, weak) NSObject *observer;// 觀察者
@property (nonatomic, weak) NSObject *observed;// 被觀察者
@property (nonatomic, copy) NSString *keyPath;// 屬性鏈
@property (nonatomic, copy) SK_ObservedValueChanged change; // 觀察者回調(diào)
@property (nonatomic, strong) NSObject *oldValue;// 被觀察屬性原值
@end
@implementation SafeKVOModel
- (instancetype)initWithObserver:(NSObject *)observer observed:(NSObject *)observed forKeyPath:(NSString *)keyPath change:(SK_ObservedValueChanged)change {
if (self = [super init]) {
self.observer = observer;
self.observed = observed;
self.keyPath = keyPath;
self.change = change;
}
return self;
}
@end
三、工具函數(shù)
通過屬性名生成setter的SEL
static forceInline SEL sk_setterSelectorFromPropertyName(NSString *propertyName) {
if (propertyName.length <= 0)
return nil;
NSString *setterString = [NSString stringWithFormat:@"set%@%@:", [[propertyName substringToIndex:1] uppercaseString], [propertyName substringFromIndex:1]];
return NSSelectorFromString(setterString);
}
通過setter方法名生成屬性名
static forceInline NSString *sk_propertyNameFromSetterString(NSString *setterString) {
if (setterString.length <= 0 || ![setterString hasPrefix: @"set"] || ![setterString hasSuffix: @":"])
return nil;
NSRange range = NSMakeRange(3, setterString.length - 4);
NSString *propertyName = [setterString substringWithRange:range];
propertyName = [propertyName stringByReplacingCharactersInRange: NSMakeRange(0, 1) withString:[[propertyName substringToIndex: 1] lowercaseString]];
return propertyName;
}
核心方法,子類重寫setter方法,內(nèi)部調(diào)用父類的setter方法修改值,注意系統(tǒng)的是現(xiàn)實(shí)在調(diào)用父類setter方法前后分別調(diào)用willChangeValueForKey和didChangeValueForKey方法,然后通過observeValueForKeyPath方法回調(diào)到父類,而我們這里直接通過自定義的block回調(diào),因此不用調(diào)用上面兩個方法
static forceInline void sk_setter(id self, SEL _cmd, id newValue) {
@synchronized (self) {
NSString *propertyName = sk_propertyNameFromSetterString(NSStringFromSelector(_cmd));
NSParameterAssert(propertyName);
if (!propertyName)
return;
NSMutableArray *observers = objc_getAssociatedObject(self, (__bridge const void *)kSafeKVOAssiociateObservers);
for (SafeKVOModel *model in observers) {
if ([model.keyPath containsString:propertyName])
model.oldValue = [model.observed valueForKeyPath:model.keyPath];
}
// 調(diào)用父類的set方法
struct objc_super superClass = {
.receiver = self,
.super_class = class_getSuperclass(object_getClass(self))
};
void (*superSetter)(void *, SEL, id) = (void *)objc_msgSendSuper;
superSetter(&superClass, _cmd, newValue);
// 觀察者回調(diào)
for (SafeKVOModel *model in observers) {
// 觀察者未釋放才需回調(diào)
if ([model.keyPath containsString:propertyName] && model.observer) {
model.change(model.observed, model.keyPath, model.oldValue, [model.observed valueForKeyPath:model.keyPath]);
model.oldValue = nil;
}
}
}
}
返回父類的Class用于重寫子類的Class方法
static forceInline Class sk_class(id self) {
return class_getSuperclass(object_getClass(self));
}
核心方法,用于動態(tài)創(chuàng)建子類并注冊到運(yùn)行時環(huán)境
static forceInline Class createSafeKVOClass(id object) {
// 獲取以SafeKVONotifying_為前綴拼接類名的子類
Class observedClass = object_getClass(object);
NSString *className = NSStringFromClass(observedClass);
NSString *subClassName = [kSafeKVOClassPrefix stringByAppendingString:className];
Class subClass = NSClassFromString(subClassName);
// 運(yùn)行時已經(jīng)加載該類則直接返回
if (subClass)
return subClass;
Class originalClass = object_getClass(object);
// 分配類和原類的內(nèi)存
subClass = objc_allocateClassPair(originalClass, subClassName.UTF8String, 0);
// 修改class實(shí)現(xiàn),返回父類Class
Method classMethod = class_getInstanceMethod(originalClass, @selector(class));
const char *types = method_getTypeEncoding(classMethod);
class_addMethod(subClass, @selector(class), (IMP)sk_class, types);
// 注冊類到運(yùn)行時環(huán)境
objc_registerClassPair(subClass);
return subClass;
}
判斷對象是否能響應(yīng)傳入的SEL
static forceInline BOOL objectHasSelector(id object, SEL selector) {
BOOL result = NO;
unsigned int count = 0;
Class observedClass = object_getClass(object);
Method *methods = class_copyMethodList(observedClass, &count);
for (NSInteger i = 0; i < count; i++) {
SEL sel = method_getName(methods[i]);
if (sel == selector) {
result = YES;
break;
}
}
free(methods);
return result;
}
四、API實(shí)現(xiàn)
添加安全觀察者,此處有個易忽略點(diǎn)就是keyPath的處理,需要通過屬性鏈中的類一一生成其子類,因?yàn)閗eyPath中的任意節(jié)點(diǎn)變化都有可能導(dǎo)致最終的屬性變化,都是我們監(jiān)聽的范圍
- (void)sk_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath observeValueChanged:(SK_ObservedValueChanged)change {
@synchronized (self) {
NSMutableArray *observers = objc_getAssociatedObject(self, (__bridge void *)kSafeKVOAssiociateObservers);
for (SafeKVOModel *observerModel in observers) {
// 已添加過同一個觀察者,無需重復(fù)添加
if (observerModel.observer == observer && observerModel.observed == self && [observerModel.keyPath isEqualToString:keyPath]) {
return;
}
}
// 通過keyPath依次執(zhí)行->創(chuàng)建子類重寫set方法操作
NSArray *keys = [keyPath componentsSeparatedByString:@"."];
NSInteger index = 0;
id object = self;
while (index < keys.count) {
SEL setterSelector = sk_setterSelectorFromPropertyName(keys[index]);
Method setterMethod = class_getInstanceMethod([object class], setterSelector);
NSParameterAssert(setterMethod);
if (!setterMethod) {
return;
}
id nextObject = [object valueForKey:keys[index]];
Class observedClass = object_getClass(object);
NSString *className = NSStringFromClass(observedClass);
if (![className hasPrefix:kSafeKVOClassPrefix]) {
// 創(chuàng)建子類并修改本類isa指針使其指向子類
observedClass = createSafeKVOClass(object);
object_setClass(object, observedClass);
}
if (!objectHasSelector(object, setterSelector)) {
// 重寫set方法在方法里調(diào)用父類的set方法并通過block回調(diào)到上層,以完成監(jiān)聽過程
const char *types = method_getTypeEncoding(setterMethod);
class_addMethod(observedClass, setterSelector, (IMP)sk_setter, types);
}
// 添加監(jiān)聽者到類的關(guān)聯(lián)對象數(shù)組
observers = objc_getAssociatedObject(object, (__bridge void *)kSafeKVOAssiociateObservers);
if (!observers) {
observers = [NSMutableArray array];
objc_setAssociatedObject(object, (__bridge void *)kSafeKVOAssiociateObservers, observers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
SafeKVOModel *kvoModel = [[SafeKVOModel alloc] initWithObserver:observer observed:self forKeyPath:keyPath change:change];
[observers addObject:kvoModel];
index++;
if (index < keys.count) {
object = nextObject;
}
}
}
}
遍歷清除觀察者,若已經(jīng)清空則修正對象的isa指針
- (void)sk_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath {
@synchronized (self) {
NSArray *keys = [keyPath componentsSeparatedByString:@"."];
NSInteger index = 0;
id object = self;
while (index < keys.count) {
SafeKVOModel *modelRemoved = nil;
NSMutableArray *observers = objc_getAssociatedObject(object, (__bridge void *)kSafeKVOAssiociateObservers);
for (SafeKVOModel *model in observers) {
if (model.observer == observer && model.observed == self && [model.keyPath isEqualToString:keyPath]) {
modelRemoved = model;
break;
}
}
if (modelRemoved) {
[observers removeObject:modelRemoved];
if (!observers.count) {
object_setClass(object, [object class]);
}
} else {
object_setClass(object, [object class]);
}
object = [object valueForKey:keys[index]];
index++;
}
}
}
總結(jié)
本工具支持了多線程,同時通過runtime和關(guān)聯(lián)對象實(shí)現(xiàn)了安全觀察者,解決了觀察者添加、移除、回調(diào)的各種crash,注意,本代碼還沒有經(jīng)過大量測試,如有需要,請務(wù)必反復(fù)測試之后再應(yīng)用于項(xiàng)目中
下載鏈接