代碼質量以及內存泄露排查總結

一.代碼質量總結

在幾周的穩定性工作中, 我對現有內涵iOS代碼進行了一次初步的review過程,主要是針對一些非必現性crash的審查。

眾所周知iOS Crash類型分為Objective-C ExceptionSignal。其中Objective-C 的 Exception 是比較好處理的,在 Crash 的時候會有詳細的描述信息,而錯誤case也相對集中一些,比如未加保護而任意的使用MutableArray && MutableDictionary 導致添加一個nil對象引起Crash,比如下面這樣的代碼

- (void)addAccount:(AccountInfoBase*)account
{
    [accountDict setObject:account forKey:[account keyName]];
    [accountArray addObject:account];
}

初步review了下,發現addObject以及setObject:forKey:兩個方法,幾乎完全沒有安全保護機制,這樣的代碼是非常不嚴謹的同時也是容易crash的。這里目前我們設置了安全容器類,使用姿勢:

@interface NSArray<__covariant ObjectType> (NHSSecurityUtil)
- (ObjectType)NH_safe_objectAtIndex:(NSUInteger)index;
@end
@interface NSMutableArray< ObjectType> (NHSSecurityUtil)
- (void)NH_safe_addObject:(ObjectType)anObject;
@end

@interface NSMutableDictionary (NHSSecurityUtil)

- (id)objectForKey:(id)aKey ofClass:(Class)aClass;

- (NSString *)stringForKey:(id)aKey;

- (void)NH_safeSetObject:(id)anobject forKey:(NSString *)akey;

- (void)NH_safeRemoveObjectForKey:(NSString *)aKey;
@end

而對于Signal類的錯誤,通常是由于內存訪問出錯引起,例如常見的 [EXC_BAD_ACCESS // SIGSEGV // SIGBUS]都是這些錯誤:

The process attempted to access invalid memory, or it attempted to access memory in a manner not allowed by the memory's protection level (e.g, writing to read-only memory).

這是平時開發中最常碰到的問題,通常指訪問了無效或者已釋放的內存,一般情況下可以通過開啟 Zombie Objects 和 Address Sanitizer 來在調試時獲取更多的 Crash 信息。

但是隨著業務的不斷開發,又由于缺乏有效的UnitTest,一些新的case不可能全部被覆蓋,同時由于一些歷史原因,部分舊代碼中不規范又模糊的寫法,又繼續被后來接手的開發人員所延續,最終導致了整個代碼不可維護。部分代碼由于主觀的經驗主義的錯誤,導致了一些潛在的crash,比較深刻的有下面幾種:

1)self.property VS _property

在review代碼過程中,發現了大量的self.property與_property大面積混用的情況,可能由于個人習慣問題,不同的開發者主要集中在下面三種寫法:

[self.property method];
[_property method];
[self->_property method];

針對這種三種寫法,沒有明確的對與錯的界限,也就是說只要理解了每種寫法的case,怎么使用都可以。但是,我認為既然選定了一種方式,就盡量統一來寫,一般場景下不要三種混用,一是混用會導致代碼臟亂不堪,二是會帶來一些潛在的bug。同時,我個人認為在業務場景中盡可能的使用self.property方式能讓代碼更佳具有維護性。主要原因有:

1.ARC中的坑

是在實際業務代碼中我們經常會出現這樣的代碼

[self.property method]

self.property 的形式,實質是調用了property的getter與setter方法,雖然在ARC場景下,幾乎99%的場景不需要我們關心內存問題,但是為了那1%的場景我們還是得需要了解下ARC的處理機制的。比如下面這種場景

@class MemoryTest;

@protocol MemoryTestDelegate <NSObject>

- (void)testMemoryTest:(MemoryTest *)obj;

@end

@interface MemoryTest :NSObject

@property(nonatomic, weak) id <MemoryTestDelegate> dangerDelegate;

@property(nonatomic, copy) NSString *name;

@end

@implementation MemoryTest

