[crash详解与防护] NSNotification crash
前言:
NSNotificationCenter 较之于 Delegate 可以实现更大的跨度的通信机制,可以为两个无引用关系的两个对象进行通信。NSNotification是iOS中一个调度消息通知的类,采用单例模式设计。因此,注册观察者后,没有在观察者dealloc时及时注销观察者,极有可能通知中心再发送通知时发送给僵尸对象而发生crash。
苹果在iOS9之后专门针对于这种情况做了处理,所以在iOS9之后,即使开发者没有移除observer,Notification crash也不会再产生了。
不过针对于iOS9之前的用户,我们还是有必要做一下NSNotification Crash的防护。
本文从notification的使用情况、crash情况进行讲解,最后提出了两种crash防护的方案:第一种方案是被动防护,就是crash的代码可能已经在代码中了,我们在底层使用swizzle的方法进行了改进;第二种方案是主动防护,就是在写代码之前,我们自己写一套机制,可以有效的防护crash的发生。
一、NSNotification的使用
(1) Notification的观察者类
//.h文件 extern NSString *const CRMPerformanceNewCellCurrentPageShouldChange; //.m文件 NSString *const CRMPerformanceNewCellCurrentPageShouldChange = @"CRMPerformanceNewCellCurrentPageShouldChange"; //addObserver [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(currentPageShouldChange:) name:CRMPerformanceNewCellCurrentPageShouldChange object:nil]; // 执行函数 - (void)currentPageShouldChange:(NSNotification*)aNotification { NSNumber *number = [aNotification object]; self.pageControl.currentPage = [number integerValue]; } - (void)dealloc{ [[NSNotificationCenter defaultCenter] removeObserver:self]; //或者 [[NSNotificationCenter defaultCenter] removeObserver:self name:aName object:anObject]; }
(2)post Notification类
[[NSNotificationCenter defaultCenter] postNotificationName:CRMPerformanceNewCellCurrentPageShouldChange object:@(performanceTabConfigure.tab)];
二、NSNotification的crash情况
“僵尸对象”(出现僵尸对象会报reason=SIGSEGV)
在退出A页面的时候没有把自身的通知观察者A给注销,导致通知发过来的时候抛给了一个已经释放的对象A,但该对象仍然被通知中心引用,也就是僵尸对象,从而导致程序崩溃 。(也就是说,在一个页面dealloc的时候,一定要把这个页面在通知中心remove掉,否则这个页面很有可能成为僵尸对象)。
但有两种情况需要注意:
(1)单例里不用dealloc方法,应用会统一管理;
(2)类别里不要用dealloc方法removeObserver,在类别对应的原始类里的dealloc方法removeObserver,因为类别会调用原始类的dealloc方法。(如果在类别里新写dealloc方法,原类里的dealloc方法就不执行了)。
三、crash 防护方案
方案一、
利用method swizzling hook NSObject的dealloc函数,在对象真正dealloc之前先调用一下[[NSNotificationCenter defaultCenter] removeObserver:self]即可。
注意到并不是所有的对象都需要做以上的操作,如果一个对象从来没有被NSNotificationCenter 添加为observer的话,在其dealloc之前调用removeObserver完全是多此一举。 所以我们hook了NSNotificationCenter的 addObserver:(id)observer selector:(SEL)aSelector name:(NSString *)aName object:(id)anObject。函数,在其添加observer的时候,对observer动态添加标记flag。这样在observer dealloc的时候,就可以通过flag标记来判断其是否有必要调用removeObserver函数了。
//NSNotificationCenter+CrashGuard.m #import "NSNotificationCenter+CrashGuard.h" #import <objc/runtime.h> #import <UIKit/UIDevice.h> #import "NSObject+NotificationCrashGuard.h" @implementation NSNotificationCenter (CrashGuard) + (void)load{ if([[[UIDevice currentDevice] systemVersion] floatValue] < 9.0) { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [[self class] swizzedMethod:sel_getUid("addObserver:selector:name:object:") withMethod:@selector(crashGuard_addObserver:selector:name:object:)]; }); } } +(void)swizzedMethod:(SEL)originalSelector withMethod:(SEL )swizzledSelector { Class class = [self class]; Method originalMethod = class_getInstanceMethod(class, originalSelector); Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector); BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); if (didAddMethod) { class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); }else{ method_exchangeImplementations(originalMethod, swizzledMethod); } } -(void)crashGuard_addObserver:(id)observer selector:(SEL)aSelector name:(NSString *)aName object:(id)anObject { NSObject *obj = (NSObject *)observer; obj.notificationCrashGuardTag = notificationObserverTag; [self crashGuard_addObserver:observer selector:aSelector name:aName object:anObject]; } @end // NSObject+NotificationCrashGuard.h #import <Foundation/Foundation.h> extern NSInteger notificationObserverTag; @interface NSObject (NotificationCrashGuard) @property(nonatomic, assign)NSInteger notificationCrashGuardTag; @end // NSObject+NotificationCrashGuard.m #import "NSObject+NotificationCrashGuard.h" #import "NSObject+Swizzle.h" #import <UIKit/UIDevice.h> #import <objc/runtime.h> NSInteger notificationObserverTag = 11118; @implementation NSObject (NotificationCrashGuard) #pragma mark Class Method + (void)load{ if([[[UIDevice currentDevice] systemVersion] floatValue] < 9.0) { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [[self class] swizzedMethod:sel_getUid("dealloc") withMethod:@selector(crashGuard_dealloc)]; }); } } #pragma Setter & Getter -(NSInteger)notificationCrashGuardTag { NSNumber *number = objc_getAssociatedObject(self, _cmd); return [number integerValue]; } -(void)setNotificationCrashGuardTag:(NSInteger)notificationCrashGuardTag { NSNumber *number = [NSNumber numberWithInteger:notificationCrashGuardTag]; objc_setAssociatedObject(self, @selector(notificationCrashGuardTag), number, OBJC_ASSOCIATION_RETAIN); } -(void)crashGuard_dealloc { if(self.notificationCrashGuardTag == notificationObserverTag) { [[NSNotificationCenter defaultCenter] removeObserver:self]; } [self crashGuard_dealloc]; }
此方案有以下缺点:
(1)ARC开发下,dealloc作为关键字,编译器是有所限制的。会产生编译错误“ARC forbids use of 'dealloc' in a @selector”。不过我们可以用运行时的方式进行解决。
(2)dealloc作为最为基础,调用次数最为频繁的方法之一。如对此方法进行替换,一是代码的引入对工程影响范围太大,二是执行的代价较大。因为大多数dealloc操作是不需要引入自动注销的,为了少数需求而对所有的执行都做修正是不适当的。
方案二、
基于以上的分析,Method Swizzling可以作为最后的备选方案,但不适合作为首选方案。
另外一个思路是在宿主释放过程中嵌入我们自己的对象,使得宿主释放时顺带将我们的对象一起释放掉,从而获取dealloc的时机点。显然AssociatedObject是我们想要的方案。相比Method Swizzling方案,AssociatedObject方案的对工程的影响范围小,而且只有使用自动注销的对象才会产生代价。
鉴于以上对比,于是采用构建一个释放通知对象,通过AssociatedObject方式连接到宿主对象,在宿主释放时进行回调,完成注销动作。
首先,看一下OC对象销毁时的处理过程,如下objc_destructInstance函数:
/****************** * objc_destructInstance * Destroys an Instance without freeing memory. * Calls C++ destructors. * Calls ARR ivar cleanup. * Remove associative references. * Returns 'obj'. Does nothing if 'obj' is nil. * Be warned that GC DOES NOT CALL THIS. if you edit this, also edit finalize. * CoreFoundation and other clients do call this under GC. ******************/ void *objc_destructInstance(id obj){ if(obj){ bool cxx = obj->hasCxxDtor(); bool assoc = !UseGC && obj->hasAssociatedObject(); bool dealloc = !UseGC; if(cxx) object_cxxDestruct(obj); if(assoc) _object_remove_assocations(obj); if(dealloc) obj->clearDeallocating(); } return obj; }
objc_destructInstance函数中:(1)object_cxxDestruct负责遍历持有的对象,并进行析构销毁。(2)_object_remove_assocations负责销毁关联对象。(3)clearDeallocating清空引用计数表并清除弱引用表, 并负责对weak持有本对象的引用置nil(Weak表是一个hash表,然后里面的key是指向对象的地址,Value是Weak指针的地址的数组)。(附《ARC下dealloc过程》《iOS 底层解析weak的实现原理》)
根据上面的分析,我们对通知添加观察时,可以为观察者动态添加一个associate Object,由这个associate Object进行添加观察操作,在观察者销毁时,associate Object会自动销毁,我们在associate Object的销毁动作中,自动remove掉观察者。
具体实现如下:
(1)我们创建一个NSObject的分类NSObject+AdNotifyEvent。在这个Category中,我们创建了添加观察者的方法,其具体实现由它的associate Object实现。这里的associate Object是类SLVObserverAssociater的对象。
// NSObject+AdNotifyEvent.h #import <Foundation/Foundation.h> #import "SLVObserverAssociater.h" @interface NSObject (AdNotifyEvent) - (void)slvWatchObject:(id)object eventName:(NSString *)event block:(SLVNotifyBlock)block; - (void)slvWatchObject:(id)object eventName:(NSString *)event level:(double)level block:(SLVNotifyBlock)block; @end // NSObject+AdNotifyEvent.m #import "NSObject+AdNotifyEvent.h" #import <objc/runtime.h> #import <objc/message.h> @implementation NSObject (AdNotifyEvent) - (SLVObserverAssociater *)observerAssociater { SLVObserverAssociater *observerAssociater = (SLVObserverAssociater *)objc_getAssociatedObject(self, _cmd); if (observerAssociater == nil) { observerAssociater = [[SLVObserverAssociater alloc] initWithObserverObject:self]; objc_setAssociatedObject(self, _cmd, observerAssociater, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } return observerAssociater; } - (void)slvWatchObject:(id)object eventName:(NSString *)event block:(SLVNotifyBlock)block { [[self observerAssociater] addNotifyEvent:event watchObject:object observerObject:self level:RFEventLevelDefault block:block]; } - (void)slvWatchObject:(id)object eventName:(NSString *)event level:(double)level block:(SLVNotifyBlock)block { [[self observerAssociater] addNotifyEvent:event watchObject:object observerObject:self level:level block:block]; } @end
(2)SLVObserverAssociater的实现
头文件:
// SLVObserverAssociater.h #import <Foundation/Foundation.h> #import "SLVNotifyLevelBlocks.h" @interface SLVObserverAssociater : NSObject @property (nonatomic, weak) id observerObject; // selfRef,观察者 @property (nonatomic, strong) NSMutableDictionary *notifyMap; // key:通知名_watchObject value:RFNotifyEventObject - (id)initWithObserverObject:(id)observerObject; - (void)addNotifyEvent:(NSString *)event watchObject:(id)watchObject observerObject:(id)observerObject level:(double)level block:(SLVNotifyBlock)block; @end @interface SLVNotifyInfo : NSObject @property (nonatomic, weak) SLVObserverAssociater *associater; @property (nonatomic, unsafe_unretained) id watchObject; // 被观察对象 @property (nonatomic, strong) NSString *event; @property (nonatomic, strong) NSMutableArray *eventInfos; @property (nonatomic, weak) id sysObserverObj; // 观察者 - (id)initWithRFEvent:(SLVObserverAssociater *)rfEvent event:(NSString *)event watchObject:(id)watchObject; - (void)add:(SLVNotifyLevelBlocks *)info; - (void)removeLevel:(double)level; - (void)handleRFEventBlockCallback:(NSNotification *)note; @end
实现文件,分三部分:(a)初始化方法(b)添加观察的方法 (c)dealloc时,注销观察的方法
// SLVObserverAssociater.m #import "SLVObserverAssociater.h" #pragma mark - SLVObserverAssociater @implementation SLVObserverAssociater #pragma mark Init - (id)initWithObserverObject:(id)observerObject { self = [super init]; if (self) { _notifyMap = [NSMutableDictionary dictionary]; _observerObject = observerObject; } return self; } #pragma mark Add Notify - (void)addNotifyEvent:(NSString *)event watchObject:(id)watchObject observerObject:(id)observerObject level:(double)level block:(SLVNotifyBlock)block { NSString *key = [NSString stringWithFormat:@"%@_%p", event, watchObject]; SLVNotifyInfo *ne = [self.notifyMap objectForKey:key]; if (ne == nil) { // 添加监听 ne = [[SLVNotifyInfo alloc] initWithRFEvent:self event:event watchObject:watchObject]; [self addNotifyEvent:ne forKey:key]; } SLVNotifyLevelBlocks *nei = [[SLVNotifyLevelBlocks alloc] init]; nei.level = level; nei.block = block; [ne add:nei]; } - (void)addNotifyEvent:(SLVNotifyInfo *)ne forKey:(NSString *)key { self.notifyMap[key] = ne; __weak SLVNotifyInfo *neRef = ne; ne.sysObserverObj = [[NSNotificationCenter defaultCenter] addObserverForName:ne.event object:ne.watchObject queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note){ [neRef handleRFEventBlockCallback:note]; }]; } #pragma mark Remove Observer - (void)dealloc { [self removeNotifyEvent:nil watchObject:nil ignoreLevel:YES level:0]; } - (void)removeNotifyEvent:(NSString *)event watchObject:(id)watchObject ignoreLevel:(BOOL)bIgnoreLevel level:(double)level { if (event == nil && watchObject == nil) { // 移除掉所有 NSArray *keys = [self.notifyMap allKeys]; for (NSString *key in keys) { [self removeNotifyEventKey:key]; } } else if (event != nil && watchObject == nil) { NSArray *keys = [self.notifyMap allKeys]; for (NSString *key in keys) { SLVNotifyInfo *ne = self.notifyMap[key]; if ([ne.event isEqualToString:event]) { [self removeNotifyEventKey:key]; } } } else if (event == nil && watchObject != nil) { NSArray *keys = [self.notifyMap allKeys]; for (NSString *key in keys) { SLVNotifyInfo *ne = self.notifyMap[key]; if (ne.watchObject == watchObject) { [self removeNotifyEventKey:key]; } } } else { NSArray *keys = [self.notifyMap allKeys]; for (NSString *key in keys) { SLVNotifyInfo *ne = self.notifyMap[key]; if ([ne.event isEqualToString:event] && ne.watchObject == watchObject) { if (bIgnoreLevel) { [self removeNotifyEventKey:key]; } else { [ne removeLevel:level]; if (ne.eventInfos.count == 0) { [self removeNotifyEventKey:key]; } } break; } } } } - (void)removeNotifyEventKey:(NSString *)key { SLVNotifyInfo *ne = self.notifyMap[key]; if (ne.sysObserverObj != nil) { [[NSNotificationCenter defaultCenter] removeObserver:ne.sysObserverObj]; ne.sysObserverObj = nil; } [self.notifyMap removeObjectForKey:key]; } @end #pragma mark - SLVNotifyInfo @implementation SLVNotifyInfo - (id)initWithRFEvent:(SLVObserverAssociater *)associater event:(NSString *)event watchObject:(id)watchObject { self = [super init]; if (self) { _associater = associater; _event = event; _watchObject = watchObject; _eventInfos = [NSMutableArray array]; } return self; } - (void)dealloc { _watchObject = nil; } - (void)add:(SLVNotifyLevelBlocks *)info { BOOL bAdd = NO; for (NSInteger i = 0; i < self.eventInfos.count; i++) { SLVNotifyLevelBlocks *eoi = self.eventInfos[i]; if (eoi.level == info.level) { [self.eventInfos replaceObjectAtIndex:i withObject:info]; bAdd = YES; break; } else if (eoi.level > info.level) { [self.eventInfos insertObject:info atIndex:i]; bAdd = YES; break; } } if (!bAdd) { [self.eventInfos addObject:info]; } } // 按lever从小到大添加block - (void)removeLevel:(double)level { for (NSInteger i = 0; i < self.eventInfos.count; i++) { SLVNotifyLevelBlocks *eoi = self.eventInfos[i]; if (eoi.level == level) { [self.eventInfos removeObjectAtIndex:i]; break; } } } - (void)handleRFEventBlockCallback:(NSNotification *)note { for (NSInteger i = self.eventInfos.count-1; i >= 0; i--) { SLVNotifyLevelBlocks *nei = self.eventInfos[i]; SLVNotifyBlock block = nei.block; if (block != nil) { block(note, self.associater.observerObject); } } } // 按顺序执行block,block是响应通知时候的内容
另外,为通知的回调block排了优先级:
#define RFEventLevelDefault 1000.0f typedef void (^SLVNotifyBlock) (NSNotification *note, id selfRef); @interface SLVNotifyLevelBlocks : NSObject @property (nonatomic, assign) double level; // block的优先级 @property (nonatomic, copy) SLVNotifyBlock block; //收到通知后的回调block @end
测试代码:
- (void)viewDidLoad { [super viewDidLoad]; [self slvWatchObject:nil eventName:@"lvNotification" block:^(NSNotification *aNotification, id weakSelf){ NSLog(@"收到一个通知,现在开始处理了。。。"); } ]; }
这样,我们在注册通知的观察者时,使用我们的分类NSObject+AdNotifyEvent中的注册观察方法就可以了,在类销毁时,就会自动在通知中心remove掉观察者了。
总结:
本文从notification的使用情况、crash情况进行讲解,最后提出了两种crash防护的方案:第一种方案是被动防护,就是crash的代码可能已经在代码中了,我们在底层使用swizzle的方法进行了改进;第二种方案是主动防护,就是在写代码之前,我们自己写一套机制,可以有效的防护crash的发生。根据上面的分析,比较推荐第二种方案。本文的第二种方案参考自《iOS释放自注销模式设计》。