[转] 浅谈iOS日志收集系统

原文地址

在应用开发中为了给用户更好操作体验与精准信息的展示,往往会收集一些用户行为信息,比如应用中用户习惯的操作流程,相关页面访问次数,用户个人信息等等。大多数应用会集成第三方厂商提供的服务来统计这些数据,当然这样做带来的好处就是不用花费时间来写相关日志收集的功能,后台也不用专门搭建相关的服务,而且第三方提供的工具也比较稳定。这让我们能有更多的时间去开发产品主要业务功能上。

开发应用前期为了让产品快速的推向市场与不断的功能变更,往往不会花费时间在这些非主要业务的功能上。但当产品逐渐成熟进入到一个平台期,如果我们想要获取更多的用户增长与留存,就要想办法在针对自身的产品功能在不用的场景获取更多的用户行为来改进产品,获取更多用户信息来精准针对不同用户展示他们更感兴趣的内容。而且这些数据也不希望保存在其他厂商的服务器上。这样我们就不得不设计一套自己的日志收集系统。本篇博客只是讲解iOS日志收集框架设计的一些思路与实现。

日志分类

首先收集日志根据日志上传的时机分为实时日志与非实时日志:

  • 实时日志:收集结束后立刻上传到服务器。
  • 非实时日志:当日志累计到一定数量时上传到服务器。

对于实时日志来说文件大小实际上在一定范围之内的。而非实时日志由于是信息累积一段时间后才会上传到服务器,所以对于非实时日志我们需要控制日志的大小不能让日志文件无限增加。当然仅仅控制大小也是不行的,如果用户使用次数很少而且我们的数据要一天统计一次那么就会出现很多天都统计不到用户的相关数据。所以我们也要控制非实时日志的过期时间。如果日志已经过期但大小没有达到限制或者大小已经达到限制但没有到达过期时间都是要上传到服务器的。

对于实时日志与非实时日志上传服务器来说都要有相关的错误处理。对于实时日志来说,如果上传失败的话如果网络连接正常要尝试重新上传,当然这不是无限上传的要有重试的次数,如果超出重试次数,那么上传失败。对于非实时日志来说也是一样的处理逻辑。

更根据收集的数据来分类日志可以分为事件日志,用户信息日志,崩溃日志

  • 事件日志:也可以理解为用户行为数据。当用户使用某个功能或者进入某个页面时会收集相关的信息来统计每个用户使用应用时习惯性操作,与偏好的功能还有页面的PV等。当然也可以获取到每个用户在当前页面所停留的时间。来判断当前页面是否能过吸引用户。

  • 用户信息日志:这些信息主要是为了针对不同的用户来展示不同的功能或者内容,当然也包括当前使用机型的信息有些基本信息可以直接放在请求头的UserAgent中而不需要单独来统计。

  • 崩溃日志:应用崩溃信息。

日志收集框架

日志收集主要用了两个开源框架来实现:plcrashreporterCocoaLumberjack。plcrashreporter 主要用来崩溃日志收集,CocoaLumberjack 用来非崩溃日志收集。
下面将主要介绍这两个框架的使用。

CocoaLumberjack 框架

关于CocoaLumberjack的相关说明与使用主要参考了这里的文档。

首先是集成CocoaLumberjack主要有三个途径,CocoaPods,Carthage,与手动集成。集成这里不多描述参考这里

配置CocoaLumberjack框架

  • 将下面的代码添加到 .pch 文件中
#define LOG_LEVEL_DEF ddLogLevel
#import <CocoaLumberjack/CocoaLumberjack.h>

在后面我们设置ddLogLevel的优先级后,DDLog 宏会通过LOG_LEVEL_DEF来得知我们定义的优先级。

在程序启动时(一般在applicationDidFinishLaunching方法中)添加如下代码:

[DDLog addLogger:[DDASLLogger sharedInstance]];
[DDLog addLogger:[DDTTYLogger sharedInstance]];

这两行代码添加了两个loggers到框架中,也就是说你的日志会被发送到Mac系统的Console.app与Xcode的控制台(与NSLog的效果一样)中。
如果想把日志写入文件中可以用下面的logger:

DDFileLogger *fileLogger = [[DDFileLogger alloc] init];
fileLogger.rollingFrequency = 60 * 60 * 24;  // 每个文件超过24小时后会被新的日志覆盖
fileLogger.logFileManager.maximumNumberOfLogFiles = 7;  //最多保存7个日志文件
[DDLog addLogger:fileLogger];

