相關(guān)API以及用法
翻開(kāi)蘋(píng)果的觀察者api,實(shí)現(xiàn)很簡(jiǎ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
如上,是通過(guò)給NSObject添加分類(lèi)實(shí)現(xiàn)的:
- NSKeyValueObserverRegistration注冊(cè)觀察者
- observeValueForKeyPath觀察者回調(diào)
- NSKeyValueObserverNotification觀察者通知
使用起來(lái)也很簡(jiǎn)單,我們定義一個(gè)Person類(lèi)捉蚤,添加三個(gè)屬性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對(duì)象觀察者全部移除");
}
- (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缆巧,控制臺(tái)輸出如下
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對(duì)象觀察者全部移除
我們?cè)谌缟衔恢么蛏蠑帱c(diǎn),然后在控制臺(tái)打印person的isa指針豌拙,輸出如下
(lldb) po self.person->isa
NSKVONotifying_Person
(lldb) po self.person->isa
Person
可以看到陕悬,對(duì)象的觀察者沒(méi)有完全移除的時(shí)候isa指向NSKVONotifying_Person,完全移除之后isa指向Person
實(shí)現(xiàn)原理
蘋(píng)果的官方文檔有KVO實(shí)現(xiàn)原理的描述按傅,很遺憾KVO的源碼沒(méi)有開(kāi)源捉超,不過(guò)通過(guò)上面在控制臺(tái)的打印結(jié)果,也能側(cè)面印證底層實(shí)現(xiàn)
當(dāng)對(duì)象的屬性被添加觀察者時(shí)唯绍,一個(gè)繼承自該對(duì)象所屬類(lèi)的子類(lèi)被動(dòng)態(tài)創(chuàng)建拼岳,接著修改該對(duì)象的isa指針,使其指向該子類(lèi)况芒,并重寫(xiě)了被觀察屬性的
setter
方法惜纸,依次調(diào)用willChangeValueForKey
、父類(lèi)的setter
方法绝骚、didChangeValueForKey
耐版,最后會(huì)調(diào)用到該對(duì)象的observeValueForKeyPath
方法,不僅如此蘋(píng)果還修改了class方法的返回值使其返回對(duì)象原本的類(lèi)皮壁,目的是隱藏觀察者的底層實(shí)現(xiàn)椭更,當(dāng)對(duì)象屬性的觀察者被全部移除之后,對(duì)象的isa指針會(huì)被修正蛾魄,重新指向原本的類(lèi)
觀察者相關(guān)的crash
- 添加次數(shù)多于移除次數(shù)虑瀑,當(dāng)監(jiān)聽(tīng)者釋放后,觸發(fā)observeValueForKeyPath時(shí)crash
- 添加次數(shù)少于移除次數(shù)指直接crash
- 觀察者沒(méi)有實(shí)現(xiàn)observeValueForKeyPath時(shí)直接crash
如上幾個(gè)crash蘋(píng)果完全有能力避免他們發(fā)生滴须,但是為什么蘋(píng)果沒(méi)有做這件事呢舌狗,因?yàn)樗恢烙脩舻恼嬲鈭D,蘋(píng)果期望在調(diào)試階段就暴露可能有問(wèn)題的邏輯扔水,讓其直接crash痛侍,然而事與愿違,通常我們是成對(duì)調(diào)用的,但是由于某種原因主届,導(dǎo)致添加和移除的次數(shù)無(wú)法匹配赵哲,最終導(dǎo)致線上大量的crash,所以crash防護(hù)需求就誕生了橡庞,沒(méi)有什么問(wèn)題是添加一個(gè)中間層解決不了的,如果有,那就再添加一層
在添加或移除觀察者之前插入一層數(shù)據(jù)結(jié)構(gòu)用于存儲(chǔ)次數(shù)再菊,比如哈希表
添加觀察者時(shí):控制只添加一次
移除觀察者時(shí):控制只移除一次
觀察鍵值改變時(shí):控制消息分發(fā)到觀察者上
為了避免被觀察者提前被釋放后泛豪,觸發(fā)observeValueForKeyPath時(shí)的crash臀叙,需要hook一下NSObject的dealloc
方法劝萤,在對(duì)象dealloc
函數(shù)調(diào)用之前,移除相關(guān)觀察者。
還是有點(diǎn)復(fù)雜阔涉!有沒(méi)有一種方案既可以實(shí)現(xiàn)安全性又不用hook系統(tǒng)方法呢?
實(shí)現(xiàn)安全的觀察者
一郭毕、API
干脆用runtime庫(kù)自己實(shí)現(xiàn)一個(gè)安全的觀察者扳肛,根據(jù)其實(shí)現(xiàn)原理金拒,仿照系統(tǒng)api,通過(guò)分類(lèi)的方式添加一個(gè)中間層,作者寫(xiě)了一個(gè)工具症副,下面講述下實(shí)現(xiàn)原理沮明,如下接口類(lèi)似系統(tǒng)api酱畅,只是把回調(diào)函數(shù)寫(xiě)成了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
同時(shí)去掉了context和options參數(shù)
原因是context參數(shù)用于給同一個(gè)屬性添加同一個(gè)觀察者同時(shí)代入上下文吁峻,回調(diào)時(shí)用于反解參數(shù),基本沒(méi)啥場(chǎng)景,options參數(shù)用于描述屬性改變的類(lèi)型,通常只用new和change,工具已經(jīng)實(shí)現(xiàn)這兩種類(lèi)型匆笤,綜上省略了context和options參數(shù)
二、安全數(shù)據(jù)模型
用于存儲(chǔ):觀察者、被觀察者、屬性鏈箩艺、觀察者回調(diào)到關(guān)聯(lián)對(duì)象
@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ù)
通過(guò)屬性名生成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);
}
通過(guò)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;
}
核心方法替饿,子類(lèi)重寫(xiě)setter
方法贸典,內(nèi)部調(diào)用父類(lèi)的setter
方法修改值盛垦,注意系統(tǒng)的是現(xiàn)實(shí)在調(diào)用父類(lèi)setter
方法前后分別調(diào)用willChangeValueForKey
和didChangeValueForKey
方法,然后通過(guò)observeValueForKeyPath
方法回調(diào)到父類(lèi)瓤漏,而我們這里直接通過(guò)自定義的block回調(diào),因此不用調(diào)用上面兩個(gè)方法
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)用父類(lèi)的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;
}
}
}
}
返回父類(lèi)的Class用于重寫(xiě)子類(lèi)的Class方法
static forceInline Class sk_class(id self) {
return class_getSuperclass(object_getClass(self));
}
核心方法颊埃,用于動(dòng)態(tài)創(chuàng)建子類(lèi)并注冊(cè)到運(yùn)行時(shí)環(huán)境
static forceInline Class createSafeKVOClass(id object) {
// 獲取以SafeKVONotifying_為前綴拼接類(lèi)名的子類(lèi)
Class observedClass = object_getClass(object);
NSString *className = NSStringFromClass(observedClass);
NSString *subClassName = [kSafeKVOClassPrefix stringByAppendingString:className];
Class subClass = NSClassFromString(subClassName);
// 運(yùn)行時(shí)已經(jīng)加載該類(lèi)則直接返回
if (subClass)
return subClass;
Class originalClass = object_getClass(object);
// 分配類(lèi)和原類(lèi)的內(nèi)存
subClass = objc_allocateClassPair(originalClass, subClassName.UTF8String, 0);
// 修改class實(shí)現(xiàn)蔬充,返回父類(lèi)Class
Method classMethod = class_getInstanceMethod(originalClass, @selector(class));
const char *types = method_getTypeEncoding(classMethod);
class_addMethod(subClass, @selector(class), (IMP)sk_class, types);
// 注冊(cè)類(lèi)到運(yùn)行時(shí)環(huán)境
objc_registerClassPair(subClass);
return subClass;
}
判斷對(duì)象是否能響應(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)
添加安全觀察者班利,此處有個(gè)易忽略點(diǎn)就是keyPath的處理饥漫,需要通過(guò)屬性鏈中的類(lèi)一一生成其子類(lèi),因?yàn)閗eyPath中的任意節(jié)點(diǎn)變化都有可能導(dǎo)致最終的屬性變化罗标,都是我們監(jiān)聽(tīng)的范圍
- (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) {
// 已添加過(guò)同一個(gè)觀察者庸队,無(wú)需重復(fù)添加
if (observerModel.observer == observer && observerModel.observed == self && [observerModel.keyPath isEqualToString:keyPath]) {
return;
}
}
// 通過(guò)keyPath依次執(zhí)行->創(chuàng)建子類(lèi)重寫(xiě)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)建子類(lèi)并修改本類(lèi)isa指針使其指向子類(lèi)
observedClass = createSafeKVOClass(object);
object_setClass(object, observedClass);
}
if (!objectHasSelector(object, setterSelector)) {
// 重寫(xiě)set方法在方法里調(diào)用父類(lèi)的set方法并通過(guò)block回調(diào)到上層,以完成監(jiān)聽(tīng)過(guò)程
const char *types = method_getTypeEncoding(setterMethod);
class_addMethod(observedClass, setterSelector, (IMP)sk_setter, types);
}
// 添加監(jiān)聽(tīng)者到類(lèi)的關(guān)聯(lián)對(duì)象數(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)清空則修正對(duì)象的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é)
本工具支持了多線程彻消,同時(shí)通過(guò)runtime和關(guān)聯(lián)對(duì)象實(shí)現(xiàn)了安全觀察者,解決了觀察者添加宙拉、移除宾尚、回調(diào)的各種crash,注意谢澈,本代碼還沒(méi)有經(jīng)過(guò)大量測(cè)試煌贴,如有需要,請(qǐng)務(wù)必反復(fù)測(cè)試之后再應(yīng)用于項(xiàng)目中
下載鏈接