在開(kāi)發(fā)APP的過(guò)程中,崩潰等異??偸亲屛覀儾豢捌錈贿^(guò)開(kāi)發(fā)階段的崩潰等問(wèn)題,都是小事,可以進(jìn)行處理,但是一旦發(fā)布的版本出現(xiàn)崩潰的問(wèn)題,那就是大問(wèn)題,不僅要連夜更新版本,還要為找這個(gè)bug不斷的嘗試,所以,我總結(jié)了看到的一些對(duì)于崩潰等異常的處理,以備自己日后參考。
若有可以改進(jìn)的,望各位大大不吝賜教。
(注:本文只介紹使用方法,不對(duì)原理做深層解析,小弟也沒(méi)這水平,接下來(lái)閑下來(lái)了,在研究代碼的實(shí)現(xiàn)原理,出一篇原理篇~)
一、關(guān)于崩潰
閃退估計(jì)是我們最不想看到的,對(duì)于用戶而言,馬上就能產(chǎn)生一種不悅,對(duì)于投資方而言,也會(huì)產(chǎn)生對(duì)技術(shù)實(shí)力的不信任感,所以,我們就需要對(duì)閃退進(jìn)行處理,這里介紹一個(gè)不錯(cuò)的三方:AvoidCrash,寫這個(gè)的大大也很牛逼,原文參照這里。
這個(gè)三方可以處理例如插入空值到字典中或數(shù)組中引起的崩潰、數(shù)組越界引起的崩潰、unrecognized selector sent to instance等等的崩潰,都能捕獲并且避免閃退。
對(duì)于插入空值、越界等,原理比較簡(jiǎn)單,就是利用Runtime的方法交換,把普通的插入和取值的方法,替換成安全插入和安全讀取的方法,具體代碼可以去看源碼。
話不多說(shuō),先上效果:
以下是可導(dǎo)致崩潰的代碼:
NSString *nilStr = nil;
NSArray *array = @[@"chenfanfang", nilStr];

若有AvoidCrash來(lái)防止崩潰,則不會(huì)崩潰,并且會(huì)將原本會(huì)崩潰情況的詳細(xì)信息打印出來(lái),如下圖:
效果不錯(cuò)吧,接下來(lái)上使用步驟:
集成:
建議使用cocoapod,僅需要pod AvoidCrash一句話即可。(手動(dòng)導(dǎo)入的步驟,可以參照上面所說(shuō)的原文)。使用方法:(只要在
AppDelegate的didFinishLaunchingWithOptions方法中調(diào)用avoidCrash方法,就可以開(kāi)始監(jiān)聽(tīng)異常。)
- (void)avoidCrash {
/*
* 項(xiàng)目初期不需要對(duì)"unrecognized selector sent to instance"錯(cuò)誤進(jìn)行處理,因?yàn)檫€沒(méi)有相關(guān)的崩潰的類
* 后期出現(xiàn)后,再使用makeAllEffective方法,把所有對(duì)應(yīng)崩潰的類添加到數(shù)組中,避免崩潰
* 對(duì)于正式線可以啟用該方法,測(cè)試線建議關(guān)閉該方法
*/
[AvoidCrash becomeEffective];
// [AvoidCrash makeAllEffective];
// NSArray *noneSelClassStrings = @[
// @"NSString"
// ];
// [AvoidCrash setupNoneSelClassStringsArr:noneSelClassStrings];
//監(jiān)聽(tīng)通知:AvoidCrashNotification, 獲取AvoidCrash捕獲的崩潰日志的詳細(xì)信息
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(dealwithCrashMessage:) name:AvoidCrashNotification object:nil];
}
- 再監(jiān)聽(tīng)異常的通知:
- (void)dealwithCrashMessage:(NSNotification *)notification {
MYLog(@"\n??\n??監(jiān)測(cè)到崩潰信息??\n??\n");
/*
* 在這邊對(duì)避免的異常進(jìn)行一些處理,比如上傳到日志服務(wù)器等。
*/
}
以上就是避免崩潰的簡(jiǎn)單用法,關(guān)于能處理哪些異常,可以自行查看Git中的項(xiàng)目介紹。
二、關(guān)于異常的統(tǒng)計(jì)
上述的方法,能夠避免崩潰,但是不能夠避免所有狀況的崩潰,作者也在不斷的根據(jù)用戶的使用情況進(jìn)行更新,盡量對(duì)所有已知的崩潰進(jìn)行避免。所以,我們還需要對(duì)異常進(jìn)行其他的收集,也能有效的幫助自己改進(jìn)APP。
這里僅做騰訊的Bugly進(jìn)行介紹,因?yàn)槠渌睦缬衙?、極光的,個(gè)人感覺(jué)都沒(méi)有Bugly好用,我也就不做介紹了,有興趣的可以自行了解。
這里參照的文章原文在此。
- 集成
集成很簡(jiǎn)單,按照官方文檔來(lái)就好,我們這里建個(gè)簡(jiǎn)單的小項(xiàng)目,模擬一些崩潰,測(cè)試下Bugly的bug上報(bào)及時(shí)性。
項(xiàng)目就取名叫NSException了,創(chuàng)建好項(xiàng)目后,去Bugly的控制臺(tái),添加我們的應(yīng)用。
添加應(yīng)用
創(chuàng)建完應(yīng)用,進(jìn)入下一個(gè)界面,我們選擇異常上報(bào)。
選擇異常上報(bào)
再到我們的APP的appDelegate中didFinishLaunchingWithOptions方法內(nèi)調(diào)用初始化方法即可。
// 頭文件
#import <Bugly/Bugly.h>
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[Bugly startWithAppId:@"此處替換為你的AppId"];
return YES;
}
AppID可以點(diǎn)擊你在控制臺(tái)創(chuàng)建的App,然后點(diǎn)產(chǎn)品設(shè)置就能看到了。

