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