问题背景 在 iOS 开发中,KVO 的使用一直都需要格外的小心,观察者的添加和移除需要匹配使用,不然在观察者和被观察者两者释放的时候部分系统下(iOS 11 以下)会出现闪退;其中出名的开源库 KVOController 是通过强持有对象来保证不会出现闪退的:
观察者释放的场景:KVOController 使用的一个单例作为观察者容器,然后分发事件,也就是有且仅有一个观察者,并且它是不会被释放的,也就是避免了闪退的问题;
被观察者释放的场景:KVOController 中被观察者都是强引用的,也就是一定要手动移除被观察者,不然对象不释放,所以也不存在被观察者释放但是还存在监听者的情况。
如果我们要自己实现一个观察者的自动移除方案需要怎么做呢?问题的本质是获取到对象的释放时机然后移除观察者,就目前看来有以下方案可选:
Associated Object 方案
ISA Swizzling 方案
Method Swizzling 方案
Associated Object 方案 在杨帝的这篇博客中《Associated Object 与 Dealloc》 有描述其在开源库 MessageThrottle 中使用 Associated Object 实现监听任意对象的释放过程,其大致原理如下:
当一个对象(Host)释放后,其关联的对象(Associated Object)也会被解除。可以在 Host 对象上添加 Associated Object,策略用 OBJC_ASSOCIATION_RETAIN。由于只有 Host 持有了这个 Associated Object,当 Host 释放后 Associated Object 也会被释放。在 Associated Object 的 dealloc 方法中告知外界其 Host 对象已经释放。Perfect!
同样 Github 上也找到一个使用相似的方案实现对象释放监听的轮子:CYLDeallocBlockExecutor 。
这个方案本身可以实现获取对象释放时机的目的,但是该方案不能解决我们实现 KVO 观察者自动移除的需求,原因是系统抛出的 KVO 异常在 Associated Object 移除之前,我们来看下这种场景下的 crash 堆栈:
Date/Time: 2021-12-23 11:47:24.000 +0800 OS Version: iOS 10.1.1 (14B100) Report Version: 104 Exception Type: EXC_CRASH (SIGABRT) Exception Codes: 0x00000000 at 0x0000000000000000 Crashed Thread: 0 Application Specific Information: *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'An instance 0x14fc238d0 of class XXClass was deallocated while key value observers were still registered with it. Current observation info: <NSKeyValueObservationInfo 0x17063df80> ( <NSKeyValueObservance 0x171459b30: Observer: 0x171664900, Key path: xx_keypath, Options: <New: YES, Old: NO, Prior: NO> Context: 0x0, Property: 0x171459c50> )' Thread 0 Crashed: 0 CoreFoundation __exceptionPreprocess +124 1 libobjc.A.dylib objc_exception_throw +56 2 CoreFoundation -[NSException initWithCoder:] +0 3 Foundation NSKVODeallocate +300 4 UIKit __destroy_helper_block_.123 +80 5 libsystem_blocks.dylib _Block_release +144 6 UIKit -[UIViewAnimationBlockDelegate .cxx_destruct] +56 7 libobjc.A.dylib object_cxxDestructFromClass(objc_object*, objc_class*) +148 8 libobjc.A.dylib objc_destructInstance +92 9 libobjc.A.dylib object_dispose +28 ...
结合 object_dispose
的源码(版本:objc4-818.2 ):
void *objc_destructInstance(id obj){ if (obj) { bool cxx = obj->hasCxxDtor(); bool assoc = obj->hasAssociatedObjects(); if (cxx) object_cxxDestruct(obj); if (assoc) _object_remove_assocations(obj, true ); obj->clearDeallocating(); } return obj; } void object_cxxDestruct(id obj){ if (obj->isTaggedPointerOrNil()) return ; object_cxxDestructFromClass(obj, obj->ISA()); }
我们可以看到 Associated Objects 的释放是在 object_cxxDestruct
之后,那该方案获取的 dealloc 时机去移除所有的 observer 是不可行的。
ISA Swizzling 方案 我们都知道 KVO 的底层实现原理就是利用了 ISA Swizzling:被监听对象(A类的实例对象)的 ISA 指针会被指向其子类 NSKVONotifying_A,并重写了所有的 setter,从而实现了在 setter 中将值的变化回调监听者。那同样的,我们可以继承自类 A 创建类 AutoRemovableKVO_A,然后再添加监听(先后顺序没有严格要求),然后向 AutoRemovableKVO_A 中添加自定义的 dealloc 方法,实现拦截 dealloc 的目的,具体实现如下:
#import "ViewController.h" #import <objc/runtime.h> #import <objc/message.h> static NSString *const AutoRemovableKVOClassNameToken = @"AutoRemovableKVO_" ;static NSString *const KVOClassNameToken = @"NSKVONotifying_" ;@interface NSObject (AutoRemovableKVO )@end @implementation NSObject (AutoRemovableKVO )+ (BOOL )_shouldHookClass:(NSString *)clzName { if (clzName == nil ) { return NO ; } return [clzName rangeOfString:AutoRemovableKVOClassNameToken].location == NSNotFound ; } + (NSString *)_newClassNameForClass:(NSString *)clzName { NSString *newClassName = nil ; if ([self _shouldHookClass:clzName]) { if ([clzName hasPrefix:KVOClassNameToken]) { NSMutableString *tmpStr = [clzName mutableCopy]; [tmpStr insertString:AutoRemovableKVOClassNameToken atIndex:KVOClassNameToken.length]; newClassName = [tmpStr copy ]; } else { newClassName = [AutoRemovableKVOClassNameToken stringByAppendingString:clzName]; } } return newClassName; } - (void )_swizzleISAIfNeeded { NSString *name = NSStringFromClass (self .class); NSString *newClassName = [NSObject _newClassNameForClass:name]; if (newClassName) { Method deallocM = class_getInstanceMethod(self .class, @selector (ar_dealloc)); if (deallocM) { SEL deallocSel = NSSelectorFromString (@"dealloc" ); Class newClz = objc_allocateClassPair(self .class, newClassName.UTF8String, 0 ); class_addMethod(newClz, deallocSel, method_getImplementation(deallocM), method_getTypeEncoding(deallocM)); objc_registerClassPair(newClz); object_setClass(self , newClz); } } } - (void )ar_dealloc { NSLog (@"%s" , __func__); } - (void )ar_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions )options context:(void *)context { [self _swizzleISAIfNeeded]; [self addObserver:observer forKeyPath:keyPath options:options context:context]; } @end @interface Base : NSObject @end @implementation Base - (void )dealloc { NSLog (@"%s" , __func__); } @end @interface SomeObject : Base @property (nonatomic , copy ) NSString *name;@end @implementation SomeObject - (void )dealloc { NSLog (@"%s" , __func__); } @end @interface ViewController ()@end @implementation ViewController - (void )viewDidLoad { [super viewDidLoad]; SomeObject *obj = [SomeObject new]; [obj ar_addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionInitial |NSKeyValueObservingOptionNew context:nil ]; obj.name = @"new name" ; } - (void )observeValueForKeyPath:(NSString *)keyPath ofObject:(id )object change:(NSDictionary <NSKeyValueChangeKey ,id > *)change context:(void *)context { NSLog (@"%@" , change); } @end
简单运行代码你会发现被监听者的 ISA 虽然替换成功了,但是仅仅调用了我们创建对象的 dealloc 方法,父类的都没有被调用,导致该对象没有被释放,这是为什么呢?回去看了下 ARC 环境下自动调用[super dealloc]
的原理,发现原来父类调用是在编译的时候插入的,从 llvm 项目中找到如下代码:
void CodeGenFunction::StartObjCMethod (const ObjCMethodDecl *OMD, const ObjCContainerDecl *CD) { if (CGM.getLangOpts ().ObjCAutoRefCount && OMD->isInstanceMethod () && OMD->getSelector ().isUnarySelector ()) { const IdentifierInfo *ident = OMD->getSelector ().getIdentifierInfoForSlot (0 ); if (ident->isStr ("dealloc" )) EHStack.pushCleanup<FinishARCDealloc>(getARCCleanupKind ()); } }
判断条件是 selector 等于 dealloc,所以自然 ar_dealloc 不满足该条件,所以编译的时候没有插入父类调用,导致对象没有释放;于是这里换个方式,显式调用原本的 dealloc ,将为子类添加 dealloc 方法的方式换成 imp_block :
- (void )_swizzleISAIfNeeded { Class clz = self .class; NSString *name = NSStringFromClass (clz); NSString *newClassName = [NSObject _newClassNameForClass:name]; if (newClassName) { SEL deallocSel = NSSelectorFromString (@"dealloc" ); Class newClz = objc_allocateClassPair(clz, newClassName.UTF8String, 0 ); Method originDeallocM = class_getInstanceMethod(self .class, deallocSel); IMP originImp = method_getImplementation(originDeallocM); IMP imp = imp_implementationWithBlock(^(__unsafe_unretained NSObject *self ){ NSLog (@"ar dealloc" ); ((void (*)(NSObject *))originImp)(self ); }); class_addMethod(newClz, deallocSel, imp, "v@:" ); objc_registerClassPair(newClz); object_setClass(self , newClz); } }
此时运行代码看到对象可以被正确的释放,三个 dealloc 方法都有调用,并且可以保证对象释放之前执行一些自定义代码,可以满足我们的要求。
Method Swizzling 方案 最后一个方案就是常规的 Method Swizzling,可以选择直接交换 NSObject 的 dealloc 方法,或者仅替换使用到的对象,后者需要做好方法是否替换的记录,其实现可以参考 ReactiveObjc/RACDeallocating.m ,前者的实现如下(里面有个问题不太理解,后续再更新):
typedef void (^ARObjectWillDeallocCallback)(NSObject *__unsafe_unretained obj);@interface NSObject (AutoRemovableKVO )@property (nonatomic , copy ) ARObjectWillDeallocCallback ar_willDeallocCallback;@end @implementation NSObject (AutoRemovableKVO )- (void )setAr_willDeallocCallback:(ARObjectWillDeallocCallback)ar_willDeallocCallback { objc_setAssociatedObject(self , @selector (ar_willDeallocCallback), ar_willDeallocCallback, OBJC_ASSOCIATION_COPY_NONATOMIC); } - (ARObjectWillDeallocCallback)ar_willDeallocCallback { return objc_getAssociatedObject(self , _cmd); } + (void )load { Class cls = self ; SEL deallocSel = NSSelectorFromString (@"dealloc" ); Method om = class_getInstanceMethod(cls, deallocSel); Method rm = class_getInstanceMethod(cls, @selector (ar_dealloc)); method_exchangeImplementations(om, rm); } - (void )ar_dealloc { if (self .ar_willDeallocCallback) { self .ar_willDeallocCallback(self ); } [self ar_dealloc]; } @end
总结 综上所述,三个方案都可以实现对象释放的监听,其中 Associated Object 不能满足 KVO 自动移除的背景需求,其他的都可以获取到合适的时机移除观察者,接下来就是 hook 添加和移除接口做好缓存以及保证线程安全即可。
参考链接