總結:iOS中多執行緒的經典崩潰

iOSeryh94發表於2020-06-22

Block 回撥的崩潰

在MRC環境下,使用Block 來設定下載成功的圖片。當self釋放後,weakSelf變成野指標,接著就悲劇了

__block ViewController *weakSelf = self;
[self.imageView imageWithUrl:@"" completedBlock:^(UIImage *image, NSError *error) {
NSLog(@"%@",weakSelf.imageView.description);
}];

多執行緒下Setter 的崩潰

Getter & Setter 寫多了,在單執行緒的情況下,是沒有問題的。但是在多執行緒的情況下,可能會崩潰。因為[_imageView release]; 這段程式碼可能會被執行兩次,oops!

UIKit 不是執行緒,所以在不是主執行緒的地方呼叫UIKit 的東西,有可能在開發階段完全沒問題,直接免測。但是一到線上,崩潰系統可能都是你的崩潰日誌。Holy shit!

解決辦法:透過hook 住setNeedsLayout,setNeedsDisplay,setNeedsDisplayInRect來檢查當前呼叫的執行緒是否是主執行緒。

-(void)setImageView:(UIImageView *)imageView
{
if (![_imageView isEqual:imageView])
{
[_imageView release];
_imageView = [imageView retain];
}
}

更多Setter 型別的崩潰

property 的屬性,寫的最多的就是nonatomic,一般情況下也是沒有問題的!

@interface ViewController ()
@property (strong,nonatomic) NSMutableArray *array;
@end

跑一下下面這段程式碼,你會看到:

malloc: error for object 0x7913d6d0: pointer being freed was not allocated

for (int i = 0; i < 100; i++) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
self.array = [[NSMutableArray alloc] init];
});
}

原因就是:物件被重複relaese 了。檢視一下 runtime 原始碼

總結:iOS中多執行緒的經典崩潰

解決辦法:屬性宣告為atomic.

一個更為常見的例子:

if(handler == nil)
{
hander = [[Handler alloc] init];
}
return handler;

如果A,B兩個執行緒同時訪問到if語句, 此時 handler == nil條件滿足, 兩個執行緒都走到下一句初始化例項.

此時A執行緒先完成初始化並賦值(這個例項我們叫它a), 然後繼續往後走到其他邏輯.而這時候, B執行緒開始做初始化並賦值(這個例項我們叫它b), handler將指向B執行緒初始化出來的物件. 而A初始化出來的例項a因為引用計數減少1(減少到0)而被釋放. 但在A執行緒中, 程式碼還會嘗試訪問a所在的地址, 這個地址裡的內容因為被釋放而變得無法預測, 從而導致野指標.

問題還有一個很關鍵的點, 在一個物件的某個方法的呼叫過程中, 這個物件的引用計數並不會增加, 到導致它如果被釋放, 後續的執行過程中對這個物件的訪問就可能會導致野指標[1].

Exception Type: SIGSEGV
Exception Codes: SEGV_ACCERR at 0x12345678
Triggered by Thread: 1

簡單加個鎖就可以解決問題了:

@synchronized(self){
if(handler == nil)
{
hander = [[Handler alloc] init];
}
}
return handler;

多執行緒下對變數的存取

if (self.xxx) {
[self.dict setObject:@"ah"  forKey:self.xxx];
}

大家第一眼看到這樣的程式碼,是不是會認為是正確的?因為在設定key的時候已經提前進行了self.xxx為非nil的判斷,只有非nil得情況下才會執行後續的指令。但是,如上程式碼只有在單執行緒的前提下才是正確的。

假設我們將上述程式碼目前執行的執行緒為Thread A,當我們執行完 if (self.xxx)的語句之後,此時CPU將執行權切換給了Thread B,而這個時候Thread B中呼叫了一句 self.xxx = nil。 使用區域性變數可以解決這個問題

__strong id val = self.xxx;
if (val) {
[self.dict setObject:@"ah" forKey:val];
}

這樣,無論多少執行緒嘗試對self.xxx進行修改,本質上的val都會保持現有的狀態,符合非nil的判斷。

作為一個開發者,有一個學習的氛圍跟一個交流圈子特別重要,這是一個我的iOS交流群: 519832104 不管你是小白還是大牛歡迎入駐,分享經驗,討論技術,大家一起交流學習成長!

另附上一份各好友收集的大廠面試題,需要iOS開發學習資料、面試真題,可以新增iOS開發進階交流群,進群可自行下載!

總結:iOS中多執行緒的經典崩潰

dispatch_group 的崩潰

dispatch_group_enter 和 leave 必須是匹配的,不然就會crash . 在多資源下載的時候,往往需要使用多執行緒併發下載,全部下載完之後通知使用者。開始下載,dispatch_group_enter ,下載完成dispatch_group_leave 。 非常簡單的流程,但是當程式碼複雜到一定程度或者是使用了一些第三方庫的時候,就很大可能出問題。