我们主要使用 DDFileLogger 来记录相关事件,并且配合DDLogFileManager 来讲相关的事件上传到服务器。在后面会介绍相关的用法。

可以设置全局的日志等级,并且可以在单独的文件中修改日志等级。
在前面我们定义了LOG_LEVEL_DEF宏。
.pch 文件定义 ddLogLevel 常量:

static const DDLogLevel ddLogLevel = DDLogLevelDebug;

上面定义了全局的宏统一为 DDLogLevelDebug。

如果想要在不同的文件中更改日志的等级只需要在使用 DDLog 前修改
ddLogLevel 的值即可。关于日志输出一共5个语句,当然也可以自己自定义日志的语句:

- DDLogError
- DDLogWarn
- DDLogInfo
- DDLogDebug
- DDLogVerbose

将 NSLog 语句转换成 DDLog:

// Convert from this:
NSLog(@"Broken sprocket detected!");
NSLog(@"User selected file:%@ withSize:%u", filePath, fileSize);

// To this:
DDLogError(@"Broken sprocket detected!");
DDLogVerbose(@"User selected file:%@ withSize:%u", filePath, fileSize);

使用不同的日志等级将会看到不同的日志输出:

如果设置 DDLogLevelError 等级,那么只会看到 Error 语句
如果设置 DDLogLevelWarn 等级,那么会看到 Error 和 Warn 语句
如果设置 DDLogLevelInfo 等级,那么会看到 Error,Warn,Info 语句
如果设置 DDLogLevelDebug 等级,那么会看到 Error,Warn,Info,Debug语句
如果设置 DDLogLevelVerbose 等级,会看到所有的 DDLog 语句
如果设置 DDLogLevelOff 等级,不会看到任何 DDLog 语句

CocoaLumberjack 架构

这个框架的核心是 DDLog 文件,这个文件提供不同的 DDLog 宏定义来替换系你的NSLog语句,例如:

DDLogWarn(@"Specified file does not exist");

这个语句实际上起到一个筛选的作用只有在相关的等级下才会输出

if(LOG_WARN) /* Execute log statement */

而且当每输出一个 DDLog 语句时,DDLog 都会将日志消息转发给所有的前面注册的 logger。

Loggers

logger 是使用日志消息执行某些操作的类。CocoaLumberjack 带有几个少数的 loggers(当然也可以自定义logger)。例如:DDASLLogger,DDASLLogger。前面已经说过可以将消息发送到系统和Xcode的控制台。DDFileLogger 可以将消息写入到文件中。可以同时注册多个 logger。
当然也可以配置相关的 Logger。例如 DDFileLogger 自带很多选项来供设置。每一个 Logger 都可以设置一个 formatter。formatter 主要用来格式化日志信息的。

Formatters

Formatters 可以允许你在 Logger 接受日志前格式化日志信息。例如可以给日志添加时间戳或者过滤日志的一些不必要信息。
Formatters 可以单独应用不同的 loggers。可以为每个 logger 提供不同的Formatters。

自定义日志消息

每一个日志消息结构都有一个 context 字段。context 字段是一个整数,它与日志信息一起传递给 CocoaLumberjack 框架。因此可以自由的定义这个字段。
这个 context 字段可以使用在很多方面,这里列举了几个例子

有些应用模块化,有多个逻辑组件,如果每个组件都使用不同的 context 字段,那么如果使用这个框架很容易知道是哪一个模块打印的日志。可以根据来自不同模块的日志对日志进行不同的格式化处理。
如果开发一个框架给其他人使用,希望别人在使用你的框架时可以很清楚的知道你的框架都做了什么操作,这对发现和诊断问题很有帮助,根据自定义的 context 字段你很容易区分这个日志消息到底是来自于你开发的框架还是其他应用的日志信息。

日志消息结构

每一个日志消息都会转换成 DDLogMessage 对象

@interface DDLogMessage : NSObject <NSCopying>
{
    // Direct accessors to be used only for performance
    ...
}