- (void)dangerStart {
    if ([self.dangerDelegate respondsToSelector:@selector(testMemoryTest:)]) {
        [self.dangerDelegate testMemoryTest:self];
    }
    [self.name stringByAppendingString:@"crash"];
}

@end


@interface ViewController () <MemoryTestDelegate>

@property(nonatomic, strong) MemoryTest *danger;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
}

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    self.danger = [[MemoryTest alloc] init];
    self.danger.dangerDelegate = self;
    
    [_danger dangerStart];//crash
}

#pragma mark - MemoryTestDelegate

- (void)testMemoryTest:(MemoryTest *)obj {
    self.danger = nil;
}

@end

大概場景是這樣的:

1.ViewController 持有類型為 MemoryTest 的property :danger;
2.屬性danger將自己的delegate設置為其持有者(ViewController);
3.在Dangerdelegate代理方法 testMemoryTest: 中,ViewController將其屬性danger置為nil;
4.ViewController通過調用方法[_danger dangerStart],
5.在[self.name stringByAppendingString:@"crash"];發生 EXC_BAD_ACCESS Crash

其實分析一些這個crash場景,就是向一個野指針中寫入了數據「訪問野指針不會crash」,那么是哪個成野指針了呢,很明顯,MemoryTest變成了野指針,那么為什么呢?明明MemoryTestdangerDelegate是weak屬性,這里就要怪ARC的坑了:

因為在被調用方中使用了self做為傳參,同時self在被調方法中被置空,相當于調用了一次release,而其中self會被clang解析成__unsafe_unretained類型,那么下面再繼續使用self的話,由于__unsafe_unretained的不會自動給釋放對象置nil,因此野指針了。因此代碼真實的樣子是這樣:

- (void)dangerStart {
    const __unsafe_unretained MemoryTest *self;
    if ([self.dangerDelegate respondsToSelector:@selector(testMemoryTest:)]) {
        [self.dangerDelegate testMemoryTest:self];
    }
    [self.name stringByAppendingString:@"crash"];
}

這樣再來看下現在代碼是不是非常后怕,那么解決途徑有哪些?

通常的解決方法簡單省事:

[self.danger dangerStart]; //not [_danger dangerStart];

首先不討論這樣是否是最好的解決辦法,暫時留個懸念,我們先來分析一下,為什么會出現這種顛覆我們三觀的crash。
先來做個對比

1.使用[_danger dangerStart]方式調用,直接取_danger事例變量,做dangerStart消息轉發;
2.使用[self.danger dangerStart],首先調用danger的getter方法,然后默認取到了事例變量,然后再做dangerStart消息轉發。

對比一些似乎就是取getter事例變量方法的時候有區別,繼續來分析getter方法有哪些問題:

熟悉autoreleasepool的同學,都知道一次方法調用后返回值會被objc_retainAutoreleasedReturnValue再局部進行一次強引用,因此有意思的事出現了:

[self.danger dangerStart]

會使danger這個事例變量的retain+1,因此在

- (void)dangerStart {
    const __unsafe_unretained MemoryTest *self;
    if ([self.dangerDelegate respondsToSelector:@selector(testMemoryTest:)]) {
        [self.dangerDelegate testMemoryTest:self];
    }
    [self.name stringByAppendingString:@"crash"];
}

中即使

[self.dangerDelegate testMemoryTest:self];

會使danger觸發一次release,也不會使其retainCount為0,所以不會發生crash。

然而回到剛才那個問題,那個外面使用

[self.danger dangerStart]

來解決這種問題到底是不是最好的解決方案呢?

在我看來不是,因為這樣只能做為一個約束,如果從SDK角度來看,SDK提供方并不能強制約束外部調用者的 代碼習慣性 問題,比如 UITableView,因此更因該把這個安全性處理放到業務提供方內部,比如這樣

- (void)safeStart {
    MemoryTest *strongSelf = self;
    if ([strongSelf.dangerDelegate respondsToSelector:@selector(testMemoryTest:)]) {
        [strongSelf.dangerDelegate testMemoryTest:strongSelf];
    }
    [strongSelf.name stringByAppendingString:@"crash"];
}

