引言
“如果某個(gè)實(shí)體表現(xiàn)出以下任何一種特性,它就具備自主性:自我修復(fù)、自我保護(hù)、自我維護(hù)、對(duì)目標(biāo)的自我控制、自我改進(jìn)?!?—— 凱文·凱利
iOS App 有時(shí)可能遇到啟動(dòng)必 crash 的絕境:每次打開(kāi) App 都閃退,無(wú)法正常使用App。
為了嘗試解決這個(gè)問(wèn)題,微信讀書(shū)開(kāi)發(fā)了 iOS 連續(xù)閃退保護(hù)工具:GYBootingProtection,檢測(cè)連續(xù)閃退,在連續(xù)閃退出現(xiàn)時(shí),嘗試自修復(fù) App:

本文探討了連續(xù)閃退問(wèn)題的產(chǎn)生原因、檢測(cè)、修復(fù)機(jī)制,以及如何在你的項(xiàng)目中引入、測(cè)試和使用?GYBootingProtection。
首先要檢測(cè)用戶(hù) App 出現(xiàn)了連續(xù)閃退的情況,有兩種檢測(cè)方法,捕獲異常和計(jì)時(shí)器。
檢測(cè)連續(xù)閃退,可以通過(guò)捕獲異常來(lái)實(shí)現(xiàn),異常有以下種類(lèi):
Mach 異常:EXC_CRASH
UNIX 信號(hào):SIGABRT
NSException 異常:應(yīng)用層,通過(guò) NSUncaughtExceptionHandler 捕獲
在念茜的漫談 iOS Crash 收集框架一文中詳細(xì)介紹了 Mach 異常和 Unix 信號(hào)捕獲 crash 的機(jī)制。簡(jiǎn)單來(lái)說(shuō),異常一般產(chǎn)生自 iOS 的微內(nèi)核 Mach,然后在 BSD 層轉(zhuǎn)換成 UNIX SIGABRT 信號(hào),以標(biāo)準(zhǔn) POSIX 信號(hào)的形式提供給用戶(hù)。NSException 是使用者在處理 App 邏輯時(shí),用編程的方法拋出。
通過(guò)以下方法捕獲異常:
利用 Mach API 捕獲 Mach 異常
通過(guò) POSIX API 注冊(cè) signal(SIGSEGV,signalHandler) 來(lái)捕獲 UNIX 異常信號(hào)
注冊(cè) NSUncaughtExceptionHandler 來(lái)捕獲應(yīng)用級(jí)異常
Crash 上報(bào)工具如 PLCrashReporter 通過(guò)注冊(cè) Mach 異常 + UNIX信號(hào) 的 handler 達(dá)到檢測(cè)的目的,對(duì)用戶(hù)提供了處理異常的接口。
可以利用 PLCrashReporter 這類(lèi)工具來(lái)檢測(cè)連續(xù)閃退:
首先維護(hù)一個(gè)計(jì)數(shù)變量,表示連續(xù)閃退次數(shù)
在 PLCrashReporter 的 crash handler 中加入邏輯:如果啟動(dòng) 5s 內(nèi) crash 使計(jì)數(shù)器加一
每次啟動(dòng)時(shí),如果連續(xù)閃退計(jì)數(shù) > n,則檢測(cè)到了連續(xù)閃退
啟動(dòng)后,執(zhí)行一個(gè)定時(shí)任務(wù),在 5s 后重置計(jì)數(shù)(如果 App 連續(xù)閃退則不會(huì)重置)

通過(guò) Mach 異常、Unix 信號(hào)、NSException 異常來(lái)檢測(cè)閃退,能獲得更多的 crash 上下文,但由于 crash 收集框架多使用這些方法,可能會(huì)有這樣的風(fēng)險(xiǎn):與第三方 crash 收集框架沖突導(dǎo)致漏檢測(cè)。另外,可能會(huì)與 App 已有的異常處理代碼產(chǎn)生耦合。
除了通過(guò)捕獲異常的方式檢測(cè)連續(xù)閃退,還可以通過(guò)計(jì)數(shù)器方法來(lái)檢測(cè):
維護(hù)一個(gè)計(jì)數(shù)變量,用于表示連續(xù)閃退的次數(shù)
在啟動(dòng) application:didFinishLaunchingWithOptions: 后使計(jì)數(shù)加一
接著使用 dispatch_after 方法在 5s 后清零計(jì)數(shù),如果 App 活不過(guò) 5 秒計(jì)數(shù)就不會(huì)被清零
如果發(fā)現(xiàn)計(jì)數(shù)變量 > n,表明 App 連續(xù) n 次連續(xù)閃退,啟動(dòng)保護(hù)流程,重置計(jì)數(shù)。
當(dāng)保護(hù)流程完成后,進(jìn)入 App 正常啟動(dòng)流程

