實(shí)戰(zhàn) iOS 連續(xù)崩潰檢測與自修復(fù)
背景 在近期 iOS 上線的版本,友盟在它的升級(jí)版本中默認(rèn)就自動(dòng)進(jìn)行用戶的崩潰收集上報(bào)匆瓜。 如果只是惡心一下開發(fā)也就算了锐锣,然而在發(fā)版本時(shí),卻發(fā)現(xiàn)友盟的代碼沒有對(duì)上報(bào)的數(shù)據(jù)著洼,做類型的安全判斷樟遣,導(dǎo)致讀取數(shù)據(jù)每次都會(huì)crash. 搞崩潰檢測的,自己卻導(dǎo)致 App 不斷崩潰 : ) 堆棧如下:
從方法名稱 [UMCrash initUMCrash:channel:] 和 [WPKSetup sendAllReports] 可以很容易看出來, 這是友盟在做初始化的時(shí)候身笤,對(duì)崩潰信息進(jìn)行了一次檢測豹悬,然后做上報(bào),在處理數(shù)據(jù)的時(shí)候掛掉了液荸。 看不到具體代碼瞻佛,控制臺(tái)看到直接原因是: [NSTaggedPointerString objectForKey:]: unrecognized selector sent to instance 由于友盟的初始化發(fā)生在很早的時(shí)候(我們一般都放在啟動(dòng)階段)。這就導(dǎo)致大多服務(wù)還沒起來娇钱,應(yīng)用就已經(jīng)崩潰了伤柄。只要出現(xiàn)了這種情況,每次打開App, 都會(huì)因?yàn)橐粯拥膯栴}忍弛,而連續(xù)閃退响迂。 連續(xù)崩潰的后果 那么像這樣的連續(xù)崩潰,會(huì)造成什么后果呢细疚? 可以總結(jié)為以下的 3 點(diǎn):
開發(fā)無感知.由于在啟動(dòng)階段就直接崩潰掉蔗彤,崩潰收集平臺(tái) bugly / 友盟 都沒有相關(guān)信息。更無從談修復(fù)疯兼。
用戶無法反饋.因?yàn)槊看芜M(jìn)來都崩潰然遏,也無法反饋相關(guān)信息給到客服。
新用戶 0 體驗(yàn) .當(dāng) App 連續(xù) 3 次都進(jìn)入就閃退吧彪,我自己來說待侵,是肯定不再使用這一款無法體驗(yàn)的 App。
解決方案 面對(duì)上述的情況姨裸,因?yàn)檎莆湛蓮?fù)現(xiàn)的場景秧倾,通過技術(shù)手段,我們的確可以 hook 掉它的上傳方法傀缩,從而解決崩潰問題那先。 例如這次,對(duì)我們發(fā)生的路徑中的 [WPKSetup sendAllReports] 進(jìn)行截獲,不再執(zhí)行赡艰。那么它當(dāng)然不再崩潰售淡。 但是上面解決方法有 3 點(diǎn)問題:
成本取舍問題,原有的功能因?yàn)橐淮闻棘F(xiàn)崩潰場景而直接禁用。
SDK 功能缺失揖闸,甚至也可能引入新的問題揍堕。
只解決了當(dāng)下的場景,缺乏必要手段解決其它的連續(xù)崩潰問題汤纸。也就是說衩茸,保得了一時(shí),保不了一世蹲嚣。
進(jìn)行連續(xù)崩潰檢測 在前面有提到過递瑰,連續(xù)崩潰的一大問題是--開發(fā)無感知。 也就是說隙畜,我們連問題發(fā)生了都不知道抖部,所以首要做到的是發(fā)現(xiàn)問題。 通常最先想到的思路议惰,就是和崩潰上報(bào)框架一樣慎颗,通過捕獲異常,來觀察它的每次崩潰言询。 捕獲異常的操作俯萎,也存在兩個(gè)缺點(diǎn):
會(huì)與已有處理異常的代碼重復(fù),耦合
與第三方 crash 收集框架的沖突运杭,導(dǎo)致漏檢測
對(duì)于第二點(diǎn)與第三方崩潰收集框架的沖突夫啊,是影響最大的地方,因?yàn)樗麄兊拇a通常對(duì)我們來說都是看不到的辆憔。
持久化一個(gè) crashCount 變量
每次啟動(dòng) crashCount = crashCount +1
在 x 秒后撇眯,crashCount = 0
通過 crashCount 來代表崩潰次數(shù),每次啟動(dòng)的時(shí)候讓它加 1,如果 App 存活過一段時(shí)間,那么證明沒有發(fā)生連續(xù)崩潰,將它置空復(fù)位部默。一旦超過我們?cè)O(shè)定的次數(shù)閾值,證明連續(xù)這幾次時(shí)間閾值內(nèi)砂碉,都沒存活過去,發(fā)生了異常崩潰。 當(dāng)然也存在誤報(bào)的情況,比如用戶在這段時(shí)間閾值內(nèi)煎楣,主動(dòng)殺掉 App。這一點(diǎn)通過調(diào)整次數(shù)和時(shí)間兩方面的閾值车伞,可以控制择懂。 控制誤報(bào) 我們可以在原來的方案中,更進(jìn)一步控制誤報(bào)帖世,想辦法監(jiān)聽用戶主動(dòng)殺App的場景:
用戶在前臺(tái)殺APP
用戶在后臺(tái)殺APP
對(duì)于誤報(bào)的情況,大多數(shù)都是第一種,在幾秒之內(nèi)日矫,啟動(dòng)時(shí)前臺(tái)殺APP赂弓,iOS 中通過 UIApplicationWillTerminateNotification 來監(jiān)聽,收到通知后哪轿,將次數(shù)置空清零盈魁。 自動(dòng)修復(fù)連續(xù)崩潰 要對(duì)于崩潰進(jìn)行修復(fù),首先需要知道這類問題的常見原因窃诉。 對(duì)于代碼 bug 的問題杨耙,如果固定進(jìn)入就必現(xiàn)崩潰的話,在測試流程就一般還是會(huì)暴露出來飘痛。當(dāng)然并不完全排除代碼崩潰的情況珊膜。 清除數(shù)據(jù) 造成線上問題連續(xù)崩潰的,肯定是一個(gè)“變量”宣脉,那么應(yīng)該是:
數(shù)據(jù)庫
存儲(chǔ)文件
服務(wù)端數(shù)據(jù)
對(duì)于 數(shù)據(jù)庫 和 存儲(chǔ)文件 的修復(fù)车柠,我們都做一個(gè)清理操作,以本地?cái)?shù)據(jù)的清理塑猖,來保證 App 的正常流程竹祷。 對(duì)于重要的數(shù)據(jù)定義,可以先傳入云端存起來羊苟。 這次我們出現(xiàn)的友盟崩潰塑陵,也正是因?yàn)樽x取了存在本地的問題數(shù)據(jù)而導(dǎo)致連續(xù)閃退的。 重新請(qǐng)求/運(yùn)行熱修復(fù)包 而對(duì)于服務(wù)端數(shù)據(jù)處理的失敗蜡励,通過與服務(wù)端排查令花,返回正常的數(shù)據(jù)進(jìn)行解決。也可以提供入口讓用戶上報(bào)或者直接與我們聯(lián)系巍虫。 甚至考慮引入動(dòng)態(tài)修復(fù)手段彭则,解決代碼 bug ,請(qǐng)求以及運(yùn)行熱修復(fù)包占遥。 具體處理 按照 微信讀書團(tuán)隊(duì)的處理俯抖,是在 didFinishLaunching 的階段做 hook。當(dāng)觸發(fā)崩潰限制數(shù)量后瓦胎,進(jìn)入修復(fù)芬萍,修復(fù)完成后再調(diào)用原方法 didFinishLaunching ,來按照原來的流程進(jìn)入到 App. 結(jié)合我們的工程實(shí)際情況搔啊,自動(dòng)修復(fù)流程與有細(xì)節(jié)差異:
Appdelegate 的 initialize 就開始有日志初始化柬祠。
在 willFinishLaunching 的階段,有一些數(shù)據(jù)和服務(wù)的初始化负芋。
applicationDidBecomeActive 也有邏輯漫蛔。
進(jìn)入首頁需要拉取的請(qǐng)求和模塊邏輯復(fù)雜。 需要解決這些問題,不止是對(duì) didFinishLaunching 的階段做 hook莽龟,還要分別對(duì)上述的情況進(jìn)行處理蠕嫁,在崩潰數(shù)超過限制后進(jìn)行攔截。 而中間整合毯盈,也發(fā)生了如服務(wù)找不到崩潰的問題等剃毒,需要一點(diǎn)點(diǎn)解決和整合。 目前臨時(shí)增加一個(gè)方法:
+ (BOOL)needFixCrashes{ NSInteger launchCrashes = [self crashCount]; if (launchCrashes >= kContinuousCrashOnLaunchNeedToReport) { return YES; } return NO;}
通過 needFixCrashes 來在各處做控制:
if(needFixCrashes){ return;}//原有正常邏輯處理
在修復(fù)后搂赋,我們?cè)俜謩e調(diào)用對(duì)應(yīng)流程赘阀。相當(dāng)于 [A do],[B do]..一個(gè)個(gè)去調(diào)用。 更好的思路
ps;作為iOS開發(fā)人員必看ios資料大全脑奠,其中你有想象不到的面試題和學(xué)習(xí)資料等等
其實(shí)對(duì)于上述流程基公,還有一個(gè)更好的做法,限于業(yè)務(wù)時(shí)間沒做捺信。
我們可以將流程中要 hook 的對(duì)象和方法酌媒,都想辦法存儲(chǔ)起來,如使用 NSMapTable 等迄靠。
在結(jié)束修復(fù)后秒咨,再按順序遍歷出來對(duì)象和方法一個(gè)個(gè)調(diào)用,走完一套啟動(dòng)的流程掌挚。
這里的好處在于復(fù)用雨席,可以直接 addObject:Selector: 的方式就增加進(jìn)去,之后修復(fù)完成吠式,不需要再寫 hard code 一個(gè)個(gè)調(diào)用陡厘。 3.3 最終流程
最終檢測流程為:
啟動(dòng)App, crash=crash+1
檢查 crash<maxCrash
crash < maxCrash,則進(jìn)入正常啟動(dòng)流程, 一段時(shí)間后就置空
crash >= maxCrash, 進(jìn)入修復(fù)引導(dǎo)
修復(fù)的流程設(shè)計(jì)為:
設(shè)置根控制器為新的控制器,并彈出修復(fù)框特占,提示“檢測到應(yīng)用可能已損壞糙置,是否嘗試修復(fù)?”
用戶選擇"取消",則上報(bào)信息到平臺(tái)是目,然后 App 退出到后臺(tái)
用戶選擇"修復(fù)",則進(jìn)行我們的數(shù)據(jù)清理操作(重要數(shù)據(jù)考慮先云備份)谤饭,然后上報(bào)信息。
修復(fù)完成后懊纳,直接重新初始化全部服務(wù)揉抵,進(jìn)入首頁。
最壞的情況嗤疯,數(shù)據(jù)清理也仍然無濟(jì)于事冤今,記錄下一段時(shí)間內(nèi)的“修復(fù)”次數(shù)。提供方式直接聯(lián)系到平臺(tái)茂缚,在條件的情況下解決閃退戏罢。
實(shí)際操作當(dāng)中屋谭,有不少業(yè)務(wù)待我們梳理,光做到在所有服務(wù)之前檢測龟糕,如果之前沒有專門的類收攏處理戴而,就要花時(shí)間來做,或者在各處進(jìn)行判斷翩蘸。總的來說淮逊,最主要的思路是:
崩潰檢測要在整個(gè) App 里催首,做到最先啟動(dòng)。代碼足夠干凈和簡單泄鹏。
修復(fù)時(shí)的數(shù)據(jù)要分類郎任,哪些重要的要備份,哪些直接刪除备籽。
修復(fù)后進(jìn)入 App,路徑要足夠完整舶治,做到順暢進(jìn)入。
4. 總結(jié)
連續(xù)崩潰問題的發(fā)生车猬,可以說是一個(gè) App 最嚴(yán)重的問題霉猛,一般來說并不會(huì)出現(xiàn)。
而隨著用戶的增多珠闰,任何問題也有可能被無限放大惜浅。
所以作為技術(shù)人員,需要做好兜底的策略伏嗜,盡量來消除此類問題坛悉,保證好用戶體驗(yàn),通過技術(shù)保護(hù)手段多留住一個(gè)用戶就是一個(gè)用戶承绸。
何況事實(shí)證明裸影,未雨綢繆是應(yīng)該的,這次連續(xù)崩潰問題不就發(fā)生在了自己身上了嗎军熏?