一、訪問了一個已經被釋放的對象
在不使用 ARC 的時候,內存要自己管理,這時重復或過早釋放都有可能導致 Crash。
例子
NSObject * aObj = [[NSObject alloc] init];
[aObj release];
NSLog(@"%@", aObj);
原因
aObj 這個對象已經被釋放,但是指針沒有置空,這時訪問這個指針指向的內存就會 Crash。
解決辦法
1.使用前要判斷非空,釋放后要置空。
正確的釋放應該是:
[aObj release];
aObj = nil;
由于ObjC的特性,調用 nil 指針的任何方法相當于無作用,所以即使有人在使用這個指針時沒有判斷至少還不會掛掉。
在ObjC里面,一切基于 NSObject 的對象都使用指針來進行調用,所以在無法保證該指針一定有值的情況下,要先判斷指針非空再進行調用。
if (aObj) {
//...
}
常見的如判斷一個字符串是否為空:
if (aString && aString.length > 0) {//...}
2.適當使用 autorelease
有些時候不能知道自己創(chuàng)建的對象什么時候要進行釋放,可以使用 autoRelease,但是不鼓勵使用。因為 autoRelease 的對象要等到最近的一個 autoReleasePool 銷毀的時候才會銷毀,如果自己知道什么時候會用完這個對象,當然立即釋放效率要更高。如果一定要用 autoRelease 來創(chuàng)建大量對象或者大數據對象,最好自己顯式地創(chuàng)建一個 autoReleasePool,在使用后手動銷毀。以前要自己手動初始化 autoReleasePool,現在可以用以下寫法:
@autoreleasepool{
for (int i = 0; i < 100; ++i) {
NSObject * aObj = [[[NSObject alloc] init] autorelease];
//....
}
}
二、訪問數組類對象越界或插入了空對象
NSMutableArray/NSMutableDictionary/NSMutableSet 等類下標越界,或者 insert 了一個 nil 對象。
原因
一個固定數組有一塊連續(xù)內存,數組指針指向內存首地址,靠下標來計算元素地址,如果下標越界則指針偏移出這塊內存,會訪問到野數據,ObjC 為了安全就直接讓程序 Crash 了。
而 nil 對象在數組類的 init 方法里面是表示數組的結束,所以使用 addObject 方法來插入對象就會使程序掛掉。如果實在要在數組里面加入一個空對象,那就使用 NSNull。
[array addObject:[NSNull null]];
解決辦法
使用數組時注意判斷下標是否越界,插入對象前先判斷該對象是否為空。
if (aObj) {
[array addObject:aObj];
}
可以使用 Cocoa 的 Category 特性直接擴展 NSMutable 類的 Add/Insert 方法。比如:
@interface NSMutableArray (SafeInsert)
-(void) safeAddObject:(id)anObject;
@end
@implementation NSMutableArray (SafeInsert)
-(void) safeAddObject:(id)anObject {
if (anObject) {
[self addObject:anObject];
}
}
@end
這樣,以后在工程里面使用 NSMutableArray 就可以直接使用 safeAddObject 方法來規(guī)避 Crash。
三、訪問了不存在的方法
ObjC 的方法調用跟 C++ 很不一樣。 C++ 在編譯的時候就已經綁定了類和方法,一個類不可能調用一個不存在的方法,否則就報編譯錯誤。而 ObjC 則是在 runtime 的時候才去查找應該調用哪一個方法。
這兩種實現各有優(yōu)劣,C++ 的綁定使得調用方法的時候速度很快,但是只能通過 virtual 關鍵字來實現有限的動態(tài)綁定。而對 ObjC 來說,事實上他的實現是一種消息傳遞而不是方法調用。
[aObj aMethod];
這樣的語句應該理解為,像 aObj 對象發(fā)送一個叫做 aMethod 的消息,aObj 對象接收到這個消息之后,自己去查找是否能調用對應的方法,找不到則上父類找,再找不到就 Crash。由于 ObjC 的這種特性,使得其消息不單可以實現方法調用,還能緊系轉發(fā),對一個 obj 傳遞一個 selector 要求調用某方法,他可以直接不理會,轉發(fā)給別的 obj 讓別的 obj 來響應,非常靈活。
例子
[self methodNotExists];
調用一個不存在的方法,可以編譯通過,運行時直接掛掉,報 NSInvalidArgumentException 異常:
-[WSMainViewController methodNotExist]: unrecognized selector sent to instance 0x1dd96160
2013-10-23 15:49:52.167 WSCrashSample[5578:907] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[WSMainViewController methodNotExist]: unrecognized selector sent to instance 0x1dd96160'
解決方案
像這種類型的錯誤通常出現在使用 delegate 的時候,因為 delegate 通常是一個 id 泛型,所以 IDE 也不會報警告,所以這種時候要用 respondsToSelector 方法先判斷一下,然后再進行調用。
if ([self respondsToSelector:@selector(methodNotExist)]) {
[self methodNotExist];
}
四、多線程并發(fā)操作
這個應該是全平臺都會遇到的問題了。當某個對象會被多個線程修改的時候,有可能一個線程訪問這個對象的時候另一個線程已經把它刪掉了,導致 Crash。比較常見的是在網絡任務隊列里面,主線程往隊列里面加入任務,網絡線程同時進行刪除操作導致掛掉。
解決方法
1.加鎖 NSLock
普通的鎖,加鎖的時候 lock,解鎖調用 unlock。
- (void)addPlayer:(Player *)player {
if (player == nil) return;
NSLock* aLock = [[NSLock alloc] init];
[aLock lock];
[players addObject:player];
[aLock unlock];
}
}
可以使用標記符 @synchronized 簡化代碼:
- (void)addPlayer:(Player *)player {
if (player == nil) return;
@synchronized(players) {
[players addObject:player];
}
}
2.NSRecursiveLock 遞歸鎖
使用普通的 NSLock 如果在遞歸的情況下或者重復加鎖的情況下,自己跟自己搶資源導致死鎖。Cocoa 提供了 NSRecursiveLock 鎖可以多次加鎖而不會死鎖,只要 unlock 次數跟 lock 次數一樣就行了。
3.NSConditionLock 條件鎖
多數情況下鎖是不需要關心什么條件下 unlock 的,要用的時候鎖上,用完了就 unlock 就完了。Cocoa 提供這種條件鎖,可以在滿足某種條件下才解鎖。這個鎖的 lock 和 unlock, lockWhenCondition 是隨意組合的,可以不用對應起來。
4.NSDistributedLock 分布式鎖
這是用在多進程之間共享資源的鎖,對 iOS 來說暫時沒用處。
5.無鎖
放棄加鎖,采用原子操作,編寫無鎖隊列解決多線程同步的問題??釟び衅榻B無鎖隊列的文章可以參考一下:無鎖隊列的實現
使用其他備選方案代替多線程:Operation Objects, GCD, Idle-time notifications, Asynchronous functions, Timers, Separate processes。
五、Repeating NSTimer
如果一個 Timer 是不停 repeat,那么釋放之前就應該先 invalidate。非repeat的timer在fired的時候會自動調用invalidate,但是repeat的不會。這時如果釋放了timer,而timer其實還會回調,回調的時候找不到對象就會掛掉。
原因
NSTimer 是通過 RunLoop 來實現定時調用的,當你創(chuàng)建一個 Timer 的時候,RunLoop 會持有這個 Timer 的強引用,如果你創(chuàng)建了一個 repeating timer,在下一次回調前就把這個 timer release了,那么 runloop 回調的時候就會找不到對象而 Crash。
解決方案
我寫了個宏用來釋放Timer
/*
* 判斷這個Timer不為nil則停止并釋放
* 如果不先停止可能會導致crash
*/
#define WVSAFA_DELETE_TIMER(timer) { \
if (timer != nil) { \
[timer invalidate]; \
[timer release]; \
timer = nil; \
} \
}