實質就是在局部做一次retain+1操作,后續的操作其實也可以直接使用self

因此對于SDK提供方以delegate形式提供的話,需要非常注意是否會發生類似的crash。

2.Useless Case of Weak-strong Dance

先來看下面這段代碼

- (void)setKVO {
    [[[RACObserve(self, name) ignore:nil] deliverOnMainThread] subscribeNext:^(NSString *  _Nullable x) {
        _nameLabel.text = x;
    }];
}

一段簡單的KVO block操作,但是使用MLeakfinder或用Instrument檢測、或在dealloc中打斷點檢測的話,就會知道這里發生了內存泄漏。

這是因為接訪問實例變量(_nameLabel), 導致weak-strong dance無效, 最終導致循環引用。

如果由于重寫了getter方法,只是想用實例變量的話可以這樣

- (void)setKVO {
    @weakify(self);
    [[[RACObserve(self, name) ignore:nil] deliverOnMainThread] subscribeNext:^(NSString *  _Nullable x) {
        @strongify(self);
        self->_nameLabel.text = x;
    }];
}

2)Lazy load VS Mutil thread

由于代碼習慣大部分開發同學都喜歡用Lazy load的形式來重寫property的getter方法,比如

- (id)propertyA
{
    if  (!_propertyA) {
        _propertyA = [SomeClass new];
    }
    return _propertyA;
}

如果考慮到多線程場景下,大部分同學應該都會這樣寫「假定propertyA長度小于設備地址總線長度」)

@property (atomic, strong) xxx *propertyA;

然后大部分開發同學可能或多或少地看過不少iOS關于原子性的操作的文章,也都知道atomic修飾符只是在存取值的時候是原子性,其他操作不是。然后回頭看了一下這個Lazy load

- (id)propertyA {
    if  (!_propertyA) {
        _propertyA = [SomeClass new];
    }
    return _propertyA;
}

嗯,讀寫的時候似乎沒有問題,不需要加鎖,然后上線總是有幾個詭異的Bug。

究其原因,在這個例子中,多個線程同時訪問時,_propertyA 可能會被賦值多次,導致后續調用過程中,內存被釋放,從而引起crash。

那么簡單,是不是加個鎖就OK了呢。比如這樣

- (id)propertyA {
    @synchronized (self) {
        if  (!_propertyA) {
            _propertyA = [SomeClass new];
        }
        return _propertyA;
    }
}

OK,上線一段時間發現,似乎crash率略有下降,但是還是有點小異常,那么是不是鎖的打開姿勢不對呢。考慮一個場景,

//class A
@synchronized (self) {
    [_sharedLock lock];
    NSLog(@"code in class A");
    [_sharedLock unlock];
}

//class B
[_sharedLock lock];
@synchronized (objectA) {
    NSLog(@"code in class B");
}
[_sharedLock unlock];

self很可能會被外部對象訪問,被用作key來生成一鎖,類似上述代碼中的@synchronized (objectA)。兩個公共鎖交替使用的場景就容易出現死鎖,因此我的建議是不要傳self來做為synchronized的key!

二.內存泄露排查

談到穩定性工作,不得不說內存泄露,因為目前IES這邊存在大量的視音頻內容,而這些又都是內存大戶, 一旦出現VC泄漏這樣的大問題, 對穩定性影響是非常大的, 所以在review code過程對內存問題極其關注。

這部分主要介紹下面兩點:

1. 幾個循環引用的例子, 均是從項目中直接拿出來的實際例子.
2. 可能引起內存問題的情況大總結 不但會談到什么情況下會有循環引用的問題, 還會談到什么情況下不會發生循環引用的問題.不但會談到什么時候weak-strong dance有用, 還會談到什么時候weak-strong dance沒用.

這里首先,根據code review過程中發現的幾個典型例子來做整理「隱藏了實際代碼,以case形式出現」

(1)Cases of Memory Leaks

1. The trick of super

