Table of Contents generated with DocToc前端
做者:阿里雲-移動雲-大前端團隊github
連續啓動 Crash 應該是 crash 類型中zui,api
在微信讀書團隊發佈的《iOS 啓動連續閃退保護方案》 一文中,給出了連續啓動crash的自修復技術的思路講解,並在GitHub上給出了技術實現,並開源了 GYBootingProtection。方案思路很好,很輕量級。微信
在微信讀書團隊給出的文章中已經有比較詳細的闡述,在此不作贅述,實現的流程圖以下所示:app
但有個實現上能夠優化下,能夠下降50%以上誤報機率,監聽用戶手動劃掉 APP 這個事件,其中一些特定場景,是能夠獲取的。另外在這裏也給出對其 API 設計的建議。最後給出優化後的實現。異步
用戶主動 kill 掉 APP 分爲兩種狀況:async
第一種場景更爲常見,能夠經過監聽 UIApplicationWillTerminateNotification
來捕獲該動做,捕獲後恢復計數。第二種狀況,沒法監聽到。但也足以下降 50% 以上的誤報機率。函數
該機制當前所處的狀態,好比:NeedFix 、isFixing,建議用枚舉來作爲API透出。好比:優化
typedef NS_ENUM(NSInteger, BootingProtectionStatus) { BootingProtectionStatusNormal, /**< APP 啓動正常 */ BootingProtectionStatusNormalChecking, /**< 正在檢測是否會在特定時間內是否會 Crash,注意:檢測狀態下「連續啓動崩潰計數」個數小於或等於上限值 */ BootingProtectionStatusNeedFix, /**< APP 出現連續啓動 Crash,須要採起修復措施 */ BootingProtectionStatusFixing, /**< APP 出現連續啓動 Crash,正在修復中... */ };
/*! * 當前啓動Crash的狀態 */ @property (nonatomic, assign, readonly) ABSBootingProtectionStatus bootingProtectionStatus; /*! * 達到須要執行上報操做的「連續啓動崩潰計數」個數。 */ @property (nonatomic, assign, readonly) NSUInteger continuousCrashOnLaunchNeedToReport; /*! * 達到須要執行修復操做的「連續啓動崩潰計數」個數。 */ @property (nonatomic, assign, readonly) NSUInteger continuousCrashOnLaunchNeedToFix; /*! * APP 啓動後通過多少秒,能夠將「連續啓動崩潰計數」清零 */ @property (nonatomic, assign, readonly) NSTimeInterval crashOnLaunchTimeIntervalThreshold;
reportBlock
上報邏輯,
repairtBlock
修復邏輯
typedef void (^BoolCompletionHandler)(BOOL succeeded, NSError *error); typedef void (^RepairBlock)(ABSBoolCompletionHandler completionHandler);
用戶執行 BoolCompletionHandler
後便可知道是否執行完畢,而且支持異步操做。
異步操做帶來的問題,能夠經過前面提到的枚舉API來實時監測狀態,來決定各類其餘操做。
下面給出優化後的代碼實現:
// // CYLBootingProtection.h // // // Created by ChenYilong on 18/01/10. // Copyright © 2018年 ChenYilong. All rights reserved. // #import <Foundation/Foundation.h> typedef void (^ABSBoolCompletionHandler)(BOOL succeeded, NSError *error); typedef void (^ABSRepairBlock)(ABSBoolCompletionHandler completionHandler); typedef void (^ABSReportBlock)(NSUInteger crashCounts); typedef NS_ENUM(NSInteger, BootingProtectionStatus) { BootingProtectionStatusNormal, /**< APP 啓動正常 */ BootingProtectionStatusNormalChecking, /**< 正在檢測是否會在特定時間內是否會 Crash,注意:檢測狀態下「連續啓動崩潰計數」個數小於或等於上限值 */ BootingProtectionStatusNeedFix, /**< APP 出現連續啓動 Crash,須要採起修復措施 */ BootingProtectionStatusFixing, /**< APP 出現連續啓動 Crash,正在修復中... */ }; /** * 啓動連續 crash 保護。 * 啓動後 `_crashOnLaunchTimeIntervalThreshold` 秒內 crash,反覆超過 `_continuousCrashOnLaunchNeedToReport` 次則上報日誌,超過 `_continuousCrashOnLaunchNeedToFix` 則啓動修復操做。 */ @interface CYLBootingProtection : NSObject /** * 啓動連續 crash 保護方法。 * 前置條件:在 App 啓動時註冊 crash 處理函數,在 crash 時調用[CYLBootingProtection addCrashCountIfNeeded]。 * 啓動後必定時間內(`crashOnLaunchTimeIntervalThreshold`秒內)crash,反覆超過必定次數(`continuousCrashOnLaunchNeedToReport`次)則上報日誌,超過必定次數(`continuousCrashOnLaunchNeedToFix`次)則啓動修復程序;在必定時間內(`crashOnLaunchTimeIntervalThreshold`秒) 秒後若沒有 crash 將「連續啓動崩潰計數」計數置零。 `reportBlock` 上報邏輯, `repairtBlock` 修復邏輯,完成後執行 `[self setCrashCount:0]` */ - (void)launchContinuousCrashProtect; /*! * 當前啓動Crash的狀態 */ @property (nonatomic, assign, readonly) BootingProtectionStatus bootingProtectionStatus; /*! * 達到須要執行上報操做的「連續啓動崩潰計數」個數。 */ @property (nonatomic, assign, readonly) NSUInteger continuousCrashOnLaunchNeedToReport; /*! * 達到須要執行修復操做的「連續啓動崩潰計數」個數。 */ @property (nonatomic, assign, readonly) NSUInteger continuousCrashOnLaunchNeedToFix; /*! * APP 啓動後通過多少秒,能夠將「連續啓動崩潰計數」清零 */ @property (nonatomic, assign, readonly) NSTimeInterval crashOnLaunchTimeIntervalThreshold; /*! * 藉助 context 可讓多個模塊註冊事件,而且事件 block 能獨立執行,互不干擾。 */ @property (nonatomic, copy, readonly) NSString *context; /*! * @details 啓動後kCrashOnLaunchTimeIntervalThreshold秒內crash,反覆超過continuousCrashOnLaunchNeedToReport次則上報日誌,超過continuousCrashOnLaunchNeedToFix則啓動修復程序;當全部操做完成後,執行 completion。在 crashOnLaunchTimeIntervalThreshold 秒後若沒有 crash 將 kContinuousCrashOnLaunchCounterKey 計數置零。 * @param context 藉助 context 可讓多個模塊註冊事件,而且事件 block 能獨立執行,互不干擾。 */ - (instancetype)initWithContinuousCrashOnLaunchNeedToReport:(NSUInteger)continuousCrashOnLaunchNeedToReport continuousCrashOnLaunchNeedToFix:(NSUInteger)continuousCrashOnLaunchNeedToFix crashOnLaunchTimeIntervalThreshold:(NSTimeInterval)crashOnLaunchTimeIntervalThreshold context:(NSString *)context; /*! * 當前「連續啓動崩潰「的狀態 */ + (BootingProtectionStatus)bootingProtectionStatusWithContext:(NSString *)context continuousCrashOnLaunchNeedToFix:(NSUInteger)continuousCrashOnLaunchNeedToFix; /*! * 設置上報邏輯,參數 crashCounts 爲啓動連續 crash 次數 */ - (void)setReportBlock:(ABSReportBlock)reportBlock; /*! * 設置修復邏輯 */ - (void)setRepairBlock:(ABSRepairBlock)repairtBlock; + (void)setLogger:(void (^)(NSString *))logger; @end
// // CYLBootingProtection.m // // // Created by ChenYilong on 18/01/10. // Copyright © 2018年 ChenYilong. All rights reserved. // #import "CYLBootingProtection.h" #import <UIKit/UIKit.h> static dispatch_queue_t _exceptionOperationQueue = 0; void (^Logger)(NSString *log); @interface CYLBootingProtection () @property (nonatomic, assign) NSUInteger continuousCrashOnLaunchNeedToReport; @property (nonatomic, assign) NSUInteger continuousCrashOnLaunchNeedToFix; @property (nonatomic, assign) NSTimeInterval crashOnLaunchTimeIntervalThreshold; @property (nonatomic, copy) NSString *context; @property (nonatomic, copy) ABSReportBlock reportBlock; @property (nonatomic, copy) ABSRepairBlock repairBlock; /*! * 設置「連續啓動崩潰計數」個數 */ - (void)setCrashCount:(NSInteger)count; /*! * 設置「連續啓動崩潰計數」個數 */ + (void)setCrashCount:(NSUInteger)count context:(NSString *)context; /*! * 「連續啓動崩潰計數」個數 */ - (NSUInteger)crashCount; /*! * 「連續啓動崩潰計數」個數 */ + (NSUInteger)crashCountWithContext:(NSString *)context; @end @implementation CYLBootingProtection + (void)initialize { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ _exceptionOperationQueue = dispatch_queue_create("com.ChenYilong.CYLBootingProtection.fileCacheQueue", DISPATCH_QUEUE_SERIAL); }); } - (instancetype)initWithContinuousCrashOnLaunchNeedToReport:(NSUInteger)continuousCrashOnLaunchNeedToReport continuousCrashOnLaunchNeedToFix:(NSUInteger)continuousCrashOnLaunchNeedToFix crashOnLaunchTimeIntervalThreshold:(NSTimeInterval)crashOnLaunchTimeIntervalThreshold context:(NSString *)context { if (!(self = [super init])) { return nil; } _continuousCrashOnLaunchNeedToReport = continuousCrashOnLaunchNeedToReport; _continuousCrashOnLaunchNeedToFix = continuousCrashOnLaunchNeedToFix; _crashOnLaunchTimeIntervalThreshold = crashOnLaunchTimeIntervalThreshold; _context = [context copy]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillTerminate:) name:UIApplicationWillTerminateNotification object:[UIApplication sharedApplication]]; return self; } /*! * App在前臺時用戶手動劃掉APP的時候,不計入檢測。 * 可是APP在後臺時劃掉APP,沒法檢測出來。 * 見:https://stackoverflow.com/a/35041565/3395008 */ - (void)applicationWillTerminate:(NSNotification *)note { BOOL isNormalChecking = [self isNormalChecking]; if (isNormalChecking) { [self decreaseCrashCount]; } } - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } /* 支持同步修復、異步修復,兩種修復方式 - 異步修復,不卡頓主UI,但有修復未完成就被再次觸發crash、或者用戶kill掉的可能。須要用戶手動根據修復狀態,來選擇性地進行操做,應該有回掉。 - 同步修復,最簡單直觀,在主線程刪除或者下載修復包。 */ - (void)launchContinuousCrashProtect { NSAssert(_repairBlock, @"_repairBlock is nil!"); [[self class] Logger:@"CYLBootingProtection: Launch continuous crash report"]; [self resetBootingProtectionStatus]; NSUInteger launchCrashes = [self crashCount]; // 上報 if (launchCrashes >= self.continuousCrashOnLaunchNeedToReport) { NSString *logString = [NSString stringWithFormat:@"CYLBootingProtection: App has continuously crashed for %@ times. Now synchronize uploading crash report and begin fixing procedure.", @(launchCrashes)]; [[self class] Logger:logString]; if (_reportBlock) { dispatch_async(dispatch_get_main_queue(),^{ _reportBlock(launchCrashes); }); } } // 修復 if ([self isUpToBootingProtectionCount]) { [[self class] Logger:@"need to repair"]; [self setIsFixing:YES]; if (_repairBlock) { ABSBoolCompletionHandler completionHandler = ^(BOOL succeeded, NSError *__nullable error){ if (succeeded) { [self resetCrashCount]; } else { [[self class] Logger:error.description]; } }; dispatch_async(dispatch_get_main_queue(),^{ _repairBlock(completionHandler); }); } } else { [self increaseCrashCount:launchCrashes]; // 正常流程,無需修復 [[self class] Logger:@"need no repair"]; // 記錄啓動時刻,用於計算啓動連續 crash // 重置啓動 crash 計數 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(self.crashOnLaunchTimeIntervalThreshold * NSEC_PER_SEC)), dispatch_get_main_queue(), ^(void){ // APP活過了閾值時間,重置崩潰計數 NSString *logString = [NSString stringWithFormat:@"CYLBootingProtection: long live the app ( more than %@ seconds ), now reset crash counts", @(self.crashOnLaunchTimeIntervalThreshold)]; [[self class] Logger:logString]; [self resetCrashCount]; }); } } //減小計數的時機:用戶手動劃掉APP - (void)decreaseCrashCount { NSUInteger oldCrashCount = [self crashCount]; [self decreaseCrashCountWithOldCrashCount:oldCrashCount]; } - (void)decreaseCrashCountWithOldCrashCount:(NSUInteger)oldCrashCount { dispatch_sync(_exceptionOperationQueue, ^{ if (oldCrashCount > 0) { [self setCrashCount:oldCrashCount-1]; } [self resetBootingProtectionStatus]; }); } //重製計數的時機:修復完成、或者用戶手動劃掉APP - (void)resetCrashCount { [self setCrashCount:0]; [self resetBootingProtectionStatus]; } //只在未達到計數上限時纔會增長計數 - (void)increaseCrashCount:(NSUInteger)oldCrashCount { dispatch_sync(_exceptionOperationQueue, ^{ [self setIsNormalChecking:YES]; [self setCrashCount:oldCrashCount+1]; }); } - (void)resetBootingProtectionStatus { [self setIsNormalChecking:NO]; [self setIsFixing:NO]; } - (BootingProtectionStatus)bootingProtectionStatus { return [[self class] bootingProtectionStatusWithContext:_context continuousCrashOnLaunchNeedToFix:_continuousCrashOnLaunchNeedToFix]; } /*! * @attention 注意之因此要檢查 `BootingProtectionStatusNormalChecking` 緣由以下: `-launchContinuousCrashProtect` 方法與 `-bootingProtectionStatus` 方法,若是 `-launchContinuousCrashProtect` 先執行,那麼會形成以下問題: 假設n爲上限,但crash(n-1)次,可是用 `-bootingProtectionStatus` 判斷出來,當前已經處於n次了。緣由以下: crash(n-1)次,正常流程,計數+1,變成n次, 隨後在檢查 `-bootingProtectionStatus` 時,發現已經處於異常狀態了,實際是正常狀態。因此須要使用`BootingProtectionStatusNormalChecking` 來進行區分。 */ + (BootingProtectionStatus)bootingProtectionStatusWithContext:(NSString *)context continuousCrashOnLaunchNeedToFix:(NSUInteger)continuousCrashOnLaunchNeedToFix { BOOL isNormalChecking = [self isNormalCheckingWithContext:context]; if (isNormalChecking) { return BootingProtectionStatusNormalChecking; } BOOL isUpToBootingProtectionCount = [self isUpToBootingProtectionCountWithContext:context continuousCrashOnLaunchNeedToFix:continuousCrashOnLaunchNeedToFix]; if (!isUpToBootingProtectionCount) { return BootingProtectionStatusNormal; } BootingProtectionStatus type; BOOL isFixingCrash = [self isFixingCrashWithContext:context]; if (isFixingCrash) { type = BootingProtectionStatusFixing; } else { type = BootingProtectionStatusNeedFix; } return type; } - (NSUInteger)crashCount { return [[self class] crashCountWithContext:_context]; } - (void)setCrashCount:(NSInteger)count { if (count >=0) { [[self class] setCrashCount:count context:_context]; } } - (void)setIsFixing:(BOOL)isFixingCrash { [[self class] setIsFixing:isFixingCrash context:_context]; } /*! * 是否正在修復 */ - (BOOL)isFixingCrash { return [[self class] isFixingCrashWithContext:_context]; } - (void)setIsNormalChecking:(BOOL)isNormalChecking { [[self class] setIsNormalChecking:isNormalChecking context:_context]; } /*! * 是否正在檢查 */ - (BOOL)isNormalChecking { return [[self class] isNormalCheckingWithContext:_context]; } + (NSUInteger)crashCountWithContext:(NSString *)context { NSString *continuousCrashOnLaunchCounterKey = [self continuousCrashOnLaunchCounterKeyWithContext:context]; NSUInteger crashCount = [[NSUserDefaults standardUserDefaults] integerForKey:continuousCrashOnLaunchCounterKey]; NSString *logString = [NSString stringWithFormat:@"crashCount:%@", @(crashCount)]; [[self class] Logger:logString]; return crashCount; } + (void)setCrashCount:(NSUInteger)count context:(NSString *)context { NSString *continuousCrashOnLaunchCounterKey = [self continuousCrashOnLaunchCounterKeyWithContext:context]; NSString *logString = [NSString stringWithFormat:@"setCrashCount:%@", @(count)]; [[self class] Logger:logString]; NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; [defaults setInteger:count forKey:continuousCrashOnLaunchCounterKey]; [defaults synchronize]; } + (void)setIsFixing:(BOOL)isFixingCrash context:(NSString *)context { NSString *continuousCrashFixingKey = [[self class] continuousCrashFixingKeyWithContext:context]; NSString *logString = [NSString stringWithFormat:@"setisFixingCrash:{%@}", @(isFixingCrash)]; [[self class] Logger:logString]; NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; [defaults setBool:isFixingCrash forKey:continuousCrashFixingKey]; [defaults synchronize]; } + (BOOL)isFixingCrashWithContext:(NSString *)context { NSString *continuousCrashFixingKey = [[self class] continuousCrashFixingKeyWithContext:context]; BOOL isFixingCrash = [[NSUserDefaults standardUserDefaults] boolForKey:continuousCrashFixingKey]; NSString *logString = [NSString stringWithFormat:@"isFixingCrash:%@", @(isFixingCrash)]; [[self class] Logger:logString]; return isFixingCrash; } + (void)setIsNormalChecking:(BOOL)isNormalChecking context:(NSString *)context { NSString *continuousCrashNormalCheckingKey = [[self class] continuousCrashNormalCheckingKeyWithContext:context]; NSString *logString = [NSString stringWithFormat:@"setIsNormalChecking:{%@}", @(isNormalChecking)]; [[self class] Logger:logString]; NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; [defaults setBool:isNormalChecking forKey:continuousCrashNormalCheckingKey]; [defaults synchronize]; } + (BOOL)isNormalCheckingWithContext:(NSString *)context { NSString *continuousCrashFixingKey = [[self class] continuousCrashNormalCheckingKeyWithContext:context]; BOOL isFixingCrash = [[NSUserDefaults standardUserDefaults] boolForKey:continuousCrashFixingKey]; NSString *logString = [NSString stringWithFormat:@"isIsNormalChecking:%@", @(isFixingCrash)]; [[self class] Logger:logString]; return isFixingCrash; } - (BOOL)isUpToBootingProtectionCount { return [[self class] isUpToBootingProtectionCountWithContext:_context continuousCrashOnLaunchNeedToFix:_continuousCrashOnLaunchNeedToFix]; } + (BOOL)isUpToBootingProtectionCountWithContext:(NSString *)context continuousCrashOnLaunchNeedToFix:(NSUInteger)continuousCrashOnLaunchNeedToFix { BOOL isUpToCount = [self crashCountWithContext:context] >= continuousCrashOnLaunchNeedToFix; if (isUpToCount) { return YES; } return NO; } - (void)setReportBlock:(ABSReportBlock)block { _reportBlock = block; } - (void)setRepairBlock:(ABSRepairBlock)block { _repairBlock = block; } /*! * 「連續啓動崩潰計數」個數,對應的Key * 默認爲 "_CONTINUOUS_CRASH_COUNTER_KEY" */ + (NSString *)continuousCrashOnLaunchCounterKeyWithContext:(NSString *)context { BOOL isValid = [[self class] isValidString:context]; NSString *validContext = isValid ? context : @""; NSString *continuousCrashOnLaunchCounterKey = [NSString stringWithFormat:@"%@_CONTINUOUS_CRASH_COUNTER_KEY", validContext]; return continuousCrashOnLaunchCounterKey; } /*! * 是否正在修復記錄,對應的Key * 默認爲 "_CONTINUOUS_CRASH_FIXING_KEY" */ + (NSString *)continuousCrashFixingKeyWithContext:(NSString *)context { BOOL isValid = [[self class] isValidString:context]; NSString *validContext = isValid ? context : @""; NSString *continuousCrashFixingKey = [NSString stringWithFormat:@"%@_CONTINUOUS_CRASH_FIXING_KEY", validContext]; return continuousCrashFixingKey; } /*! * 是否正在檢查是否在特定時間內會Crash,對應的Key * 默認爲 "_CONTINUOUS_CRASH_CHECKING_KEY" */ + (NSString *)continuousCrashNormalCheckingKeyWithContext:(NSString *)context { BOOL isValid = [[self class] isValidString:context]; NSString *validContext = isValid ? context : @""; NSString *continuousCrashFixingKey = [NSString stringWithFormat:@"%@_CONTINUOUS_CRASH_CHECKING_KEY", validContext]; return continuousCrashFixingKey; } #pragma mark - #pragma mark - log and util Methods + (void)setLogger:(void (^)(NSString *))logger { Logger = [logger copy]; } + (void)Logger:(NSString *)log { if (Logger) Logger(log); } + (BOOL)isValidString:(id)notValidString { if (!notValidString) { return NO; } if (![notValidString isKindOfClass:[NSString class]]) { return NO; } NSInteger stringLength = 0; @try { stringLength = [notValidString length]; } @catch (NSException *exception) {} if (stringLength == 0) { return NO; } return YES; } @end
下面是相應的驗證步驟:
等待15秒會有對應計數清零的操做日誌輸出:
2018-01-18 16:25:37.162980+0800 BootingProtection[89773:15553277] ?類名與方法名:-[AppDelegate onBeforeBootingProtection]_block_invoke(在第45行),描述:CYLBootingProtection: Launch continuous crash report 2018-01-18 16:25:37.163140+0800 BootingProtection[89773:15553277] ?類名與方法名:-[AppDelegate onBeforeBootingProtection]_block_invoke(在第45行),描述:setIsNormalChecking:{0} 2018-01-18 16:25:37.165738+0800 BootingProtection[89773:15553277] ?類名與方法名:-[AppDelegate onBeforeBootingProtection]_block_invoke(在第45行),描述:setisFixingCrash:{0} 2018-01-18 16:25:37.166883+0800 BootingProtection[89773:15553277] ?類名與方法名:-[AppDelegate onBeforeBootingProtection]_block_invoke(在第45行),描述:crashCount:0 2018-01-18 16:25:37.167102+0800 BootingProtection[89773:15553277] ?類名與方法名:-[AppDelegate onBeforeBootingProtection]_block_invoke(在第45行),描述:crashCount:0 2018-01-18 16:25:37.167253+0800 BootingProtection[89773:15553277] ?類名與方法名:-[AppDelegate onBeforeBootingProtection]_block_invoke(在第45行),描述:setIsNormalChecking:{1} 2018-01-18 16:25:37.167938+0800 BootingProtection[89773:15553277] ?類名與方法名:-[AppDelegate onBeforeBootingProtection]_block_invoke(在第45行),描述:setCrashCount:1 2018-01-18 16:25:37.168806+0800 BootingProtection[89773:15553277] ?類名與方法名:-[AppDelegate onBeforeBootingProtection]_block_invoke(在第45行),描述:need no repair 2018-01-18 16:25:52.225197+0800 BootingProtection[89773:15553277] ?類名與方法名:-[AppDelegate onBeforeBootingProtection]_block_invoke(在第45行),描述:CYLBootingProtection: long live the app ( more than 15 seconds ), now reset crash counts 2018-01-18 16:25:52.225378+0800 BootingProtection[89773:15553277] ?類名與方法名:-[AppDelegate onBeforeBootingProtection]_block_invoke(在第45行),描述:setCrashCount:0 2018-01-18 16:25:52.226234+0800 BootingProtection[89773:15553277] ?類名與方法名:-[AppDelegate onBeforeBootingProtection]_block_invoke(在第45行),描述:setIsNormalChecking:{0} 2018-01-18 16:25:52.226595+0800 BootingProtection[89773:15553277] ?類名與方法名:-[AppDelegate onBeforeBootingProtection]_block_invoke(在第45行),描述:setisFixingCrash:{0}
閱讀原文