[crash詳解與防禦] NSNotification crash

前言:html

  NSNotificationCenter 較之於 Delegate 能夠實現更大的跨度的通訊機制,能夠爲兩個無引用關係的兩個對象進行通訊。NSNotification是iOS中一個調度消息通知的類,採用單例模式設計。所以,註冊觀察者後,沒有在觀察者dealloc時及時註銷觀察者,極有可能通知中心再發送通知時發送給殭屍對象而發生crash。ios

  蘋果在iOS9以後專門針對於這種狀況作了處理,因此在iOS9以後,即便開發者沒有移除observer,Notification crash也不會再產生了。數組

不過針對於iOS9以前的用戶,咱們仍是有必要作一下NSNotification Crash的防禦。函數

  本文從notification的使用狀況、crash狀況進行講解,最後提出了兩種crash防禦的方案:第一種方案是被動防禦,就是crash的代碼可能已經在代碼中了,咱們在底層使用swizzle的方法進行了改進;第二種方案是主動防禦,就是在寫代碼以前,咱們本身寫一套機制,能夠有效的防禦crash的發生。post

1、NSNotification的使用測試

(1) Notification的觀察者類this

//.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類atom

[[NSNotificationCenter defaultCenter] postNotificationName:CRMPerformanceNewCellCurrentPageShouldChange object:@(performanceTabConfigure.tab)];

2、NSNotification的crash狀況spa

「殭屍對象」(出現殭屍對象會報reason=SIGSEGV)設計

在退出A頁面的時候沒有把自身的通知觀察者A給註銷,致使通知發過來的時候拋給了一個已經釋放的對象A,但該對象仍然被通知中心引用,也就是殭屍對象,從而致使程序崩潰 。(也就是說,在一個頁面dealloc的時候,必定要把這個頁面在通知中心remove掉,不然這個頁面頗有可能成爲殭屍對象)。

但有兩種狀況須要注意:

(1)單例裏不用dealloc方法,應用會統一管理;

(2)類別裏不要用dealloc方法removeObserver,在類別對應的原始類裏的dealloc方法removeObserver,由於類別會調用原始類的dealloc方法。(若是在類別裏新寫dealloc方法,原類裏的dealloc方法就不執行了)。

 3、crash 防禦方案  

  方案1、

  利用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操做是不須要引入自動註銷的,爲了少數需求而對全部的執行都作修正是不適當的。

 方案2、

  基於以上的分析,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釋放自注銷模式設計》。

相關文章
相關標籤/搜索