iOS App連續(xù)閃退時(shí)上報(bào)crash日志的方法詳解
前言
當(dāng)一個(gè)iOS應(yīng)用程序崩潰時(shí),系統(tǒng)會(huì)創(chuàng)建一份crash日志保存在設(shè)備上。這份crash日志記錄著應(yīng)用程序崩潰時(shí)的信息,通常包含著每個(gè)執(zhí)行線程的棧調(diào)用信息(低內(nèi)存閃退日志例外),對(duì)于開(kāi)發(fā)人員定位問(wèn)題很有幫助。
為保障線上 App 的用戶體驗(yàn),我們一般都會(huì)對(duì)線上 App 的 crash 率做實(shí)時(shí)監(jiān)控,一旦檢測(cè)到 spike,可以即刻調(diào)查原因,但這一切的前提是 crash 日志能夠準(zhǔn)確上報(bào)。
crash 日志上報(bào)有兩個(gè)難點(diǎn):
- crash handler 安裝之前的代碼要絕對(duì)穩(wěn)定
如果日志采集器還沒(méi)成功啟動(dòng)就 crash 了,自然什么日志也無(wú)法采集到。這一點(diǎn)并沒(méi)有太多技巧可言,只能嚴(yán)格限制 handler 啟動(dòng)之前可以執(zhí)行的代碼。 - App 無(wú)限循環(huán) crash 時(shí)上報(bào)
crash 日志上報(bào)時(shí),會(huì)發(fā)送網(wǎng)絡(luò)請(qǐng)求,如果請(qǐng)求成功之前 App 又發(fā)生 crash 該如何處理?用戶甚至?xí)萑霟o(wú)限循環(huán)的 crash 中。
這篇文章介紹下出現(xiàn)第二種情況時(shí),如何準(zhǔn)確上報(bào) crash 日志。
首先我們需要一種比較可靠的方式,可以在 app 啟動(dòng)時(shí)判斷上次是否發(fā)生了啟動(dòng) crash。介紹一個(gè)可行的思路。
如何檢測(cè)連續(xù)閃退
連續(xù)閃退包含兩個(gè)元素,閃退和連續(xù)。只有這兩個(gè)元素同時(shí)具備時(shí),才會(huì)影響我們的日志上傳。閃退的定義可以簡(jiǎn)單為
app crash 時(shí)間 - app 啟動(dòng)時(shí)間 <= 5s (或者其他 threshold)
連續(xù)的定義為,至少接連出現(xiàn)兩次或者以上。一般 2 次就夠了,很多時(shí)候用戶連續(xù)經(jīng)歷兩次閃退,就會(huì)放棄嘗試。
我們可以通過(guò)記錄若干個(gè)特殊的時(shí)間點(diǎn) timestamp 來(lái)試圖還原 App crash 場(chǎng)景下的生命周期。
- App 啟動(dòng) timestamp,定義為 launchTs
App 每次啟動(dòng)時(shí),記錄當(dāng)前時(shí)間,寫(xiě)入時(shí)間數(shù)組。 - App crash timestamp,定義為 crashTs
App 每次啟動(dòng)時(shí),通過(guò) crash 采集庫(kù),獲取上次 crash report 的時(shí)間戳,寫(xiě)入時(shí)間數(shù)組。 - App 正常退出 timestamp,定義為 terminateTs
App 在接收到 UIApplicationWillTerminateNotification 通知時(shí),記錄當(dāng)前時(shí)間戳,寫(xiě)入時(shí)間數(shù)組。注意,還有很多種 App 退出行為的時(shí)間戳是無(wú)法被準(zhǔn)確記錄的。
之所以要記錄 terminateTs,是為了排除一種特殊情況,即用戶啟動(dòng) App 之后立即手動(dòng) kill app。如果我們正確記錄了上面三個(gè)時(shí)間戳,那么我們可以得到一個(gè)與 App crash 行為相關(guān)的時(shí)間線。比如:
launchTs => crashTs => launchTs => terminateTs
或者
launchTs => launchTs => launchTs
或者
launchTs => crashTs => launchTs => crashTs => launchTs
請(qǐng)自行腦洞上面三種時(shí)間線的行為特征。很明顯,第三種時(shí)間線看上去是連續(xù) crash 了兩次。我們只需要加上時(shí)間間隔判斷,就能得知是否為連續(xù)兩次閃退了。注意,如果兩個(gè) crashTs 之間如果存在 terminateTs,則不能被認(rèn)為是連續(xù)閃退。檢測(cè)代碼比較簡(jiǎn)單,我就不貼了。
這個(gè)時(shí)間線只是記錄與 crash 相關(guān)的 App 啟動(dòng)和退出行為,還有很多特殊的時(shí)間點(diǎn)沒(méi)有記錄,比如 App 在 前臺(tái)發(fā)生 out of memory(FOOM),App 在前臺(tái) main thread 卡住被系統(tǒng) Watch Dog 殺掉,iOS 系統(tǒng)升級(jí)時(shí) App 被強(qiáng)殺,App 從 AppStore 升級(jí)時(shí)被強(qiáng)殺等等,這些特殊的時(shí)間點(diǎn)都沒(méi)有記錄,不過(guò)這些并不影響我們的 App 連續(xù)閃退檢測(cè),所以可以忽略。
這里指的注意的是,因?yàn)閱?dòng)時(shí)要從 disk 讀取時(shí)間線記錄,涉及磁盤(pán)讀寫(xiě),會(huì)對(duì) App 的啟動(dòng)時(shí)間產(chǎn)生影響,一個(gè)優(yōu)化點(diǎn)是,在每次寫(xiě)入時(shí)間點(diǎn)移除掉較老的 timestamp,比如只記錄最近 5 個(gè)時(shí)間戳?;蛘咴跊](méi)有讀取到 crash 日志時(shí),甚至不用啟動(dòng)連續(xù)閃退檢測(cè)的整個(gè)流程。
接下來(lái),我們看假設(shè)檢測(cè)到連續(xù)閃退,我們?nèi)绾卫^續(xù)上傳日志。
同步等待 Crash 日志上傳
最直白的方式,在 App 的代碼繼續(xù)執(zhí)行之前,先等待日志上傳成功。
把網(wǎng)絡(luò)請(qǐng)求改成同步的?這會(huì)卡住 UI 線程,網(wǎng)絡(luò)差的場(chǎng)景下會(huì)被系統(tǒng) watch dog 強(qiáng)殺,顯然不可取。
我們可以依舊保持異步網(wǎng)絡(luò)請(qǐng)求,但是,暫時(shí)中斷 UI 線程的流程,讓整個(gè) App 處于 UI 線程的 runloop 等待中,一旦網(wǎng)絡(luò)請(qǐng)求成功,則跳回到 UI 線程的原有代碼流程。
看著簡(jiǎn)單的實(shí)現(xiàn),有幾個(gè)細(xì)節(jié)需要注意。首先我們需要增加一個(gè) App 交互,一旦進(jìn)入 runloop 等待,展示一個(gè) loading 界面,告知用戶耐心等待。其次,這個(gè)等待時(shí)間不能過(guò)長(zhǎng),我個(gè)人建議不超過(guò) 5s,一旦超過(guò) 5s,無(wú)論 crash 日志上傳的 request 是否成功,都恢復(fù) App 原有代碼流程。5s 內(nèi)日志都無(wú)法上傳成功的情況應(yīng)該比較小,除非日志文件過(guò)大。
這種做法缺陷也很明顯,一是改動(dòng)比較大(修改了原有代碼流程),二是需要增加新的 UI 交互,三是延長(zhǎng)了用戶的等待時(shí)間。
我們來(lái)看另一種取巧的做法。
啟用后臺(tái)進(jìn)程上傳 Crash 日志
其實(shí)最理想的日志上傳,是將上傳的 request 放到另一個(gè)不同的進(jìn)程,那么即使 App 又發(fā)生閃退,也不會(huì)影響到另一個(gè)進(jìn)程代碼的執(zhí)行。
問(wèn)題是,iOS app 都處于 sandbox 環(huán)境下,系統(tǒng)不允許代碼 fork 一個(gè)新進(jìn)程。
幸運(yùn)的是,從 iOS 8 開(kāi)始,系統(tǒng)對(duì) NSURLSession 新增了一個(gè) background session 特性。這個(gè)特性允許 NSURLSession 將網(wǎng)絡(luò)請(qǐng)求放入到一個(gè)單獨(dú)的進(jìn)程中執(zhí)行。我個(gè)人感覺(jué),這個(gè)特性設(shè)計(jì),原本是為了增強(qiáng)某些 App 后臺(tái)下載音視頻等資源的體驗(yàn)。我實(shí)際測(cè)試下來(lái),發(fā)現(xiàn)不管下載或者是上傳,我們都可以將網(wǎng)絡(luò)請(qǐng)求放入另一個(gè)進(jìn)程。代碼也很簡(jiǎn)單,比如我寫(xiě)一段如下的測(cè)試代碼:
NSURLSessionConfiguration *config = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"com.mrpeak.background.crashupload"]; NSURLSession *session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:[NSOperationQueue new]]; NSURL *url = [NSURL URLWithString:@"https://images.unsplash.com/photo-1515816949419-7caf0a210607?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=f46b60857b4826e733da34993ec26a2f&auto=format&fit=crop&w=1534&q=80"]; NSURLSessionDownloadTask *task = [session downloadTaskWithURL:url]; [task resume]; exit(0);
執(zhí)行之后,我們可以在 console 中看到如下日志:
可以清楚的看到 nsurlsessiond 進(jìn)程如何替我們完成網(wǎng)絡(luò)請(qǐng)求,并試圖喚醒已經(jīng)異常退出的 App。
當(dāng)然這種最理想的方式,也有一些細(xì)節(jié)需要處理。比如如何告知 App 某個(gè) crash 日志上傳成功,并從本地移除。由于連續(xù)閃退的 App 處于極度不穩(wěn)定的狀態(tài),所以任何代碼邏輯都無(wú)法確保順利完成。
我個(gè)人感覺(jué)一種比較理想的方式是,給后臺(tái)進(jìn)程上報(bào)的日志加上某個(gè)特殊的 flag,然后在后臺(tái)通過(guò) client request ID 和這個(gè) flag 來(lái)做去重和整理。
線上 App 連續(xù)閃退是一種極其惡劣和可怕的故障,可怕之處在于,發(fā)生大面積連續(xù)閃退且無(wú)法被監(jiān)控時(shí),你正哼著小曲敲著代碼,老板突然發(fā)現(xiàn)自己手機(jī)上 App 啟動(dòng)不了了,一打開(kāi) AppStore,發(fā)現(xiàn)一星差評(píng)潮水般涌來(lái),如果是主流 App 甚至還會(huì)上科技新聞,不難預(yù)料一口黑漆漆的大鍋正在成形。下次 App 的升級(jí)介紹里一定會(huì)出現(xiàn) “fire peter” 了。
總結(jié)
以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,如果有疑問(wèn)大家可以留言交流,謝謝大家對(duì)腳本之家的支持。
- 查看iOS Crash logs的方法
- iOS10適配之權(quán)限Crash問(wèn)題的完美解決方案
- iOS Crash常規(guī)跟蹤方法及Bugly集成運(yùn)用詳細(xì)介紹
- iOS開(kāi)發(fā)之WKWebViewJavascriptBridge Xcode9中導(dǎo)致crash的解決
- 查看iOS已上架App的Crash信息定位、應(yīng)對(duì)處理方式的實(shí)例
- iOS Crash文件分析方法匯總
- iOS開(kāi)發(fā)筆記之鍵盤(pán)、靜態(tài)庫(kù)、動(dòng)畫(huà)和Crash定位
- iOS監(jiān)控筆記之啟動(dòng)crash
- iOS中程序異常Crash友好化處理詳解
相關(guān)文章
iOS應(yīng)用開(kāi)發(fā)中UITableView的分割線的一些設(shè)置技巧
這篇文章主要介紹了iOS應(yīng)用開(kāi)發(fā)中UITableView分割線的一些設(shè)置技巧,包括消除分割線的方法,示例代碼為傳統(tǒng)的Objective-C語(yǔ)言,需要的朋友可以參考下2016-03-03IOS 開(kāi)發(fā)之 NSMutableArray與NSArray 的區(qū)別
這篇文章主要介紹了IOS 開(kāi)發(fā)之 NSMutableArray與NSArray 的區(qū)別的相關(guān)資料,希望通過(guò)本文能掌握這部分內(nèi)容,需要的朋友可以參考下2017-09-09iOS?項(xiàng)目嵌入Flutter?運(yùn)行(最新推薦)
這篇文章主要介紹了iOS?項(xiàng)目嵌入Flutter?運(yùn)行,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-03-03IOS內(nèi)存泄漏檢查方法及重寫(xiě)MLeakFinder
這篇文章主要介紹了IOS內(nèi)存泄漏檢查方法及如何重寫(xiě)MLeakFinder,幫助ios開(kāi)發(fā)者維護(hù)自身程序,感興趣的朋友可以了解下2021-04-04iOS利用AVPlayer播放網(wǎng)絡(luò)音樂(lè)的方法教程
最近工作中遇到了一個(gè)需求,需要做一個(gè)在線音樂(lè)類的APP,通過(guò)一段時(shí)間的努力實(shí)現(xiàn)了,所以這篇文章主要給大家介紹了關(guān)于iOS利用AVPlayer播放網(wǎng)絡(luò)音樂(lè)的方法教程,文中介紹的非常詳細(xì),需要的朋友可以參考借鑒,下面來(lái)一起看看吧。2017-05-05iOS內(nèi)存管理中引用計(jì)數(shù)的學(xué)習(xí)
文章給大家分享了關(guān)于iOS內(nèi)存管理中引用計(jì)數(shù)的相關(guān)知識(shí)點(diǎn),對(duì)此有需要的朋友可以跟著學(xué)習(xí)下。2018-05-05iOS開(kāi)發(fā)之?dāng)?shù)字每隔3位用逗號(hào)分隔
以前在做電商app時(shí)經(jīng)常會(huì)針對(duì)稍大的金額展示出來(lái),需要每隔千位添加逗號(hào)便于用戶識(shí)別,下面通過(guò)本文給大家分享ios中數(shù)字每隔3位用逗號(hào)分隔的實(shí)例代碼,需要的朋友參考下吧2017-09-09iOS通過(guò)block在兩個(gè)頁(yè)面間傳值的方法
不知道大家有沒(méi)有發(fā)現(xiàn),在實(shí)際開(kāi)發(fā)中使用block的地方特別多,block比delegate和notification有著更簡(jiǎn)潔的優(yōu)勢(shì),下面這篇文章我們來(lái)簡(jiǎn)單了解一下block在兩個(gè)頁(yè)面之間的傳值。有需要的朋友們可以參考借鑒,下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧。2016-11-11