dispatch_group_t serviceGroup = dispatch_group_create();
dispatch_group_notify(serviceGroup, dispatch_get_main_queue(), ^{
NSLog(@"Finish downloading :%@", downloadUrls);
});
// t 是一個包含一堆字串的陣列
[downloadUrls enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
dispatch_group_enter(serviceGroup);
SDWebImageCompletionWithFinishedBlock completion =
^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
dispatch_group_leave(serviceGroup);
NSLog(@"idx:%zd",idx);
};
[[SDWebImageManager sharedManager] downloadImageWithURL:[NSURL URLWithString: downloadUrls[idx]] options:SDWebImageLowPriority progress:nil completed:completion];
}];

使用多執行緒進行併發下載,直到所有圖片都下載完成(可以失敗)進行回撥,其中圖片下載使用的是SDWebImage.發生崩潰的場景是:有10 張圖片,分開兩次下載(A & B)。其中在B組裡面有一張圖片和A組下載的圖片重複了。假設A組下載對應GroupA ,B組GroupB

下面擷取SDWebImage原始碼:

dispatch_barrier_sync(self.barrierQueue, ^{
SDWebImageDownloaderOperation *operation = self.URLOperations[url];
if (!operation) {
operation = createCallback();
// ****注意這行****
self.URLOperations[url] = operation;
__weak SDWebImageDownloaderOperation *woperation = operation;
operation.completionBlock = ^{
SDWebImageDownloaderOperation *soperation = woperation;
if (!soperation) return;
if (self.URLOperations[url] == soperation) {
[self.URLOperations removeObjectForKey:url];
};
};
}
// ****注意這行****
id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
}

SDWebImage的下載器會根據URL做下載任務對應NSOperation對映,相同的URL會對映到同一個未執行的NSOperation。當A組圖片下載完成後,相同的url 回撥是 GroupB 而不是Group A。此時Group B的計數為1 。當B 組圖片全部下載完後,結束計數為 5+1 。因為enter 的次數為5 ,leave 的次數為6 ,因此會崩潰!

最後一個持有者釋放後的崩潰

物件A被 manager 持有,在A中呼叫 [Manager removeObjectA]。A物件的 retainCount -1, 當retainCount 等於零時,物件A已經開始釋放了。在呼叫removeObjectA 後,緊接著呼叫 [self doSomething] ,就會崩潰。

-(void)finishEditing
{
[Manager removeObject:self];
[self doSomething];
}

這種情況一般會發生在陣列或者字典包含物件,而且是物件的最後持有者。當在物件處理不好,就會有上面的崩潰。還有一種情況就是,當陣列或者字典裡面的物件已經被釋放了,當遍歷陣列或者取字典裡面的值發生崩潰。這種情況,會讓人很崩潰,因為有時候堆疊是這樣的:

Thread 0 Crashed:
0 libobjc.A.dylib 0x00000001816ec160 _objc_release :16 (in libobjc.A.dylib)
1 libobjc.A.dylib 0x00000001816edae8 __ZN12_GLOBAL__N_119AutoreleasePoolPage3popEPv :508 (in libobjc.A.dylib)
2 CoreFoundation 0x0000000181f4c9fc __CFAutoreleasePoolPop :28 (in CoreFoundation)
3 CoreFoundation 0x0000000182022bc0 ___CFRunLoopRun :1636 (in CoreFoundation)
4 CoreFoundation 0x0000000181f4cc50 _CFRunLoopRunSpecific :384 (in CoreFoundation)
5 GraphicsServices 0x0000000183834088 _GSEventRunModal :180 (in GraphicsServices)
6 UIKit 0x0000000187236088 _UIApplicationMain :204 (in UIKit)
7 Tmall4iPhone 0x00000001000b7ae4 main main.m:50 (in Tmall4iPhone)
8 libdyld.dylib 0x0000000181aea8b8 _start :4 (in libdyld.dylib)

產生這種堆疊可能的場景是:

釋放Dictionary的時候,某個值(value)因為被其他程式碼提前釋放變成野指標, 此時再次被釋放觸發Crash. 如果可以在每個Dictionary釋放的時候, 把所有的key/value打出來, 如果某個key/value剛好被打出來之後, crash就發生了, 那麼掛就掛在剛被打出來的key/value上.

物件的釋放執行緒要和它處理事情的執行緒一致

物件A在主執行緒監聽Notification事件,如果這個物件被其它執行緒釋放了。此刻,如果物件A 正在執行notification 相關的操作,再訪問物件相關資源就野指標了,發生crash.

performSelector:withObject:afterDelay:

呼叫此方法,如果不是在主執行緒,那麼必須要確保當前執行緒的ruuloop是存在的,performSelector_xxx_afterDelay 依賴runlopp才能執行。另外使用  performSelector:withObject:afterDelay:和  cancelPreviousPerformRequestsWithTarget組合的時候要小心。

  • afterDelay會增加receiver的引用計數,cancel則會對應減一
  • 如果在receiver的引用計數只剩下1 (僅為delay)時,呼叫cancel之後會立即銷燬receiver,後續再呼叫receiver的方法就會crash

__weak typeof(self) weakSelf = self;
[NSObject cancelPreviousPerformRequestsWithTarget:self];
if (!weakSelf)
{
//NSLog(@"self被銷燬");
return;
}
[self doOther];

總結

以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作具有一定的參考學習價值,如果有疑問大家可以留言交流,謝謝大家的支援。

點選此處,立即與iOS大牛交流學習


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69977274/viewspace-2699895/,如需轉載,請註明出處,否則將追究法律責任。

相關文章