注册

自定义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
代码验证:

9b7d8bafe0fb1d2686ba12c8abae52d8.png3e0ca9921791057dfafab51c4c908150.png

它的定义如下:

@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_propertyNSKeyValueProperty)中。

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过了。

deallocself.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(非必现),堆栈如下:


5a9cf38d14e4d65061f2990e77f1989d.png028ab1f6a77584989a70147f0fcfd1d3.png

UIScreen 观察了 CADisplay 的 cloned。但是在释放的过程中UIScreen却没有调用到Hookhp_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中确实先移除后添加:

f65e9f051e091c6d798e6a531335c99d.png

是否进行注册是通过rax控制的。也就是__UIScreenIsCapturedValueOverride.isCapturedValue控制的。
经过测试只要在系统自动调用UIScreen initialize之前调用一个UIScreen相关方法就不走kvo设置逻辑了,比如:

[UIScreen mainScreen]
//[UIScreen class]

目前不清楚原因。所以处理这个问题有两个思路:

  1. + load进行方法交换的时候先调用[UIScreen class]
  1. 在注册的时候对系统类或者自己的类进行过滤。
  • 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;
}


0 个评论

要回复文章请先登录注册