1. 如何追蹤app崩潰率,如何解決線上閃退
當(dāng)iOS設(shè)備上的App應(yīng)用閃退時(shí),操作系統(tǒng)會(huì)生成一個(gè)crash日志,保存在設(shè)備上。crash日志上有很多有用的信息,比如每個(gè)正在執(zhí)行線程的完整堆棧跟蹤信息和內(nèi)存映像,這樣就能夠通過(guò)解析這些信息進(jìn)而定位crash發(fā)生時(shí)的代碼邏輯,從而找到App閃退的原因。通常來(lái)說(shuō),crash產(chǎn)生來(lái)源于兩種問(wèn)題:違反iOS系統(tǒng)規(guī)則導(dǎo)致的crash和App代碼邏輯BUG導(dǎo)致的crash,下面分別對(duì)他們進(jìn)行分析。
違反iOS系統(tǒng)規(guī)則產(chǎn)生crash的三種類型
- 內(nèi)存報(bào)警閃退當(dāng)iOS檢測(cè)到內(nèi)存過(guò)低時(shí),它的VM系統(tǒng)會(huì)發(fā)出低內(nèi)存警告通知,嘗試回收一些內(nèi)存;如果情況沒(méi)有得到足夠的改善,iOS會(huì)終止后臺(tái)應(yīng)用以回收更多內(nèi)存;最后,如果內(nèi)存還是不足,那么正在運(yùn)行的應(yīng)用可能會(huì)被終止掉。在Debug模式下,可以主動(dòng)將客戶端執(zhí)行的動(dòng)作邏輯寫(xiě)入一個(gè)log文件中,這樣程序童鞋可以將內(nèi)存預(yù)警的邏輯寫(xiě)入該log文件,當(dāng)發(fā)生如下截圖中的內(nèi)存報(bào)警時(shí),就是提醒當(dāng)前客戶端性能內(nèi)存吃緊,可以通過(guò)Instruments工具中的Allocations 和 Leaks模塊庫(kù)來(lái)發(fā)現(xiàn)內(nèi)存分配問(wèn)題和內(nèi)存泄漏問(wèn)題。
- 響應(yīng)超時(shí)當(dāng)應(yīng)用程序?qū)σ恍┨囟ǖ氖录ū热鐔?dòng)、掛起、恢復(fù)、結(jié)束)響應(yīng)不及時(shí),蘋(píng)果的Watchdog機(jī)制會(huì)把應(yīng)用程序干掉,并生成一份相應(yīng)的crash日志。這些事件與下列UIApplicationDelegate方法相對(duì)應(yīng),當(dāng)遇到Watchdog日志時(shí),可以檢查上圖中的幾個(gè)方法是否有比較重的阻塞UI的動(dòng)作。
application:didFinishLaunchingWithOptions:
applicationWillResignActive:
applicationDidEnterBackground:
applicationWillEnterForeground:
applicationDidBecomeActive:
applicationWillTerminate:
- 用戶強(qiáng)制退出
一看到“用戶強(qiáng)制退出”,首先可能想到的雙擊Home鍵,然后關(guān)閉應(yīng)用程序。不過(guò)這種場(chǎng)景一般是不會(huì)產(chǎn)生crash日志的,因?yàn)殡p擊Home鍵后,所有的應(yīng)用程序都處于后臺(tái)狀態(tài),而iOS隨時(shí)都有可能關(guān)閉后臺(tái)進(jìn)程,當(dāng)應(yīng)用阻塞界面并停止響應(yīng)時(shí)這種場(chǎng)景才會(huì)產(chǎn)生crash日志。這里指的“用戶強(qiáng)制退出”場(chǎng)景,是稍微比較復(fù)雜點(diǎn)的操作:先按住電源鍵,直到出現(xiàn)“滑動(dòng)關(guān)機(jī)”的界面時(shí),再按住Home鍵,這時(shí)候當(dāng)前應(yīng)用程序會(huì)被終止掉,并且產(chǎn)生一份相應(yīng)事件的crash日志。
應(yīng)用邏輯的Bug
大多數(shù)閃退崩潰日志的產(chǎn)生都是因?yàn)閼?yīng)用中的Bug,這種Bug的錯(cuò)誤種類有很多,比如SEGV:(Segmentation Violation,段違例),無(wú)效內(nèi)存地址,比如空指針,未初始化指針,棧溢出等; SIGABRT:收到Abort信號(hào),可能自身調(diào)用abort()或者收到外部發(fā)送過(guò)來(lái)的信號(hào); SIGBUS:總線錯(cuò)誤。與SIGSEGV不同的是,SIGSEGV訪問(wèn)的是無(wú)效地址(比如虛存映射不到物理內(nèi)存),而SIGBUS訪問(wèn)的是有效地址,但總線訪問(wèn)異常(比如地址對(duì)齊問(wèn)題); SIGILL:嘗試執(zhí)行非法的指令,可能不被識(shí)別或者沒(méi)有權(quán)限; SIGFPE:Floating Point Error,數(shù)學(xué)計(jì)算相關(guān)問(wèn)題(可能不限于浮點(diǎn)計(jì)算),比如除零操作; SIGPIPE:管道另一端沒(méi)有進(jìn)程接手?jǐn)?shù)據(jù);
常見(jiàn)的崩潰原因基本都是代碼邏輯問(wèn)題或資源問(wèn)題,比如數(shù)組越界,訪問(wèn)野指針或者資源不存在,或資源大小寫(xiě)錯(cuò)誤等。
crash的收集
如果是在windows上你可以通過(guò)itools或pp助手等輔助工具查看系統(tǒng)產(chǎn)生的歷史crash日志,然后再根據(jù)app來(lái)查看。如果是在Mac 系統(tǒng)上,只需要打開(kāi)xcode->windows->devices,選擇device logs進(jìn)行查看,如下圖,這些crash文件都可以導(dǎo)出來(lái),然后再單獨(dú)對(duì)這個(gè)crash文件做處理分析。

市場(chǎng)上已有的商業(yè)軟件提供crash收集服務(wù),這些軟件基本都提供了日志存儲(chǔ),日志符號(hào)化解析和服務(wù)端可視化管理等服務(wù):
- Crashlytics (www.crashlytics.com)
- Crittercism (www.crittercism.com)
- Bugsense (www.bugsense.com)
- HockeyApp (www.hockeyapp.net)
- Flurry(www.flurry.com)
開(kāi)源的軟件也可以拿來(lái)收集crash日志,比如Razor,QuincyKit(git鏈接)等,這些軟件收集crash的原理其實(shí)大同小異,都是根據(jù)系統(tǒng)產(chǎn)生的crash日志進(jìn)行了一次提取或封裝,然后將封裝后的crash文件上傳到對(duì)應(yīng)的服務(wù)端進(jìn)行解析處理。很多商業(yè)軟件都采用了Plcrashreporter這個(gè)開(kāi)源工具來(lái)上傳和解析crash,比如HockeyApp,Flurry和crittercism等。

由于自己的crash信息太長(zhǎng),找了一張示例:
- crash標(biāo)識(shí)是應(yīng)用進(jìn)程產(chǎn)生crash時(shí)的一些標(biāo)識(shí)信息,它描述了該crash的唯一標(biāo)識(shí)(E838FEFB-ECF6-498C-8B35-D40F0F9FEAE4),所發(fā)生的硬件設(shè)備類型(iphone3,1代表iphone4),以及App進(jìn)程相關(guān)的信息等;
- 基本信息描述的是crash發(fā)生的時(shí)間和系統(tǒng)版本;
- 異常類型描述的是crash發(fā)生時(shí)拋出的異常類型和錯(cuò)誤碼;
- 線程回溯描述了crash發(fā)生時(shí)所有線程的回溯信息,每個(gè)線程在每一幀對(duì)應(yīng)的函數(shù)調(diào)用信息(這里由于空間限制沒(méi)有全部列出);
- 二進(jìn)制映像是指crash發(fā)生時(shí)已加載的二進(jìn)制文件。以上就是一份crash日志包含的所有信息,接下來(lái)就需要根據(jù)這些信息去解析定位導(dǎo)致crash發(fā)生的代碼邏輯, 這就需要用到符號(hào)化解析的過(guò)程(洋名叫:symbolication)。
解決線上閃退
首先保證,發(fā)布前充分測(cè)試。發(fā)布后依然有閃退現(xiàn)象,查看崩潰日志,及時(shí)修復(fù)并發(fā)布。
2. iOS應(yīng)用生命周期
應(yīng)用程序的狀態(tài)
Not running未運(yùn)行:程序沒(méi)啟動(dòng)。
Inactive未激活:程序在前臺(tái)運(yùn)行,不過(guò)沒(méi)有接收到事件。在沒(méi)有事件處理情況下程序通常停留在這個(gè)狀態(tài)。
Active激活:程序在前臺(tái)運(yùn)行而且接收到了事件。這也是前臺(tái)的一個(gè)正常的模式。
Backgroud后臺(tái):程序在后臺(tái)而且能執(zhí)行代碼,大多數(shù)程序進(jìn)入這個(gè)狀態(tài)后會(huì)在在這個(gè)狀態(tài)上停留一會(huì)。時(shí)間到之后會(huì)進(jìn)入掛起狀態(tài)(Suspended)。有的程序經(jīng)過(guò)特殊的請(qǐng)求后可以長(zhǎng)期處于Backgroud狀態(tài)。
Suspended掛起:程序在后臺(tái)不能執(zhí)行代碼。系統(tǒng)會(huì)自動(dòng)把程序變成這個(gè)狀態(tài)而且不會(huì)發(fā)出通知。當(dāng)掛起時(shí),程序還是停留在內(nèi)存中的,當(dāng)系統(tǒng)內(nèi)存低時(shí),系統(tǒng)就把掛起的程序清除掉,為前臺(tái)程序提供更多的內(nèi)存。
下面看一下AppDelegate.m文件,這個(gè)關(guān)乎著應(yīng)用程序的生命周期:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// 當(dāng)應(yīng)用程序啟動(dòng)時(shí)執(zhí)行,應(yīng)用程序啟動(dòng)入口,只在應(yīng)用程序啟動(dòng)時(shí)執(zhí)行一次。若用戶直接啟動(dòng),lauchOptions內(nèi)無(wú)數(shù)據(jù),若通過(guò)其他方式啟動(dòng)應(yīng)用,lauchOptions包含對(duì)應(yīng)方式的內(nèi)容。
return YES;
}
- (void)applicationWillResignActive:(UIApplication *)application {
//當(dāng)程序從active轉(zhuǎn)為inactive時(shí)會(huì)調(diào)用這個(gè)方法。例如:來(lái)電話、信息或者當(dāng)用戶退出程序,程序從前臺(tái)專為后臺(tái)的過(guò)場(chǎng)。
//用這個(gè)方法可以暫定正在執(zhí)行的任務(wù),暫停計(jì)時(shí)器以及降低OpenGL的幀率。游戲可以用這個(gè)方法來(lái)暫停游戲。
}
- (void)applicationDidEnterBackground:(UIApplication *)application {
//用這個(gè)方法可以釋放資源、保存用戶數(shù)據(jù)、取消計(jì)時(shí)器以及儲(chǔ)存用來(lái)恢復(fù)程序的狀態(tài)信息,以免程序被終止。
//如果你的程序支持后臺(tái)模式,當(dāng)用戶退出程序時(shí),這個(gè)方法可以用來(lái)替代applicationWillTerminate:
}
- (void)applicationWillEnterForeground:(UIApplication *)application {
//從后臺(tái)到inactive時(shí)回調(diào)用這個(gè)方法,用這個(gè)方法你可以取消之前進(jìn)入后臺(tái)很多操作
}
- (void)applicationDidBecomeActive:(UIApplication *)application {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
//程序已經(jīng)進(jìn)入前臺(tái)并且處于active狀態(tài)這個(gè)方法可以重啟之前在inactive狀態(tài)下被暫停的任務(wù);如果程序之前處于后臺(tái),可以選擇在這個(gè)方法里刷新UI界面
}
- (void)applicationWillTerminate:(UIApplication *)application {
//當(dāng)程序即將被終止時(shí)調(diào)用,如果需要的話可以再次保存數(shù)據(jù),可以參見(jiàn)applicationDidEnterBackground:
}
初次啟動(dòng):
iOS_didFinishLaunchingWithOptions
iOS_applicationDidBecomeActive
按下home鍵:
iOS_applicationWillResignActive
iOS_applicationDidEnterBackground
點(diǎn)擊程序圖標(biāo)進(jìn)入:
iOS_applicationWillEnterForeground
iOS_applicationDidBecomeActive
當(dāng)應(yīng)用程序進(jìn)入后臺(tái)時(shí),應(yīng)該保存用戶數(shù)據(jù)或狀態(tài)信息,所有沒(méi)寫(xiě)到磁盤(pán)的文件或信息,在進(jìn)入后臺(tái)時(shí),最后都寫(xiě)到磁盤(pán)去,因?yàn)槌绦蚩赡茉诤笈_(tái)被殺死。釋放盡可能釋放的內(nèi)存。
- (void)applicationDidEnterBackground:(UIApplication *)application
方法有大概5秒的時(shí)間讓你完成這些任務(wù)。如果超過(guò)時(shí)間還有未完成的任務(wù),你的程序就會(huì)被終止而且從內(nèi)存中清除。
如果還需要長(zhǎng)時(shí)間的運(yùn)行任務(wù),可以在該方法中調(diào)用
[application beginBackgroundTaskWithExpirationHandler:^{
//....此處執(zhí)行你的代碼....
}];
程序終止
程序只要符合以下情況之一,只要進(jìn)入后臺(tái)或掛起狀態(tài)就會(huì)終止:
- iOS4.0以前的系統(tǒng)
- app是基于iOS4.0之前系統(tǒng)開(kāi)發(fā)的。
- 設(shè)備不支持多任務(wù)
- 在Info.plist文件中,程序包含了 UIApplicationExitsOnSuspend 鍵。
系統(tǒng)常常是為其他app啟動(dòng)時(shí)由于內(nèi)存不足而回收內(nèi)存最后需要終止應(yīng)用程序,但有時(shí)也會(huì)是由于app很長(zhǎng)時(shí)間才響應(yīng)而終止。如果app當(dāng)時(shí)運(yùn)行在后臺(tái)并且沒(méi)有暫停,系統(tǒng)會(huì)在應(yīng)用程序終止之前調(diào)用app的代理的方法
- (void)applicationWillTerminate:(UIApplication *)application
,這樣可以讓你可以做一些清理工作。你可以保存一些數(shù)據(jù)或app的狀態(tài)。這個(gè)方法也有5秒鐘的限制。超時(shí)后方法會(huì)返回程序從內(nèi)存中清除。
3.Runtime
Objective-C 是面相運(yùn)行時(shí)的語(yǔ)言(runtime oriented language),就是說(shuō)它會(huì)盡可能的把編譯和鏈接時(shí)要執(zhí)行的邏輯延遲到運(yùn)行時(shí)。這就給了你很大的靈活性,你可以按需要把消息重定向給合適的對(duì)象,你甚 至可以交換方法的實(shí)現(xiàn),等等。
RunTime簡(jiǎn)稱運(yùn)行時(shí)。就是系統(tǒng)在運(yùn)行的時(shí)候的一些機(jī)制,其中最主要的是消息機(jī)制。OC的函數(shù)調(diào)用成為消息發(fā)送。屬于動(dòng)態(tài)調(diào)用過(guò)程。在編譯的時(shí)候并不能決定真正調(diào)用哪個(gè)函數(shù)(事實(shí)證明,在編 譯階段,OC可以調(diào)用任何函數(shù),即使這個(gè)函數(shù)并未實(shí)現(xiàn),只要申明過(guò)就不會(huì)報(bào)錯(cuò)。而C語(yǔ)言在編譯階段就會(huì)報(bào)錯(cuò))。只有在真正運(yùn)行的時(shí)候才會(huì)根據(jù)函數(shù)的名稱找 到對(duì)應(yīng)的函數(shù)來(lái)調(diào)用。
以下面的代碼為例:
[obj makeText];
其中obj是一個(gè)對(duì)象,makeText是一個(gè)函數(shù)名稱。對(duì)于這樣一個(gè)簡(jiǎn)單的調(diào)用。在編譯時(shí)RunTime會(huì)將上述代碼轉(zhuǎn)化成
objc_msgSend(obj,@selector(makeText));
首先,編譯器將代碼[obj makeText];轉(zhuǎn)化為objc_msgSend(obj, @selector (makeText));,在objc_msgSend函數(shù)中。首先通過(guò)obj的isa指針找到obj對(duì)應(yīng)的class。在Class中先去cache中 通過(guò)SEL查找對(duì)應(yīng)函數(shù)method(猜測(cè)cache中method列表是以SEL為key通過(guò)hash表來(lái)存儲(chǔ)的,這樣能提高函數(shù)查找速度),若 cache中未找到。再去methodList中查找,若methodlist中未找到,則取superClass中查找。若能找到,則將method加 入到cache中,以方便下次查找,并通過(guò)method中的函數(shù)指針跳轉(zhuǎn)到對(duì)應(yīng)的函數(shù)中去執(zhí)行。
Objective-C Runtime 是什么?Objective-C 的 Runtime 是一個(gè)運(yùn)行時(shí)庫(kù)(Runtime Library),它是一個(gè)主要使用 C 和匯編寫(xiě)的庫(kù),為 C 添加了面相對(duì)象的能力并創(chuàng)造了 Objective-C。這就是說(shuō)它在類信息(Class information) 中被加載,完成所有的方法分發(fā),方法轉(zhuǎn)發(fā),等等。Objective-C runtime 創(chuàng)建了所有需要的結(jié)構(gòu)體,讓 Objective-C 的面相對(duì)象編程變?yōu)榭赡堋?br> 具體還可以參考這篇文章
Method Swizzling 原理
在Objective-C中調(diào)用一個(gè)方法,其實(shí)是向一個(gè)對(duì)象發(fā)送消息,查找消息的唯一依據(jù)是selector的名字。利用Objective-C的動(dòng)態(tài)特性,可以實(shí)現(xiàn)在運(yùn)行時(shí)偷換selector對(duì)應(yīng)的方法實(shí)現(xiàn),達(dá)到給方法掛鉤的目的。每個(gè)類都有一個(gè)方法列表,存放著selector的名字和方法實(shí)現(xiàn)的映射關(guān)系。IMP有點(diǎn)類似函數(shù)指針,指向具體的Method實(shí)現(xiàn)。
我們可以利用 method_exchangeImplementations 來(lái)交換2個(gè)方法中的IMP,我們可以利用 class_replaceMethod 來(lái)修改類,我們可以利用 method_setImplementation 來(lái)直接設(shè)置某個(gè)方法的IMP,……歸根結(jié)底,都是偷換了selector的IMP。