关于 Objective-C 中监听对象释放的几种尝试

问题背景

在 iOS 开发中,KVO 的使用一直都需要格外的小心,观察者的添加和移除需要匹配使用,不然在观察者和被观察者两者释放的时候部分系统下(iOS 11 以下)会出现闪退;其中出名的开源库 KVOController 是通过强持有对象来保证不会出现闪退的:

  1. 观察者释放的场景:KVOController 使用的一个单例作为观察者容器,然后分发事件,也就是有且仅有一个观察者,并且它是不会被释放的,也就是避免了闪退的问题;
  2. 被观察者释放的场景:KVOController 中被观察者都是强引用的,也就是一定要手动移除被观察者,不然对象不释放,所以也不存在被观察者释放但是还存在监听者的情况。

如果我们要自己实现一个观察者的自动移除方案需要怎么做呢?问题的本质是获取到对象的释放时机然后移除观察者,就目前看来有以下方案可选:

  1. Associated Object 方案
  2. ISA Swizzling 方案
  3. 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) {
// Read all of the flags at once for performance.
bool cxx = obj->hasCxxDtor();
bool assoc = obj->hasAssociatedObjects();

// This order is important.
if (cxx) object_cxxDestruct(obj);
if (assoc) _object_remove_assocations(obj, /*deallocating*/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];
// Do any additional setup after loading the view.
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) {
// 省略...
// In ARC, certain methods get an extra cleanup.
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 {
// NSLog(@"xxx"); // 加上这句就会 crash,为啥?
if (self.ar_willDeallocCallback) {
self.ar_willDeallocCallback(self);
}
[self ar_dealloc];
}
@end

总结

综上所述,三个方案都可以实现对象释放的监听,其中 Associated Object 不能满足 KVO 自动移除的背景需求,其他的都可以获取到合适的时机移除观察者,接下来就是 hook 添加和移除接口做好缓存以及保证线程安全即可。

参考链接