在应用开发中为了给用户更好操作体验与精准信息的展示,往往会收集一些用户行为信息,比如应用中用户习惯的操作流程,相关页面访问次数,用户个人信息等等。大多数应用会集成第三方厂商提供的服务来统计这些数据,当然这样做带来的好处就是不用花费时间来写相关日志收集的功能,后台也不用专门搭建相关的服务,而且第三方提供的工具也比较稳定。这让我们能有更多的时间去开发产品主要业务功能上。
开发应用前期为了让产品快速的推向市场与不断的功能变更,往往不会花费时间在这些非主要业务的功能上。但当产品逐渐成熟进入到一个平台期,如果我们想要获取更多的用户增长与留存,就要想办法在针对自身的产品功能在不用的场景获取更多的用户行为来改进产品,获取更多用户信息来精准针对不同用户展示他们更感兴趣的内容。而且这些数据也不希望保存在其他厂商的服务器上。这样我们就不得不设计一套自己的日志收集系统。本篇博客只是讲解iOS日志收集框架设计的一些思路与实现。
日志分类
首先收集日志根据日志上传的时机分为实时日志与非实时日志:
- 实时日志:收集结束后立刻上传到服务器。
- 非实时日志:当日志累计到一定数量时上传到服务器。
对于实时日志来说文件大小实际上在一定范围之内的。而非实时日志由于是信息累积一段时间后才会上传到服务器,所以对于非实时日志我们需要控制日志的大小不能让日志文件无限增加。当然仅仅控制大小也是不行的,如果用户使用次数很少而且我们的数据要一天统计一次那么就会出现很多天都统计不到用户的相关数据。所以我们也要控制非实时日志的过期时间。如果日志已经过期但大小没有达到限制或者大小已经达到限制但没有到达过期时间都是要上传到服务器的。
对于实时日志与非实时日志上传服务器来说都要有相关的错误处理。对于实时日志来说,如果上传失败的话如果网络连接正常要尝试重新上传,当然这不是无限上传的要有重试的次数,如果超出重试次数,那么上传失败。对于非实时日志来说也是一样的处理逻辑。
更根据收集的数据来分类日志可以分为事件日志,用户信息日志,崩溃日志
事件日志:也可以理解为用户行为数据。当用户使用某个功能或者进入某个页面时会收集相关的信息来统计每个用户使用应用时习惯性操作,与偏好的功能还有页面的PV等。当然也可以获取到每个用户在当前页面所停留的时间。来判断当前页面是否能过吸引用户。
用户信息日志:这些信息主要是为了针对不同的用户来展示不同的功能或者内容,当然也包括当前使用机型的信息有些基本信息可以直接放在请求头的UserAgent中而不需要单独来统计。
崩溃日志:应用崩溃信息。
日志收集框架
日志收集主要用了两个开源框架来实现:plcrashreporter 与 CocoaLumberjack。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__)
这样我们可以得到一个日志等级为 DDLogFlagVerbose
的 LSYEventVerbose
宏。使用这个宏输出的日志得到的 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
,此时 DDLogVerbose
和 JHEventVerbose
都不会有输出,但是在viewDidLoad
方法里我们针对这个文件将优先级改成DDLogLevelVerbose
后 DDLogVerbose
与 JHEventVerbose
都会正常输出,其他文件的优先级还是 DDLogLevelDebug
。
这三个输出都会被 DDASLLogger
, DDTTYLogger
, JHEventLogger
接收到,也就是我们会在 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
自带的符号化工具 symbolicatecrash
在 Xcode 8.3
中的位置如下:
/Applications/Xcode.app/Contents/SharedFrameworks/DVTFoundation.framework/Versions/A/Resources/symbolicatecrash
获取应用的符号化文件 yourappname.app.dSYM
将 symbolicatecrash
和 yourappname.app.dSYM
放到与 plcrashutil
相同目录下:
./symbolicatecrash app.crash yourappname.app.dSYM > app.log
此时生成的 app.log
文件即是符号化解析后的文件。