iOS crash分析實(shí)踐

背景

分享一些過去兩個(gè)月遇到的crash。

正文

一、運(yùn)行時(shí)錯(cuò)誤

1盟戏、UICollectionView的調(diào)用順序

從堆棧可以看出是indexPath無效甥桂,通常是indexPath的section或者row超過了數(shù)據(jù)的大凶グ搿;

根據(jù)堆棧信息和日志信息格嘁,可以找到用戶操作路徑,是通過scheme進(jìn)入分類廊移;
但是直接用真機(jī)復(fù)現(xiàn)糕簿,相同的操作并不會(huì)導(dǎo)致crash;
通過分析crash出現(xiàn)的機(jī)型和系統(tǒng)特征狡孔,發(fā)現(xiàn)都是iOS 13以下系統(tǒng)懂诗,而剛剛嘗試的是iOS 13的真機(jī);
于是用iOS 12模擬器嘗試同樣的路徑苗膝,可以成功復(fù)現(xiàn)殃恒。
分析原因后,定位到是先調(diào)用scrollToItemAtIndexPath辱揭,再調(diào)用reloadData導(dǎo)致的異常离唐。
解決方案也很簡(jiǎn)單,調(diào)整為正確的順序即可问窃。

尋找復(fù)現(xiàn)路徑的時(shí)候亥鬓,要盡量從兩個(gè)方向去復(fù)現(xiàn):
1、用戶設(shè)備條件域庇,包括iOS系統(tǒng)版本嵌戈、iphone機(jī)型、網(wǎng)絡(luò)環(huán)境等要保持一致听皿;
2熟呛、App運(yùn)行上下文,包括App版本尉姨、操作路徑庵朝、運(yùn)行環(huán)境等;

2、HTML轉(zhuǎn)碼NSAttributedString耗時(shí)過長(zhǎng)

業(yè)務(wù)需要把html格式的字符串轉(zhuǎn)成NSAttributedString偿短,原來HTML轉(zhuǎn)碼成NSAttributedString使用的是系統(tǒng)自帶的方法:

- (nullable instancetype)initWithData:(NSData *)data options:(NSDictionary<NSAttributedStringDocumentReadingOptionKey, id> *)options documentAttributes:(NSDictionary<NSAttributedStringDocumentAttributeKey, id> * __nullable * __nullable)dict error:(NSError **)error;

舉一個(gè)實(shí)際的例子:

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.

    NSString *pathStr = [[NSBundle mainBundle] pathForResource:@"test" ofType:@"data"];
    NSString *htmlStr = [NSString stringWithContentsOfURL:[NSURL fileURLWithPath:pathStr] encoding:NSUTF8StringEncoding error:nil];

    NSLog(@"load string:%@", htmlStr);
    NSData *htmlData = [NSData dataWithBytes:htmlStr.UTF8String length:[htmlStr lengthOfBytesUsingEncoding:NSUTF8StringEncoding]];
    NSDictionary *dic = @{NSDocumentTypeDocumentAttribute:NSHTMLTextDocumentType,
                          NSCharacterEncodingDocumentAttribute:[NSNumber numberWithInt:NSUTF8StringEncoding]};
    NSDate *date = [NSDate date];
    NSAttributedString *attrStr = [[NSAttributedString alloc] initWithData:htmlData options:dic documentAttributes:nil error:nil];

    NSLog(@"transform time:%f", [[NSDate date] timeIntervalSinceDate:date]);
}

這個(gè)邏輯測(cè)試階段一切正常欣孤, 直到遇到一段HTML文本,其中帶有img標(biāo)簽昔逗。
這段HTML文本在轉(zhuǎn)碼的時(shí)候會(huì)同步對(duì)圖片資源進(jìn)行加載降传,導(dǎo)致線程阻塞,如果阻塞時(shí)間過長(zhǎng)勾怒,還會(huì)引發(fā)crash婆排。
堆棧如下:


此時(shí),再回顧系統(tǒng)api的解釋笔链,才有了更深刻的理解:

It will try to synchronize with the main thread, fail, and time out. Calling it from the main thread works (but can still time out if the HTML contains references to external resources, which should be avoided at all costs).

解決方案1段只、轉(zhuǎn)碼前,手動(dòng)過濾掉<img>的標(biāo)簽鉴扫;
解決方案2赞枕、改用DTCoreText的html轉(zhuǎn)NSAttributedString;

使用一個(gè)不熟悉的系統(tǒng)API接口坪创,最好花時(shí)間閱讀下接口說明炕婶;