@property (readonly, nonatomic) NSString *message;
@property (readonly, nonatomic) DDLogLevel level;
@property (readonly, nonatomic) DDLogFlag flag;
@property (readonly, nonatomic) NSInteger context;
@property (readonly, nonatomic) NSString *file;
@property (readonly, nonatomic) NSString *fileName;
@property (readonly, nonatomic) NSString *function;
@property (readonly, nonatomic) NSUInteger line;
@property (readonly, nonatomic) id tag;
@property (readonly, nonatomic) DDLogMessageOptions options;
@property (readonly, nonatomic) NSDate *timestamp;
@property (readonly, nonatomic) NSString *threadID; // ID as it appears in NSLog calculated from the machThreadID
@property (readonly, nonatomic) NSString *threadName;
@property (readonly, nonatomic) NSString *queueLabel;

可以注意到 context 这个字段,默认的这个字段每个信息都是0。

当然我们可以很容易自定义这个字段:

#define LSY_CONTEXT 100

#define LSYEventVerbose(frmt, ...) LOG_MAYBE(LOG_ASYNC_ENABLED, LOG_LEVEL_DEF, DDLogFlagVerbose, LSY_CONTEXT, nil, __PRETTY_FUNCTION__, frmt, ##__VA_ARGS__)

这样我们可以得到一个日志等级为 DDLogFlagVerboseLSYEventVerbose 宏。使用这个宏输出的日志得到的 context 字段值为100。

自定义Logger

Logger 允许你直接将日志消息指向任何地方。
DDLog 头文件中定义了 DDLoger 协议。由三个强制方法组成:

@protocol DDLogger <NSObject>
- (void)logMessage:(DDLogMessage *)logMessage;
/**
 * Formatters may optionally be added to any logger.
 *
 * If no formatter is set, the logger simply logs the message as it is given in logMessage,
 * or it may use its own built in formatting style.
 **/
@property (nonatomic, strong) id <DDLogFormatter> logFormatter;
@optional
/**
 * Since logging is asynchronous, adding and removing loggers is also asynchronous.
 * In other words, the loggers are added and removed at appropriate times with regards to log messages.
 *
 * - Loggers will not receive log messages that were executed prior to when they were added.
 * - Loggers will not receive log messages that were executed after they were removed.
 *
 * These methods are executed in the logging thread/queue.
 * This is the same thread/queue that will execute every logMessage: invocation.
 * Loggers may use these methods for thread synchronization or other setup/teardown tasks.
 **/
- (void)didAddLogger;
- (void)willRemoveLogger;
/**
 * Some loggers may buffer IO for optimization purposes.
 * For example, a database logger may only save occasionaly as the disk IO is slow.
 * In such loggers, this method should be implemented to flush any pending IO.
 *
 * This allows invocations of DDLog's flushLog method to be propogated to loggers that need it.
 *
 * Note that DDLog's flushLog method is invoked automatically when the application quits,
 * and it may be also invoked manually by the developer prior to application crashes, or other such reasons.
 **/
- (void)flush;
/**
 * Each logger is executed concurrently with respect to the other loggers.
 * Thus, a dedicated dispatch queue is used for each logger.
 * Logger implementations may optionally choose to provide their own dispatch queue.
 **/
@property (nonatomic, DISPATCH_QUEUE_REFERENCE_TYPE, readonly) dispatch_queue_t loggerQueue;
/**
 * If the logger implementation does not choose to provide its own queue,
 * one will automatically be created for it.
 * The created queue will receive its name from this method.
 * This may be helpful for debugging or profiling reasons.
 **/
@property (nonatomic, readonly) NSString *loggerName;
@end

此外,如果自定义的 logger 继承 DDAbstractLogger 那么会自动实现 (logFormatter & setLogFormatter:) 这两个强制的方法,因此实现一个 logger 很容易:

MyCustomLogger.h:

#import <Foundation/Foundation.h>
#import "DDLog.h"

@interface MyCustomLogger : DDAbstractLogger <DDLogger>
{
}
@end

MyCustomLogger.m

#import "MyCustomLogger.h"
@implementation MyCustomLogger

- (void)logMessage:(DDLogMessage *)logMessage {
    NSString *logMsg = logMessage.message;

    if (self->logFormatter)
        logMsg = [self->logFormatter formatLogMessage:logMessage];
    if (logMsg) {
        // Write logMsg to wherever...
    }
}
@end

logFormatter 设计为 logger 的可选组件。 这是为了简单,如果不需要格式化任何信息则不需要添加 logFormatter 这个属性。并且 logFormatter
和 logger 之间是可重用的,单个 logFormatter 可应用于多个 logger。

自定义格式化消息(Formatters)

格式化日志消息对于不同的 logger 来说是可选的属性,如果设置了这个属性,那么在操作日志消息前可以对日志消息的结构做更改,还可以加上其他的一些信息。
格式化日志消息还可以用来筛选日志消息,你可以自由的觉得哪些消息需要被展示出来或者写入文件,哪些消息需要过滤掉。
记住自定义格式化消息(Formatters)可以单独地应用于 Logger,因此可以对每个 logger 的消息进行格式化或者筛选过滤。

使用

如果想自定义一个 Formatters 是很简单的,至于要实现在头文件 DDLog.h 中的 DDLogFormatter 协议即可,这个协议仅仅有一个必须实现的方法:

@protocol DDLogFormatter <NSObject>
@required

/**
 * Formatters may optionally be added to any logger.
 * This allows for increased flexibility in the logging environment.
 * For example, log messages for log files may be formatted differently than log messages for the console.
 *
 * For more information about formatters, see the "Custom Formatters" page:
 * Documentation/CustomFormatters.md
 *
 * The formatter may also optionally filter the log message by returning nil,
 * in which case the logger will not log the message.
 **/
- (NSString *)formatLogMessage:(DDLogMessage *)logMessage;

@optional
// ...
@end

下面通过一个实例来说明如何自定义 Formatters :

MyCustomFormatter.h

#import <Foundation/Foundation.h>
#import "DDLog.h"
@interface MyCustomFormatter : NSObject <DDLogFormatter>
@end

MyCustomFormatter.m

#import "MyCustomFormatter.h"

@implementation MyCustomFormatter

- (NSString *)formatLogMessage:(DDLogMessage *)logMessage {
    NSString *logLevel;
    switch (logMessage->_flag) {
        case DDLogFlagError    : logLevel = @"E"; break;
        case DDLogFlagWarning  : logLevel = @"W"; break;
        case DDLogFlagInfo     : logLevel = @"I"; break;
        case DDLogFlagDebug    : logLevel = @"D"; break;
        default                : logLevel = @"V"; break;
    }

    return [NSString stringWithFormat:@"%@ | %@", logLevel, logMessage->_message];
}

@end

如果此时想要过滤掉这条日志消息那么直接返回 nil 即可。

然后将这个自定义的 Formatters 添加到 Logger 中:

[DDTTYLogger sharedInstance].logFormatter = [[MyCustomFormatter alloc] init];
日志文件管理

我们可以将写入本地的日志文件压缩或者上传到服务器 。
日志文件有两个组件,一个组件是将日志信息写入到本地文件中,然后根据文件大小或者过期时间来决定是否刷新文件,另一个组件是用来管理这些日志文件的。一旦日志文件写入完成来决定这些文件是应该被压缩还是被上传到服务器,或者两者都需要。
框架自带的 DDFileLogger 实现被分为两个组件。DDFileLogger 是将日志消息写入文件的组件。而 DDLogFileManager 是一个管理日志文件的协议,并决定文件将要被刷新时如何处理它。

首先看一下 DDFileLogger 的初始化:

@interface DDFileLogger : NSObject <DDLogger>
...

- (instancetype)init;
- (instancetype)initWithLogFileManager:(id <DDLogFileManager>)logFileManager NS_DESIGNATED_INITIALIZER;
...
@end

默认初始化方法简单的使用了 DDLogFileManagerDefault 这个类。这个类只提供了删除旧日志文件的方法。
还有一个初始化方法就需要传入一个自定义的日志文件管理类。

使用

如果想使用 DDFileLogger ,这个框架自带将日志写入文件的类,并且需要自定义一个文件管理,那么就需要实现 DDLogFileManager 协议。当然,如果连Logger都是自定义的话那么就不需要按照框架这样分两个组件去实现。这个前提是使用 DDFileLogger 并想自定义文件管理。

@protocol DDLogFileManager <NSObject>
@required
// Public properties
@property (readwrite, assign) NSUInteger maximumNumberOfLogFiles;
// Public methods
- (NSString *)logsDirectory;
- (NSArray *)unsortedLogFilePaths;
- (NSArray *)unsortedLogFileNames;
- (NSArray *)unsortedLogFileInfos;
- (NSArray *)sortedLogFilePaths;
- (NSArray *)sortedLogFileNames;
- (NSArray *)sortedLogFileInfos;
// Private methods (only to be used by DDFileLogger)
- (NSString *)createNewLogFile;
@optional
// Notifications from DDFileLogger
- (void)didArchiveLogFile:(NSString *)logFilePath;
- (void)didRollAndArchiveLogFile:(NSString *)logFilePath;

@end

如果自定义实现日志文件管理,那么需要实现上面 @required 的方法。当文件需要刷新时会通知 @optional 的两个方法。

可能对 @required 的方法有些困惑,查看 DDFileLogger 的实现实际上只用到了 sortedLogFileInfos 的方法来获取当前操作文件的信息。如果自定义日志文件管理的话只需要实现 sortedLogFileInfos 就可以了。但是如果在外部访问这些属性不发生错误那么最好全部都实现。

简单实现

下面我们将会通过代码,自定义一条消息来过滤不需要的日志,并且将相关日志写入文件并上传的服务器。
.pch 文件中,进行如下配置:

#import "CocoaLumberjack.h"

static DDLogLevel ddLogLevel = DDLogLevelVerbose;

#define JHEVENT_CONTEXT 100

#define JHEventVerbose(frmt, ...) LOG_MAYBE(LOG_ASYNC_ENABLED, LOG_LEVEL_DEF, DDLogFlagVerbose, JHEVENT_CONTEXT, nil, __PRETTY_FUNCTION__, frmt, ##__VA_ARGS__)

上面定义了日志输出的优先级为 DDLogLevelDebug 。并且自定义了日志输出的消息类型,CONTEXT 为100,用于筛选日志消息。

在程序启动时:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

    [DDLog addLogger:[DDASLLogger sharedInstance]]; 
    [DDLog addLogger:[DDTTYLogger sharedInstance]]; 
    JHEventFileManager *fileManager = [[JHEventFileManager alloc] init];    //自定义日志文件管理
    JHEventLogger *fileLogger = [[JHEventLogger alloc] initWithLogFileManager:fileManager]; //自定义文件Logger
    fileLogger.rollingFrequency = 60 * 60 * 24; // 有效期是24小时
    fileLogger.logFileManager.maximumNumberOfLogFiles = 2;  //最多文件数量为2个
    fileLogger.logFormatter = [[JHEventFormatter alloc] init];  //日志消息格式化
    fileLogger.maximumFileSize = 1024*50;   //每个文件数量最大尺寸为50k
    fileLogger.logFileManager.logFilesDiskQuota = 200*1024;     //所有文件的尺寸最大为200k
    [DDLog addLogger:fileLogger];

    return YES;
}

上面一共添加了三种 Logger,通过 DDLog 输出的消息会被这三种 Logger 接收。上面使用时定义了几个参数相互约束来控制日志文件的有效期和大小的。当单个文件大于50k时会新建一个日志文件。当第二个文件大于50k时会将最早的文件删除掉。当文件有限期超过24小时或者所有文件的尺寸大于200k时也会将最早的日志文件删除掉。

JHEventFileManager 的实现如下:
JHEventFileManager.h

@interface JHEventFileManager : DDLogFileManagerDefault

@end

JHEventFileManager.m

@implementation JHEventFileManager
- (void)didArchiveLogFile:(NSString *)logFilePath
{

}
- (void)didRollAndArchiveLogFile:(NSString *)logFilePath
{

}
@end

这个类直接继承框架自带的 DDLogFileManagerDefault 类,并没有重写一个实现 <DDLogFileManager> 协议的新类。根据不同的业务可以参考
DDLogFileManagerDefault 类,重新写一个新的日志文件管理。

实现上面的方法主要当日志文件将要被刷新删除时会调用,此时我们可以获取到这个文件将文件上传到服务器。
关于 JHEventLogger 类的实现也是直接继承系统的 DDFileLogger

JHEventLogger.h

@interface JHEventLogger : DDFileLogger

@end

JHEventLogger.m

- (void)logMessage:(DDLogMessage *)logMessage {
    [super logMessage:logMessage];
}
- (void)willLogMessage
{
    [super willLogMessage];
}

- (void)didLogMessage
{
    [super didLogMessage];
}

也可以直接使用 DDFileLogger 类。这样做可以在写入日志时获取相关的通知,从而进行其他操作。

JHEventFormatter 用来筛选和格式化日志信息:

JHEventFormatter.h

@interface JHEventFormatter : NSObject <DDLogFormatter>

@end

JHEventFormatter.m

