[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釋放自注銷模式設計》。

 

 

 

 

 

 

 
 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM