[iOS初級教程之三]Crash分析實踐
一、引言
Crash分析與治理是移動端開發人員的必備技能,Crash相關數據也是衡量應用程序質量的重要指標。本篇文章,我們將討論在iOS開發中基礎的Crash治理實踐經驗,幫助初學者快速的掌握Crash治理技能,提升工作能力。文章將從如下幾個方面進行介紹:
- Crash的統計和分析
- 如何通過友盟APM平臺做監控和報警
- SDK收集工具的集成
- 各種類型的Crash分析實踐
Crash治理的重要一步是對Crash進行統計和分析,有了Crash的統計數據,我們才能具體的對某些Crash問題進行分析和處理,友盟U-APM平臺提供了非常好的輔助工具,開發者的接入非常簡單容易,其可以幫助開發者快速發現問題,統計問題,分析問題最終解決問題。
二、U-APM SDK的集成
客戶端應用集成U-APM SDK主要用來進行崩潰檢測,卡頓檢測以及場景記錄等功能。如果使用CocoaPods工具其接入非常簡單,在Podfile文件中添加如下依賴即可:
pod 'UMCommon'
pod 'UMDevice'
pod 'UMAPM'
pod 'UMCCommonLog'
其中UMCommon是友盟SDK基礎的支持庫,提供SDK初始化等功能,UMDevice庫與設備信息功能相關,UMAPM用來做性能與崩潰統計,UMCCommonLog是一個調試庫,在開發時我們可以將其引用,用來查看上報情況。
如果項目沒有使用CocoaPods,也可以採用手動引入的方式來集成SDK。在如下地址可以根據需求下載到指定的SDK資源:
https://developer.umeng.com/sdk/android?spm=a213m0.21038855.9168240680.3.6a311904uispVD
手動集成SDK還需要做一些簡單的工程配置:
1.需要依賴如下系統庫:
CoreTelephony.framework
libz.tbd
libsqlite.tbd
libc++.tbd
2.在工程的Targets->BuildSettings 中 , Other Linker Flags增加-ObjC參數。
完成了上面的配置過程,需要編寫代碼來完成U-APM SDK的接入工作,示例代碼如下:
#import "AppDelegate.h"
#import <UMCommon/UMCommon.h>
#import <UMAPM/UMCrashConfigure.h>
#import <UMCommonLog/UMCommonLogHeaders.h>
@interface AppDelegate ()
@end
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// 1. 初始化SDK
[UMConfigure initWithAppkey:@"602505af668f9e17b8aef059" channel:nil];
// 2. 進行異常捕獲
[UMCrashConfigure setCrashCBBlock:^NSString * _Nullable{
return @"發生了我們測試的Crash";
}];
// 3. 初始化Log
[UMCommonLogManager setUpUMCommonLogManager];
// 4. 開啓Log
[UMConfigure setLogEnabled:YES];
return YES;
}
@end
由於需要選擇一個較早的實際來進行SDK的初始化,因此我們通常會將初始化的相關代碼放入didFinishLaunching方法中,也可以根據具體需求選擇初始化的時機,接入SDK基本分爲了4個步驟,上面示例代碼中有詳細的註釋,在第1步初始化SDK中,傳入的AppKey的值是在友盟後臺創建應用後得到的。第2步調用的setCrashCBBlock用來設置上報Crash時的額外信息,通常在這個回調中我們可以將當前登錄的用戶信息等進行上報。
我們可以手動寫一些常見的會產生Crash的代碼,在真機上運行上面示例代碼後,可以在友盟的APM後臺看到記錄的異常信息,在上報的日誌的自定義字段中,可以看到我們設置的額外上報數據,如下圖所示:
此時,你會發現我們收集到的很多堆棧信息都是未符號化的,即都是內存地址,並沒與類與方法的信息,這是因爲我們還需要配置下應用的符號表,使用Xcode在構建工程時,默認只會在生產環境生成符號表,我們也可以將Build Settings->Debug Information Format選項設置爲DWARF with DSYM File來使其在Debug環境下也生成符號表,如下:
編譯後生成的符號表會與App包放在同一文件下,我們需要在友盟U-APM後臺的設置頁面將此符號表文件進行上傳,之後就可以正常的對堆棧信息進行解析。如下圖:
三、分析用戶路徑與監控告警
有時候我們記錄到了線上的Crash,並且定位到了具體的頁面,但是依然無法復現出相同的問題。很多情況下這是因爲我們的復現路徑與用戶的操作路徑並不一樣,在友盟APM後臺,對於收集到了異常問題,除了有詳細的堆棧日誌和自定義的上報數據外,還可以獲取到用戶的頁面操作路徑和設備信息,頁面操作路徑是非常重要的分析數據,根據這個路徑我們可以大致還原出用戶打開應用程序後的操作路徑,方便我們對問題進行分析復現。如下圖所示:
在設備信息頁面可以對設備與操作系統相關信息進行查看,如下圖:
U-APM後臺還提供了非常強大的監控與告警功能,我們可以設置一定的閾值作爲報警條件。當某一刻異常問題觸發了我們的報警規則,我們可以及時的收到反饋並及時的做出響應。在U-APM後臺的檢測報警功能頁面,我們可以創建一種告警計劃,如下圖所示:
在創建告警計劃時,可以設置一些觸發條件,例如在最近一小時內觸發的錯誤數超過閾值,則進行告警。對於告警的方式,有釘釘機器人提醒,郵件,企業微信等,可以參照文檔根據需要進行配置。
四、常見Crash分析實踐
1.未實現的選擇器
未實現的選擇器應該是開發中最常見的Crash原因之一,初學者在編寫代碼時,經常會在控制檯看到如下類型的錯誤提示:
unrecognized selector sent to instance
這通常是因爲調用了沒有實現的方法或者執行方法的對象類型不對,我們將這類問題統稱爲未實現的選擇器問題。產生這類的問題的場景通常有如下幾種:
①.聲明方法未實現
例如在.h文件中聲明瞭一個方法,並在其他地方對此方法進行了調用,但是此方法並沒有在.m文件中實現,此時編譯工程是不會有問題的,在運行時如果調用到了此未實現的方法會產生崩潰。
②.協議方法未實現
這種場景與聲明方法未實現類似,有時候,協議中定義的方法並不一定都是必須實現的,爲了避免出現此類問題,我們可以在調用協議方法之前先進行安全判斷,如下:
if ([self.delegate respondsToSelector:@selector(protocolMehtod)]) {
[self.delegate protocolMehtod];
}
③.copy修飾了可變屬性
在定義屬性時,如果我們將一個可變的屬性使用了copy進行修飾,則在賦值時會隱式的將其拷貝成不可變的類型,這時如果我們調用了可變屬性的方法就會產生異常,例如:
@property (nonatomic, copy) NSMutableArray *mutableArray;
這種場景具有很好的隱祕性,無論是賦值還是方法的調用,Xcode的自動檢查功能都不能提前將問題指出,也不會有警告產生。
④.動態調用了未知方法
Objective-C本身是一種動態的語言,有很多種方式可以動態的進行方法的調用,這類調用是不會做編譯時檢查的,方法名寫錯或對象類型不對都會產生異常,因此最好在動態方法調用前,都進行安全判斷,例如:
if ([self respondsToSelector:@selector(unknow)]) {
[self performSelector:@selector(unknow)];
}
⑤.低版本使用了高版本的API
當低版本系統使用了高版本纔有的接口時,也會產生未實現的選擇器異常,對於這種場景,Xcode會有警告提示,我們可以在調用方法前,先進行版本的判斷,示例如下:
// iOS 13 之後API
if (@available(iOS 13.0, *)) {
[self canPerformUnwindSegueAction:@selector(test) fromViewController:self sender:nil];
} else {
// Fallback on earlier versions
}
2.KVC相關異常
KVC(Key Value Coding)又稱鍵值編碼,其指在iOS開發中,可以允許開發者通過key名直接訪問對象的屬性或者給對象的屬性賦值,而不需要調用明確的存取方法。這樣就可以在運行時動態地訪問和修改對象的屬性。很多高級的iOS開發技巧都是基於KVC實現的。
KVC的幾個核心方法列舉如下:
//核心方法:
- (nullable id)valueForKey:(NSString *)key;
- (void)setValue:(nullable id)value forKey:(NSString *)key;
- (nullable id)valueForKeyPath:(NSString *)keyPath;
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
//高級方法:
+ (BOOL)accessInstanceVariablesDirectly;
- (nullable id)valueForUndefinedKey:(NSString *)key;
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
- (void)setNilValueForKey:(NSString *)key;
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;
KVC相關的Crash場景主要有兩種:
①. 所使用了值爲nil的key
當我們使用KVC的方式向對象的屬性進行賦值時,要保證Key值不爲nil,否則會產生異常,在使用時要做下Key值的判空,如下:
NSString *key = nil;
if (key) {
[self setValue:@"value" forKey:key];
}
②. 使用了對象中不存在的key值
在調用setValue:forKey:方法時,即是傳入的Key值不爲nil,也有可能會產生異常,默認情況下,如果要操作的屬性對象中並不存在,則也會產生Crash,我們可以實現KVC中的如下兩個方法來做兼容:
// 爲不存在的屬性進行KVC賦值時會調用這個方法
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
NSLog(@"setForUndefinedKey:%@, %@", key, value);
}
// 獲取不存在的屬性的值的時候會調用這個方法
- (id)valueForUndefinedKey:(NSString *)key {
return nil;
}
3.野指針相關異常
由於野指針問題產生的相關異常通常是比較難處理和定位的。野指針通常指所指向的對象已經被釋放的指針,其所指向的內存地址存儲的數據也被稱爲殭屍對象。我們可以通過開啓Xcode的殭屍對象功能來在開發階段提前進行預防。在Xcode的scheme編輯中,將Zombie Objects進行勾選即可。如下:
野指針相關問題的異常場景主要有如下幾種:
①. 使用了未初始化的對象
②. ARC下,使用了assign或unsafe_unretained修飾對象
如下:
@property (nonatomic, assign) UIView *subView;
這種場景下,對象釋放後,ARC不會自動的幫我們做指針置空操作。
③.runtime關聯對象使用了不合適的修飾,如OBJC_ASSOCIATION_ASSIGN
原因與場景2類似,對於對象屬性的修飾要使用正確的修飾符。
4.KVO相關異常
KVO全稱Key Value Observing,是Apple提供的一套事件通知機制。其允許一個對象監聽另外一個對象特定屬性的變化,由於KVO的實現機制的原因,一般繼承自NSObject的對象才能使用,並且其只對屬性纔會發生作用。
KVO和NSNotificationCenter都是iOS中觀察者模式的一種實現。區別在於,相對於被觀察者和觀察者之間的關係,KVO是一對一的,而不一對多的。KVO對被監聽對象無侵入性,不需要修改其內部代碼即可實現監聽,KVO可以監聽單個屬性的變化,也可以監聽集合對象的變化。
在某些場景下如果不恰當的使用KVO,也會產生Crash,常見場景如下:
①.被觀察者是局部變量
②.觀察者是局部變量
③.未實現監聽方法
④.重複移除監聽對象
要避免上述問題,在使用KVO時要把握兩個核心重點:
1. 注意監聽對象與被監聽對象的生命週期
2. addObserver和removeObserver要成對出現
5.集合對象操作相關Crash
這類Crash主要指不當的操作數組或字典所產生的的。
①.數組越界問題
②.向數組中添加nil元素
③.遍歷數組過程中使用了錯誤的方式修改了數組
④. 字典設置nil值
6.多線程操作相關Crash
和野指針問題類似,多線程產生的異常往往也是比較難定位和解決的。這類異常通常並不好復現,我們在編寫代碼時要將盡量將邏輯梳理清楚。常見問題場景如下:
①. group enter 與 group leave
在使用GCD多多線程開發時,dispatch_group_t是很常用的一種進行任務依賴編程的方式,需要注意,在使用dispatch_group_t時,要確保group enter 與 group leave是成對調用的,否則極易出現死鎖問題。
②.子線程做UI操作
在子線程中操作UI不僅會造成頁面更新不及時,頁面混亂等問題,也極易產生異常從而Crash,因此在做UI操作時,一定要保證是在主線程執行。
③.多線程對對象進行釋放
在多個線程中對變量進行賦值操作會造成,會造成變量所引用的舊的對象的多線程釋放問題,會出現偶現crash。因此,如果有多線程對外部變量進行賦值的操作,我們可以使用信號量進行加鎖,保證變量的賦值是串行的,示例代碼如下:
__block NSObject *obj;
dispatch_semaphore_t sem = dispatch_semaphore_create(1);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while (YES) {
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
obj = [NSObject new];
dispatch_semaphore_signal(sem);
}
});
while (YES) {
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
obj = [NSObject new];
dispatch_semaphore_signal(sem);
}
④.多線程同時操作數組
多線程同時對數組進行操作也是比較危險的行爲,例如當一個線程在對數據進行遍歷時,另一個線程改變了數組元素的個數,會由於索引錯亂而產生意想不到的問題甚至Crash。在多線程中遍歷數組時,可以將數組拷貝一份在進行操作。
7.watch dog異常
爲了防止一個應用佔用過多的系統資源,Apple設計了一個名爲“看門狗”( watchdog )的機制。在不同的場景下,“看門狗”會監測應用的性能。如果超出了該場景所規定的運行時間,“看門狗”就會強制終結這個應用的進程。開發者們在 crashlog 裏面,會看到諸如 0x8badf00d 這樣的錯誤代碼。異常代碼:“0x8badf00d ”(看上去非常像 bad food)。
Watch Dog的Crash本身並不是代碼錯誤,其是一種保護機制,當我們收集到的異常有發現這類問題時,就要着重考慮下應用的性能,同時檢查是否會有死鎖等異常邏輯的產生。
五、建議
1. 重視每一個Crash處理
2. 有監控,緊急問題可以及時響應
3. 積累治理經驗
4. 代碼規範,安全
5. 邏輯設計儘量簡單,多線程場景要清晰