@implementation JHEventFormatter
- (NSString *)formatLogMessage:(DDLogMessage *)logMessage {
    NSString *logLevel;
    switch (logMessage->_flag) {
        case DDLogFlagError    : logLevel = @"E"; break;
        case DDLogFlagWarning  : logLevel = @"W"; break;
        case DDLogFlagInfo     : logLevel = @"I"; break;
        case DDLogFlagDebug    : logLevel = @"D"; break;
        default                : logLevel = @"V"; break;
    }
    if (logMessage.context == JHEVENT_CONTEXT) {
        return [NSString stringWithFormat:@"%@ | %@", logLevel, logMessage->_message];
    }
    return nil;
    }
@end

只有当我们消息是由 JHEventVerbose 宏打印时才会将日志写入本地。
当我们使用时如下:

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
   ddLogLevel = DDLogLevelVerbose;
}
- (IBAction)event_1:(id)sender {
    DDLogError(@"error log");
}
- (IBAction)event_2:(id)sender {
    DDLogVerbose(@"Verbose log");
}
- (IBAction)event_3:(id)sender {
    JHEventVerbose(@"JHEventVerbose");
}
@end

我们在 .pch 文件中定义了 DDLog 的优先级为 DDLogLevelDebug,此时 DDLogVerboseJHEventVerbose 都不会有输出,但是在viewDidLoad 方法里我们针对这个文件将优先级改成DDLogLevelVerboseDDLogVerboseJHEventVerbose 都会正常输出,其他文件的优先级还是 DDLogLevelDebug

这三个输出都会被 DDASLLoggerDDTTYLoggerJHEventLogger 接收到,也就是我们会在 Xcode 控制台与 Mac 上的控制台看到这三个输出。但是只有 JHEventVerbose 的输出会保存到本地,因为我们之前在格式化的时候通过 context 字段将前两个信息已经过滤掉了。

上面就是 CocoaLumberjack 的简单介绍与使用,更多的使用方法还是需要参考 这里 的文档。

plcrashreporter

说明:有关 plcrashreporter 的介绍大多数都是参考的 plcrashreporter开发团队提供的文档,如果想看更多或者下载安装 plcrashreporter 的请移步 这里

CrashReporter提供了一个用于 iOS 和 Mac OS X 的进程中崩溃报告框架,并为 iOS 提供了大部分崩溃报告服务,包括 HockeyApp ,Flurry 和 Crittercism。

特点:
  • 仅支持使用公开API/ABI的崩溃报告。
  • 首次在2008年发布,并用于成千上万的应用程序。 PLCrashReporter已经有了大量的测试用户。
  • 提供所有活动线程的调用栈。
  • 最精准的可用堆栈展开,使用 DWARF 和 Apple Compact Unwind 框架数据。
  • 不妨碍lldb/gdb中的调试。
  • 易于集成现有或定制的崩溃报告服务。
  • 为崩溃的线程提供完整的寄存器状态。

解码崩溃报告
崩溃报告作为 protobuf 编码消息输出,可以使用 CrashReporter 库或任何 Google Protobuf 解码器进行解码。

除了内置库解码支持外,你可以使用附带的 plcrashutil 二进制文件将崩溃报告转换为苹果标准的iPhone文本格式,这可传递给符号化工具。

./bin/plcrashutil convert --format=iphone example_report.plcrash | symbolicatecrash

将来的发布版本可能包括可重用的格式化程序,用于直接从手机输出不同的格式 。

构建
构建一个可嵌入的framework

user@max:~/plcrashreporter-trunk> xcodebuild -configuration Release -target 'Disk Image'

这将在 build/Release/PLCrashReporter-{version}.dmg 中输出一个新的版本,其中包含可嵌入的 Mac OS X 框架和 iOS 静态框架。


PLCrashReporter介绍

Plausile CrashReporter 实现了在 iPhone 和 Mac OS X 上进程中的崩溃报告。

支持以下功能:

  • 实现进程内信号处理。
  • 不干扰gdb中的调试
  • 处理未被捕获的Objective-C异常和致命信号(SIGSEGV,SIGBUS等)。
  • 提供所有活动线程(调用栈,寄存器drump)的完整线程状态。
  • 如果您的应用程序崩溃,将会写入崩溃报告。 当应用程序下一次运行时,您可以检查挂起的崩溃报告,并将报告提交到您自己的HTTP服务器,发送电子邮件,甚至在本地内部报告。
崩溃报告格式

崩溃日志使用 google protobuf 解码,也可以使用 PLCrashReport API进行解码。除此之外附带的 plcrashutil 可以处理将二进制崩溃报告转换为符号兼容的 iPhone 文本格式。