[[[[RACObserve(self, testArray) skip:1] distinctUntilChanged] deliverOnMainThread] subscribeNext:^(id  _Nullable x) {
        [super loadMoreData];
    }];

一個隱蔽的循環引用,里面沒有出現任何的self操作,但是調用了<font color=red size=4 >super</font>。而super 是個編譯器指令,當調用<font color=red size=4 >[super loadMoreData]</font>的時候,它告訴編譯器到父類中去找方法,但<font color=red size=4 >super</font>和<font color=red size=4 >self</font>其實是一個,因此<font color=red size=4 >super</font>造成了強引用,只需要改成<font color=red size=4 >self</font>就可以解決了。

2.Not only self

[self.view.subviews enumerateObjectsUsingBlock:^(__kindof UIView * _Nonnull v, NSUInteger idx, BOOL * _Nonnull stop) {
        [v bk_whenTapped:^{
            v.backgroundColor = [UIColor redColor];
        }];
    }];

同樣的block中沒有出現self,也沒有出現super,但是依然內存泄漏了,究其原因,可能是對循環引用的理解有出入

v, v -> tap_block -> v 導致循環引用

當然了,解決辦法也很簡單:

[self.view.subviews enumerateObjectsUsingBlock:^(__kindof UIView * _Nonnull v, NSUInteger idx, BOOL * _Nonnull stop) {
        @weakify(v);
        [v bk_whenTapped:^{
            @strongify(view);
            v.backgroundColor = [UIColor redColor];
        }];
    }];  

3.Incorrect usage of KVOController

KVOController是FaceBook的一個開源庫,官方說法是一個簡單安全的 KVO工具,其實看一下issue就知道這個東西并不安全,是一種相對的安全,或是說只是適用于MVVM架構下的安全。為什么這么說呢,看一下下面這段代碼

__weak __typeof(&*self)weakSelf = self;
    self.fbKVO= [FBKVOController controllerWithObserver:self];
    [self.fbKVO
     observe:self keyPath:@"ugcPublishBarName" options: NSKeyValueObservingOptionNew block:^(id observer,
                 id object,
                 NSDictionary *change)
     {
         __strong __typeof(weakSelf)strongSelf = weakSelf;
         NSString *barName = change[NSKeyValueChangeNewKey];
         dispatch_async(dispatch_get_main_queue(), ^{
             [strongSelf refreshBarName:barName];
         });
     }];

檢測一下,又是一個隱藏的內存泄漏,為什么呢,在于KVOController的使用姿勢不對,可以看下這個 issue ,KVOController的原理也非常簡單,網上有很多分析,可以參考下之前寫過的這篇 KVOController分享,簡單來說:

KVOController會retain observee, 造成 所以形成 self(observer) -> self.KVOController -> self(observee) 的循環引用

只要 observee 反過來強引用 observer 就會造成循環引用, weak-strong dance都沒用, 本例中是它的一種特殊情況(observee 就是 observer), 所以要使用方多注意。那么,解決途徑是什么呢?

推薦兩種:

(1)打死還用KVOController
__weak __typeof(&*self)weakSelf = self;
    
    NSObject *kvo = [[NSObject alloc] init];
    [kvo.KVOController observe:self keyPath:@"ugcPublishBarName" options: NSKeyValueObservingOptionNew block:^(id observer,
                                                                                                               id object,
                                                                                                               NSDictionary *change)
     {
         __strong __typeof(weakSelf)strongSelf = weakSelf;
         NSString *barName = change[NSKeyValueChangeNewKey];
         dispatch_async(dispatch_get_main_queue(), ^{
             [strongSelf refreshBarName:barName];
         });
     }];

可能有同學搜到用下面方法也可以

[self.KVOControllerNonRetaining
     observe:self keyPath:@"ugcPublishBarName" options: NSKeyValueObservingOptionNew block:^(id observer,
                 id object,
                 NSDictionary *change)
     {
         __strong __typeof(weakSelf)strongSelf = weakSelf;
         NSString *barName = change[NSKeyValueChangeNewKey];
         dispatch_async(dispatch_get_main_queue(), ^{
             [strongSelf refreshBarName:barName];
         });
     }];

