iOS中多線程的經(jīng)典崩潰總結(jié)大全
前言
iOS崩潰是讓iOS開(kāi)發(fā)人員比較頭痛的事情,app崩潰了,說(shuō)明代碼寫(xiě)的有問(wèn)題,這時(shí)如何快速定位到崩潰的地方很重要。調(diào)試階段是比較容易找到出問(wèn)題的地方的,但是已經(jīng)上線的app并分析崩潰報(bào)告就比較麻煩了。
本文將給大家總結(jié)介紹關(guān)于iOS中多線程的一些經(jīng)典崩潰,下面話不多說(shuō)了,來(lái)一起看看詳細(xì)的介紹吧。
0x0 Block 回調(diào)的崩潰
在MRC環(huán)境下,使用Block 來(lái)設(shè)置下載成功的圖片。當(dāng)self釋放后,weakSelf變成野指針,接著就悲劇了
__block ViewController *weakSelf = self; [self.imageView imageWithUrl:@"" completedBlock:^(UIImage *image, NSError *error) { NSLog(@"%@",weakSelf.imageView.description); }];
0x1 多線程下Setter 的崩潰
Getter & Setter 寫(xiě)多了,在單線程的情況下,是沒(méi)有問(wèn)題的。但是在多線程的情況下,可能會(huì)崩潰。因?yàn)閇_imageView release]; 這段代碼可能會(huì)被執(zhí)行兩次,oops!
UIKit 不是線程,所以在不是主線程的地方調(diào)用UIKit 的東西,有可能在開(kāi)發(fā)階段完全沒(méi)問(wèn)題,直接免測(cè)。但是一到線上,崩潰系統(tǒng)可能都是你的崩潰日志。Holy shit!
解決辦法:通過(guò)hook 住setNeedsLayout,setNeedsDisplay,setNeedsDisplayInRect來(lái)檢查當(dāng)前調(diào)用的線程是否是主線程。
- (void)setImageView:(UIImageView *)imageView { if (![_imageView isEqual:imageView]) { [_imageView release]; _imageView = [imageView retain]; } }
0x2 更多Setter 類型的崩潰
property 的屬性,寫(xiě)的最多的就是nonatomic,一般情況下也是沒(méi)有問(wèn)題的!
@interface ViewController () @property (strong,nonatomic) NSMutableArray *array; @end
跑一下下面這段代碼,你會(huì)看到:
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]; }); }
原因就是:對(duì)象被重復(fù)relaese 了。查看一下runtime 源碼
解決辦法:屬性聲明為atomic.
一個(gè)更為常見(jiàn)的例子:
if(handler == nil) { hander = [[Handler alloc] init]; } return handler;
如果A,B兩個(gè)線程同時(shí)訪問(wèn)到if語(yǔ)句, 此時(shí)handler == nil
條件滿足, 兩個(gè)線程都走到下一句初始化實(shí)例.
此時(shí)A線程先完成初始化并賦值(這個(gè)實(shí)例我們叫它a), 然后繼續(xù)往后走到其他邏輯.而這時(shí)候, B線程開(kāi)始做初始化并賦值(這個(gè)實(shí)例我們叫它b), handler將指向B線程初始化出來(lái)的對(duì)象. 而A初始化出來(lái)的實(shí)例a因?yàn)橐糜?jì)數(shù)減少1(減少到0)而被釋放. 但在A線程中, 代碼還會(huì)嘗試訪問(wèn)a所在的地址, 這個(gè)地址里的內(nèi)容因?yàn)楸会尫哦兊脽o(wú)法預(yù)測(cè), 從而導(dǎo)致野指針.
問(wèn)題還有一個(gè)很關(guān)鍵的點(diǎn), 在一個(gè)對(duì)象的某個(gè)方法的調(diào)用過(guò)程中, 這個(gè)對(duì)象的引用計(jì)數(shù)并不會(huì)增加, 到導(dǎo)致它如果被釋放, 后續(xù)的執(zhí)行過(guò)程中對(duì)這個(gè)對(duì)象的訪問(wèn)就可能會(huì)導(dǎo)致野指針[1].
Exception Type: SIGSEGV Exception Codes: SEGV_ACCERR at 0x12345678 Triggered by Thread: 1
簡(jiǎn)單加個(gè)鎖就可以解決問(wèn)題了:
@synchronized(self){ if(handler == nil) { hander = [[Handler alloc] init]; } } return handler;
0x3 多線程下對(duì)變量的存取
if (self.xxx) { [self.dict setObject:@"ah" forKey:self.xxx]; }
大家第一眼看到這樣的代碼,是不是會(huì)認(rèn)為是正確的?因?yàn)樵谠O(shè)置key的時(shí)候已經(jīng)提前進(jìn)行了self.xxx為非nil的判斷,只有非nil得情況下才會(huì)執(zhí)行后續(xù)的指令。但是,如上代碼只有在單線程的前提下才是正確的。
假設(shè)我們將上述代碼目前執(zhí)行的線程為Thread A,當(dāng)我們執(zhí)行完if (self.xxx)
的語(yǔ)句之后,此時(shí)CPU將執(zhí)行權(quán)切換給了Thread B,而這個(gè)時(shí)候Thread B中調(diào)用了一句self.xxx = nil
。 使用局部變量可以解決這個(gè)問(wèn)題
__strong id val = self.xxx; if (val) { [self.dict setObject:@"ah" forKey:val]; }
這樣,無(wú)論多少線程嘗試對(duì)self.xxx進(jìn)行修改,本質(zhì)上的val都會(huì)保持現(xiàn)有的狀態(tài),符合非nil的判斷。
0x4 dispatch_group 的崩潰
dispatch_group_enter 和 leave 必須是匹配的,不然就會(huì)crash . 在多資源下載的時(shí)候,往往需要使用多線程并發(fā)下載,全部下載完之后通知用戶。開(kāi)始下載,dispatch_group_enter ,下載完成dispatch_group_leave 。 非常簡(jiǎn)單的流程,但是當(dāng)代碼復(fù)雜到一定程度或者是使用了一些第三方庫(kù)的時(shí)候,就很大可能出問(wèn)題。
dispatch_group_t serviceGroup = dispatch_group_create(); dispatch_group_notify(serviceGroup, dispatch_get_main_queue(), ^{ NSLog(@"Finish downloading :%@", downloadUrls); }); // t 是一個(gè)包含一堆字符串的數(shù)組 [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]; }];
使用多線程進(jìn)行并發(fā)下載,直到所有圖片都下載完成(可以失敗)進(jìn)行回調(diào),其中圖片下載使用的是SDWebImage.發(fā)生崩潰的場(chǎng)景是:有10 張圖片,分開(kāi)兩次下載(A & B)。其中在B組里面有一張圖片和A組下載的圖片重復(fù)了。假設(shè)A組下載對(duì)應(yīng)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的下載器會(huì)根據(jù)URL做下載任務(wù)對(duì)應(yīng)NSOperation映射,相同的URL會(huì)映射到同一個(gè)未執(zhí)行的NSOperation。當(dāng)A組圖片下載完成后,相同的url 回調(diào)是 GroupB 而不是Group A。此時(shí)Group B的計(jì)數(shù)為1 。當(dāng)B 組圖片全部下載完后,結(jié)束計(jì)數(shù)為 5+1 。因?yàn)閑nter 的次數(shù)為5 ,leave 的次數(shù)為6 ,因此會(huì)崩潰!
0x5 最后一個(gè)持有者釋放后的崩潰
對(duì)象A被 manager 持有,在A中調(diào)用[Manager removeObjectA]
。A對(duì)象的retainCount -1
, 當(dāng)retainCount 等于零時(shí),對(duì)象A已經(jīng)開(kāi)始釋放了。在調(diào)用removeObjectA 后,緊接著調(diào)用[self doSomething]
,就會(huì)崩潰。
- (void)finishEditing { [Manager removeObject:self]; [self doSomething]; }
這種情況一般會(huì)發(fā)生在數(shù)組或者字典包含對(duì)象,而且是對(duì)象的最后持有者。當(dāng)在對(duì)象處理不好,就會(huì)有上面的崩潰。還有一種情況就是,當(dāng)數(shù)組或者字典里面的對(duì)象已經(jīng)被釋放了,當(dāng)遍歷數(shù)組或者取字典里面的值發(fā)生崩潰。這種情況,會(huì)讓人很崩潰,因?yàn)橛袝r(shí)候堆棧是這樣的:
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)
產(chǎn)生這種堆??赡艿膱?chǎng)景是:
釋放Dictionary的時(shí)候,某個(gè)值(value)因?yàn)楸黄渌a提前釋放變成野指針, 此時(shí)再次被釋放觸發(fā)Crash. 如果可以在每個(gè)Dictionary釋放的時(shí)候, 把所有的key/value打出來(lái), 如果某個(gè)key/value剛好被打出來(lái)之后, crash就發(fā)生了, 那么掛就掛在剛被打出來(lái)的key/value上.
0x6 對(duì)象的釋放線程要和它處理事情的線程一致
對(duì)象A在主線程監(jiān)聽(tīng)Notification事件,如果這個(gè)對(duì)象被其它線程釋放了。此刻,如果對(duì)象A 正在執(zhí)行notification 相關(guān)的操作,再訪問(wèn)對(duì)象相關(guān)資源就野指針了,發(fā)生crash.
0x7 performSelector:withObject:afterDelay:
調(diào)用此方法,如果不是在主線程,那么必須要確保當(dāng)前線程的ruuloop是存在的,performSelector_xxx_afterDelay 依賴runlopp才能執(zhí)行。另外使用 performSelector:withObject:afterDelay:
和 cancelPreviousPerformRequestsWithTarget
組合的時(shí)候要小心。
- afterDelay會(huì)增加receiver的引用計(jì)數(shù),cancel則會(huì)對(duì)應(yīng)減一
- 如果在receiver的引用計(jì)數(shù)只剩下1 (僅為delay)時(shí),調(diào)用cancel之后會(huì)立即銷毀receiver,后續(xù)再調(diào)用receiver的方法就會(huì)crash
__weak typeof(self) weakSelf = self; [NSObject cancelPreviousPerformRequestsWithTarget:self]; if (!weakSelf) { //NSLog(@"self被銷毀"); return; } [self doOther];
總結(jié)
以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,如果有疑問(wèn)大家可以留言交流,謝謝大家對(duì)腳本之家的支持。
相關(guān)文章
詳解iOS 用于解決循環(huán)引用的block timer
這篇文章主要介紹了詳解iOS 用于解決循環(huán)引用的block timer,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-12-12iOS三級(jí)聯(lián)動(dòng)選擇器的實(shí)現(xiàn)代碼示例
本篇文章主要介紹了iOS三級(jí)聯(lián)動(dòng)選擇器的實(shí)現(xiàn)代碼示例,這里整理了詳細(xì)的代碼,有需要的小伙伴可以參考下2017-09-09淺談IOS中AFNetworking網(wǎng)絡(luò)請(qǐng)求的get和post步驟
本篇文章主要介紹了淺談IOS中AFNetworking網(wǎng)絡(luò)請(qǐng)求的get和post步驟的相關(guān)資料,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下。2017-02-02在iOS10系統(tǒng)中微信后退無(wú)法發(fā)起ajax請(qǐng)求的問(wèn)題解決辦法
這篇文章主要介紹了在iOS10系統(tǒng)中微信后退無(wú)法發(fā)起ajax請(qǐng)求的問(wèn)題解決辦法,一般可以通過(guò)延時(shí)發(fā)送請(qǐng)求解決,下面通過(guò)本文給大家分享下解決辦法,需要的朋友參考下吧2017-01-01