PLCrashReporter类列表与功能简述
class brief
PLCrashHostInfoVersion major.minor.revision版本号
PLCrashProcessInfo 提供访问有关目标进程的基本信息的方法
PLCrashReport 提供PLCrashReporter框架生成的崩溃日志的解码
PLCrashReportApplicationInfo 崩溃日志应用程序数据
PLCrashReportBinaryImageInfo 崩溃日志二进制图像信息
PLCrashReporter 崩溃记录
PLCrashReporterCallbacks 支持PLCrashReporter回调,允许主机应用程序在发生崩溃之后在程序终止之前执行其他任务的回调
PLCrashReporterConfig 崩溃记录配置
PLCrashReportExceptionInfo 如果由于未被捕获的Objective-C异常触发崩溃,将会提供异常名称和原因
PLCrashReportFileHeader 崩溃日志文件头格式
<PLCrashReportFormatter> 崩溃报告格式接受PLCrashReport实例化,根据实现指定的协议进行格式化(如实现文本输出支持),并返回结果
PLCrashReportMachExceptionInfo 提供访问异常类型和代码
PLCrashReportMachineInfo 崩溃日志主机架构信息
PLCrashReportProcessInfo 崩溃日志进程数据
PLCrashReportProcessorInfo 崩溃日志进程记录
PLCrashReportRegisterInfo 崩溃日志通用寄存器信息
PLCrashReportSignalInfo 提供对signal名称和siganl代码的访问
PLCrashReportStackFrameInfo 崩溃日志堆栈信息
PLCrashReportSymbolInfo 崩溃日志符号信息
PLCrashReportSystemInfo 崩溃日志系统数据
PLCrashReportTextFormatter 将PLCrashReport数据格式化为可读的文本
PLCrashReportThreadInfo 崩溃日志每个线程状态信息

更多详细介绍请 参考


iPhone使用实例

 - (void) handleCrashReport {
     PLCrashReporter *crashReporter = [PLCrashReporter sharedReporter];
     NSData *crashData;
     NSError *error;

     // Try loading the crash report
     crashData = [crashReporter loadPendingCrashReportDataAndReturnError: &error];
     if (crashData == nil) {
         NSLog(@"Could not load crash report: %@", error);
         goto finish;
     }

     // We could send the report from here, but we'll just print out
     // some debugging info instead
     PLCrashReport *report = [[[PLCrashReport alloc] initWithData: crashData error: &error] autorelease];
     if (report == nil) {
         NSLog(@"Could not parse crash report");
         goto finish;
     }

     NSLog(@"Crashed on %@", report.systemInfo.timestamp);
     NSLog(@"Crashed with signal %@ (code %@, address=0x%" PRIx64 ")", report.signalInfo.name,
           report.signalInfo.code, report.signalInfo.address);

     // Purge the report
 finish:
     [crashReporter purgePendingCrashReport];
     return;
 }

 - (void) applicationDidFinishLaunching: (UIApplication *) application {
     PLCrashReporter *crashReporter = [PLCrashReporter sharedReporter];
     NSError *error;
     // Check if we previously crashed
     if ([crashReporter hasPendingCrashReport])
         [self handleCrashReport];   
     // Enable the Crash Reporter
     if (![crashReporter enableCrashReporterAndReturnError: &error])
         NSLog(@"Warning: Could not enable crash reporter: %@", error);         
}

注意:在Xcode调试模式下是捕获不到异常的,包括真机调试和模拟器调试,此时需要断开调试模式后制造异常,捕获后通过Xcode再次运行应用就可以查看保存在本地的异常记录了。

上面的写法是 PLCrashReporter 文档中给出的实例代码,通过 hasPendingCrashReport 方法来判断是否存在奔溃信息,如果存在就会调用 handleCrashReport 方法来处理。处理结束后会调用 purgePendingCrashReport方法来清除之前保存的奔溃报告。如果我们想要把奔溃信息上传到服务器那么就会以下问题: 如果在上传的过程过程中又出现了新的崩溃信息,那么旧的信息就会被新的奔溃信息所覆盖丢失,这样做只能本地保存一份崩溃日志。如果旧的奔溃日志成功上传到服务器还好,如果因为网络原因没有上传成功,那么此时再出现新的崩溃,老的数据就会丢失。
所以使用的时候还应该对上面的代码进行一下修改:

