iOS 常見Crash 及解決方案

一、訪問了一個已經被釋放的對象

在不使用 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; \
    } \
}

參考:

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

相關閱讀更多精彩內容

友情鏈接更多精彩內容