iOS 常見(jiàn)Crash 及解決方案

一、訪問(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; \
    } \
}

參考:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容