- Bug上傳測(cè)試
接下來(lái)我們?cè)赩iewConroller中隨便創(chuàng)造一個(gè)閃退的bug
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSArray *arr = @[@"", @""];
arr[5];
}
運(yùn)行,崩潰,刷新Bugly的控制臺(tái),你會(huì)發(fā)現(xiàn),bug已經(jīng)統(tǒng)計(jì)到了。所以,Bugly的崩潰上傳是在崩潰后立刻上傳的。

我們點(diǎn)進(jìn)異常問(wèn)題中去看一下,崩潰信息大致是這樣的,我們可以很直觀的看到崩在哪個(gè)方法里了。

進(jìn)階
如果我們就這樣使用Bugly是不是太可惜了,我們來(lái)看看Bugly還有什么功能;查看頭文件,會(huì)發(fā)現(xiàn)Bugly有三個(gè)類暴露出來(lái),分別是Bugly、BuglyConfig和BuglyLog。
1.BuglyConfig類主要用于個(gè)性話配置Bugly類,由一些屬性和BuglyDelegate代理組成。
- 屬性:
BuglyConfig大部分屬性有設(shè)有默認(rèn)值,一般不用更改,但是關(guān)于卡頓監(jiān)控的屬性確是默認(rèn)關(guān)閉的:
/**
* 卡頓監(jiān)控開(kāi)關(guān),默認(rèn)關(guān)閉
*/
@property (nonatomic) BOOL blockMonitorEnable;
/**
* 卡頓監(jiān)控判斷間隔,單位為秒
*/
@property (nonatomic) NSTimeInterval blockMonitorTimeout;
如果需要上報(bào)卡頓,只需要將blockMonitorEnable設(shè)為true,給blockMonitorTimeout設(shè)置一個(gè)合理的值即可;
- 代理:
BuglyConfig可以設(shè)置一個(gè)代理,來(lái)自定義上傳崩潰的附屬信息;
@protocol BuglyDelegate <NSObject>
@optional
/**
* 發(fā)生異常時(shí)回調(diào)
* @param exception 異常信息
* @return 返回需上報(bào)記錄,隨異常上報(bào)一起上報(bào)
*/
- (NSString * BLY_NULLABLE)attachmentForException:(NSException * BLY_NULLABLE)exception;
@end
我們的初始化就改成:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
BuglyConfig *config = [[BuglyConfig alloc] init];
//監(jiān)聽(tīng)卡頓
config.blockMonitorEnable = YES;
config.blockMonitorTimeout = 3;
config.consolelogEnable = YES;
config.delegate = self;
[Bugly startWithAppId:@"此處替換為你的AppId" config:config];
// [self avoidCrash];
return YES;
}
- (NSString *)attachmentForException:(NSException *)exception {
NSLog(@"異常事件代理");
return [NSString stringWithFormat:@"TEST: %@",exception.userInfo];
}
再運(yùn)行一次,崩潰,然后我們看看Bugly控制臺(tái)上報(bào)記錄:

文件內(nèi)部記錄的就是異常的代理方法所上報(bào)的內(nèi)容。
2.上傳打印日志,BuglyLog類主要用于打印日志,有6種級(jí)別:
typedef NS_ENUM(NSUInteger, BuglyLogLevel) {
BuglyLogLevelSilent = 0,
BuglyLogLevelError = 1,
BuglyLogLevelWarn = 2,
BuglyLogLevelInfo = 3,
BuglyLogLevelDebug = 4,
BuglyLogLevelVerbose = 5,
};
BuglyLog除了控制臺(tái)打印,還有一個(gè)重要功能就是上報(bào)打印內(nèi)容,內(nèi)容將在崩潰時(shí)一同被上報(bào);但是這個(gè)功能是默認(rèn)不上報(bào)的,需要配置BuglyConfig的reportLogLevel屬性;如config.reportLogLevel = BuglyLogLevelWarn,將會(huì)上報(bào)BuglyLogLevelWarn和BuglyLogLevelError級(jí)別的打印日志。
3.自定義上報(bào)異常,我們?cè)倩氐紹ugly類,除了初始化Bugly的方法外,還有一些其他的方法:
自定義上報(bào)錯(cuò)誤
/**
* 上報(bào)自定義異常
* @param exception 異常信息
*/
+ (void)reportException:(nonnull NSException *)exception;
/**
* 上報(bào)錯(cuò)誤
* @param error 錯(cuò)誤信息
*/
+ (void)reportError:(NSError *)error;
重點(diǎn)來(lái)了!?。。?/h4>
配合上AvoidCrash,使用上報(bào)自定義異常方法,我們就既能避免崩潰,又能監(jiān)聽(tīng)異常!
//AvoidCrash異常通知監(jiān)聽(tīng)方法,在這里我們可以調(diào)用reportException方法進(jìn)行上報(bào)
- (void)dealwithCrashMessage:(NSNotification *)notification {
NSLog(@"\n??\n??監(jiān)測(cè)到崩潰信息??\n??\n");
NSException *exception = [NSException exceptionWithName:@"AvoidCrash" reason:[notification valueForKeyPath:@"userInfo.errorName"] userInfo:notification.userInfo];
[Bugly reportException:exception];
}
以上就是AvoidCrash+Bugly優(yōu)化APP的運(yùn)行處理。
雖然Bugly的崩潰列表中我們能看到得到代碼的崩潰信息,但想更具體的分析代碼位置,就要用到符號(hào)表了。
三、符號(hào)表
沒(méi)有符號(hào)表,我們就無(wú)法定位崩潰中的符號(hào)對(duì)應(yīng)的代碼所在的類以及類中的行數(shù)位置。我們?cè)诿看螛?gòu)建版本、debug的時(shí)候,都會(huì)生成dSYM后綴名的符號(hào)表文件,而我們App在手機(jī)上運(yùn)行的時(shí)候,崩潰后產(chǎn)生的崩潰信息,不可能定位到代碼的多少多少行,因?yàn)檫@些信息對(duì)于App運(yùn)行是沒(méi)有意義的,存儲(chǔ)在App中勢(shì)必會(huì)增大安裝包的體積,所以App的崩潰信息都是存儲(chǔ)為各種符號(hào),具體符號(hào)代表什么,需要去符號(hào)表中查找對(duì)應(yīng)的含義。
我們每次debug、構(gòu)建版本,都會(huì)生成dSYM文件,都對(duì)應(yīng)了一個(gè)UUID(像我們的手機(jī)一樣,都有一個(gè)唯一標(biāo)志),按下圖指示,我們就能找到我們所使用的App版本對(duì)應(yīng)的dSYM文件的UUID,通過(guò)這個(gè)UUID,我們就能找到存儲(chǔ)在我們電腦中的dSYM文件,將這個(gè)文件上傳到bugly,bugly會(huì)自動(dòng)幫我們找到崩潰符號(hào)的含義。

需要注意的是,構(gòu)建版本會(huì)自動(dòng)生成dSYM文件,但debug的時(shí)候,是沒(méi)有的,需要我們手動(dòng)開(kāi)啟。在build setting中搜索debug,將下面兩項(xiàng)內(nèi)容修改為正確的設(shè)置:

有了符號(hào)表的UUID,我們打開(kāi)終端,按UUID找到符號(hào)表的路徑。
mdfind "com_apple_xcode_dsym_uuids == A8E87810-70A7-3335-B638-C8B01BE15D79"后面的一串字母數(shù)字組合,就是我們的UUID,這里需要將UUID按一定格式處理下,也就是在特定位置插入“-”,具體格式如下:

來(lái)到終端,運(yùn)行上面的命令,就定位到了dSYM文件的位置:

打開(kāi)文件路徑,就找到了dSYM文件:

拷貝出來(lái),壓縮為zip文件,上傳到bugly上。

刷新頁(yè)面,再回去看剛才的問(wèn)題,定位到了為ViewController.m的第24行:

這樣我們就定位到了有問(wèn)題的地方。
官網(wǎng)文檔也提供了自動(dòng)上傳dSYM文件的操作流程,有興趣的可以試試,避免以后每次新版本都要手動(dòng)上傳dSYM文件。
dSYM文件也可以手動(dòng)查找:

找到你構(gòu)建的版本,右鍵show in finder:

然后在定位到的文件上右鍵顯示包內(nèi)容就OK了:

總結(jié)
以上就是Bugly收集異常的過(guò)程,由于我也只是剛剛接觸Bugly,所以自己也有幾個(gè)問(wèn)題沒(méi)有解決,例如對(duì)于Bugly的符號(hào)表的dSYM文件的上傳,每次新版本dSYM文件都會(huì)改變?那手動(dòng)是有點(diǎn)麻煩,自動(dòng)的方法也得去看看。
還有很多需要深入學(xué)習(xí)的,我也會(huì)繼續(xù)學(xué)習(xí)繼續(xù)分享,同樣的,希望各位大大能夠指出一些可以改進(jìn)的或者理解有誤的,幫助小弟進(jìn)步,例如AvoidCrash作者所說(shuō)的“一些處理”,有的話萬(wàn)分感激。