二、多線程

1莱预、dispatch_once引起死鎖

dispatch_once常被用于iOS單例實(shí)現(xiàn)方法柠掂,也會(huì)被用來處理某些只需要執(zhí)行一次的場(chǎng)景,比如下文的獲取某些tag的方法:

+ (NSInteger)coolCommentTag { 
    static NSInteger ret = 5; // 實(shí)驗(yàn)寫死默認(rèn)值依沮,客戶端不關(guān)心涯贞,透?jìng)鹘o后臺(tái) 
    static dispatch_once_t onceToken; 
    dispatch_once(&onceToken, ^{ 
        NSDictionary *abDict = [GET_SERVICE(SSTestManager) getExperimentValueForKey:@"social_comment_tag" withExposure:YES]; 
        if (SSIsValidatedDict(abDict)) { 
            ret = [abDict bda_integerValueForKey:@"comment_tag"]; 
        } 
    });      
    return ret; 
} 

這段代碼會(huì)造成一個(gè)crash:

堆棧關(guān)鍵信息:dispatch_gate_wait_slow;
注意到上圖危喉,crash的是子線程宋渔,這時(shí)候要習(xí)慣性看看主線程在處理什么邏輯

從這兩個(gè)堆棧信息,我們基本可以確定是因?yàn)槎嗑€程死鎖導(dǎo)致的卡死姥饰。
場(chǎng)景分析:
1傻谁、主線程:did注冊(cè) =》TTNet初始化 =》發(fā)起網(wǎng)絡(luò)請(qǐng)求 =》 獲取公共參數(shù) =》 dispatch_once ;
2列粪、子線程:RPC(網(wǎng)絡(luò)層)初始化 =》初始化 =》 獲取公共參數(shù) =》 dispatch_once 审磁;
然后發(fā)生死鎖

修復(fù)方式:去掉dispatch_once,改用線程安全的方式岂座;

經(jīng)驗(yàn)總結(jié):
網(wǎng)絡(luò)層相關(guān)接口有可能是子線程發(fā)起态蒂;
dispatch_once內(nèi)的代碼要盡可能簡(jiǎn)單;
子線程發(fā)生crash時(shí)费什,要習(xí)慣性看看主線程钾恢;

三、內(nèi)存相關(guān)

1、dealloc訪問weak指針

weak指針經(jīng)常被應(yīng)用到在代理瘩蚪、block避免循環(huán)引用泉懦,避免有一個(gè)特性就是對(duì)象dealloc的時(shí)候,指向?qū)ο蟮膚eak指針會(huì)被置為nil疹瘦。

但是使用不當(dāng)?shù)臅r(shí)候崩哩,weak指針也很容易造成crash,如下圖:


場(chǎng)景舉例:
1言沐、書架某個(gè)按鈕邓嘹,使用getter的方式獲取险胰;getter方法中用block去響應(yīng)按鈕點(diǎn)擊汹押,block因?yàn)槌钟辛送獠縮elf,所以會(huì)用weak-strong-dance起便;如果在dealloc方法訪問到該按鈕棚贾,則會(huì)發(fā)生crash;
2榆综、詳情頁(yè)的某個(gè)view鸟悴,使用getter的方式獲取,在dealloc方法時(shí)訪問了該getter奖年;

getter實(shí)現(xiàn)

修復(fù)方式

經(jīng)驗(yàn)總結(jié):
1、getter實(shí)現(xiàn)應(yīng)該簡(jiǎn)單化沛贪,盡量少的去設(shè)置很多屬性和創(chuàng)建block等陋守,僅僅作為懶加載去創(chuàng)建對(duì)象;
2利赋、dealloc方法不要訪問getter和setter的方法水评;

思考題,為什么對(duì)象dealloc創(chuàng)建對(duì)象的weak指針會(huì)crash媚送?

2中燥、子線程釋放對(duì)象

block是常見的回調(diào)方式,當(dāng)我們調(diào)用某個(gè)異步方法塘偎,想在回調(diào)時(shí)繼續(xù)保持運(yùn)行上下文疗涉,就會(huì)傳入一個(gè)回調(diào)block,等到就緒時(shí)再執(zhí)行block吟秩。
比如說常見的rpc請(qǐng)求:

[SSBookApiService getBookToneInfo:^(SSBookToneInfoRequest *request) {
        request.bookId = bookId;
    } completion:^(BDRpcError *error, SSBookToneInfoResponse *response) {
        XXX.XXX
    }
}];

