自定义KVO(三)
三、系统kvo容错处理
在上面自定义kvo
中处理了自动移除观察者逻辑,以及将回调使用block
实现。在实际使用系统kvo
的时候有以下问题:
1.多次添加同一观察者会进行多次回调。
2.某个属性没有被观察,在dealloc
中移除会造成crash
。
3.多次移除观察者也会造成crash
。
4.不移除观察者有可能造成crash
。(观察者释放后被观察者调用回调)
那么要避免就要在添加和移除以及dealloc
过程中做容错处理。
在NSObject(NSKeyValueObservingCustomization)
中发现了observationInfo
:
/*
Take or return a pointer that identifies information about all of the observers that are registered with the receiver, the options that were used at registration-time, etc.
The default implementation of these methods store observation info in a global dictionary keyed by the receivers' pointers. For improved performance, you can override these methods to store the opaque data pointer in an instance variable.
Overrides of these methods must not attempt to send Objective-C messages to the passed-in observation info, including -retain and -release.
*/
@property (nullable) void *observationInfo NS_RETURNS_INNER_POINTER;
observationInfo
中保存了所有观察者信息。那么
observationInfo
保存在哪里呢?直接代码验证下:NSLog(@"observed before %@",self.obj.observationInfo);
NSLog(@"observe before %@",self.observationInfo);
[self.obj addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
NSLog(@"observe after %@",self.observationInfo);
NSLog(@"observed after %@",self.obj.observationInfo);
observed before (null)
observe before (null)
observe after (null)
observed after <NSKeyValueObservationInfo 0x60000100c700> (
<NSKeyValueObservance 0x600001ee0c90: Observer: 0x7fd6eb112cb0, Key path: name, Options: <New: YES, Old: NO, Prior: NO> Context: 0x0, Property: 0x600001ee1050>
)
可以看到在注册后存入到了被观察者中,类型是NSKeyValueObservationInfo
,它是一个私有类。NSKeyValueObservationInfo
中保存的是NSKeyValueObservance
。NSKeyValueObservationInfo
保存了NSKeyValueObservance
集合,NSKeyValueObservance
中保存了观察者注册的时候的信息。
既然是在Foundation
框架中,那么dump
一下这个动态库的头文件(越狱手机使用classdump-dyld
导出头文件)。
NSKeyValueObservationInfo头文件:
@class NSArray;
@interface NSKeyValueObservationInfo : NSObject {
NSArray* _observances;
unsigned long long _cachedHash;
BOOL _cachedIsShareable;
}
@property (nonatomic,readonly) BOOL containsOnlyInternalObservationHelpers;
-(void)dealloc;
-(unsigned long long)hash;
-(id)_initWithObservances:(id*)arg1 count:(unsigned long long)arg2 hashValue:(unsigned long long)arg3 ;
-(id)description;
-(BOOL)containsOnlyInternalObservationHelpers;
-(BOOL)isEqual:(id)arg1 ;
-(id)_copyByAddingObservance:(id)arg1 ;
@end
NSKeyValueObservance头文件:
@class NSObject, NSKeyValueProperty;
@interface NSKeyValueObservance : NSObject {
NSObject* _observer;
NSKeyValueProperty* _property;
void* _context;
NSObject* _originalObservable;
unsigned _options : 6;
unsigned _cachedIsShareable : 1;
unsigned _isInternalObservationHelper : 1;
}
-(id)_initWithObserver:(id)arg1 property:(id)arg2 options:(unsigned long long)arg3 context:(void*)arg4 originalObservable:(id)arg5 ;
-(unsigned long long)hash;
-(id)description;
-(BOOL)isEqual:(id)arg1 ;
-(void)observeValueForKeyPath:(id)arg1 ofObject:(id)arg2 change:(id)arg3 context:(void*)arg4 ;
@end
_observances
中保存的是NSKeyValueObservance
。代码验证:
@class NSKeyValueContainerClass, NSString;
@interface NSKeyValueProperty : NSObject <NSCopying> {
NSKeyValueContainerClass* _containerClass;
NSString* _keyPath;
}
-(Class)isaForAutonotifying;
-(id)_initWithContainerClass:(id)arg1 keyPath:(id)arg2 propertiesBeingInitialized:(CFSetRef)arg3 ;
-(id)dependentValueKeyOrKeysIsASet:(BOOL*)arg1 ;
-(void)object:(id)arg1 withObservance:(id)arg2 didChangeValueForKeyOrKeys:(id)arg3 recurse:(BOOL)arg4 forwardingValues:(SCD_Struct_NS48)arg5 ;
-(BOOL)object:(id)arg1 withObservance:(id)arg2 willChangeValueForKeyOrKeys:(id)arg3 recurse:(BOOL)arg4 forwardingValues:(SCD_Struct_NS48*)arg5 ;
-(void)object:(id)arg1 didAddObservance:(id)arg2 recurse:(BOOL)arg3 ;
-(id)restOfKeyPathIfContainedByValueForKeyPath:(id)arg1 ;
-(void)object:(id)arg1 didRemoveObservance:(id)arg2 recurse:(BOOL)arg3 ;
-(BOOL)matchesWithoutOperatorComponentsKeyPath:(id)arg1 ;
-(id)copyWithZone:(NSZone*)arg1 ;
-(id)keyPath;
-(void)dealloc;
-(id)keyPathIfAffectedByValueForKey:(id)arg1 exactMatch:(BOOL*)arg2 ;
-(id)keyPathIfAffectedByValueForMemberOfKeys:(id)arg1 ;
@end
_observer
存储在NSKeyValueObservance
中。_keyPath
存储在NSKeyValueObservance
的_property
(NSKeyValueProperty
)中。
3.1Hook注册和移除方法
要对系统方法进行容错处理那么最好的办法就是Hook
了,直接对添加和移除的3
个方法进行Hook
处理:
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self hp_methodSwizzleWithClass:self oriSEL:@selector(addObserver:forKeyPath:options:context:) swizzledSEL:@selector(hp_addObserver:forKeyPath:options:context:) isClassMethod:NO];
[self hp_methodSwizzleWithClass:self oriSEL:@selector(removeObserver:forKeyPath:context:) swizzledSEL:@selector(hp_removeObserver:forKeyPath:context:)isClassMethod:NO];
[self hp_methodSwizzleWithClass:self oriSEL:@selector(removeObserver:forKeyPath:) swizzledSEL:@selector(hp_removeObserver:forKeyPath:)isClassMethod:NO];
});
}
- 由于
removeObserver:forKeyPath:
底层调用的不是removeObserver:forKeyPath:context:
所以两个方法都要Hook
。
那么核心逻辑就是怎么判断observer
对应的keyPath
是否存在。由于observationInfo
存储的是私有类,那么直接通过kvc
获取值:
- (BOOL)keyPathIsExist:(NSString *)sarchKeyPath observer:(id)observer {
BOOL findKey = NO;
id info = self.observationInfo;
if (info) {
NSArray *observances = [info valueForKeyPath:@"_observances"];
for (id observance in observances) {
id tempObserver = [observance valueForKey:@"_observer"];
if (tempObserver == observer) {
NSString *keyPath = [observance valueForKeyPath:@"_property._keyPath"];
if ([keyPath isEqualToString:sarchKeyPath]) {
findKey = YES;
break;
}
}
}
}
return findKey;
}
Hook
的具体实现:
- (void)hp_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context {
if ([self keyPathIsExist:keyPath observer:observer]) {//observer 观察者已经添加了对应key的观察,再次添加不做处理。
return;
}
[self hp_addObserver:observer forKeyPath:keyPath options:options context:context];
}
- (void)hp_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context {
if ([self keyPathIsExist:keyPath observer:observer]) {//key存在才移除
[self hp_removeObserver:observer forKeyPath:keyPath context:context];
}
}
- (void)hp_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath {
if ([self keyPathIsExist:keyPath observer:observer]) {//key存在才移除
[self hp_removeObserver:observer forKeyPath:keyPath];
}
}
- 这样就解决了重复添加和移除的问题。
3.2 自动移除观察者
3.1
中解决了重复添加和移除的问题,还有一个问题是dealloc
的时候自动移除。这块思路与自定义kvo
相同,通过Hook
观察者的的dealloc
实现。
- (void)hp_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context {
if ([self keyPathIsExist:keyPath observer:observer]) {//observer 观察者已经添加了对应key的观察,再次添加不做处理。
return;
}
NSString *className = NSStringFromClass([self class]);
NSString *newClassName = [NSString stringWithFormat:@"NSKVONotifying_%@",className];
Class newClass = NSClassFromString(newClassName);
if (!newClass) {//类不存在的时候进行 hook 观察者 dealloc
//hook dealloc
[[observer class] hp_methodSwizzleWithClass:[observer class] oriSEL:NSSelectorFromString(@"dealloc") swizzledSEL:@selector(hp_dealloc) isClassMethod:NO];
}
[self hp_addObserver:observer forKeyPath:keyPath options:options context:context];
}
- (void)hp_dealloc {
[self hp_removeSelfAllObserverd];
[self hp_dealloc];
}
- 当
kvo
子类已经存在的时候证明已经hook
过了。
在dealloc
中self.observationInfo
是获取不到信息的,因为observationInfo
是存储在被观察者中的。所以还需要自己存储信息。
修改如下:
static NSString *const kHPSafeKVOObserverdAssiociateKey = @"HPSafeKVOObserverdAssiociateKey";
@interface HPSafeKVOObservedInfo : NSObject
@property (nonatomic, weak) id observerd;
@property (nonatomic, copy) NSString *keyPath;
@property (nonatomic, strong) id context;
@end
@implementation HPSafeKVOObservedInfo
- (instancetype)initWitObserverd:(NSObject *)observerd forKeyPath:(NSString *)keyPath context:(nullable void *)context {
if (self=[super init]) {
_observerd = observerd;
_keyPath = keyPath;
_context = (__bridge id)(context);
}
return self;
}
@end
- (void)hp_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context {
if ([self keyPathIsExist:keyPath observer:observer]) {//observer 观察者已经添加了对应key的观察,再次添加不做处理。
return;
}
NSString *className = NSStringFromClass([self class]);
NSString *newClassName = [NSString stringWithFormat:@"NSKVONotifying_%@",className];
Class newClass = NSClassFromString(newClassName);
if (!newClass) {//类不存在的时候进行 hook 观察者 dealloc
//hook dealloc
[[observer class] hp_methodSwizzleWithClass:[observer class] oriSEL:NSSelectorFromString(@"dealloc") swizzledSEL:@selector(hp_dealloc) isClassMethod:NO];
}
//保存被观察者信息
HPSafeKVOObservedInfo *kvoObservedInfo = [[HPSafeKVOObservedInfo alloc] initWitObserverd:self forKeyPath:keyPath context:context];
NSMutableArray *observerdArray = objc_getAssociatedObject(observer, (__bridge const void * _Nonnull)(kHPSafeKVOObserverdAssiociateKey));
if (!observerdArray) {
observerdArray = [NSMutableArray arrayWithCapacity:1];
}
[observerdArray addObject:kvoObservedInfo];
objc_setAssociatedObject(observer, (__bridge const void * _Nonnull)(kHPSafeKVOObserverdAssiociateKey), observerdArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
//调用原始方法
[self hp_addObserver:observer forKeyPath:keyPath options:options context:context];
}
在hp_dealloc
中主动调用移除方法:
- (void)hp_dealloc {
[self hp_removeSelfAllObserverd];
[self hp_dealloc];
}
- (void)hp_removeSelfAllObserverd {
NSMutableArray *observerdArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kHPSafeKVOObserverdAssiociateKey));
for (HPSafeKVOObservedInfo *info in observerdArray) {
if (info.observerd) {
//调用系统方法,已经hook了,走hook逻辑。
if (info.context) {
[info.observerd removeObserver:self forKeyPath:info.keyPath context:(__bridge void * _Nullable)(info.context)];
} else {
[info.observerd removeObserver:self forKeyPath:info.keyPath];
}
}
}
}
这样在dealloc
的时候就主动清空了已经释放掉的observer
。
3.3 问题处理
上面这样处理后在退出页面的时候发生了crash
(非必现),堆栈如下:
UIScreen
观察了 CADisplay
的 cloned
。但是在释放的过程中UIScreen
却没有调用到Hook
的hp_dealloc
中,对应的汇编实现:int -[UIScreen dealloc](int arg0) {
[r0 _invalidate];
__UIScreenWriteDisplayConfiguration(r0, 0x0, 0x0);
r0 = [[&stack[0] super] dealloc];
return r0;
}
int -[UIScreen _invalidate](int arg0) {
var_10 = r20;
stack[-24] = r19;
r31 = r31 + 0xffffffffffffffe0;
saved_fp = r29;
stack[-8] = r30;
r19 = arg0;
*(int16_t *)(arg0 + 0xb0) = *(int16_t *)(arg0 + 0xb0) & 0xffffffffffffffcf;
r0 = [NSNotificationCenter defaultCenter];
r0 = [r0 retain];
[r0 removeObserver:r19];
[r0 release];
if ([r19 _isCarScreen] == 0x0) goto loc_7495b4;
loc_749570:
r0 = __UIInternalPreferenceUsesDefault_751e78(0x19080b0, @"ApplySceneUserInterfaceStyleToCarScreen", 0xec7178);
if (((*(int8_t *)0x19080b4 & 0x1) == 0x0) || (r0 != 0x0)) goto loc_74959c;
loc_7495b4:
[r19 _endObservingBacklightLevelNotifications];
[r19 _setSoftwareDimmingWindow:0x0];
r0 = *(r19 + 0x90);
r0 = [r0 _setScreen:0x0];
return r0;
loc_74959c:
CFNotificationCenterRemoveObserver(CFNotificationCenterGetDarwinNotifyCenter(), r19, @"CarPlayUserInterfaceStyleDidChangeNotification", 0x0);
goto loc_7495b4;
}
那么意味着是否没有替换成功?在
_UIScreenWriteDisplayConfiguration
中确实先移除后添加:rax
控制的。也就是__UIScreenIsCapturedValueOverride.isCapturedValue
控制的。经过测试只要在系统自动调用
UIScreen initialize
之前调用一个UIScreen
相关方法就不走kvo
设置逻辑了,比如:[UIScreen mainScreen]
//[UIScreen class]
目前不清楚原因。所以处理这个问题有两个思路:
- 在
+ load
进行方法交换的时候先调用[UIScreen class]
。
- 在注册的时候对系统类或者自己的类进行过滤。
- 2.1只排除
UIScreen
if ([observer isKindOfClass:[UIScreen class]]) {
[self hp_addObserver:observer forKeyPath:keyPath options:options context:context];
return;
}
- 2.2排除系统类
NSString *className = NSStringFromClass([observer class]);
if ([className hasPrefix:@"NS"] || [className hasPrefix:@"UI"]) { //排除某些系统类。
[self hp_addObserver:observer forKeyPath:keyPath options:options context:context];
return;
}
- 2.3 只处理自己的类
NSString *className = NSStringFromClass([observer class]);
if (![className hasPrefix:@"HP"]) { //排除某些系统类。
[self hp_addObserver:observer forKeyPath:keyPath options:options context:context];
return;
}