但是這個放到設計場景中并不是一個通用的解決辦法,經常容易因忘記解綁而crash

(2)使用RACObserve

由于項目中已經使用了RAC來作為支撐,因此直接簡單粗暴的使用RACObserve

@weakify(self);
    [[[RACObserve(self, ugcPublishBarName) skip:1] deliverOnMainThread] subscribeNext:^(id  _Nullable x) {
        @strongify(self);
        [self refreshBarName:x];
    }];

不在需要關心是否會觀察自己,不管是MVC還是MVVM都可以。

那么再次回到剛才留的問題,為什么KVOController在MVVM架構下會比較適合呢,這是因為MVVM架構的核心是拆分View「View&& ViewController」放到ViewModel中,那么反應到代碼中,基本就是對ViewModel的各種KVO了。

@weakify(self);
    [self.KVOController observe:self.viewModel keyPath:@"name" options:NSKeyValueObservingOptionNew block:^(id  _Nullable observer, id  _Nonnull object, NSDictionary<NSString *,id> * _Nonnull change) {
        @strongify(self);
        self.nameLabel.text = nil;
    }];

這樣就可以把FBKVOController和self隔離開了。

4. NSTimer

說到NSTimer,其實也是老生常談了,但是在這里提出來主要是想說一個新的思路去解決,先來看一段線上代碼

- (void)setAutoScroll:(BOOL)autoScroll
{
    
    _autoScroll = autoScroll;
    
    if (autoScroll) {
        if (!self.autoScrollTimer || !self.autoScrollTimer.isValid) {
            self.autoScrollTimer = [NSTimer scheduledTimerWithTimeInterval:DISCOVERY_BANNER_SCROLLINTERVAL target:self selector:@selector(handleScrollTimer:) userInfo:nil repeats:YES];
        }
    } else {
        if (self.autoScrollTimer && self.autoScrollTimer.isValid) {
            [self.autoScrollTimer invalidate];
            self.autoScrollTimer = nil;
        }
    }
    
}

很明顯,由于timer使用不當, self -> self.timer -> self 循環引用, 也就是說dealloc永遠調用不到.

那么這里來說這個問題主要是提供一種新的方式來解決,可能現有的解決方案無非下面兩種

1.手工打破循環, 在viewWillDisapear時調用timer的invalidate方法
2.使用 GCD Timer,比如MSWeakTimer.

但這里我比較喜歡使用RAC的方式

- (void)setAutoScroll:(BOOL)autoScroll
{
    
    _autoScroll = autoScroll;
    
    if (autoScroll) {
        if (!self.autoScrollTimer || !self.autoScrollTimer.isValid) {
            @weakify(self);
            [[RACSignal interval:DISCOVERY_BANNER_SCROLLINTERVAL onScheduler:[RACScheduler mainThreadScheduler]] subscribeNext:^(NSDate * _Nullable x) {
                @strongify(self);
                [self handleScrollTimer:nil];
            }];
        }
    } else {
        if (self.autoScrollTimer && self.autoScrollTimer.isValid) {
            [self.autoScrollTimer invalidate];
            self.autoScrollTimer = nil;
        }
    }
}

非常直觀,也不需要在dealloc中解除timer。

5.No retain-cycle but issue

先來看一段代碼

NSTimeInterval largeTime = 1000.f;    
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(largeTime * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [self doSomething];
    });

這個不是循環引用, 但是會造成VC在1000s后才釋放, 雖然一般不會用dispatch_after delay 1000s, 但是在復雜的業務場景中, 可能存在復雜的dispatch_after嵌套等情況.解決辦法: weakify(self), 然后如果self已經釋放, 就直接return.

NSTimeInterval largeTime = 1000.f;
@weakify(self);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(largeTime * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        @strongify(self);
        if (!self) {
            return;
        }
        [self doSomething];
    });

6.Debug memory links