block保持上下文的方式咱扣,就是持有block中所有訪問到的外部對(duì)象。
這種持有特性可能會(huì)導(dǎo)致一些意想不到的情況涵防,比如說在子線程銷毀一個(gè)對(duì)象闹伪。

如圖,子線程的堆棧都是在dealloc方法,一層層遞歸偏瓤。

通過代碼分析杀怠,可以知道是BDRpcAsyncOperation持有了某個(gè)block;該block持有了其他對(duì)象(因?yàn)閎lock中訪問了該對(duì)象)厅克。
結(jié)果BDRpcAsyncOperation在子線程銷毀時(shí)赔退,dealloc方法層層遞進(jìn),最終觸發(fā)了某個(gè)對(duì)象在子線程銷毀已骇,而該對(duì)象會(huì)在dealloc方法處理一些UI相關(guān)的邏輯离钝。

修復(fù)方法:
1、block訪問到的外部對(duì)象褪储,非局部變量盡可能使用weak-strong的方式來聲明卵渴;用weak指針來聲明外部的變量,如果該對(duì)象在block回調(diào)前被釋放鲤竹,則會(huì)變?yōu)閚il浪读;
2、如果需要block持有該對(duì)象辛藻,則可以在dealloc的時(shí)候使用dispatch_async的方式碘橘,放到主線程去執(zhí)行;(不能訪問self相關(guān))

總結(jié)

1吱肌、尋找復(fù)現(xiàn)路徑的時(shí)候痘拆,要盡可能還原現(xiàn)場(chǎng),才能更好復(fù)現(xiàn)氮墨;
2纺蛆、使用一個(gè)不熟悉的系統(tǒng)API接口,最好花時(shí)間閱讀下接口說明规揪;
3桥氏、子線程發(fā)生crash時(shí),要習(xí)慣性看看主線程猛铅;

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末字支,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子奸忽,更是在濱河造成了極大的恐慌堕伪,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,123評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件栗菜,死亡現(xiàn)場(chǎng)離奇詭異刃跛,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)苛萎,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門桨昙,熙熙樓的掌柜王于貴愁眉苦臉地迎上來检号,“玉大人,你說我怎么就攤上這事蛙酪∑肟粒” “怎么了?”我有些...
    開封第一講書人閱讀 156,723評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵桂塞,是天一觀的道長(zhǎng)凹蜂。 經(jīng)常有香客問我,道長(zhǎng)阁危,這世上最難降的妖魔是什么玛痊? 我笑而不...
    開封第一講書人閱讀 56,357評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮狂打,結(jié)果婚禮上擂煞,老公的妹妹穿的比我還像新娘。我一直安慰自己趴乡,他們只是感情好对省,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,412評(píng)論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著晾捏,像睡著了一般蒿涎。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上惦辛,一...
    開封第一講書人閱讀 49,760評(píng)論 1 289
  • 那天劳秋,我揣著相機(jī)與錄音,去河邊找鬼胖齐。 笑死俗批,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的市怎。 我是一名探鬼主播,決...
    沈念sama閱讀 38,904評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼辛慰,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼区匠!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起帅腌,我...
    開封第一講書人閱讀 37,672評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤驰弄,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后速客,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體戚篙,經(jīng)...
    沈念sama閱讀 44,118評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,456評(píng)論 2 325
  • 正文 我和宋清朗相戀三年溺职,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了岔擂。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片位喂。...
    茶點(diǎn)故事閱讀 38,599評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖乱灵,靈堂內(nèi)的尸體忽然破棺而出塑崖,到底是詐尸還是另有隱情,我是刑警寧澤痛倚,帶...
    沈念sama閱讀 34,264評(píng)論 4 328
  • 正文 年R本政府宣布规婆,位于F島的核電站,受9級(jí)特大地震影響蝉稳,放射性物質(zhì)發(fā)生泄漏抒蚜。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,857評(píng)論 3 312
  • 文/蒙蒙 一耘戚、第九天 我趴在偏房一處隱蔽的房頂上張望嗡髓。 院中可真熱鬧,春花似錦毕莱、人聲如沸器贩。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)蛹稍。三九已至,卻和暖如春部服,著一層夾襖步出監(jiān)牢的瞬間唆姐,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評(píng)論 1 264
  • 我被黑心中介騙來泰國(guó)打工廓八, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留奉芦,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,286評(píng)論 2 360
  • 正文 我出身青樓剧蹂,卻偏偏與公主長(zhǎng)得像声功,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子宠叼,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,465評(píng)論 2 348