最近在掘金翻譯計(jì)劃和小伙伴一起翻譯的一篇的iOS相關(guān)文章撬即,文中能夠了解到一些iOS的文件讀取機(jī)制和我們線上閃退問題如何發(fā)現(xiàn),以及一些處理方案呈队。其實(shí)還是有很多iOS開發(fā)的小技巧的剥槐。
原文地址:How Not to Crash
原文作者:Padraig
譯文出自:掘金翻譯計(jì)劃
譯者:Gocy
校對(duì)者:lovelyCiTY,DeadLion
應(yīng)用崩潰時(shí)有發(fā)生。崩潰會(huì)打斷用戶當(dāng)前的工作流掂咒,導(dǎo)致數(shù)據(jù)的丟失才沧,還會(huì)擾亂應(yīng)用在后臺(tái)的操作。對(duì)于開發(fā)者而言绍刮,那些最難修復(fù)的崩潰往往是那些難以重現(xiàn)温圆,甚至難以檢測(cè)到的崩潰。
我最近發(fā)現(xiàn)并修復(fù)了一個(gè) bug 孩革,而它正是導(dǎo)致 Castro 反復(fù)出現(xiàn)難以檢測(cè)的崩潰的罪魁禍?zhǔn)祝ㄗg者注: Castro 是原文作者開發(fā)的一款應(yīng)用)岁歉,我將處理這個(gè)問題的過程分享給大家并附上一些我的建議,或許能幫助你定位類似的問題。
我和 Oisin 在九月份發(fā)布了 Castro 2.1 版本锅移,那之后不久熔掺,從 iTunes Connect 上報(bào)的 Castro 崩潰數(shù)量便急劇上升。
有趣的是非剃,這些崩潰并沒有出現(xiàn)在我們平時(shí)使用的崩潰上報(bào)服務(wù) HockeyApp 中置逻,因此我們實(shí)際上在晚些時(shí)候才發(fā)現(xiàn)我們的應(yīng)用出現(xiàn)了問題。想要查看到應(yīng)用的所有崩潰备绽,開發(fā)者需要從 iTunes Connect 或是 Xcode 中查看崩潰上報(bào)券坞。(更新: Greg Parker指出“第三方崩潰上報(bào)系統(tǒng)在對(duì)應(yīng)的應(yīng)用進(jìn)程中建立 handler 來記錄應(yīng)用行為,但如果操作系統(tǒng)從外部終止進(jìn)程肺素,這個(gè) handler 就永遠(yuǎn)無法執(zhí)行了恨锚。”)倍靡,另外猴伶, HockeyApp 的聯(lián)合創(chuàng)始人 Andreas Linde引用了一篇文章來界定那些Hockey 能以及不能檢測(cè)到的崩潰。)
如果你是一名應(yīng)用開發(fā)者并且登陸了開發(fā)者賬號(hào)塌西, Xcode 允許你檢視 Apple 官方從你的當(dāng)前帳號(hào)下的 app 用戶那收集到的崩潰日志他挎。這項(xiàng)功能在 Window 導(dǎo)航欄下的 Organizer 窗口中的 Crashes 標(biāo)簽中。你可以選擇特定的應(yīng)用版本捡需, Xcode 會(huì)下載 Apple 從用戶手上收集到的崩潰日志雇盖,前提是用戶同意將信息分享給開發(fā)者。
我發(fā)現(xiàn) Xcode 的這個(gè)功能也非常容易崩潰栖忠,尤其是當(dāng)點(diǎn)擊崩潰日志中線程的詳情按鈕進(jìn)行切換的時(shí)候。一個(gè)簡(jiǎn)便的解決方案是贸街,在列表中右鍵選中相應(yīng)的崩潰庵寞,并選擇在 Finder 中顯示。如果你要研究研究包中的內(nèi)容薛匪,你可以把這些崩潰日志簡(jiǎn)單地當(dāng)作文本文件捐川。
許多不同的代碼路徑都觸發(fā)了這個(gè)崩潰,但崩潰最終都指向一個(gè)數(shù)據(jù)庫查詢方法逸尖。
一開始我認(rèn)為是多線程引發(fā)的問題古沥,畢竟在被線程問題折磨了多年之后,我總是第一時(shí)間想到它娇跟。我以文本文件的格式打開崩潰日志岩齿,因?yàn)檫@樣比直接用 Xcode 打開展示了更多的細(xì)節(jié)。崩潰的異常類型是EXC_CRASH (SIGKILL)苞俘,對(duì)應(yīng)的信息是EXC_CORPSE_NOTIFY盹沈,程序被終止的原因是Code 0xdead10cc。于是我試著找出0xdead10cc是什么含義吃谣。 Google 或是 Apple Developer 論壇都沒有多少相關(guān)的信息乞封,但Technical Note 2151中提到:
異常碼 0xdead10cc 出現(xiàn)意味著應(yīng)用程序因?yàn)樵诤笈_(tái)操作系統(tǒng)資源(譬如通訊錄數(shù)據(jù)庫)而被 iOS 系統(tǒng)終止做裙。
這時(shí)候我意識(shí)到 iOS 強(qiáng)制關(guān)閉我的應(yīng)用是因?yàn)槲疫`反了系統(tǒng)規(guī)則,而不是說我的代碼出了什么小問題肃晚。但是锚贱, Castro 并沒有用到通訊錄數(shù)據(jù)庫或是任何我能想到的類似的系統(tǒng)資源。我還懷疑原因是不是應(yīng)用在后臺(tái)長(zhǎng)時(shí)間運(yùn)行而沒有取消关串,但我也發(fā)現(xiàn)日志中有一些應(yīng)用僅僅運(yùn)行了兩秒鐘就發(fā)生崩潰的記錄拧廊。
經(jīng)過推理,我最終將可能原因定位到我們的數(shù)據(jù)庫相關(guān)的 SQLite 文件上悍缠,因?yàn)榻^大部分的堆棧信息都顯示崩潰是在操作數(shù)據(jù)庫的時(shí)候發(fā)生的卦绣。但 2.1 版本上的哪個(gè)改動(dòng),突然就引起了這個(gè)崩潰呢飞蚓?
Castro 2.1 版本引入了對(duì) iMessage 的支持來讓用戶輕松地分享他們最近聽過的播客滤港。為了讓 message app 能夠訪問數(shù)據(jù)庫,我們將數(shù)據(jù)庫邏輯移動(dòng)到了應(yīng)用共享容器中趴拧。
我猜想文件的鎖機(jī)制對(duì)在共享區(qū)域的文件有更嚴(yán)格的要求溅漾。或許當(dāng) iOS 準(zhǔn)備掛起一個(gè)應(yīng)用的時(shí)候著榴,系統(tǒng)會(huì)檢查這個(gè)應(yīng)用是否正在使用一些可能被其他進(jìn)程使用的文件添履,如果有, iOS 就會(huì)直接終止這個(gè)應(yīng)用脑又。這看起來是個(gè)有理有據(jù)的解釋暮胧。
如何重現(xiàn)正在修復(fù)的崩潰是鍛煉開發(fā)者的絕佳實(shí)踐。這可能涉及到臨時(shí)改寫一部分代碼來刻意提高崩潰出現(xiàn)的可能性问麸。如果我們能穩(wěn)定地看到崩潰的發(fā)生往衷,就能夠逐步的驗(yàn)證我們的猜測(cè),同時(shí)我們測(cè)試修復(fù)的正確性就有了參考严卖。而與之對(duì)應(yīng)的另一個(gè)方法是盲目地進(jìn)行修復(fù)席舍,發(fā)布版本,然后等著看是否會(huì)有崩潰上報(bào)哮笆。有時(shí)候来颤,只有盲目修復(fù)一條路可走,但這條路枯燥乏味稠肘,而且到頭來應(yīng)用依然不斷地在用戶側(cè)發(fā)生崩潰福铅。
而這個(gè)崩潰就非常不容易重現(xiàn),我覺得這里批評(píng)一下 iOS 的開發(fā)環(huán)境并不過分项阴。操作系統(tǒng)粗野地執(zhí)行著自己的規(guī)則本讥,大部分時(shí)候,這樣做很好,因?yàn)檫@樣可以提高安全性拷沸,延長(zhǎng)電池壽命和穩(wěn)定性色查。但在這樣的大環(huán)境下進(jìn)行應(yīng)用的測(cè)試和修復(fù),就增加了不必要的麻煩撞芍。這些規(guī)則的變化悄無聲息秧了,而要人為地在應(yīng)用周期可能出現(xiàn)的每一個(gè)狀態(tài)下進(jìn)行測(cè)試非常不方便,有時(shí)候甚至根本無法完成序无。
在本例中验毡,我意識(shí)到在 debugger 模式下進(jìn)行測(cè)試無法觸發(fā)程序后臺(tái)掛起的狀態(tài)。實(shí)際上帝嗡,debugger會(huì)阻止掛起晶通,而且模擬器也不會(huì)精準(zhǔn)的模擬掛起。如果不在 debugger 模式下的話哟玷,那么就只剩下反復(fù)測(cè)試然后查看設(shè)備日志這一個(gè)選擇了狮辽。
macOS Sierra 上的全新 Console App 提供了訪問任何當(dāng)前連接中的 iPhone 的系統(tǒng)日志的功能,而在 Sierra 之前巢寡,我都是靠 Lemon Jar 的iOS Console來完成這個(gè)操作喉脖,但是,看到 Apple 官方提供能夠訪問日志的工具抑月,了解這樣的技術(shù)是被官方所接受并支持的树叽,感覺也是極好的。你值得花時(shí)間去學(xué)習(xí)如何使用全新的 Console App 谦絮,它呈現(xiàn)出許多 Xcode 調(diào)試器無法呈現(xiàn)的操作题诵。由于這份日志是整個(gè)系統(tǒng)所有日志的統(tǒng)一輸出,所以會(huì)有許多不相關(guān)的冗余信息层皱,但你可以輕松地創(chuàng)建一個(gè)過濾器仇轻,將顯示的內(nèi)容限定在與你的應(yīng)用相關(guān)的范圍內(nèi)。
為了刻意重現(xiàn)崩潰dead10cc:
我在applicationDidEnterBackground方法中做了幾百次數(shù)據(jù)庫查詢操作奶甘。
在我的 Mac 上打開 Console 應(yīng)用,并過濾信息祭椰,僅顯示 Castro 相關(guān)臭家。
我從 Xcode 上運(yùn)行安裝應(yīng)用,但以直接點(diǎn)擊應(yīng)用圖標(biāo)的形式打開應(yīng)用方淤。
我按 Home 鍵將應(yīng)用退到后臺(tái)钉赁,并立刻打開 Pokémon Go ,以期系統(tǒng)會(huì)由于內(nèi)存吃緊而掛起 Castro 携茂。
在重復(fù)了幾次上述步驟之后你踩,我發(fā)現(xiàn) Console 中已經(jīng)出現(xiàn)了我嘗試重現(xiàn)的崩潰信息。調(diào)用堆棧看起來和真實(shí)場(chǎng)景的崩潰一模一樣带膜,現(xiàn)在我就非常自信地知道崩潰的原因何在了吩谦。
接著我發(fā)現(xiàn)并修復(fù)了項(xiàng)目中一個(gè)在后臺(tái)訪問數(shù)據(jù)庫觸發(fā)的錯(cuò)誤:在網(wǎng)絡(luò)狀況變化時(shí),應(yīng)用會(huì)在沒有創(chuàng)建 background task 的情況下進(jìn)行數(shù)據(jù)庫刷新操作膝藕。如果在刷新操作尚未完成時(shí)應(yīng)用進(jìn)入掛起狀態(tài)式廷, iOS 就會(huì)強(qiáng)制終止應(yīng)用運(yùn)行。
理解后臺(tái)獲劝磐臁( Background Fetch Gotcha )
我還要再分享一件讓我驚訝的事情滑废。在 Castro 2 版本,我們?cè)谟行聞〖l(fā)布后通知客戶端袜爪,從而客戶端會(huì)刷新用戶的推送內(nèi)容蠕趁。當(dāng) iOS 將這條消息轉(zhuǎn)發(fā)給我們的應(yīng)用的時(shí)候,它會(huì)調(diào)用didReceiveRemoteNotification方法辛馆,而在這個(gè)方法中俺陋,我們有一個(gè) completion block 的回調(diào)。官方文檔中提到:
你的應(yīng)用至多只有三十秒時(shí)間來處理推送消息怀各,而后調(diào)用相應(yīng)的 completion handler block 倔韭。實(shí)際開發(fā)中,一旦你處理完推送瓢对,就應(yīng)該盡快地調(diào)用這個(gè) handler block 寿酌。系統(tǒng)會(huì)記錄下應(yīng)用在后臺(tái)所耗費(fèi)的時(shí)間、電量硕蛹、以及數(shù)據(jù)處理所消耗的流量醇疼。
令人抓狂的點(diǎn)在于:就像我在前文中提到的, Castro 有時(shí)候運(yùn)行不到兩秒就被終止了法焰,我從調(diào)用棧信息明確看到這時(shí)候還沒有調(diào)用 completion block 秧荆,所以說,盡管文檔寫著說應(yīng)用可以安安心心的運(yùn)行個(gè) 30 秒埃仪,但我的應(yīng)用還是被掛起了乙濒。
這實(shí)在是出乎意料,于是我決定使用一次開發(fā)者 Technical Support Incidents 來看看到底發(fā)生了什么事(譯者注:Technical Support Incidents是蘋果提供的一項(xiàng)技術(shù)支持服務(wù) )卵蛉。我從負(fù)責(zé)我的請(qǐng)求的工程師 Kevin Elliott 那得到了一些非常有幫助的回應(yīng):
正如我所懷疑的那樣颁股,dead10cc問題源于文件上鎖:
“真正觸發(fā)崩潰的原因是, iOS 在掛起你的應(yīng)用的時(shí)候傻丝,檢查到在你的應(yīng)用容器中有一個(gè)被鎖住的文件(本例中就是一個(gè) SQLite 鎖)甘有。這個(gè)檢查的目的在于管理和減少應(yīng)用內(nèi)的數(shù)據(jù)損壞。本例的問題在于葡缰,一個(gè)文件處于被鎖狀態(tài)亏掀,意味著它很可能正在被修改忱反,處于一個(gè)數(shù)據(jù)不連貫的狀態(tài)。也就是說滤愕,一個(gè)應(yīng)用對(duì)一個(gè)文件加鎖的唯一理由就是它接下來要對(duì)這個(gè)文件進(jìn)行一系列的讀/寫操作温算,并且需要保證這些寫操作能夠順利完成而不被其他的寫操作插隊(duì)。簡(jiǎn)單的說就是该互,一個(gè)文件還處于被鎖狀態(tài)意味著對(duì)應(yīng)的應(yīng)用還沒有完成數(shù)據(jù)的寫入米者,而處于這種狀態(tài)下的文件可能會(huì)有以下的幾個(gè)問題:
如果應(yīng)用在掛起狀態(tài)被強(qiáng)制終止,那些“應(yīng)該卻還未被寫入”的數(shù)據(jù)便不會(huì)被寫入宇智,導(dǎo)致數(shù)據(jù)損壞蔓搞。
如果這個(gè)文件在兩個(gè)應(yīng)用之間共享,此時(shí)第二個(gè)應(yīng)用/應(yīng)用擴(kuò)展開始運(yùn)行随橘,那這個(gè)應(yīng)用將要么被迫解除這個(gè)鎖喂分,并試圖將文件恢復(fù)到一個(gè)穩(wěn)定連續(xù)的狀態(tài),而讓第一個(gè)應(yīng)用繼續(xù)處在一個(gè)不連續(xù)的狀態(tài)机蔗,要么就完全忽略這個(gè)共享文件蒲祈。”
至于那 30 秒的后臺(tái)運(yùn)行時(shí)間:
...正確的做法應(yīng)該是徹底規(guī)避這個(gè)問題 - 如果你不能在 delegate 方法中完成所有的操作(譯者注:這里的 delegate 方法即指didReceiveRemoteNotification方法)萝嘁,那么就直接另起一個(gè) background task 梆掸,這樣 iOS 在(completion block 中)掛起你的應(yīng)用之前就會(huì)先通知你...
另外, Kevin 也建議應(yīng)用進(jìn)入后臺(tái)的時(shí)候應(yīng)該關(guān)閉數(shù)據(jù)庫牙言,以此來確保應(yīng)用已經(jīng)完成了數(shù)據(jù)刷新并能更準(zhǔn)確的找到少見的 bug :
將關(guān)閉文件作為一項(xiàng)常規(guī)操作酸钦,從而將一些隱蔽而奇怪的 bug (應(yīng)用在后臺(tái)有時(shí)不太對(duì)勁),轉(zhuǎn)化成穩(wěn)定出現(xiàn)的問題(應(yīng)用在后臺(tái)無法正常運(yùn)行)咱枉,這時(shí)候你就可以直接去定位問題了卑硫。
這看起來是個(gè)明智的做法;我從沒想過要在應(yīng)用進(jìn)入后臺(tái)的時(shí)候關(guān)閉一部分功能蚕断,但其實(shí)這么做非常合理欢伏。在 Castro 的下一個(gè)版本更新中,我將會(huì)嘗試在退后臺(tái)時(shí)關(guān)閉數(shù)據(jù)庫亿乳。
通過把任何會(huì)在后臺(tái)持續(xù)運(yùn)行的操作放到一系列 background task 中硝拧,我成功地在 beta 版本中解決了這一問題。我們會(huì)盡快發(fā)布包含這個(gè)修復(fù)的更新葛假。
以下是我所學(xué)到的東西的小小總結(jié):
Apple 官方會(huì)上報(bào)一些其他服務(wù)不會(huì)上報(bào)的崩潰障陶。所以除了外部服務(wù)之外,也要查看在 iTunes Connect 和 Xcode 上面的崩潰信息桐款。
文件的鎖機(jī)制對(duì)于在共享區(qū)域的文件有著更嚴(yán)格的要求。
依賴于 background fetch 的 completion block 是遠(yuǎn)遠(yuǎn)不夠的夷恍,不要在一個(gè)現(xiàn)行的 background task 之外做任何后臺(tái)操作魔眨。
想要調(diào)試那些僅僅在應(yīng)用生命周期的特定條件下出現(xiàn)的問題是非常困難的媳维。如果你還沒有嘗試過新的 Sierra Console.app ,現(xiàn)在就開始學(xué)習(xí)吧遏暴。
別忘了Technical Support Incidents侄刽,你每年的開發(fā)者賬號(hào)可都為這兩次機(jī)會(huì)買了單噢。(多謝啦 Kevin 大兄弟E罅埂)
如果你欣賞這篇文章州丹,或許你也會(huì)對(duì)Supertop podcast和我們的播客應(yīng)用Castro感興趣。
這篇文章的標(biāo)題引用了 Brent Simmons 的"How Not to Crash”系列杂彭,我強(qiáng)烈推薦還沒看過的讀者去看看這個(gè)系列墓毒。