隨著MLeaksFinder的出現,檢測循環引用,只需要簡單的一句話就可以了,但是在Debug模式開發的話,經常會碰到一些“誤報”的問題,比如

[[RACSignal interval:1 onScheduler:[RACScheduler mainThreadScheduler] withLeeway:0] subscribeNext:^(NSDate * _Nullable x) {
        NSAssert(x, @"");
        [MGJRouter openUrl:kNHDetailRouterUrl];
    }];

這段代碼來看似乎沒有用到任何的self,只是簡單地做了個router跳轉,但是使用MLeaksFinder的話,會非常奇怪的出現一個memory leak的AlertView「更奇怪的是Debug模式下出現,Release下不出現」,這里我們來解析一些這段代碼,主要可疑點很明顯是 NSAssert(x, @""),那么展開看下 NSAssert是什么呢

#define NSAssert(condition, desc, ...)  \
    do {                \
    __PRAGMA_PUSH_NO_EXTRA_ARG_WARNINGS \
    if (!(condition)) {     \
            NSString *__assert_file__ = [NSString stringWithUTF8String:__FILE__]; \
            __assert_file__ = __assert_file__ ? __assert_file__ : @"<Unknown File>"; \
        [[NSAssertionHandler currentHandler] handleFailureInMethod:_cmd \
        object:self file:__assert_file__ \
            lineNumber:__LINE__ description:(desc), ##__VA_ARGS__]; \
    }               \
        __PRAGMA_POP_NO_EXTRA_ARG_WARNINGS \
    } while(0)
#endif

碰到了最喜歡的

do{
}while(NO);

繼續觀察里面有一段可疑的self

[[NSAssertionHandler currentHandler] handleFailureInMethod:_cmd \
        object:self file:__assert_file__ \
            lineNumber:__LINE__ description:(desc), ##__VA_ARGS__];

因此答案很明顯了。那么有什么解決方式,當然可以使用weak-strong dance,但是在一個沒有self的場景下似乎顯得有點啰嗦。這里推薦使用不帶selfNSCAssert,這樣就可以避免在開發時期被誤殺

2. Summary of Memory Leaks

1) weak-strong dance 和 block

weak-strong dance使用情況的分析:

一般來說 weak-strong dance 可以避免大部分循環引用問題, 但是也不能盲目的使用.

簡單介紹下weak-strong dance, 老司機可以跳過. 原始的寫法是:

__weak typeof(self) weakSelf = self;
__strong typeof(weakSelf) strongSelf = weakSelf;

其中weak打破了循環引用, 在self釋放時, weakSelf自動置空, 至于為什么又用strong的原因是為了防止block中的代碼執行一半, self釋放了, weakself也就是nil了. block中代碼執行一半就半途而廢了.

后來我們引入libextobjc中的 @weakify 和 @strongfiy 來簡寫. 其原理還是一樣的.

需要注意的是, 在嵌套的blocks中, 只需@weakify(self)一次, 但必須在每個blocks里都@strongify(self), 可以參考這個issue

前面說weak-strong dance并不是萬能的, 我們從block的使用來具體分析一下.

block的使用可以分成三種:

1 臨時性的,只用在棧當中,不會存儲起來

比如數組的enumerateObjectsUsingBlock方法比如masonry的mas_makeConstraints直接執行block, 不曾持有block

在這些情況下, 不需要weak-strong dance.可以看到mas_makeConstraints實現就是拿了block直接用, 沒有持有

- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
    self.translatesAutoresizingMaskIntoConstraints = NO;
    MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
    block(constraintMaker);
    return [constraintMaker install];
}
2 需要存儲起來,但只會調用一次,或者有一個完成時期

比如一個 UIView 的動畫, 動畫完成之后, 需要使用 block 通知外面, 一旦調用 block 之后, 這個 block 就可以釋放了.再比如網絡庫的successBlock, 它會在網絡請求結束釋放該block.

再比如GCD的相關方法, 排隊執行完就釋放該block.在這些情況下, 有時需要weak-strong dance

