App上線后,我們最怕出現(xiàn)的情況就是崩潰了,但是線下我們測試好好的App,為什么上線后就發(fā)生崩潰了呢?這些崩潰日志信息是怎么采集的?能夠采集的全嗎?采集后又要怎么分析,解決呢?
App上線后,是很脆弱的,導(dǎo)致其崩潰的問題,不僅包括編寫代碼時的各種馬虎,還包括那些被系統(tǒng)強殺的疑難雜癥.下面我們先看常見的幾個編寫代碼時的小馬虎,是如何讓應(yīng)用崩潰的.
1.數(shù)組越界:在取數(shù)據(jù)索引時越界,App會發(fā)生崩潰,還有一種情況,就是給數(shù)據(jù)添加一個nil元素會崩潰
2.多線程問題:在子線程中進行UI更新可能會發(fā)生崩潰,多個線程進行數(shù)據(jù)的讀取操作,因為處理時機不一致,比如有一個線程在置空數(shù)據(jù)的同時另一個線程在讀取這個數(shù)據(jù),可能會出現(xiàn)崩潰情況
3.主線程為響應(yīng):如果主線程超過系統(tǒng)規(guī)定的時間無響應(yīng),會被Watchdog殺掉,這時,崩潰問題對應(yīng)的異常編碼是0x8badf00d.
4.野指針:指針指向一個已刪除的對象訪問內(nèi)存區(qū)域時,會出現(xiàn)野指針崩潰,野指針問題是需要我們重點關(guān)注的,因為它是導(dǎo)致App崩潰的最常見,也是最難定位的一種情況,
一般崩潰都是由崩潰監(jiān)控系統(tǒng)收集的,收集的是堆棧信息,這些堆棧信息也是解決崩潰問題的重要依據(jù)
崩潰信息的收集并沒有那么簡單,因為,有的崩潰日志是可以通過信號捕捉到的,而很多崩潰日志是通過信號捕捉不到的,下圖可視

