[iOS初級教程之三]Crash分析實踐

[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. 邏輯設計儘量簡單,多線程場景要清晰
 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章