精华内容
参与话题
问答
  • 浅谈iOS日志收集系统

    万次阅读 2017-05-08 19:52:48
    在浅谈iOS日志收集系统中介绍了如何收集iOS崩溃日志与如何解析iOS崩溃日志,主要用到了两个工具: plcrashutil:将plcrash文件转换成苹果标准崩溃格式 symbolicatecrash:符号化崩溃信息 结合这两个工具写了一个脚本...

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

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

    • 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解码,也可以使用PLCrashReportAPI进行解码。除此之外附带的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的可执行文件。
    我们制造几次崩溃后会在上面代码保存的目录下发现如下文件:
    tupian
    将其中一个崩溃文件与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
    symbolicatecrashyourappname.app.dSYM放到与plcrashutil相同目录下:

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

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

    展开全文
  • 实际开发测试中,经常出现测试提交的问题或反馈在开发端不能复现的问题,通常都是Xcode联调打印日志去排查,而且打印信息可能不具体,所以想到了宏定义NSLog打印更多信息,并且输出到日志中,直接可以从设备上拿到...

    实际开发测试中,经常出现测试提交的问题或反馈在开发端不能复现的问题,通常都是Xcode联调打印日志去排查,而且打印信息可能不具体,所以想到了宏定义NSLog打印更多信息,并且输出到日志中,直接可以从设备上拿到日志去分析,基于以上要求,测试完成了一下功能希望能帮助需要的人、

    ## 主要分为两步操作 ##

    • 一.自定义输出

      在pch文件中添加以便我们在整个项目中都是使用,代码如下
      
    #ifdef DEBUG
    # define GALLog(fmt, ...) NSLog((@"[路径:%s]\n" "[函数名:%s]\n" "[行号:%d] \n" fmt), [[NSString stringWithFormat:@"%s", __FILE__].lastPathComponent UTF8String], __FUNCTION__, __LINE__, ##__VA_ARGS__);
    #else
    # define GALLog(fmt,...) NSLog(@"" fmt);
    //# define GALLog(...);
    #endif

    名词解释:

    宏 说明
    func 打印当前函数或方法,c字符串
    LINE 打印当前行号,整数
    FILE 打印当前文件路径,c字符串
    FUNCTION 打印当前函数或方法(在C++中会包含参数类型),c字符串

    举例:
    宏定义打印

    在viewDidLoad中使用自定义的 GALLog(@”123”); 打印信息如图非常详细。

    • 二.将日志写入文件

      这一步也非常简单,只需要在AppDelegate.m的didFinishLaunchingWithOptions 中调用以下方法即可。
      这里写图片描述

    具体代码给出如下:

    #pragma mark - 日志收集
    - (void)redirectNSlogToDocumentFolder
    {
        NSString *documentDirectory = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
    
        NSDateFormatter *dateformat = [[NSDateFormatter  alloc]init];
        [dateformat setDateFormat:@"yyyy-MM-dd-HH-mm-ss"];
        NSString *fileName = [NSString stringWithFormat:@"LOG-%@.txt",[dateformat stringFromDate:[NSDate date]]];
        NSString *logFilePath = [documentDirectory stringByAppendingPathComponent:fileName];
    
        // 先删除已经存在的文件
        NSFileManager *defaultManager = [NSFileManager defaultManager];
        [defaultManager removeItemAtPath:logFilePath error:nil];
    
        NSLog(@"%@",logFilePath);
    
        // 将log输入到文件
        freopen([logFilePath cStringUsingEncoding:NSASCIIStringEncoding], "a+", stdout);
    
        freopen([logFilePath cStringUsingEncoding:NSASCIIStringEncoding], "a+", stderr);
    }

    重点在于 - freopen
    freopen()函数用于文件流的的重定向,一般是将 stdin、stdout 和 stderr 重定向到文件。
    所谓重定向,就是改变文件流的源头或目的地。stdout(标准输出流)的目的地是显示器,printf()是将流中的内容输出到显示器;可以通过freopen()将stdout 的目的地改为一个文件(如output.txt),再调用 printf(),就会将内容输出到这个文件里面,而不是显示器。 freopen()函数的原型为: FILE *freopen(char *filename, char *type, FILE *stream);

    这里通过这个方法就可以将前面我们打印在控制台的信息全部写去到文件中去,可以通过设备直接拿到日志文件去分析。


    注意

    **通过以上两步就可以实现前面所要求的功能,有几点需要注意的在这里提及一下;
    1.这里所写的宏定义输出是在DEBUG模式下可用的,在release模式下我这里还是系统的NSLog输出,使用时可以自己调整。
    上面的#if #endif宏定义就是如果定义了DEBUG,那么就使用GALLog输出;否则这段代码就直接忽略不执行.这个Xcode的默认设置,我们可以取消DEBUG模式,开启RELEASE发布模式,如下面的截图所示.
      选择Product->Scheme->Edit Scheme**
      这里写图片描述

    **2.如果debug模式下在AppDelegate.m的didFinishLaunchingWithOptions 中调用了redirectNSlogToDocumentFolder,那么Xcode控制台是不会打印的,打印信息会全部写入日志文件中,所以这里使用的时候需要注意。
    3.拿到的日志我们可以通过接口定期上传到我们自己的服务器。**

    展开全文
  • linux: Clear清除屏幕 pwd显示工作路径 cd -返回上次所在目录 ls -l显示文件和目录详细资料 ls -a显示隐藏文件 ls *[0-9]*显示包含数字的文件名和目录名 wc file1统计行数,字数,字符数 ...mkdir dalianren...

    linux:

    Clear 清除屏幕

    pwd显示工作路径

    cd -返回上次所在目录

    ls -l显示文件和目录详细资料

    ls -a显示隐藏文件

    ls *[0-9]*显示包含数字的文件名和目录名

    wc file1 统计行数,字数,字符数

    netstat 显示网络状态信息

    netstat -a列出所有当前连接

    netstat -t 列出所有tcp链接
    netstat -u 列出所有udp链接

    netstat -s 打印出网络统计数据,包括某个协议下的收发包数量

    mkdir dalianren创建一个大连人的目录

    rmdir dalianren删除一个大连人的目录

    rm -rf dalianren删除文件及内容

    mv dalianren dalianbisheng重命名
    cp -a 路径1 路径2将路径1下的东西复制到路径2上

    gzip file1压缩文件

    gunzip file1解压文件

    bzip2 file1 压缩文件

    bunzip2 file1加压文件

    zip file1.zip file1创建一个zip格式的压缩包

    unzip file1.zip解压一个zip格式的压缩包

    zip -r aaa.zip file1 file2 将一个文件同时压缩到一个zip中

    tar -cvf aaa.tar file1创建一个非压缩的tar包

    tar -tf aaa.tar 显示包中的内容

    tar -xvf aaa.tar 释放一个包

    tar -xvf aaa.tar -C路径 将压缩包释放到指定目录下

    cat file1从第一个字节开始正向查看文件内容

    head -2 file1查看一个文件的前2行

    tail -2 file1 查看一个文件的后2行

    touch dalian.txt创建一个文件

    vim dalian.txt 编辑文件

    echo 写入内容>>文件名

    cat dalian.txt无法查看

    vim dalian.txt 
    :set ff=unix

    Tail dalian.txt 显示文件的最后10行

    Tail +20 dalian.txt 显示文件从第20行到文件末尾

    Tail -c 10 dalian.txt显示文件的最后10个字符

    Tail -n 100 dalian.txt 显示最后100行

     

    tail -f 实时查看

    tail -f dalian.txt|grep 1 查看dalian.txt中包含1的数据

    ps当前进程的快照

    top实时查看当前进程

    ps -a显示所有进程

    ps -ax显示没有控制终端的进程

    ps -ef|grep KugouMusic 检查酷狗进程是否存在

    ps -ef 是用标准的格式展示进程

    ps aux 使用bsd的格式显示

    kill -9 pid杀死进程

    find 文件名 

    shutdown -h now立即关机

    service firewalld stop关闭防火墙

    scp /Users/yuliguo/Downloads/jdk-8u261-linux-x64.tar.gz  root@192.168.31.8:/usr/local/java本地拷贝至远程服务器

    sar -n DEV 1 20 sar 查看网卡流量

    文件权限:-rw-r--rw-

    r:可读(4

    w:可写(2)对目录来说则可新建文件

    x:可执行(1)对目录来说则可进入该目录

    其中的第一个字符表示的是文件类型(- 表示普通文件,d 表示目录文件,c : 为字符串设备,若路由器等设备b : 块设备,硬盘、光驱等)后面的九个字符就是表示的文件权限了,每三个字符为一组

    第一组(rw-):表示文件所有者的权限,上图中的文件的所有者为root用户,具备可读 可写 
    第二组(r--):文件所属组的权限,上图中的文件所属组也为root组,具备可读
    第三组(rw-):其他人的权限(跟本文件无关的人),具备可读 可写

    chmod 444 test.txt 讲文件修改成可读权限

     

    crontab -l展示所有定时

    crontab -e编辑
    每个工作日 11 p.m 运行

    0 23 * * 1-5 /root/bin/incremental-backup.sh

    Vi常用命令: 

    30G移动到30行

    G:移动到最后一行

    gg:移动到第一行

    /abc  从光标开始向下搜索abc;

    ?abc 向上搜索abc

    lsof -i :8080 查看8080端口是谁占用的

     

    adb 常用命令:

    Adb Monkey命令:

    Adb version 查看adb 版本号

    Adb devices 查看当前连接设备

    Adb get-state获取设备状态

    Adb start-server 启动adb

    Adb kill-server 杀死adb

    Adb shell wm size获取屏幕大小

    Adb shell pm list package 显示手机上所有的包名

    Adb shell am start 包名/activity  启动app

    Adb shell dumpsys battery 查看电池信息

    Adb shell top 查看cpu

    Adb shell ps 查看进程

    Adb shell input keyevent 26 锁屏

    Adb shell ifconfig|grep Mask 查询手机ip

    Adb shell pm  list package -3查看第三方的应用

    Adb install 安装应用

    Adb uninstall 卸载应用

    Adb shell pm clear 包名 清理应用缓存

    Adb shell dumpsys 包名 查看应用信息

    Adb shell pm path 包名 查看应用安装的路径

    Adb shell am start -w -s 冷启动

     

    adb -s d51ad9ff shell

    adb -s FUKRZ9MF99999999  install

    //进入模拟器

    adb -e shell 

    /进入手机

    adb -d shell 

    获取系统版本号:

    adb shell getprop ro.build. version .release

    连接夜神模拟器
adb connect 127.0.0.1:62001

    adb shell "dumpsys window | grep mCurrentFocus" 查看当前页面的activity

    adb shell monkey -p cn.soulapp.android -v-v-v -s100 --throttle 500 --ignore-timeouts –ignore-crashes 100

    adb shell ps|grep monkey

    adb shell kill pid

     

    此方法可以隐藏状态栏  至于音量键可以在执行adb命令时候--pct-syskeys 0屏蔽掉

    例如    adb shell monkey -p com.tencent.mobileqq --pct-syskeys 10 1000

     

     

     

    查看iso设备udid

    idevice_id -l

     

    获取应用bundleid

    ideviceinstaller -l

     

    ideviceinfo -u [udid] # 指定设备,获取设备信息

    ideviceinfo -u [udid] -k DeviceName # 指定设备,获取设备名称:iPhone6s

    idevicename -u [udid] # 指定设备,获取设备名称:iPhone6s

    ideviceinfo -u [udid] -k ProductVersion # 指定设备,获取设备版本:10.3.1

    ideviceinfo -u [udid] -k ProductType # 指定设备,获取设备类型:iPhone8,1

    ideviceinfo -u [udid] -k ProductName # 指定设备,获取设备系统名称

     

     

    ios查看日志的方式
https://www.jianshu.com/p/755666303387

    mysql

    卸载mysql

    cd ~

    open /usr

    sudo rm /usr/local/mysql

    sudo rm -rf /usr/local/mysql*

    sudo rm -rf /Library/StartupItems/MySQLCOM

    sudo rm -rf /Library/PreferencePanes/My*

    rm -rf ~/Library/PreferencePanes/My*

    sudo rm -rf /Library/Receipts/mysql*

    sudo rm -rf /Library/Receipts/MySQL*

    sudo rm -rf /var/db/receipts/com.mysql.*

    sudo /usr/local/mysql-5.7.28-macos10.14-x86_64/support-files /mysql.server stop

     

     

    mysql修改密码

    cd /usr/local/mysql/bin

    sudo su

    ./mysqld_safe --skip-grant-tables & 

    mysql -uroot -p 

    flush privileges;

    set password for 'root'@'localhost'=password('新密码');

    set password for 'root'@'localhost'=password('新密码');

     

     

    Charles破解:输入Registered Name: https://zhile.io 、 License Key: 48891cf209c6d32bf4

    https://www.jianshu.com/p/82f63277d50f

    到这个目录下
    /Applications/PyCharm.app/Contents/bin/

    把下载好的JetbrainsIdesCrack-3.4-release-enc.jar这个文件放到bin目录下

    然后修改bin目录下的pycharm.vmoptions这个文件

    打开pycharm ,直接以免费的身份进去,然后进去之后随意打开个项目

    然后点击help-register-输入code

    56ZS5PQ1RF-eyJsaWNlbnNlSWQiOiI1NlpTNVBRMVJGIiwibGljZW5zZWVOYW1lIjoi5q2j54mI5o6I5p2DIC4iLCJhc3NpZ25lZU5hbWUiOiIiLCJhc3NpZ25lZUVtYWlsIjoiIiwibGljZW5zZVJlc3RyaWN0aW9uIjoiRm9yIGVkdWNhdGlvbmFsIHVzZSBvbmx5IiwiY2hlY2tDb25jdXJyZW50VXNlIjpmYWxzZSwicHJvZHVjdHMiOlt7ImNvZGUiOiJJSSIsInBhaWRVcFRvIjoiMjAyMC0wMy0xMCJ9LHsiY29kZSI6IkFDIiwicGFpZFVwVG8iOiIyMDIwLTAzLTEwIn0seyJjb2RlIjoiRFBOIiwicGFpZFVwVG8iOiIyMDIwLTAzLTEwIn0seyJjb2RlIjoiUFMiLCJwYWlkVXBUbyI6IjIwMjAtMDMtMTAifSx7ImNvZGUiOiJHTyIsInBhaWRVcFRvIjoiMjAyMC0wMy0xMCJ9LHsiY29kZSI6IkRNIiwicGFpZFVwVG8iOiIyMDIwLTAzLTEwIn0seyJjb2RlIjoiQ0wiLCJwYWlkVXBUbyI6IjIwMjAtMDMtMTAifSx7ImNvZGUiOiJSUzAiLCJwYWlkVXBUbyI6IjIwMjAtMDMtMTAifSx7ImNvZGUiOiJSQyIsInBhaWRVcFRvIjoiMjAyMC0wMy0xMCJ9LHsiY29kZSI6IlJEIiwicGFpZFVwVG8iOiIyMDIwLTAzLTEwIn0seyJjb2RlIjoiUEMiLCJwYWlkVXBUbyI6IjIwMjAtMDMtMTAifSx7ImNvZGUiOiJSTSIsInBhaWRVcFRvIjoiMjAyMC0wMy0xMCJ9LHsiY29kZSI6IldTIiwicGFpZFVwVG8iOiIyMDIwLTAzLTEwIn0seyJjb2RlIjoiREIiLCJwYWlkVXBUbyI6IjIwMjAtMDMtMTAifSx7ImNvZGUiOiJEQyIsInBhaWRVcFRvIjoiMjAyMC0wMy0xMCJ9LHsiY29kZSI6IlJTVSIsInBhaWRVcFRvIjoiMjAyMC0wMy0xMCJ9XSwiaGFzaCI6IjEyMjkxNDk4LzAiLCJncmFjZVBlcmlvZERheXMiOjAsImF1dG9Qcm9sb25nYXRlZCI6ZmFsc2UsImlzQXV0b1Byb2xvbmdhdGVkIjpmYWxzZX0=-SYSsDcgL1WJmHnsiGaHUWbaZLPIe2oI3QiIneDtaIbh/SZOqu63G7RGudSjf3ssPb1zxroMti/bK9II1ugHz/nTjw31Uah7D0HqeaCO7Zc0q9BeHysiWmBZ+8bABs5vr25GgIa5pO7CJhL7RitXQbWpAajrMBAeZ2En3wCgNwT6D6hNmiMlhXsWgwkw2OKnyHZ2dl8yEL+oV5SW14t7bdjYGKQrYjSd4+2zc4FnaX88yLnGNO9B3U6G+BuM37pxS5MjHrkHqMTK8W3I66mIj6IB6dYXD5nvKKO1OZREBAr6LV0BqRYSbuJKFhZ8nd6YDG20GvW6leimv0rHVBFmA0w==-MIIElTCCAn2gAwIBAgIBCTANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1KZXRQcm9maWxlIENBMB4XDTE4MTEwMTEyMjk0NloXDTIwMTEwMjEyMjk0NlowaDELMAkGA1UEBhMCQ1oxDjAMBgNVBAgMBU51c2xlMQ8wDQYDVQQHDAZQcmFndWUxGTAXBgNVBAoMEEpldEJyYWlucyBzLnIuby4xHTAbBgNVBAMMFHByb2QzeS1mcm9tLTIwMTgxMTAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxcQkq+zdxlR2mmRYBPzGbUNdMN6OaXiXzxIWtMEkrJMO/5oUfQJbLLuMSMK0QHFmaI37WShyxZcfRCidwXjot4zmNBKnlyHodDij/78TmVqFl8nOeD5+07B8VEaIu7c3E1N+e1doC6wht4I4+IEmtsPAdoaj5WCQVQbrI8KeT8M9VcBIWX7fD0fhexfg3ZRt0xqwMcXGNp3DdJHiO0rCdU+Itv7EmtnSVq9jBG1usMSFvMowR25mju2JcPFp1+I4ZI+FqgR8gyG8oiNDyNEoAbsR3lOpI7grUYSvkB/xVy/VoklPCK2h0f0GJxFjnye8NT1PAywoyl7RmiAVRE/EKwIDAQABo4GZMIGWMAkGA1UdEwQCMAAwHQYDVR0OBBYEFGEpG9oZGcfLMGNBkY7SgHiMGgTcMEgGA1UdIwRBMD+AFKOetkhnQhI2Qb1t4Lm0oFKLl/GzoRykGjAYMRYwFAYDVQQDDA1KZXRQcm9maWxlIENBggkA0myxg7KDeeEwEwYDVR0lBAwwCgYIKwYBBQUHAwEwCwYDVR0PBAQDAgWgMA0GCSqGSIb3DQEBCwUAA4ICAQAF8uc+YJOHHwOFcPzmbjcxNDuGoOUIP+2h1R75Lecswb7ru2LWWSUMtXVKQzChLNPn/72W0k+oI056tgiwuG7M49LXp4zQVlQnFmWU1wwGvVhq5R63Rpjx1zjGUhcXgayu7+9zMUW596Lbomsg8qVve6euqsrFicYkIIuUu4zYPndJwfe0YkS5nY72SHnNdbPhEnN8wcB2Kz+OIG0lih3yz5EqFhld03bGp222ZQCIghCTVL6QBNadGsiN/lWLl4JdR3lJkZzlpFdiHijoVRdWeSWqM4y0t23c92HXKrgppoSV18XMxrWVdoSM3nuMHwxGhFyde05OdDtLpCv+jlWf5REAHHA201pAU6bJSZINyHDUTB+Beo28rRXSwSh3OUIvYwKNVeoBY+KwOJ7WnuTCUq1meE6GkKc4D/cXmgpOyW/1SmBz3XjVIi/zprZ0zf3qH5mkphtg6ksjKgKjmx1cXfZAAX6wcDBNaCL+Ortep1Dh8xDUbqbBVNBL4jbiL3i3xsfNiyJgaZ5sX7i8tmStEpLbPwvHcByuf59qJhV/bZOl8KqJBETCDJcY6O2aqhTUy+9x93ThKs1GKrRPePrWPluud7ttlgtRveit/pcBrnQcXOl1rHq7ByB8CFAxNotRUYL9IF5n3wJOgkPojMy6jetQA5Ogc8Sm7RG6vg1yow==

     

     

     

     

     

    展开全文
  • iOS Crash日志 收集

    2017-07-03 09:30:00
    今天在微信公众号上看到一篇文章,做一下简化整理,大家可以尝试一起来做一下自己的Crash日志记录 开发iOS应用,解决Crash问题始终是一个难题。Crash分为两种,一种是由EXC_BAD_ACCESS引起的,原因是访问了不属于本...
        

    今天在微信公众号上看到一篇文章,做一下简化整理,大家可以尝试一起来做一下自己的Crash日志记录

    开发iOS应用,解决Crash问题始终是一个难题。Crash分为两种,一种是由EXC_BAD_ACCESS引起的,原因是访问了不属于本进程的内存地址,有可能是访问已被释放的内存;另一种是未被捕获的Objective-C异常(NSException),导致程序向自身发送了SIGABRT信号而崩溃。其实对于未捕获的Objective-C异常,我们是有办法将它记录下来的,如果日志记录得当,能够解决绝大部分崩溃的问题。

    一. 系统Crash

    对于系统Crash而引起的程序异常退出,可以通过UncaughtExceptionHandler机制捕获;也就是说在程序中catch以外的内容,被系统自带的错误处理而捕获。我们要做的就是用自定义的函数替代该ExceptionHandler即可。

    二. 处理signal

    使用Objective-C的异常处理是不能得到signal的,如果要处理它,我们还要利用unix标准的signal机制,注册SIGABRT, SIGBUS, SIGSEGV等信号发生时的处理函数。该函数中我们可以输出栈信息,版本信息等其他一切我们所想要的。

    下面是一些信号说明:
    1) SIGHUP
    本信号在用户终端连接(正常或非正常)结束时发出, 通常是在终端的控制进程结束时, 通知同一session内的各个作业, 这时它们与控制终端不再关联。
    登录Linux时,系统会分配给登录用户一个终端(Session)。在这个终端运行的所有程序,包括前台进程组和后台进程组,一般都属于这个 Session。当用户退出Linux登录时,前台进程组和后台有对终端输出的进程将会收到SIGHUP信号。这个信号的默认操作为终止进程,因此前台进 程组和后台有终端输出的进程就会中止。不过可以捕获这个信号,比如wget能捕获SIGHUP信号,并忽略它,这样就算退出了Linux登录, wget也 能继续下载。
    此外,对于与终端脱离关系的守护进程,这个信号用于通知它重新读取配置文件。

    2) SIGINT
    程序终止(interrupt)信号, 在用户键入INTR字符(通常是Ctrl-C)时发出,用于通知前台进程组终止进程。

    3) SIGQUIT
    和SIGINT类似, 但由QUIT字符(通常是Ctrl-)来控制. 进程在因收到SIGQUIT退出时会产生core文件, 在这个意义上类似于一个程序错误信号。

    4) SIGILL
    执行了非法指令. 通常是因为可执行文件本身出现错误, 或者试图执行数据段. 堆栈溢出时也有可能产生这个信号。

    5) SIGTRAP
    由断点指令或其它trap指令产生. 由debugger使用。

    6) SIGABRT
    调用abort函数生成的信号。

    7) SIGBUS
    非法地址, 包括内存地址对齐(alignment)出错。比如访问一个四个字长的整数, 但其地址不是4的倍数。它与SIGSEGV的区别在于后者是由于对合法存储地址的非法访问触发的(如访问不属于自己存储空间或只读存储空间)。

    8) SIGFPE
    在发生致命的算术运算错误时发出. 不仅包括浮点运算错误, 还包括溢出及除数为0等其它所有的算术的错误。

    9) SIGKILL
    用来立即结束程序的运行. 本信号不能被阻塞、处理和忽略。如果管理员发现某个进程终止不了,可尝试发送这个信号。

    10) SIGUSR1
    留给用户使用

    11) SIGSEGV
    试图访问未分配给自己的内存, 或试图往没有写权限的内存地址写数据.

    12) SIGUSR2
    留给用户使用

    13) SIGPIPE
    管道破裂。这个信号通常在进程间通信产生,比如采用FIFO(管道)通信的两个进程,读管道没打开或者意外终止就往管道写,写进程会收到SIGPIPE信号。此外用Socket通信的两个进程,写进程在写Socket的时候,读进程已经终止。

    14) SIGALRM
    时钟定时信号, 计算的是实际的时间或时钟时间. alarm函数使用该信号.

    15) SIGTERM
    程序结束(terminate)信号, 与SIGKILL不同的是该信号可以被阻塞和处理。通常用来要求程序自己正常退出,shell命令kill缺省产生这个信号。如果进程终止不了,我们才会尝试SIGKILL。

    17) SIGCHLD
    子进程结束时, 父进程会收到这个信号。
    如果父进程没有处理这个信号,也没有等待(wait)子进程,子进程虽然终止,但是还会在内核进程表中占有表项,这时的子进程称为僵尸进程。这种情 况我们应该避免(父进程或者忽略SIGCHILD信号,或者捕捉它,或者wait它派生的子进程,或者父进程先终止,这时子进程的终止自动由init进程 来接管)。

    18) SIGCONT
    让一个停止(stopped)的进程继续执行. 本信号不能被阻塞. 可以用一个handler来让程序在由stopped状态变为继续执行时完成特定的工作. 例如, 重新显示提示符

    19) SIGSTOP
    停止(stopped)进程的执行. 注意它和terminate以及interrupt的区别:该进程还未结束, 只是暂停执行. 本信号不能被阻塞, 处理或忽略.

    20) SIGTSTP
    停止进程的运行, 但该信号可以被处理和忽略. 用户键入SUSP字符时(通常是Ctrl-Z)发出这个信号

    21) SIGTTIN
    当后台作业要从用户终端读数据时, 该作业中的所有进程会收到SIGTTIN信号. 缺省时这些进程会停止执行.

    22) SIGTTOU
    类似于SIGTTIN, 但在写终端(或修改终端模式)时收到.

    23) SIGURG
    有”紧急”数据或out-of-band数据到达socket时产生.

    24) SIGXCPU
    超过CPU时间资源限制. 这个限制可以由getrlimit/setrlimit来读取/改变。

    25) SIGXFSZ
    当进程企图扩大文件以至于超过文件大小资源限制。

    26) SIGVTALRM
    虚拟时钟信号. 类似于SIGALRM, 但是计算的是该进程占用的CPU时间.

    27) SIGPROF
    类似于SIGALRM/SIGVTALRM, 但包括该进程用的CPU时间以及系统调用的时间.

    28) SIGWINCH
    窗口大小改变时发出.

    29) SIGIO
    文件描述符准备就绪, 可以开始进行输入/输出操作.

    30) SIGPWR
    Power failure

    31) SIGSYS
    非法的系统调用。

    关键点
    在以上列出的信号中,程序不可捕获、阻塞或忽略的信号有:SIGKILL,SIGSTOP
    不能恢复至默认动作的信号有:SIGILL,SIGTRAP
    默认会导致进程流产的信号有:SIGABRT,SIGBUS,SIGFPE,SIGILL,SIGIOT,SIGQUIT,SIGSEGV,SIGTRAP,SIGXCPU,SIGXFSZ
    默认会导致进程退出的信号有:
    SIGALRM,SIGHUP,SIGINT,SIGKILL,SIGPIPE,SIGPOLL,SIGPROF,SIGSYS,SIGTERM,SIGUSR1,SIGUSR2,SIGVTALRM
    默认会导致进程停止的信号有:SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU
    默认进程忽略的信号有:SIGCHLD,SIGPWR,SIGURG,SIGWINCH
    此外,SIGIO在SVR4是退出,在4.3BSD中是忽略;SIGCONT在进程挂起时是继续,否则是忽略,不能被阻塞。
    【具体原因为啥,我也不知道

    展开全文
  • iOS崩溃日志收集

    2017-05-01 15:06:49
    iOS-App崩溃分为两种,一种是异常:NSException,一种是信号:Signal。 二、异常崩溃 1.NSException对象 (1)reason:崩溃原因。 (2)callStackSymbols:调用堆栈。 (3)name:崩溃名称 (4)userInfo:崩溃对象。 2....
  • ios 崩溃日志收集

    2015-12-29 11:36:06
    iOS开发中遇到程序崩溃是很正常的事情,如何在程序崩溃时捕获到异常信息并通知开发者,是大多数软件都选择的方法。下面就介绍如何在iOS中实现: 在程序启动时加上一个异常捕获监听,用来处理程序崩溃时的回调动作 ...
  • iOS 崩溃日志收集及分析

    万次阅读 2017-01-11 23:35:26
    最近几天,项目中在增加推送功能,选用的极光推送...话说,由于iOS10之后,苹果对推送进行了重大更新,主要是新增了 User Notifications Framework 框架, 具体信息可以查看苹果官方文档,这里就不多解释了。于是我就突
  • iOS错误日志收集及分析

    千次阅读 2017-06-09 15:17:09
    错误日志收集 崩溃日志收集 自定义其他错误日志上传 崩溃日志分析错误日志收集    很多APP统计分析SDK都集成了崩溃日志收集功能,如“百度移动统计SDK”。但由于各种原因,这些有时候并不能满足我们的需求,比如...
  • iOS Crash日志收集与解析

    千次阅读 2016-12-09 20:33:41
    Crash日志收集与解析一、本地crash日志收集1、 把发生crash的设备连接到你的电脑上,用 iTunes 或 itools Mac OS X:~/Library/Logs/CrashReporter/MobileDeviceWindows 7/Vista: C:\Users\计算机登录名\AppData\...
  • iOS 崩溃日志 收集与发送服务器

空空如也

1 2 3 4 5 ... 13
收藏数 247
精华内容 98
热门标签
关键字:

ios日志收集