@implementation AppDelegate
static void save_crash_report (PLCrashReporter *reporter) {

    NSFileManager *fm = [NSFileManager defaultManager];
    NSError *error;

    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *documentsDirectory = [paths objectAtIndex:0];
    if (![fm createDirectoryAtPath: documentsDirectory withIntermediateDirectories: YES attributes:nil error: &error]) {
        NSLog(@"Could not create documents directory: %@", error);
        return;
    }

    NSData *data = [reporter loadPendingCrashReportDataAndReturnError: &error];
    if (data == nil) {
        NSLog(@"Failed to load crash report data: %@", error);
        return;
    }

    NSString *outputPath = [documentsDirectory stringByAppendingPathComponent: [NSString stringWithFormat:@"demo_%f.plcrash",[NSDate date].timeIntervalSince1970]];
    if (![data writeToFile: outputPath atomically: YES]) {
        NSLog(@"Failed to write crash report");
    }
    else{
        NSLog(@"Saved crash report to: %@", outputPath);
        [reporter purgePendingCrashReport];
    }
}
static void post_crash_callback (siginfo_t *info, ucontext_t *uap, void *context) {
    // this is not async-safe, but this is a test implementation
    NSLog(@"post crash callback: signo=%d, uap=%p, context=%p", info->si_signo, uap, context);
}

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

    PLCrashReporter *crashReport = [PLCrashReporter sharedReporter];
    NSError *error;

    if ([crashReport hasPendingCrashReport]) {
        [self handleCrashReport];
    }
    if (![crashReport enableCrashReporterAndReturnError:&error]) {
        NSLog(@"Warning: Could not enable crash reporter: %@", error);
    }
    return YES;
}

-(void)handleCrashReport
{
    PLCrashReporter *crashReporter = [PLCrashReporter sharedReporter];
    save_crash_report(crashReporter);
    PLCrashReporterCallbacks cb = {
        .version = 0,
        .context = (void *) 0xABABABAB,
        .handleSignal = post_crash_callback
    };
    [crashReporter setCrashCallbacks: &cb];

}

@end

上面的代码每次有崩溃日志时都会将日志再备份一次到本地,以防日志丢失,备份后将日志上传到服务器,上传成功后将备份的日志删除掉。如果失败下次启动时也可以检查备份目录有多少上传失败的文件,然后根据情况重新上传。上面的代码还添加了崩溃发生时的回调,具体可以参照上面列表中介绍类的信息 PLCrashReporterCallbacks:支持 PLCrashReporter回调,允许主机应用程序在发生崩溃之后在程序终止之前执行其他任务的回调


崩溃日志解析

上面提到了崩溃日志解析有几种方式,这里介绍使用附带的 plcrashutil 工具进行解析。现在最新的 PLCrashReporter 发布版本是 1.2。下载这个版本在 Tools 文件夹里会看见 plcrashutil 的可执行文件。

将其中一个崩溃文件与 plcrashutil 可执行文件放在一个目录下。并且在 shell 中 cd 到这个目录后执行如下命令:

./plcrashutil convert --format=iphone demo_1494240181.851469.plcrash > app.crash

这条命令会将 plcrash 文件转换成苹果标准崩溃格式。

配置环境变量执行:

export DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer

找到 Xcode 自带的符号化工具 symbolicatecrashXcode 8.3 中的位置如下:

/Applications/Xcode.app/Contents/SharedFrameworks/DVTFoundation.framework/Versions/A/Resources/symbolicatecrash

获取应用的符号化文件 yourappname.app.dSYMsymbolicatecrashyourappname.app.dSYM 放到与 plcrashutil 相同目录下:

./symbolicatecrash app.crash yourappname.app.dSYM > app.log

此时生成的 app.log 文件即是符号化解析后的文件。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,907评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,546评论 1 289
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,705评论 0 238
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,624评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,940评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,371评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,672评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,396评论 0 195
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,069评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,350评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,876评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,243评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,847评论 3 231
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,004评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,755评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,378评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,266评论 2 259

推荐阅读更多精彩内容

  • 1、通过CocoaPods安装项目名称项目信息 AFNetworking网络请求组件 FMDB本地数据库组件 SD...
    X先生_未知数的X阅读 15,934评论 3 118
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,087评论 18 139
  • 一、 好像是去年开始,听到两个同事报名写作班每天坚持写500字以上的文章时我就蠢蠢欲动也想要尝试每天写500字以上...
    林潇Ena阅读 367评论 2 0