引言
“如果某個實體表現(xiàn)出以下任何一種特性嘁锯,它就具備自主性:自我修復宪祥、自我保護、自我維護家乘、對目標的自我控制蝗羊、自我改進∪示猓” —— 凱文·凱利
iOS App 有時可能遇到啟動必 crash 的絕境:每次打開 App 都閃退耀找,無法正常使用App。
為了嘗試解決這個問題业崖,微信讀書開發(fā)了 iOS 連續(xù)閃退保護工具:GYBootingProtection野芒,檢測連續(xù)閃退,在連續(xù)閃退出現(xiàn)時双炕,嘗試自修復 App:
本文探討了連續(xù)閃退問題的產(chǎn)生原因狞悲、檢測、修復機制雄家,以及如何在你的項目中引入效诅、測試和使用?GYBootingProtection。
首先要檢測用戶 App 出現(xiàn)了連續(xù)閃退的情況趟济,有兩種檢測方法乱投,捕獲異常和計時器。
檢測連續(xù)閃退顷编,可以通過捕獲異常來實現(xiàn)戚炫,異常有以下種類:
Mach 異常:EXC_CRASH
UNIX 信號:SIGABRT
NSException 異常:應用層,通過 NSUncaughtExceptionHandler 捕獲
在念茜的漫談 iOS Crash 收集框架一文中詳細介紹了 Mach 異常和 Unix 信號捕獲 crash 的機制媳纬。簡單來說双肤,異常一般產(chǎn)生自 iOS 的微內核 Mach,然后在 BSD 層轉換成 UNIX SIGABRT 信號钮惠,以標準 POSIX 信號的形式提供給用戶茅糜。NSException 是使用者在處理 App 邏輯時,用編程的方法拋出素挽。
通過以下方法捕獲異常:
利用 Mach API 捕獲 Mach 異常
通過 POSIX API 注冊 signal(SIGSEGV,signalHandler) 來捕獲 UNIX 異常信號
注冊 NSUncaughtExceptionHandler 來捕獲應用級異常
Crash 上報工具如 PLCrashReporter 通過注冊 Mach 異常 + UNIX信號 的 handler 達到檢測的目的蔑赘,對用戶提供了處理異常的接口。
可以利用 PLCrashReporter 這類工具來檢測連續(xù)閃退:
首先維護一個計數(shù)變量,表示連續(xù)閃退次數(shù)
在 PLCrashReporter 的 crash handler 中加入邏輯:如果啟動 5s 內 crash 使計數(shù)器加一
每次啟動時缩赛,如果連續(xù)閃退計數(shù) > n耙箍,則檢測到了連續(xù)閃退
啟動后,執(zhí)行一個定時任務酥馍,在 5s 后重置計數(shù)(如果 App 連續(xù)閃退則不會重置)
通過 Mach 異常辩昆、Unix 信號、NSException 異常來檢測閃退旨袒,能獲得更多的 crash 上下文汁针,但由于 crash 收集框架多使用這些方法,可能會有這樣的風險:與第三方 crash 收集框架沖突導致漏檢測砚尽。另外扇丛,可能會與 App 已有的異常處理代碼產(chǎn)生耦合。
除了通過捕獲異常的方式檢測連續(xù)閃退尉辑,還可以通過計數(shù)器方法來檢測:
維護一個計數(shù)變量,用于表示連續(xù)閃退的次數(shù)
在啟動 application:didFinishLaunchingWithOptions: 后使計數(shù)加一
接著使用 dispatch_after 方法在 5s 后清零計數(shù)较屿,如果 App 活不過 5 秒計數(shù)就不會被清零
如果發(fā)現(xiàn)計數(shù)變量 > n隧魄,表明 App 連續(xù) n 次連續(xù)閃退,啟動保護流程隘蝎,重置計數(shù)购啄。
當保護流程完成后,進入 App 正常啟動流程
而計數(shù)器方法邏輯簡單嘱么,與原有的代碼耦合小狮含。雖然有誤報可能(在啟動后立即被 kill 掉,誤認為 crash)曼振,但是可以通過設置閾值來減小誤報的誤報率几迄。
綜上權衡,我們使用計時器方法檢測連續(xù)閃退冰评。
檢測到連續(xù)閃退后映胁,接下來要嘗試對閃退進行修復,這里先分析可能的閃退原因甲雅,再結合微信讀書的例子說明修復流程解孙。
連續(xù)閃退,可能是 App 啟動關鍵路徑中執(zhí)行了必 crash 的代碼抛人,原因可能有:
數(shù)據(jù)庫損壞:在日常使用如異常退出弛姜、斷電,或者錯誤的操作(參考:sqlite corruption causes)妖枚。
文件損壞:處理文件時如果沒有?@try...catch廷臼,損壞文件會拋出?NSException?導致 crash
網(wǎng)絡返回數(shù)據(jù)處理異常:比如預期返回數(shù)組,但實際返回了字典,對字典對象執(zhí)行?-objectAtIndex?方法會產(chǎn)生?crash: unknow selector send to object中剩;忌穿,或返回破損的 Tar 包,在解壓失敗導致 crash结啼。
代碼 bug:當必 crash 的代碼出現(xiàn)在啟動關鍵路徑中掠剑,就會導致連續(xù)閃退。
針對 1郊愧,可以通過工具修復數(shù)據(jù)庫朴译,或者刪除 DB。針對2属铁,可以刪除文件來進行修復眠寿。對于 3 和 4,我們需要具體地分析 crash 案例焦蘑,通過 JSPatch 來進行修復盯拱。
為了應對上述導致連續(xù)閃退的原因,微信讀書的修復流程為:
進入 didFinishLaunch 時檢查是否有連續(xù)閃退例嘱,無則執(zhí)行 5
彈 Toast 提示用戶是否修復狡逢,輕觸『修復』執(zhí)行2,否則執(zhí)行 5
嘗試下載并執(zhí)行 JSPatch 補丁
這里是為了解決上述第4點 - 代碼 bug 導致的閃退拼卵,使用 JSPatch?[github]可以進行熱修復奢浑。在 didFinishLaunching 時,會卡住界面發(fā)請求檢查是否有可用的 JSPatch 腳本腋腮,如果有則加載執(zhí)行雀彼,解決代碼 bug 導致的閃退。
嘗試刪除?Documents?/?Library?/?Caches?目錄下的所有文件
這里直接刪除了所有用戶數(shù)據(jù)即寡,適用于微信讀書這種所有數(shù)據(jù)都在云端徊哑,刪除后可以完全從云端恢復。如果你的 App 不屬于這種場景嘿悬,那么應該在 repairBlock 中自定義修復邏輯实柠,比如:
a. 不刪除文件,只修復數(shù)據(jù)庫
b. 修復前把用戶數(shù)據(jù)備份到云端
c. 收集 crash 樣本善涨,查明原因窒盐,定制 JSPatch 修復補丁并下發(fā)
退出微信讀書登錄狀態(tài)
進入原 didFinishLaunch
連續(xù)閃退檢測 + 保護流程如圖所示:
檢測和連續(xù) crash 并修復需要修改原?-application:didFinishLaunchingWithOptions:?邏輯,有幾種方法:
直接修改?-application:didFinishLaunchingWithOptions:?方法钢拧。
新建一個?SubAppDelegate?類來繼承?AppDelegate蟹漓,覆蓋?-application:didFinishLaunchingWithOptions:?方法,然后把?main()?函數(shù)中的?AppDelegate?替換為?SubAppDelegate
新建一個?AppDelegate?擴展源内,然后用 method swizzle 的方法替換?-application:didFinishLaunchingWithOptions:?方法葡粒。
上述三種方案,對現(xiàn)有項目改動代價是 1 > 2 > 3。因此嗽交,我們使用對源碼修改代價最小的方案 3 來替換?-application:didFinishLaunchingWithOptions:卿嘲。
檢測的邏輯 GYBootingProtection 已經(jīng)處理好,修復的處理預留了接口夫壁,可以由用戶自定義拾枣,把自定義的修復流程傳入 repairBlock 即可。
下載?(github)?源碼 盒让,將?src?目錄下所有文件拖拽到你的 Xcode 項目
在?AppDelegate+GYBootingProtection.m?的?onBeforeBootingProtection?方法中添加檢測前需要執(zhí)行的代碼梅肤,比如設置crash上報:
1
2
3
4
5
6
7
8
9
10
- (void)onBeforeBootingProtection {
[GYBootingProtection setLogger:^(NSString*msg) {
// setup logger
NSLog(@"%@", msg);
? }];
[GYBootingProtection setReportBlock:^(NSIntegercrashCounts) {
// setup crash report
? }];
}
在?onBootingProtection?方法中添加修復邏輯,比如刪除文件:
1
2
3
4
5
6
7
- (void)onBootingProtection {
// 檢查 JSPatch 更新
...
// 刪除 Documents Library Caches 目錄下所有文件
[GYBootingProtection deleteAllFilesUnderDocumentsLibraryCaches];
? ...
}
如需執(zhí)行異步的修復邏輯邑茄,在?onBootingProtectionWithCompletion:?方法添加修復邏輯姨蝴,并在完成修復后調用 completion :
1
2
3
4
5
6
7
8
- (void)onBootingProtectionWithCompletion:(BoolCompletionBlock)completion {
[selfonBootingProtection];
// 異步修復
[selfasyncRepairWithCompletion:^(void) {
// 正常啟動流程
if(completion) completion();
? }];
}
首先制造連續(xù)閃退場景:
啟動后 5 秒內,雙擊 Home 通過上劃手勢 kill 掉 App肺缕,重復多次左医。(也可以在代碼里人為制造crash)
當連續(xù)閃退超過 5 次時,會提示用戶修復:
用戶輕觸修復同木,App 重置初始狀態(tài)炒辉,連續(xù)閃退問題解決:
https://github.com/liuslevis/GYBootingProtection
https://wereadteam.github.io/2016/05/23/GYBootingProtection/