從這張圖片中可以看出KVO問題,通知線程問題,數(shù)據(jù)越界,野指針等崩潰信息,可以通過信號捕捉,但是,像后臺任務(wù)超時,內(nèi)存被打爆,主線程卡頓超閾值等信息,是無法通過信號捕捉到的,接下來我們就看兩種不同的崩潰情況怎么捕捉
信號可捕捉的崩潰日志收集
收集崩潰日志最簡單的方法就是打開Xcode的菜單選擇Product -> Archive.然后在提交時選上"Upload your app`s symbols to receive symbolicated reports from Apple",以后你就可以直接在Xcode的Archive里看到符號化的崩潰日志了.
這種查看日志的方式,每次都是純手工操作,時效性較差,目前很多公司的崩潰日志監(jiān)控系統(tǒng),都是通過PLCarshReporter這樣的三方開源庫捕捉崩潰日志的,然后上傳到自己服務(wù)器上進行整體監(jiān)控,而沒有服務(wù)端開發(fā)能力或者對數(shù)據(jù)不敏感的公司,這會使用Bugly這樣的三方提供的完整的監(jiān)控捕捉崩潰SDK進行集成統(tǒng)計.
那么為什么PLCrashReporter和Bugly這類工具,是怎么知道App是什么時候崩潰呢?
在崩潰日志里,我們會經(jīng)??吹较旅孢@段說明
Type: EXC_BAD_ACCESS (SIGSEGV)
它表示的事,EXC_BAD_ACCESS這個異常會通過SIGSEGV信號發(fā)現(xiàn)有問題的線程,雖然信號的種類有很多,但是都可以通過注冊signalHandler來捕捉,起實現(xiàn)代碼如下
void registerSignalHandler(void){
signal(SIGSEGV, handleSignalException);//試圖訪問未分配給自己的內(nèi)存, 或試圖往沒有寫權(quán)限的內(nèi)存地址寫數(shù)據(jù).
signal(SIGFPE, handleSignalException);//在發(fā)生致命的算術(shù)運算錯誤時發(fā)出. 不僅包括浮點運算錯誤, 還包括溢出及除數(shù)為0等其它所有的算術(shù)的錯誤。
signal(SIGBUS, handleSignalException);//非法地址, 包括內(nèi)存地址對齊(alignment)出錯。比如訪問一個四個字長的整數(shù), 但其地址不是4的倍數(shù)。它與SIGSEGV的區(qū)別在于后者是由于對合法存儲地址的非法訪問觸發(fā)的(如訪問不屬于自己存儲空間或只讀存儲空間)。
signal(SIGPIPE, handleSignalException);//管道破裂。這個信號通常在進程間通信產(chǎn)生,比如采用FIFO(管道)通信的兩個進程,讀管道沒打開或者意外終止就往管道寫,寫進程會收到SIGPIPE信號。此外用Socket通信的兩個進程,寫進程在寫Socket的時候,讀進程已經(jīng)終止
signal(SIGHUP, handleSignalException);//本信號在用戶終端連接(正常或非正常)結(jié)束時發(fā)出, 通常是在終端的控制進程結(jié)束時, 通知同一session內(nèi)的各個作業(yè), 這時它們與控制終端不再關(guān)聯(lián)。登錄Linux時,系統(tǒng)會分配給登錄用戶一個終端(Session)。在這個終端運行的所有程序,包括前臺進程組和后臺進程組,一般都屬于這個 Session。當用戶退出Linux登錄時,前臺進程組和后臺有對終端輸出的進程將會收到SIGHUP信號。這個信號的默認操作為終止進程,因此前臺進 程組和后臺有終端輸出的進程就會中止。不過可以捕獲這個信號,比如wget能捕獲SIGHUP信號,并忽略它,這樣就算退出了Linux登錄, wget也 能繼續(xù)下載。此外,對于與終端脫離關(guān)系的守護進程,這個信號用于通知它重新讀取配置文件。
signal(SIGINT, handleSignalException);//程序終止(interrupt)信號, 在用戶鍵入INTR字符(通常是Ctrl-C)時發(fā)出,用于通知前臺進 程組終止進程。
signal(SIGQUIT, handleSignalException);//和SIGINT類似, 但由QUIT字符(通常是Ctrl-)來控制. 進程在因收到SIGQUIT退出時會產(chǎn)生core文件, 在這個意義上類似于一個程序錯誤信號。
signal(SIGABRT, handleSignalException);//調(diào)用abort函數(shù)生成的信號。
signal(SIGILL, handleSignalException);//執(zhí)行了非法指令. 通常是因為可執(zhí)行文件本身出現(xiàn)錯誤, 或者試圖執(zhí)行數(shù)據(jù)段. 堆棧溢出時也有可能產(chǎn)生這個信號。
}
void handleSignalException(int signal){
NSMutableString * crashString = [[NSMutableString alloc]init];
void * callStack[128];
int i, frames = backtrace(callStack,128);
char ** traceChar = backtrace_symbols(callStack,frames);
for (i = 0; i < frames; ++i) {
[crashString appendString:@"%s\n",traceChar[i]];
}
NSLog(crashString);
}
上面這段代碼會各種信號都進行了注冊,捕捉到異常信號后,在處理方法handleSignalException里通過backtrace_symbols方法可以獲取到當前的堆棧信息,堆棧信息先保存到本地,下次啟動時在上傳到奔潰監(jiān)控服務(wù)器就可以了
信號不可捕捉的崩潰日志收集
我們可能會遇到App退到后臺后,即使代碼邏輯沒有問題也容易出現(xiàn)崩潰,而且這些崩潰往往是因為系統(tǒng)強殺掉了某些進程導(dǎo)致的,而系統(tǒng)強殺拋出的信號還由于系統(tǒng)限制無法被捕捉到.
那么,后臺容易崩潰的原因是什么呢?如何避免后臺崩潰?怎么去收集后臺信號捕捉不到的那些崩潰信息呢?還有那些信號捕捉不到的崩潰情況?怎么去監(jiān)控其他無法通過信號捕捉的崩潰信息?
下面我們就帶著這五個問題來了解信號不可捕捉的崩潰
后臺容易崩潰的原因是什么?
首先我們知道iOS后臺包活的5種方式:Background Mode,Background Fetch,Silent Push,PushKit,Background Task
1.使用Background Mode方式的話,AppStore在審核時會提高對App的要求,通常情況下,只有那些地圖,音樂播放,VoIP類的App才能通過審核
2.Background Fetch 方式的喚醒時間不穩(wěn)定,而且用戶可以在系統(tǒng)設(shè)置關(guān)閉這種方式,導(dǎo)致他的使用場景很少
3.Silent Push是推送的一種,會在后臺喚起App 30秒.會調(diào)起application:didReceiveRemoteNotifiacation這個delegate和普通的remote pushnotification推送調(diào)用的delegate是一樣的
4.PushKit后臺喚醒App后能夠包活30秒,他主要用于提升VoIp應(yīng)用的體驗
5.Background Task方式,是使用最多的,App退到后臺后,默認都會使用這種方式
接下來看一下使用最多的Background Task方式:
在程序退到后臺后,只有幾秒鐘的時間可以執(zhí)行代碼,接下來會被系統(tǒng)掛起,進程掛起后所有的線程都會暫停,不管這個線程是文件讀寫還是內(nèi)存讀寫都會被暫停,但是,數(shù)據(jù)讀寫過程無法暫停只能被中斷,中斷時數(shù)據(jù)讀寫異常而且容易損壞文件,所以系統(tǒng)會選擇主動殺掉進程.
Background Task就是系統(tǒng)提供了baginBackgroundTaskWithExpirationHandler方法來延長后臺執(zhí)行時間,可以解決退到后臺還需要一些時間去處理一些任務(wù)的訴求.
Background Task方式的使用方法,如下代碼
- (void)applicationDidEnterBackground:(UIApplication *)application {
self.backgroundTaskIdentifier = [application beginBackgroundTaskWithExpirationHandler:^{
[self youTask];
}];
}
這段代碼中,youTask任務(wù)最多執(zhí)行3分鐘,3分鐘內(nèi)youTask運行完成,你的App就會掛起,如果youTask在三分鐘內(nèi)沒有執(zhí)行完的話,就會被系統(tǒng)強行殺掉進程,造成崩潰.
如何避免后臺崩潰呢?
如果我們想要避免這種崩潰發(fā)生的話,就需要嚴格控制后臺數(shù)據(jù)的讀寫操作,比如,可以先判斷需要處理的數(shù)據(jù)大小,如果數(shù)據(jù)過大,也就是在后臺限制時間內(nèi)或延長后臺執(zhí)行時間后也處理不完的話,可以考慮在程序下次啟動或后臺喚醒時在進行處理
怎么去收集后臺信號捕捉不到的那些崩潰信息呢?
采用Background Task方式時,我們可以根據(jù)beginBackgroundTaskWithExpirationHandler會讓后臺包活三分鐘,先設(shè)置一個定時器,在接近三分鐘的時候判斷后臺程序是否還在執(zhí)行,如果還在執(zhí)行的話,我們就可以判斷該程序即將后臺崩潰,進行上報記錄,已達到監(jiān)控的效果
還有那些信號捕捉不到的崩潰情況?怎么去監(jiān)控其他無法通過信號捕捉的崩潰信息?
其他捕捉不到的崩潰情況還有很多,主要就是內(nèi)存打爆和主線程卡頓時間超過閾值被watchdog殺掉,其實監(jiān)控這兩種崩潰的思路和監(jiān)控后臺崩潰類似,我們要先找到他們的閾值,然后在臨近閾值時還在執(zhí)行的后臺程序,判斷為將要崩潰,收集信息并上報
注:以上文章為學習戴銘(iOS開發(fā)課程)