一.代碼質量總結
在幾周的穩定性工作中, 我對現有內涵iOS代碼進行了一次初步的review過程,主要是針對一些非必現性crash的審查。
眾所周知iOS Crash類型分為Objective-C Exception 和 Signal。其中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變成了野指針,那么為什么呢?明明MemoryTest的dangerDelegate是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的場景下似乎顯得有點啰嗦。這里推薦使用不帶self的NSCAssert,這樣就可以避免在開發時期被誤殺了
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的穩定性。