比如網絡超時設為30s, 那個有可能網絡庫在30s后再會釋放block里的對象. 造成資源的浪費. 這時候就需要weak-strong dance. 再比如例子5也需要weak-strong dance.

比如UIView的動畫, 0.3s后動畫完成, 0s延遲, 就不需要weak-strong dance.

3 需要存儲起來,可能會調用多次

比如按鈕的點擊事件,假如采用 block 實現,這種 block 就需要長期存儲,并且會調用多次。調用之后,block 也不可以刪除,可能還有下一次按鈕的點擊。

在這些情況下, 都需要weak-strong dance. 并且有可能還不夠

2) delegate 用 strong 修飾

雖然最最低級的錯誤, 幾乎不會有人再犯了,使用"內存泄露"做關鍵詞搜索主客的commit記錄時, 我發現曾經有七處地方寫錯然后被修正過來了. 還是很恐怖的.可以在主客里面使用strong) id<.*?>搜索

3) Toll-Free Bridging

在CF對象和NS對象之間轉換時, 我們會使用__bridge來橋接, 除了__bridge_transfer會將CF對象交給ARC管理, __bridge和__bridge_retained都需要手工管理CF對象.具體可以參考Mika Ash老師的這篇文章.

4) 可能造成延遲dealloc的情況

  • dispatch_after:1000sblock里面使用weakself, 判斷weakself為空就return

  • performSelector

[self performSelector:@selector(method1:) withObject:nil afterDelay:1000];

解決辦法: 在dealloc中調用

  [NSObject cancelPreviousPerformRequestsWithTarget:self]
  • NSOperationQueue解決辦法: 在dealloc中調用[queue cancelAllOperations]
    block和performSelector等的使用一定要考慮到對象的生命周期,block等會延長對象的生命,延遲釋放,由此可能會造成邏輯上時序的問題.

5) NSNotificationCenter

需要注意的是 NSNotificationCenter 需要 removeObserver 不是由于循環引用的問題,通知中心維護的是觀察者是unsafe_unretained 引用, 類似于assgin, 不是weak, 不會自動置空, 使用unsafe_unretained的原因是兼容老版本的系統, 所以要及時removeObserver, 否則可能造成訪問野指針crash.
另外, 在VC中使用

addObserverForName:object:queue:usingBlock:

后, 在dealloc中調用

[[NSNotificationCenter defaultCenter] removeObserver:self];

無效, 原因是

addObserverForName:object:queue:usingBlock:

的observer不再是self了, 而是

id observer = addObserverForName:object:queue:usingBlock:

的observer. 所以正確移除的辦法是保留observer的引用然后移除. 在具體的使用中, weak-strong dance之后, 并不會造成VC的無法釋放, 只會造成observer空跑, 影響不是很大. 但還是建議使用RACObserve來避免這個問題.

6) WeakProxy

NSTimer 或者 [self xxx_observe:self forKeyPath:xxx]等這些會強引用observer的API, 在dealloc中去釋放是沒有用的, 在上面例子4已經提到了. 還有一種方法解決這個問題, 就是WeakProxy, FLAnimatedImage里面有一個實現, 用法是:

FLWeakProxy *weakProxy = [FLWeakProxy weakProxyForObject:self];

self.timer = [NSTimer scheduledTimerWithTimeInterval:.01 target:weakProxy selector:@selector(scanAnimation) userInfo:nil repeats:YES];

FLWeakProxy 只持有self的weak引用, 并通過OC的消息轉發機制將消息轉發給self處理, 這樣timer就不會強引用self, dealloc里的[self.timer invalidate]就可以得到調用.

總結

可能每個人都有自己的代碼風格,但是不同的風格應該是建立在代碼穩定性與可用性基礎之上的,因此拋開架構的宏觀大層次,我們更應該注重一些小的細節,比如內存釋放引起的Crash問題「P.S:這類是不會有Crash上報的, 并且Crash上報中一些無線索的Crash很有可能是內存問題造成的, 很難排查」 。希望這次分享可以幫到大家, 一起加強APP的穩定性。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容