最近基于第三方崩溃收集平台BugTag 做了一开源项目,功能极其的相似。

App崩溃信息收集平台

目前分为三个端:

  • 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;
}

为了尽可能的覆盖用到的代理方法,我们把重用的都重新走一遍。我们在需要的代理回调的地方,通过通知把当前的页面的类传出去。

在我们接受通知之后,写入到我们的操作日志的文件里面。

image-20190420093247987

请求信息

这个对于使用 AFNetworking框架的用户就好办的多了,就算不是使用 NSURLSessionTask也是支持的,多一步配置而已。

我们新建一个类实现 AFNetworkActivityLoggerProtocol协议。

我们在下面的两个方法里面处理我们收到的请求信息即可

- (void)URLSessionTaskDidStart:(NSURLSessionTask *)task
- (void)URLSessionTaskDidFinish:(NSURLSessionTask *)task withResponseObject:(id)responseObject inElapsedTime:(NSTimeInterval )elapsedTime withError:(NSError *)error
image-20190420094123804

用户的日志信息

这个自动化手机就有点难办,搜了很多资料,对于拦截 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);
}

我们拿到崩溃信息之后,我们再次的抛出即可。