開端
因為最近APP總會有很多Crash信息定位在主函數(shù)main里面,所以發(fā)現(xiàn)很多出現(xiàn)的BUG不好定位修改,所以就想通過iOS的runtime機制,自己寫個防crash機制,并且收集信息.
當然通過google,和百度的一系列查詢后,大概Copy了個簡單的Crash信息防護,和收集系統(tǒng),借鑒的資料有網(wǎng)易一個團隊的大白系統(tǒng) http://www.reibang.com/p/02157577d4e7 (我記得他們說要開源,等了好久,還沒有消息),
還有chenfanfang的很多文章 和春田花花幼兒園的簡書
通過runtime如何防止Crash
相信做個iOS開發(fā)的朋友都知道,OC是個動態(tài)語言,其中很主要的就是消息機制,在運行的過程中通過消息機制動態(tài)的調(diào)用對應函數(shù).所以我們就可以想辦法在處理對應函數(shù)的時候,替換掉系統(tǒng)的方法來執(zhí)行.
如果出現(xiàn)錯誤,我們可以把錯誤信息獲取到,同時讓Crash的方法不在繼續(xù)執(zhí)行,無效化.
示例代碼
比如我們創(chuàng)建個可變數(shù)組,之后再插入數(shù)據(jù),因為數(shù)組越界會造成程序crash,并且控制臺提示信息.
NSMutableArray *muArray = [NSMutableArray new];
[muArray setObject:@"crash" atIndexedSubscript:1];
控制臺信息
reason: '*** -[__NSArrayM setObject:atIndexedSubscript:]: index 1 beyond bounds for empty array'
接下來就是我們創(chuàng)造個類,來替換掉系統(tǒng)NSMutableArray的setObject 方法
#import <Foundation/Foundation.h>
@interface NSMutableArray (Crash)
@end
@implementation 就是對應的作用域,@implementation NSMutableArray意思就是對應所有可變數(shù)組.
#import "NSMuableArray+Crash.h"
#import <objc/runtime.h>
#define AvoidCrashSeparator @"================================================================"
#define AvoidCrashSeparatorWithFlag @"========================AvoidCrash Log=========================="
#define AvoidCrashDefaultIgnore @"This framework default is to ignore this operation to avoid crash."
#define key_errorName @"errorName"
#define key_errorReason @"errorReason"
#define key_errorPlace @"errorPlace"
#define key_defaultToDo @"defaultToDo"
#define key_callStackSymbols @"callStackSymbols"
#define key_exception @"exception"
@implementation NSMutableArray (Crash)
+(void)load
{
// 執(zhí)行一次.
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class muArrayClass = NSClassFromString(@"__NSArrayM");
SEL originalMethodSel = @selector(setObject:atIndexedSubscript:);
SEL swizzledMethodSel = @selector(KsetObject:atIndexedSubscript:);
Method originalMethod = class_getInstanceMethod(muArrayClass, originalMethodSel);
Method swizzledMethod = class_getInstanceMethod(muArrayClass, swizzledMethodSel);
BOOL didAddMethod =
class_addMethod(muArrayClass,
originalMethodSel,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(muArrayClass,
originalMethodSel,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
}
else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
- (void)KsetObject:(id)object atIndexedSubscript:(NSInteger)index{
// 可能crash的方法,并且獲取crash的信息
@try {
// 因為交換過方法,所以在此調(diào)用這個其實是調(diào)用的系統(tǒng)原先的方法.
[self KsetObject:object atIndexedSubscript:index];
} @catch (NSException *exception) {
[self noteErrorWithException:exception defaultToDo:AvoidCrashDefaultIgnore];
} @finally {
// 這里面的代碼一定會執(zhí)行.
}
}
/**
* 獲取堆棧主要崩潰精簡化的信息<根據(jù)正則表達式匹配出來>
*
* @param callStackSymbols 堆棧主要崩潰信息
*
* @return 堆棧主要崩潰精簡化的信息
*/
- (NSString *)getMainCallStackSymbolMessageWithCallStackSymbols:(NSArray<NSString *> *)callStackSymbols {
//mainCallStackSymbolMsg的格式為 +[類名 方法名] 或者 -[類名 方法名]
__block NSString *mainCallStackSymbolMsg = nil;
//匹配出來的格式為 +[類名 方法名] 或者 -[類名 方法名]
NSString *regularExpStr = @"[-\\+]\\[.+\\]";
NSRegularExpression *regularExp = [[NSRegularExpression alloc] initWithPattern:regularExpStr options:NSRegularExpressionCaseInsensitive error:nil];
for (int index = 2; index < callStackSymbols.count; index++) {
NSString *callStackSymbol = callStackSymbols[index];
[regularExp enumerateMatchesInString:callStackSymbol options:NSMatchingReportProgress range:NSMakeRange(0, callStackSymbol.length) usingBlock:^(NSTextCheckingResult * _Nullable result, NSMatchingFlags flags, BOOL * _Nonnull stop) {
if (result) {
NSString* tempCallStackSymbolMsg = [callStackSymbol substringWithRange:result.range];
//get className
NSString *className = [tempCallStackSymbolMsg componentsSeparatedByString:@" "].firstObject;
className = [className componentsSeparatedByString:@"["].lastObject;
NSBundle *bundle = [NSBundle bundleForClass:NSClassFromString(className)];
//filter category and system class
if (![className hasSuffix:@")"] && bundle == [NSBundle mainBundle]) {
mainCallStackSymbolMsg = tempCallStackSymbolMsg;
}
*stop = YES;
}
}];
if (mainCallStackSymbolMsg.length) {
break;
}
}
return mainCallStackSymbolMsg;
}
/**
* 提示崩潰的信息(控制臺輸出、通知)
*
* @param exception 捕獲到的異常
* @param defaultToDo 這個框架里默認的做法
*/
- (void)noteErrorWithException:(NSException *)exception defaultToDo:(NSString *)defaultToDo {
//堆棧數(shù)據(jù)
NSArray *callStackSymbolsArr = [NSThread callStackSymbols];
//獲取在哪個類的哪個方法中實例化的數(shù)組 字符串格式 -[類名 方法名] 或者 +[類名 方法名]
NSString *mainCallStackSymbolMsg = [self getMainCallStackSymbolMessageWithCallStackSymbols:callStackSymbolsArr];
if (mainCallStackSymbolMsg == nil) {
mainCallStackSymbolMsg = @"崩潰方法定位失敗,請您查看函數(shù)調(diào)用棧來排查錯誤原因";
}
NSString *errorName = exception.name;
NSString *errorReason = exception.reason;
//errorReason 可能為 -[__NSCFConstantString avoidCrashCharacterAtIndex:]: Range or index out of bounds
//將avoidCrash去掉
errorReason = [errorReason stringByReplacingOccurrencesOfString:@"avoidCrash" withString:@""];
NSString *errorPlace = [NSString stringWithFormat:@"Error Place:%@",mainCallStackSymbolMsg];
NSDictionary *errorInfoDic = @{
key_errorName : errorName,
key_errorReason : errorReason,
key_errorPlace : errorPlace,
key_defaultToDo : defaultToDo,
key_exception : exception,
key_callStackSymbols : callStackSymbolsArr
};
//將錯誤信息放在字典里,用通知的形式發(fā)送出去
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"%@",errorInfoDic);
});
}
@end
數(shù)組越界APP并沒有Crash,打印exception信息
2017-07-27 10:21:00.099509+0800 CrashDemo[6760:791263] {
callStackSymbols = (
0 CrashDemo 0x000000010091bf10 -[NSMutableArray(NSMutableArray_Crash) noteErrorWithException:defaultToDo:] + 144
1 CrashDemo 0x000000010091b634 -[NSMutableArray(NSMutableArray_Crash) KsetObject:atIndexedSubscript:] + 296
2 CrashDemo 0x000000010091b014 -[ViewController viewDidLoad] + 144
3 UIKit 0x000000018d2fd184 <redacted> + 1040
4 UIKit 0x000000018d2fcd5c <redacted> + 28
5 UIKit 0x000000018d303a8c <redacted> + 136
6 UIKit 0x000000018d300cf8 <redacted> + 272
7 UIKit 0x000000018d370664 <redacted> + 48
8 UIKit 0x000000018d55f3a4 <redacted> + 3616
9 UIKit 0x000000018d56414c <redacted> + 1712
10 UIKit 0x000000018d7e5780 <redacted> + 136
11 UIKit 0x000000018daaaec4 <redacted> + 160
12 UIKit 0x000000018d7e567c <redacted> + 252
13 UIKit 0x000000018d7e5b20 <redacted> + 756
14 UIKit 0x000000018df26978 <redacted> + 244
15 UIKit 0x000000018df2682c <redacted> + 448
16 UIKit 0x000000018dcb56b8 <redacted> + 220
17 UIKit 0x000000018de4262c _performActionsWithDelayForTransitionContext + 112
18 UIKit 0x000000018dcb5568 <redacted> + 252
19 UIKit 0x000000018daaa544 <redacted> + 364
20 UIKit 0x000000018d562890 <redacted> + 540
21 UIKit 0x000000018d953214 <redacted> + 364
22 FrontBoardServices 0x0000000185ea2968 <redacted> + 364
23 FrontBoardServices 0x0000000185eab270 <redacted> + 224
24 libdispatch.dylib 0x00000001009d18ac _dispatch_client_callout + 16
25 libdispatch.dylib 0x00000001009dde84 _dispatch_block_invoke_direct + 232
26 FrontBoardServices 0x0000000185ed6b04 <redacted> + 36
27 FrontBoardServices 0x0000000185ed67a8 <redacted> + 404
28 FrontBoardServices 0x0000000185ed6d44 <redacted> + 56
29 CoreFoundation 0x00000001837e0a80 <redacted> + 24
30 CoreFoundation 0x00000001837e0a00 <redacted> + 88
31 CoreFoundation 0x00000001837e0288 <redacted> + 204
32 CoreFoundation 0x00000001837dde60 <redacted> + 1048
33 CoreFoundation 0x00000001836ff9dc CFRunLoopRunSpecific + 436
34 GraphicsServices 0x000000018555cfac GSEventRunModal + 100
35 UIKit 0x000000018d360ef0 UIApplicationMain + 208
36 CrashDemo 0x000000010091c418 main + 124
37 libdyld.dylib 0x000000018321ea14 <redacted> + 4
);
defaultToDo = "This framework default is to ignore this operation to avoid crash.";
errorName = NSRangeException;
errorPlace = "Error Place:-[ViewController viewDidLoad]";
errorReason = "*** -[__NSArrayM setObject:atIndexedSubscript:]: index 1 beyond bounds for empty array";
exception = "*** -[__NSArrayM setObject:atIndexedSubscript:]: index 1 beyond bounds for empty array";
}
這個信息通過正則表達式處理,可以獲取到自己想要的信息,包括造成崩潰的方法,具體位置.
我的這個處理方法是從chenfanfang那里copy的
個人感覺在crash的處理方面用起來還是很方便的,有時候需要在APP內(nèi)做統(tǒng)計事件,比如點擊,比如頁面的進入次數(shù),這些其實可以通過在項目初期創(chuàng)建個很好的基類容器來實現(xiàn),但是如果后期加入,并且初期的基類并沒有很好的構造,這個時候就會發(fā)現(xiàn)runtime有很大的用處.
這篇文章其實主要就是用代碼來展示,主要原因還是作者很少寫文章,也不太會措辭,哈哈哈.
統(tǒng)計頁面進入示例
可以通過改寫系統(tǒng)的- (void)viewWillAppear:(BOOL)animated 方法.
#import "Statistics+ViewController.h"
#import <objc/runtime.h>
@implementation UIViewController (Statistics_ViewController)
+(void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method originalMethod = class_getInstanceMethod([self class], @selector(viewWillAppear:));
Method swizzledMethod = class_getInstanceMethod([self class], @selector(KviewWillAppear:));
method_exchangeImplementations(originalMethod, swizzledMethod);
});
}
- (void)KviewWillAppear:(BOOL)animated
{
[self KviewWillAppear:animated];
NSLog(@"進入%@",[self class]);
}
攔截tableview的點擊方法
tableview點擊方法因為是tableViewDelegate的方法,所以交換方法要先交換系統(tǒng)的Delegate方法,交換成功后再交換Cell的DidSelect方法.
#import "DJ+TableView.h"
#import <objc/runtime.h>
#import <objc/message.h>
@implementation UITableView (DJ_TableView)
+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method originalMethod = class_getInstanceMethod(self, @selector(setDelegate:));
Method swizzledMethod = class_getInstanceMethod(self, @selector(DJsetDelegate:));
BOOL didAddMethod =
class_addMethod(self,
@selector(setDelegate:),
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(self,
@selector(DJsetDelegate:),
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
}
else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
- (void)DJsetDelegate:(id<UITableViewDelegate>)delegate
{
[self DJsetDelegate:delegate];
if (class_addMethod([delegate class], NSSelectorFromString(@"DJdidSelectRowAtIndexPath"), (IMP)DJdidSelectRowAtIndexPath, "v@:@@")) {
Method didSelectOriginalMethod = class_getInstanceMethod([delegate class], NSSelectorFromString(@"DJdidSelectRowAtIndexPath"));
Method didSelectSwizzledMethod = class_getInstanceMethod([delegate class], @selector(tableView:didSelectRowAtIndexPath:));
method_exchangeImplementations(didSelectOriginalMethod, didSelectSwizzledMethod);
}
}
void DJdidSelectRowAtIndexPath(id self, SEL _cmd, id tableView, id indexPath)
{
SEL selector = NSSelectorFromString(@"DJdidSelectRowAtIndexPath");
((void(*)(id, SEL, id, id))objc_msgSend)(self, selector, tableView, indexPath);
NSLog(@"點擊了");
}
@end
runtime雖然很多人感覺是動用了系統(tǒng)層的語法,怕在使用過程中遇到未知的問題,但是我在使用過程中感覺還是利大于弊,需要對全局進行操作的時候方便很多.并且runtime所對應的AOP編程方式,也更適用于做這種架構的編程.