[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释放自注销模式设计》。

 

 

 

 

 

 

 
 

 

posted @ 2017-03-04 16:18  Xylophone  Views(1935)  Comments(0Edit  收藏  举报