而計(jì)數(shù)器方法邏輯簡(jiǎn)單,與原有的代碼耦合小。雖然有誤報(bào)可能(在啟動(dòng)后立即被 kill 掉,誤認(rèn)為 crash),但是可以通過(guò)設(shè)置閾值來(lái)減小誤報(bào)的誤報(bào)率。
綜上權(quán)衡,我們使用計(jì)時(shí)器方法檢測(cè)連續(xù)閃退。
檢測(cè)到連續(xù)閃退后,接下來(lái)要嘗試對(duì)閃退進(jìn)行修復(fù),這里先分析可能的閃退原因,再結(jié)合微信讀書(shū)的例子說(shuō)明修復(fù)流程。
連續(xù)閃退,可能是 App 啟動(dòng)關(guān)鍵路徑中執(zhí)行了必 crash 的代碼,原因可能有:
數(shù)據(jù)庫(kù)損壞:在日常使用如異常退出、斷電,或者錯(cuò)誤的操作(參考:sqlite corruption causes)。
文件損壞:處理文件時(shí)如果沒(méi)有?@try...catch,損壞文件會(huì)拋出?NSException?導(dǎo)致 crash
網(wǎng)絡(luò)返回?cái)?shù)據(jù)處理異常:比如預(yù)期返回?cái)?shù)組,但實(shí)際返回了字典,對(duì)字典對(duì)象執(zhí)行?-objectAtIndex?方法會(huì)產(chǎn)生?crash: unknow selector send to object;,或返回破損的 Tar 包,在解壓失敗導(dǎo)致 crash。
代碼 bug:當(dāng)必 crash 的代碼出現(xiàn)在啟動(dòng)關(guān)鍵路徑中,就會(huì)導(dǎo)致連續(xù)閃退。
針對(duì) 1,可以通過(guò)工具修復(fù)數(shù)據(jù)庫(kù),或者刪除 DB。針對(duì)2,可以刪除文件來(lái)進(jìn)行修復(fù)。對(duì)于 3 和 4,我們需要具體地分析 crash 案例,通過(guò) JSPatch 來(lái)進(jìn)行修復(fù)。
為了應(yīng)對(duì)上述導(dǎo)致連續(xù)閃退的原因,微信讀書(shū)的修復(fù)流程為:
進(jìn)入 didFinishLaunch 時(shí)檢查是否有連續(xù)閃退,無(wú)則執(zhí)行 5
彈 Toast 提示用戶(hù)是否修復(fù),輕觸『修復(fù)』執(zhí)行2,否則執(zhí)行 5
嘗試下載并執(zhí)行 JSPatch 補(bǔ)丁
這里是為了解決上述第4點(diǎn) - 代碼 bug 導(dǎo)致的閃退,使用 JSPatch?[github]可以進(jìn)行熱修復(fù)。在 didFinishLaunching 時(shí),會(huì)卡住界面發(fā)請(qǐng)求檢查是否有可用的 JSPatch 腳本,如果有則加載執(zhí)行,解決代碼 bug 導(dǎo)致的閃退。
嘗試刪除?Documents?/?Library?/?Caches?目錄下的所有文件
這里直接刪除了所有用戶(hù)數(shù)據(jù),適用于微信讀書(shū)這種所有數(shù)據(jù)都在云端,刪除后可以完全從云端恢復(fù)。如果你的 App 不屬于這種場(chǎng)景,那么應(yīng)該在 repairBlock 中自定義修復(fù)邏輯,比如:
a. 不刪除文件,只修復(fù)數(shù)據(jù)庫(kù)
b. 修復(fù)前把用戶(hù)數(shù)據(jù)備份到云端
c. 收集 crash 樣本,查明原因,定制 JSPatch 修復(fù)補(bǔ)丁并下發(fā)
退出微信讀書(shū)登錄狀態(tài)
進(jìn)入原 didFinishLaunch
連續(xù)閃退檢測(cè) + 保護(hù)流程如圖所示:

檢測(cè)和連續(xù) crash 并修復(fù)需要修改原?-application:didFinishLaunchingWithOptions:?邏輯,有幾種方法:
直接修改?-application:didFinishLaunchingWithOptions:?方法。
新建一個(gè)?SubAppDelegate?類(lèi)來(lái)繼承?AppDelegate,覆蓋?-application:didFinishLaunchingWithOptions:?方法,然后把?main()?函數(shù)中的?AppDelegate?替換為?SubAppDelegate
新建一個(gè)?AppDelegate?擴(kuò)展,然后用 method swizzle 的方法替換?-application:didFinishLaunchingWithOptions:?方法。
上述三種方案,對(duì)現(xiàn)有項(xiàng)目改動(dòng)代價(jià)是 1 > 2 > 3。因此,我們使用對(duì)源碼修改代價(jià)最小的方案 3 來(lái)替換?-application:didFinishLaunchingWithOptions:。
檢測(cè)的邏輯 GYBootingProtection 已經(jīng)處理好,修復(fù)的處理預(yù)留了接口,可以由用戶(hù)自定義,把自定義的修復(fù)流程傳入 repairBlock 即可。
下載?(github)?源碼 ,將?src?目錄下所有文件拖拽到你的 Xcode 項(xiàng)目
在?AppDelegate+GYBootingProtection.m?的?onBeforeBootingProtection?方法中添加檢測(cè)前需要執(zhí)行的代碼,比如設(shè)置crash上報(bào):

在?onBootingProtection?方法中添加修復(fù)邏輯,比如刪除文件:

如需執(zhí)行異步的修復(fù)邏輯,在?onBootingProtectionWithCompletion:?方法添加修復(fù)邏輯,并在完成修復(fù)后調(diào)用 completion :

首先制造連續(xù)閃退場(chǎng)景:
啟動(dòng)后 5 秒內(nèi),雙擊 Home 通過(guò)上劃手勢(shì) kill 掉 App,重復(fù)多次。(也可以在代碼里人為制造crash)
當(dāng)連續(xù)閃退超過(guò) 5 次時(shí),會(huì)提示用戶(hù)修復(fù):

用戶(hù)輕觸修復(fù),App 重置初始狀態(tài),連續(xù)閃退問(wèn)題解決:
