最近基于第三方崩溃收集平台BugTag
做了一开源项目,功能极其的相似。
目前分为三个端:
-
Web
前端因为对前端一概不知,基于快速搭建框架
LayUI
搭建起来的,只能凑合的用,代码实现极其的烂。 -
Swift
服务器服务器是基于
Swift
语言框架Perfect
第三方的服务器框架,搭建起来的。虽然不是很流行,但是对于我不用其他服务器语言来说,真是大大福音。 -
iOS SDK
是基于
Objective-C
语言制作的崩溃上报信息SDK
对于 Web
前端和 Swift
的服务器没有什么可以说的,主要说一下对于 iOS SDK
封装的一些心得。
iOS SDK包含的功能
- 崩溃自动上报
- 主动上报
崩溃信息包含
- 崩溃当前界面的截屏
- 用户从启动到上报前的操作行为流
- 用户打印的
Log
日志 - 用户的请求信息
- 用户的设备信息
崩溃时当前的屏幕截图
这个功能十分的好做,只要对当前的 UIWindow 进行截屏即可。
- (UIImage *)getCurrentViewImage {
UIImage *image;
UIView *view = [UIApplication sharedApplication].keyWindow;
CGRect screenCaptureRect = view.bounds;
UIGraphicsBeginImageContextWithOptions(screenCaptureRect.size, NO, 0.0f);
[view.layer renderInContext:UIGraphicsGetCurrentContext()];
image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
用户设备信息
我们可以引用第三方库作为支持
pod 'GBDeviceInfo'
GBDeviceInfo *device = [GBDeviceInfo deviceInfo];
-
设备版本
[NSString stringWithFormat:@"%@.%@.%@",@(device.osVersion.major),@(device.osVersion.minor),@(device.osVersion.patch)]
-
设备名称
device.modelString
-
屏幕大小
[NSString stringWithFormat:@"%@x%@",@([UIScreen mainScreen].currentMode.size.width),@([UIScreen mainScreen].currentMode.size.height)]
-
可用内存
[NSString stringWithFormat:@"%@G",@(device.physicalMemory)]
-
CPU频率
[NSString stringWithFormat:@"%@GHZ",@(device.cpuInfo.frequency)]
-
CPU内核数量
[NSString stringWithFormat:@"%@个",@(device.cpuInfo.numberOfCores)]
-
CPU二级缓存大小
[NSString stringWithFormat:@"%@KB",@(device.cpuInfo.l2CacheSize)]
-
App版本大小
[[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"]
-
SDK版本大小
[[[NSBundle bundleForClass:NSClassFromString(@"GlobalegrowBugTag")] infoDictionary] objectForKey:@"CFBundleShortVersionString"]
-
电池电量信息
[@([UIDevice currentDevice].batteryLevel) stringValue]
-
设备的 UUID 信息
[UIDevice currentDevice].identifierForVendor.UUIDString
用户操作流程
因为我们上报的用户操作流程 用户日志 用户的请求信息都是基于用户针对于 App 的一次会话来说的。所以我们可以把这些信息储存在一个临时目录,在 App 启动的时候清理。
我们既然需要做自动上报的 SDK,就尽可能的不能让接入方做配置。这里用的是尽可能,因为下面的日志系统还是需要接入方简单的配置一下的。
目前我们需要用户的操作流程并不是很多,主要上报用户切换到那个 Tab或者 Push 或者 Pop 到哪一个界面和用户模态弹出那个界面。
对于切换 Tab,我们可以监听UITabBarController
的代理,这里就有一个问题,我这里监听了UITabBarController
的代理,那么其他人就没法得到回调。
在这里我们在重写代理之前,保存之前已经赋值过的代理。
- (void)listenTabbarController:(UITabBarController *)tabbarController {
if (![_tabbarController isEqual:tabbarController]) {
if (_tabbarController) {
_tabbarController.delegate = _delegate;
}
_tabbarController = tabbarController;
_delegate = tabbarController.delegate;
tabbarController.delegate = self;
[self switchTabbarWithViewController:tabbarController.selectedViewController];
}
}
- (void)switchTabbarWithViewController:(UIViewController *)viewController {
NSString *className = ({
className = NSStringFromClass([viewController class]);
if ([viewController isKindOfClass:[UINavigationController class]]) {
UINavigationController *nav = (UINavigationController *)viewController;
className = NSStringFromClass([nav.topViewController class]);
}
className;
});
[[NSNotificationCenter defaultCenter] postNotificationName:GlobalegrowListenTabbarControllerDidSelectedNotification
object:[NSString stringWithFormat:@"tabbar switch %@",className]];
}
#pragma mark - UITabBarControllerDelegate
- (BOOL)tabBarController:(UITabBarController *)tabBarController shouldSelectViewController:(UIViewController *)viewController {
if (_delegate && [_delegate respondsToSelector:@selector(tabBarController:shouldSelectViewController:)]) {
return [_delegate tabBarController:tabBarController shouldSelectViewController:viewController];
}
return YES;
}
- (void)tabBarController:(UITabBarController *)tabBarController didSelectViewController:(UIViewController *)viewController {
[self switchTabbarWithViewController:viewController];
if (_delegate && [_delegate respondsToSelector:@selector(tabBarController:didSelectViewController:)]) {
[_delegate tabBarController:tabBarController didSelectViewController:viewController];
}
}
- (void)tabBarController:(UITabBarController *)tabBarController willBeginCustomizingViewControllers:(NSArray<__kindof UIViewController *> *)viewControllers {
if (_delegate && [_delegate respondsToSelector:@selector(tabBarController:willBeginCustomizingViewControllers:)]) {
[_delegate tabBarController:tabBarController willBeginCustomizingViewControllers:viewControllers];
}
}
- (void)tabBarController:(UITabBarController *)tabBarController willEndCustomizingViewControllers:(NSArray<__kindof UIViewController *> *)viewControllers changed:(BOOL)changed {
if (_delegate && [_delegate respondsToSelector:@selector(tabBarController:willEndCustomizingViewControllers:changed:)]) {
[_delegate tabBarController:tabBarController willEndCustomizingViewControllers:viewControllers changed:changed];
}
}
- (void)tabBarController:(UITabBarController *)tabBarController didEndCustomizingViewControllers:(NSArray<__kindof UIViewController *> *)viewControllers changed:(BOOL)changed {
if (_delegate && [_delegate respondsToSelector:@selector(tabBarController:didEndCustomizingViewControllers:changed:)]) {
[_delegate tabBarController:tabBarController didEndCustomizingViewControllers:viewControllers changed:changed];
}
}
- (nullable id <UIViewControllerInteractiveTransitioning>)tabBarController:(UITabBarController *)tabBarController
interactionControllerForAnimationController: (id <UIViewControllerAnimatedTransitioning>)animationController {
if (_delegate && [_delegate respondsToSelector:@selector(tabBarController:interactionControllerForAnimationController:)]) {
return [_delegate tabBarController:tabBarController interactionControllerForAnimationController:animationController];
}
return nil;
}
- (nullable id <UIViewControllerAnimatedTransitioning>)tabBarController:(UITabBarController *)tabBarController
animationControllerForTransitionFromViewController:(UIViewController *)fromVC
toViewController:(UIViewController *)toVC {
if (_delegate && [_delegate respondsToSelector:@selector(tabBarController:fromVC:toVC:)]) {
return [_delegate tabBarController:tabBarController animationControllerForTransitionFromViewController:fromVC toViewController:toVC];
}
return nil;
}
为了尽可能的覆盖用到的代理方法,我们把重用的都重新走一遍。我们在需要的代理回调的地方,通过通知把当前的页面的类传出去。
在我们接受通知之后,写入到我们的操作日志的文件里面。
请求信息
这个对于使用 AFNetworking
框架的用户就好办的多了,就算不是使用 NSURLSessionTask
也是支持的,多一步配置而已。
我们新建一个类实现 AFNetworkActivityLoggerProtocol
协议。
我们在下面的两个方法里面处理我们收到的请求信息即可
- (void)URLSessionTaskDidStart:(NSURLSessionTask *)task
- (void)URLSessionTaskDidFinish:(NSURLSessionTask *)task withResponseObject:(id)responseObject inElapsedTime:(NSTimeInterval )elapsedTime withError:(NSError *)error
用户的日志信息
这个自动化手机就有点难办,搜了很多资料,对于拦截 Apple System Log
系统会让用户无法再控制台打印 Log,这十分的不方便。目前的做法只能是写一个类似 NSLog
的宏来代替 NSLog
.
#define GBT_LOG(formatter,...) GBT_LOG_PRINT([NSDate date], [self class], __LINE__,formatter,##__VA_ARGS__)
NSString *GBT_LOG_PRINT(NSDate *date, Class class, NSUInteger line,NSString *formatter,...) {
va_list args;
va_start(args, formatter);
NSString *log = [[NSString alloc] initWithFormat:formatter arguments:args];
if (log.length > 0) {
NSString *printLog = [NSString stringWithFormat:@"%@ %@ %@ %@",date,class,@(line),log];
GlobalegrowBugTagLoggerModel *logger = [[GlobalegrowBugTagLoggerModel alloc] init];
logger.log = printLog;
[[GlobalegrowBugTag shareGlobalegrowBugTag] writeLogInLogFile:logger];
#ifdef DEBUG
NSLog(@"%@",log);
#else
if ([NSProcessInfo processInfo].environment[@"GBT_LOG"]) {
NSLog(@"%@",log);
}
#endif
}
va_end(args);
return log;
}
我们对于 Release
只要在 Xcode
的运行变量里面开启
#define GBT_LOG(formatter,...) GBT_LOG_PRINT([NSDate date], [self class], __LINE__,formatter,##__VA_ARGS__)
NSString *GBT_LOG_PRINT(NSDate *date, Class class, NSUInteger line,NSString *formatter,...) {
va_list args;
va_start(args, formatter);
NSString *log = [[NSString alloc] initWithFormat:formatter arguments:args];
if (log.length > 0) {
NSString *printLog = [NSString stringWithFormat:@"%@ %@ %@ %@",date,class,@(line),log];
GlobalegrowBugTagLoggerModel *logger = [[GlobalegrowBugTagLoggerModel alloc] init];
logger.log = printLog;
[[GlobalegrowBugTag shareGlobalegrowBugTag] writeLogInLogFile:logger];
#ifdef DEBUG
NSLog(@"%@",log);
#else
if ([NSProcessInfo processInfo].environment[@"GBT_LOG"]) {
NSLog(@"%@",log);
}
#endif
}
va_end(args);
return log;
}
我们对于 Release
只要在 Xcode 的运行变量里面开启GBT_LOG
变量,还是允许输入 Log 信息的。这样即使在线上,我们依然可以收集用户的 Log.
只要接入方全部使用我们的 Log宏,对于一些特殊的要求的可以使用我们的返回值再次使用即可。
自动收集崩溃
之前做原生支付 SDK 的时候,遇到要主动上报崩溃。但是我们拦截了崩溃,别人怎么拦截。或者第三方拦截,我们怎么接受的问题。
这个崩溃流就好比堆栈,后来的先接受。如果接收到不抛出,前面就无法接受到崩溃,则崩溃流中断。我们为了防止第三方没有抛出崩溃信息,我们可以让接入方把代码卸载入口最下面。
static NSUncaughtExceptionHandler *GlobalegrowPreviousHandler;
void GlobalegrowHandleException(NSException *exception) {
NSString *json = [NSString stringWithFormat:@"reason:\n%@\n\n\callStackSymbols:\n%@",exception.reason,exception.callStackSymbols];
[[NSNotificationCenter defaultCenter] postNotificationName:GlobalegrowExceptionNotificationName object:json];
GlobalegrowPreviousHandler(exception);
}
void GlobalegrowRegisterSignalHandler(void) {
GlobalegrowPreviousHandler = NSGetUncaughtExceptionHandler();
NSSetUncaughtExceptionHandler(&GlobalegrowHandleException);
}
我们拿到崩溃信息之后,我们再次的抛出即可。