ios日志_ios日志收集 - CSDN
精华内容
参与话题
  • iOS如何实时查看App运行日志

    万次阅读 2018-05-30 10:53:34
    前言 可能大数人看到这个标题觉得DEBUG时在Xcode里就能看到App运行时的打印在控制台里的日志,这还需要专门写篇文章介绍下吗?其实不然,有些场景下我们还是需要实时查看App运行时的日志的,比如测试人员拿着测试机...

    前言

         可能大数人看到这个标题觉得DEBUG时在Xcode里就能看到App运行时的打印在控制台里的日志,这还需要专门写篇文章介绍下吗?其实不然,有些场景下我们还是需要实时查看App运行时的日志的,比如测试人员拿着测试机发现问题了过来找你,这时候要看运行时日志,又不能重新DEBUG安装版本,怎么办?再比如,收到并点击推送的通知后启动App,这时候要查看我们打印的日志,怎么办?还有,弄过VoIP推送同学的都知道,这个推送可以直接启动我们的App,如果要观察启动过程中的日志,这该怎么办?总不能只能连着Xcode运行时,我们才能看到日志吧。有同学可能会说,把日志写入文件,启动完成后,再把文件拷贝出来看不就完了,好了,这篇文章你一定要认真看完。

    查看日志的几种方式

        这里我们说的日志是输出到控制台的日志,就是我们常用的NSLog输出的日志。其实大部分App会集成日志模块,比如CocoaLumberjack,还有以前笔者学习Runloop时也造过这样的轮子SSLogger。利用第三方日志模块查看日志不在本文讨论范围内,本文只讨论如何实时查看输出在console控制台的日志。
        大致有以前几种方式查看日志,一种是连着Xcode调试时,可以直接在Xcode下面的控制台输出面板上看到我们App的日志。如图:

    Xcode查看log

        还一种也是Xcode,通过Window->Devices打开devices界面,选择我们的手机,也能看到手机中运行的进程输出的日志。如图:

    Devices查看log

        上面这两种方式大部分iOS开发应该都知道,不知道的就该检讨下了。
        本文重点介绍另外两种查看运行时日志的方式: libimobiledevice  和 Console.app(控制台应用)

    使用libimobiledevice工具查看日志

        libimobiledevice 是一系列工具集,有很多实用的工具,截屏,安装ipa文件等等。我们要介绍的是idevicesyslog这个工具,这个工具可以把手机中的日志输出到我们指定的文件里。

    • 如何安装libimobiledevice

    安装libimobiledevice很简单,在命令行输入:

    brew instll libimobiledevice --HEAD

    注意,需要加上 –HEAD 选项,如果不加,安装是老版本,不支持iOS10的手机,所以安装时,需要加上 –HEAD 选项, 如图:

    libimobiledevice安装

    如果电脑上没有brew,请看这里:brew安装传送门

    • 如何使用idevicesyslog
      很简单,在命令行输入:

      idevicesyslog

      就可以在屏幕上看见手机上所有的日志了。idevicesyslog

    • 如何查看我们想要的日志

        1)将设备日志重定向到文件

    idevicesyslog >> iphone.log &

    该命令是将日志导入到iphone.log这个文件,并且是在后台执行。

        2)用tail -f和grep查看log

    tail -f iphone.log
    tail -f iphone.log | grep 'QQ’  # 查看包含QQ的行

    如图:

    idevicesyslog使用

    使用Mac自带的控制台应用查看iOS日志

        在用过各种办法查看App日志后,还是觉得苹果系统自带的工具用起来最方便,不用安装,不用命令行,傻瓜式操作,非常方便。但是还有好多同学不知道怎么用,这里简单的给大家介绍下。
        连上手机,在LaunchPad其他文件中打开控制台应用,或者搜索Console.app打开控制台应用,可以看到左侧一栏中有我们的手机设备,选择就可以,如图

    控制台应用

        右侧输出的日志不仅有我们自己的App还有其他进程的日志,怎么过滤?非常简单,选中一条我们的日志,在进程名字上右键,选择显示进程名,然后就只会显示我们App的日志。如果显示的列名中没有进程名,我们可以在其他列名上右键,在弹出的菜单中勾选上进程就可以了。如图显示QQ打印出的日志:

    控制台应用

    日志还是有点多,看不清,怎么办,在右上方输入自己想要过滤出的日志,然后就会只显示你想要的,如图显示QQ打印出的日志中所有带有“***”的行

    控制台应用

    是不是很简单方便
    其实这个控制台应用功能还是很多的,本文只是介绍下简单用法,大家快去试试其他功能吧。

    总结

        本文介绍的两种实时查看日志的方法能让我们在一些不能连上Xcode调试的场景下查看日志,对于定位解决问题很有帮助。idevicesyslog 工具使用方法更灵活,可以用grep查找更符合自己想要的日志;控制台应用查看日志更方便一点。



    来源博客:http://wellphone.me

    前言

         可能大数人看到这个标题觉得DEBUG时在Xcode里就能看到App运行时的打印在控制台里的日志,这还需要专门写篇文章介绍下吗?其实不然,有些场景下我们还是需要实时查看App运行时的日志的,比如测试人员拿着测试机发现问题了过来找你,这时候要看运行时日志,又不能重新DEBUG安装版本,怎么办?再比如,收到并点击推送的通知后启动App,这时候要查看我们打印的日志,怎么办?还有,弄过VoIP推送同学的都知道,这个推送可以直接启动我们的App,如果要观察启动过程中的日志,这该怎么办?总不能只能连着Xcode运行时,我们才能看到日志吧。有同学可能会说,把日志写入文件,启动完成后,再把文件拷贝出来看不就完了,好了,这篇文章你一定要认真看完。

    查看日志的几种方式

        这里我们说的日志是输出到控制台的日志,就是我们常用的NSLog输出的日志。其实大部分App会集成日志模块,比如CocoaLumberjack,还有以前笔者学习Runloop时也造过这样的轮子SSLogger。利用第三方日志模块查看日志不在本文讨论范围内,本文只讨论如何实时查看输出在console控制台的日志。
        大致有以前几种方式查看日志,一种是连着Xcode调试时,可以直接在Xcode下面的控制台输出面板上看到我们App的日志。如图:

    Xcode查看log

        还一种也是Xcode,通过Window->Devices打开devices界面,选择我们的手机,也能看到手机中运行的进程输出的日志。如图:

    Devices查看log

        上面这两种方式大部分iOS开发应该都知道,不知道的就该检讨下了。
        本文重点介绍另外两种查看运行时日志的方式: libimobiledevice  和 Console.app(控制台应用)

    使用libimobiledevice工具查看日志

        libimobiledevice 是一系列工具集,有很多实用的工具,截屏,安装ipa文件等等。我们要介绍的是idevicesyslog这个工具,这个工具可以把手机中的日志输出到我们指定的文件里。

    • 如何安装libimobiledevice

    安装libimobiledevice很简单,在命令行输入:

    brew instll libimobiledevice --HEAD

    注意,需要加上 –HEAD 选项,如果不加,安装是老版本,不支持iOS10的手机,所以安装时,需要加上 –HEAD 选项, 如图:

    libimobiledevice安装

    如果电脑上没有brew,请看这里:brew安装传送门

    • 如何使用idevicesyslog
      很简单,在命令行输入:

      idevicesyslog

      就可以在屏幕上看见手机上所有的日志了。idevicesyslog

    • 如何查看我们想要的日志

        1)将设备日志重定向到文件

    idevicesyslog >> iphone.log &

    该命令是将日志导入到iphone.log这个文件,并且是在后台执行。

        2)用tail -f和grep查看log

    tail -f iphone.log
    tail -f iphone.log | grep 'QQ’  # 查看包含QQ的行

    如图:

    idevicesyslog使用

    使用Mac自带的控制台应用查看iOS日志

        在用过各种办法查看App日志后,还是觉得苹果系统自带的工具用起来最方便,不用安装,不用命令行,傻瓜式操作,非常方便。但是还有好多同学不知道怎么用,这里简单的给大家介绍下。
        连上手机,在LaunchPad其他文件中打开控制台应用,或者搜索Console.app打开控制台应用,可以看到左侧一栏中有我们的手机设备,选择就可以,如图

    控制台应用

        右侧输出的日志不仅有我们自己的App还有其他进程的日志,怎么过滤?非常简单,选中一条我们的日志,在进程名字上右键,选择显示进程名,然后就只会显示我们App的日志。如果显示的列名中没有进程名,我们可以在其他列名上右键,在弹出的菜单中勾选上进程就可以了。如图显示QQ打印出的日志:

    控制台应用

    日志还是有点多,看不清,怎么办,在右上方输入自己想要过滤出的日志,然后就会只显示你想要的,如图显示QQ打印出的日志中所有带有“***”的行

    控制台应用

    是不是很简单方便
    其实这个控制台应用功能还是很多的,本文只是介绍下简单用法,大家快去试试其他功能吧。

    总结

        本文介绍的两种实时查看日志的方法能让我们在一些不能连上Xcode调试的场景下查看日志,对于定位解决问题很有帮助。idevicesyslog 工具使用方法更灵活,可以用grep查找更符合自己想要的日志;控制台应用查看日志更方便一点。


    展开全文
  • iOS日志获取和实时浏览器显示日志

    千次阅读 2018-01-16 22:46:25
    平时我们写代码的时候,为了调试方便,总是会在代码中写入很多的NSLog(也可能是其它的日志框架等,例如大名鼎鼎的CocoaLumberjack),但是我们对于NSLog到底了解多少?NSLog的信息为什么Xcode能够获取的到?我们能自己写个...

    平时我们写代码的时候,为了调试方便,总是会在代码中写入很多的NSLog(也可能是其它的日志框架等,例如大名鼎鼎的CocoaLumberjack),但是我们对于NSLog到底了解多少?NSLog的信息为什么Xcode能够获取的到?我们能自己写个程序获取所有的NSlog么?NSLog写入的信息到底在哪里?

    NSLog输出到哪?

    我们都知道,NSLog是一个C函数,它的函数声明是

    1
    void NSLog(NSString *format, ...)

    系统对其说明是:Logs an error message to the Apple System Log facility.,它是用来输出信息到标准的Error控制台上去.其内部其实是使用Apple System Log(ASL:苹果自己实现的输出日志的一套接口)的API.在iOS真机设备上,使用ASL记录的log被缓存在一个文件中,直到设备被重启。

    这里提到的ASL,都是放在ash.h这个头文件中,这套api可以获取指定的日志数据,具体可以参考ASL参考

    从上面可以直到,NSLog默认被系统输出到了一个文件中,这个文件是哪个呢?NSLog默认的输出到了系统的 /var/log/syslog这个文件中,当然了,如果你的机器没有越狱,你是查看不了这个文件的.我手机是越狱的,于是乎验证了下,使用iTools等工具将真机的/var/log/syslog文件导出,下面就是这个文件的部分内容的截取 log.png 从中,我们可以看到,所有的APP的NSLog全部都是写到这个文件中的!!!

    8579568eba9e647a921f503787c10.jpg

    标准的err控制台

    我们现在了解到了NSLog就是输出到文件syslog中,既然要往文件中写,那么肯定就有文件的句柄了,这个文件的句柄是多少呢? 在C语言中,我们有三个默认的句柄

    1
    2
    3
    #define stdin __stdinp
    #define stdout __stdoutp
    #define stderr __stderrp

    其对应的iOS系统层面的上述三个句柄其实也就是下面的三个

    1
    2
    3
    #define STDIN_FILENO 0 /* standard input file descriptor */
    #define STDOUT_FILENO 1 /* standard output file descriptor */
    #define STDERR_FILENO 2 /* standard error file descriptor */

    我们的NSLog输出的是到 STDERR_FILENO 上,我们可以使用c语言的输出到文件的fprintf来验证一下

    1
    2
    NSLog(@"ViewController viewDidLoad");
    fprintf (stderr, "%s\n""ViewController viewDidLoad222");

    在Xcode的控制台可以看到输出

    1
    2
    2016-06-15 12:57:17.286 TestNSlog[68073:1441419] ViewController viewDidLoad
    ViewController viewDidLoad222

    由于fprintf并不会像NSLog那样,在内部调用ASL接口,所以只是单纯的输出信息,并没有添加日期,进程名,进程id等,也不会自动换行。

    NSLog的重定向

    既然NSLog是写到STDERR_FILENO中去的,那么根据Unix的知识,我们可以重定向这个文件,让NSLog直接写到文件中去

    1
    2
    3
    4
    5
    6
    //to log to document directory
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *documentsPath = [paths objectAtIndex:0];
    NSString *loggingPath = [documentsPath stringByAppendingPathComponent:@"/mylog.log"];
    //redirect NSLog
    freopen([loggingPath cStringUsingEncoding:NSASCIIStringEncoding], "a+", stderr);

    利用c语言的freopen函数,进行重定向,将写往stderr的内容重定向到我们制定的文件中去,一旦执行了上述代码,那么在这个之后的NSLog将不会在控制台显示了,会直接输出在文件mylog.log中! 在模拟器中,我们可以使用终端的tail命令(tail -f mylog.log)对这个文件进行实时查看,就如同我们在xcode的输出窗口中看到的那样,你还可以结合grep命令进行实时过滤查看,非常方便在大量的日志信息中迅速定位到我们要的日志信息

    1465984868218459.gif

    在真机中,这种重定向有什么用处呢? 由于重定向到的文件是我们沙盒中的文件,那么就可以在我们的程序中写一段代码将这个文件发送给我们,远程的用户app出了问题,把日志发送给我们,我们就可以根据日志信息,找寻可能的问题所在!

    也可以开启app的文件夹itunse共享。

    配置共享文件夹:

    在应用程序的Info.plist文件中添加UIFileSharingEnabled键,并将键值设置为YES。将您希望共享的文件放在应用程序的Documents目录。一旦设备插入到用户计算机,iTunes 9.1就会在选中设备的Apps标签中显示一个File Sharing区域。此后,用户就可以向该目录添加文件或者将文件移动到桌面计算机中

    就是说,一旦设备连接上电脑,可以通过iTune查看指定应用程序的共享文件夹,将文件拷贝到你的电脑上看

    一般我们都会在应用中放置一个开关,开启或者关闭Log日志的重定向,在上面,我们使用标准C的freopen将stderr重定向到我们的文件中了,那么问题来了,怎么重定向回去呢???

    1
    FILE * freopen ( const char * filename, const char * mode, FILE * stream );

    要想重定向回去,那么我们需要知道stderr原来的文件路径,很遗憾,这个在不同平台中是不一样的,在iOS平台,由于沙盒机制,我们也并不能直接使用沙盒外的文件 对此,freopen将无能为力,要重定向回去,只能使用Unix的方法dup和dup2!

    1
    2
    3
    4
    5
    6
    //在ios上可用的方式,还是得借助dup和dup2
    int originH1 = dup(STDERR_FILENO);
    FILE * myFile = freopen([loggingPath cStringUsingEncoding:NSASCIIStringEncoding], "a+", stderr);//这句话已经重定向了,现在NSLog都输出到文件中去了,
    //……………….
    //恢复原来的
    dup2(originH1, STDERR_FILENO);//就可以了

    其它重定向STDERR_FILENO的方式集锦

    方式一 采用dup2的重定向方式

    (选自:http://lizaochengwen.iteye.com/blog/1476080)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    - (void)redirectSTD:(int )fd{
        NSPipe * pipe = [NSPipe pipe] ;
        NSFileHandle *pipeReadHandle = [pipe fileHandleForReading] ;
        int pipeFileHandle = [[pipe fileHandleForWriting] fileDescriptor];
        dup2(pipeFileHandle, fd) ;
        [[NSNotificationCenter defaultCenter] addObserver:self
         selector:@selector(redirectNotificationHandle:)
         name:NSFileHandleReadCompletionNotification
         object:pipeReadHandle] ;
        [pipeReadHandle readInBackgroundAndNotify];
    }
    - (void)redirectNotificationHandle:(NSNotification *)nf{
        NSData *data = [[nf userInfo] objectForKey:NSFileHandleNotificationDataItem];
        NSString *str = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] ;
        //这里可以做我们需要的操作,例如将nslog显示到一个textview中,或者是存放到另一个文件中等等
        //self.logTextView.text = [NSString stringWithFormat:@"%@\n%@",self.logTextView.text, str];
        NSRange range;
        //range.location = [self.logTextView.text length] - 1;
        range.length = 0;
        //[self.logTextView scrollRangeToVisible:range];
        [[nf object] readInBackgroundAndNotify];
    }

    使用的时候

    1
    [self redirectSTD:STDERR_FILENO];

    就可以将NSLOg的输出重定向到我们的通知中去!!!

    方式二 使用GCD的dispatch Source

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    - (dispatch_source_t)_startCapturingWritingToFD:(int)fd  {
        int fildes[2];
        pipe(fildes);  // [0] is read end of pipe while [1] is write end
        dup2(fildes[1], fd);  // Duplicate write end of pipe "onto" fd (this closes fd)
        close(fildes[1]);  // Close original write end of pipe
        fd = fildes[0];  // We can now monitor the read end of the pipe
        char* buffer = malloc(1024);
        NSMutableData* data = [[NSMutableData alloc] init];
        fcntl(fd, F_SETFL, O_NONBLOCK);
        dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, fd, 0, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0));
        dispatch_source_set_cancel_handler(source, ^{
            free(buffer);
        });
        dispatch_source_set_event_handler(source, ^{
            @autoreleasepool {
                while (1) {
                    ssize_t size = read(fd, buffer, 1024);
                    if (size <= 0) {
                        break;
                    }
                    [data appendBytes:buffer length:size];
                    if (size < 1024) {
                        break;
                    }
                }
                NSString *aString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
                //printf("aString = %s",[aString UTF8String]);
                //NSLog(@"aString = %@",aString);
                //读到了日志,可以进行我们需要的各种操作了
            }
        });
        dispatch_resume(source);
        return source;
    }

    使用的时候

    1
    _sourt_t = [self _startCapturingWritingToFD:STDERR_FILENO];

    记得,要自己保留返回的dispatchsourcet对象,不然其释放了,你就获取不到了!

    ASL读取日志

    以上的方式,都是重定向文件,一旦重定向后,那么NSLog就不会再写到系统的syslog中去了,也就意味着不能使用ASL接口获取到重定向后的数据了。

    不重定向NSLog,怎么读取所有的log呢?

    ASL读取log的核心代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    + (NSMutableArray*)allLogMessagesForCurrentProcess
    {
        asl_object_t query = asl_new(ASL_TYPE_QUERY);
        // Filter for messages from the current process. Note that this appears to happen by default on device, but is required in the simulator.
        NSString *pidString = [NSString stringWithFormat:@"%d", [[NSProcessInfo processInfo] processIdentifier]];
        asl_set_query(query, ASL_KEY_PID, [pidString UTF8String], ASL_QUERY_OP_EQUAL);
        aslresponse response = asl_search(NULL, query);
        aslmsg aslMessage = NULL;
        NSMutableArray *logMessages = [NSMutableArray array];
        while ((aslMessage = asl_next(response))) {
            [logMessages addObject:[SystemLogMessage logMessageFromASLMessage:aslMessage]];
        }
        asl_release(response);
        return logMessages;
    }
    //这个是怎么从日志的对象aslmsg中获取我们需要的数据
    +(instancetype)logMessageFromASLMessage:(aslmsg)aslMessage
    {
        SystemLogMessage *logMessage = [[SystemLogMessage alloc] init];
        const char *timestamp = asl_get(aslMessage, ASL_KEY_TIME);
        if (timestamp) {
            NSTimeInterval timeInterval = [@(timestamp) integerValue];
            const char *nanoseconds = asl_get(aslMessage, ASL_KEY_TIME_NSEC);
            if (nanoseconds) {
                timeInterval += [@(nanoseconds) doubleValue] / NSEC_PER_SEC;
            }
            logMessage.timeInterval = timeInterval;
            logMessage.date = [NSDate dateWithTimeIntervalSince1970:timeInterval];
        }
        const char *sender = asl_get(aslMessage, ASL_KEY_SENDER);
        if (sender) {
            logMessage.sender = @(sender);
        }
        const char *messageText = asl_get(aslMessage, ASL_KEY_MSG);
        if (messageText) {
            logMessage.messageText = @(messageText);//NSLog写入的文本内容
        }
        const char *messageID = asl_get(aslMessage, ASL_KEY_MSG_ID);
        if (messageID) {
            logMessage.messageID = [@(messageID) longLongValue];
        }
        return logMessage;
    }

    ASL的好处是没有重定向文件,所以不会影响Xcode等控制台的输出,它是一种非侵入式的读取的方式,类似于我们读取数据库的文件,我们只是读取数据,并没有将原来的数据库文件删除。

    在app中内置一个小型的http web服务器

    上面的方式,当测试,或者平时我们没有连接XCode时,想查看日志信息,还是不太方便,试想,如果我们在需要的时候,可以直接用浏览器查看输出的log信息那该多好?

    结合上面的ASL和一个小型的web服务器,我们就可以实现了,

    对于httpserver github上比较知名的有

    CocoaHTTPServer,这个已经三年没更新了,不推荐使用 GCDWebServer 作者一直在维护,据说性能也不错,推荐使用这个,下面的demo也使用的这个

    摘录其中的部分代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    #define kMinRefreshDelay 500  // In milliseconds
    @interface HttpServerLogger ()
    @property (nonatomic,strong) GCDWebServer* webServer;
    @end
    @implementation HttpServerLogger
    + (instancetype)shared {
        static dispatch_once_t onceToken;
        static HttpServerLogger *shared;
        dispatch_once(&onceToken, ^{
            shared = [HttpServerLogger new];
        });
        return shared;
    }
    - (GCDWebServer *)webServer {
        if (!_webServer) {
            _webServer = [[GCDWebServer alloc] init];
            __weak __typeof__(self) weakSelf = self;
            // Add a handler to respond to GET requests on any URL
            [_webServer addDefaultHandlerForMethod:@"GET"
              requestClass:[GCDWebServerRequest class]
              processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
                      return [weakSelf createResponseBody:request];
              }];
            NSLog(@"Visit %@ in your web browser", _webServer.serverURL);
        }
        return _webServer;
    }
    - (void)startServer{
         // Use convenience method that runs server on port 8080
        // until SIGINT (Ctrl-C in Terminal) or SIGTERM is received
        [self.webServer startWithPort:8080 bonjourName:nil];
    }
    - (void)stopServer {
        [_webServer stop];
        _webServer = nil;
    }
    //当浏览器请求的时候,返回一个由日志信息组装成的html返回给浏览器
    - (GCDWebServerDataResponse *)createResponseBody :(GCDWebServerRequest* )request{
        GCDWebServerDataResponse *response = nil;
        NSString* path = request.path;
        NSDictionary* query = request.query;
        //NSLog(@"path = %@,query = %@",path,query);
        NSMutableString* string;
        if ([path isEqualToString:@"/"]) {
            string = [[NSMutableString alloc] init];
            [string appendString:@""];
            [string appendString:@""];
            [string appendFormat:@"%s[%i]", getprogname(), getpid()];
            [string appendString:@""];
            [string appendFormat:@"", kMinRefreshDelay];
            [string appendString:@""];
            [string appendString:@""];
            [string appendString:@"
    "];
            [self _appendLogRecordsToString:string afterAbsoluteTime:0.0];
            [string appendString:@""];
            [string appendString:@"
    "];
            [string appendString:@""];
            [string appendString:@""];
        }
        else if ([path isEqualToString:@"/log"] && query[@"after"]) {
            string = [[NSMutableString alloc] init];
            double time = [query[@"after"] doubleValue];
            [self _appendLogRecordsToString:string afterAbsoluteTime:time];
        }
        else {
           string = [@"
    无数据
    " mutableCopy];
        }
        if (string == nil) {
            string = [@"" mutableCopy];
        }
        response = [GCDWebServerDataResponse responseWithHTML:string];
        return response;
    }
    - (void)_appendLogRecordsToString:(NSMutableString*)string afterAbsoluteTime:(double)time {
        __block double maxTime = time;
        NSArray *allMsg = [SystemLogManager allLogAfterTime:time];
        [allMsg enumerateObjectsUsingBlock:^(SystemLogMessage * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            const char* style = "color: dimgray;";
            NSString* formattedMessage = [self displayedTextForLogMessage:obj];
            [string appendFormat:@"
    %@", style, formattedMessage];
            if (obj.timeInterval > maxTime) {
                maxTime = obj.timeInterval ;
            }
        }];
        [string appendFormat:@"
    ", maxTime];
    }
    - (NSString *)displayedTextForLogMessage:(SystemLogMessage *)msg{
        NSMutableString *string = [[NSMutableString alloc] init];
        [string appendFormat:@"
    ",[SystemLogMessage logTimeStringFromDate:msg.date ],msg.sender, msg.messageText];
    %@    %@    %@    
        return string;
    }
    @end

    使用的时候,开启webserver服务,在同一个局域网下, 使用 http://机子的ip:8080来请求 演示2.gif

    1465986055665970.gif

    上述演示代码下载 TestLog

    几个优秀的第三方日志框架

    CocoaLumberjack

    另一个日志替代品XLFacility,其中实现了本地存储,重定向,web服务等,是本demo的重要参考代码

    CCLogSystem

    ASL的swift版本的封装CleanroomASLswift

    轻量级的iOS和mac上的http serverCocoaHTTPServer

    轻量级的iOS和mac上的http serverGCDWebServer

    参考

    官方的ASL说明

    freopen实现

    read-log-messages-posted-to-the-device-console

    readout-at-runtime-in-an-application

    how-to-nslog-into-a-file

    展开全文
  • iOS搭建Log日志系统

    千次阅读 2018-09-26 15:44:47
    CocoaLumberjack是适用于Mac和iOS的快速简单但功能强大且灵活的日志框架。 本文主要介绍基于CocoaLumberjack搭建iOS项目Log日志系统。 CocoaLumberjack开源地址 一、CocoaLumberjack架构 CocoaLumberjack架构 ...

    CocoaLumberjack

    CocoaLumberjack是适用于Mac和iOS的快速简单但功能强大且灵活的日志框架。
    本文主要介绍基于CocoaLumberjack搭建iOS项目Log日志系统。

    CocoaLumberjack开源地址

    一、CocoaLumberjack架构

    CocoaLumberjack架构

    二、简单使用

    1、使用CocoaPods安装CocoaLumberjack

    platform :ios, '7.0'
    pod 'CocoaLumberjack'           # Log日志
    

    2、全局定义DDLog日志级别,才可以编译通过

    • DDLogLevelOff 关闭Log
    • DDLogLevelError 打印Error级别Log
    • DDLogLevelWarning 打印Error、Warning级别Log
    • DDLogLevelInfo 打印Error、Warn、Info级别Log
    • DDLogLevelDebug 打印Error、Warn、Info、Debug级别Log
    • DDLogLevelVerbose 打印Error、Warn、Info、Debug、Verbose级别Log
    #ifdef DEBUG
      static const int ddLogLevel = DDLogLevelVerbose;
    #else
      static const int ddLogLevel = DDLogLevelWarning;
    #endif
    

    3、初始化DDLog配置

    • DDASLLogger 输出出到Console.app
    • DDTTYLogger 输出到Xcode控制台
    • DDLogFileManager 输出到文件
    • DDAbstractDatabaseLogger 输出到DB
    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 
        [DDLog addLogger:[DDTTYLogger sharedInstance]];             // TTY = Xcode console
        [DDLog addLogger:[DDASLLogger sharedInstance]];             // ASL = Apple System Logs
        DDFileLogger *fileLogger = [[DDFileLogger alloc] init];     // File Logger
        fileLogger.rollingFrequency = 60 * 60 * 24;                 // 刷新频率为24小时
        fileLogger.logFileManager.maximumNumberOfLogFiles = 7;      // 最大文件数量为7个
        [DDLog addLogger:fileLogger];
    return YES;
    }
    

    4、简单使用不同Log等级

    DDLogVerbose(@"Verbose");       // 详细日志
    DDLogDebug(@"Debug");           // 调试日志
    DDLogInfo(@"Info");             // 信息日志
    DDLogWarn(@"Warn");             // 警告日志
    DDLogError(@"Error");           // 错误日志
    

    三、搭建Log日志系统

    实现需求:

    1、实现不同Log日志等级。
    2、实现定时任务上传Log日志到服务器。

    1、创建MagicLogManager类,管理Log日志系统

    通过MagicLogManager来配置日志信息、获取日志路径、获取日志内容。
    所有Log日志默认保存在Library/Caches/Logs/目录下,文件名为bundleid+空格+日期.log。

    注意:自定义颜色需要插件XcodeColors:https://github.com/robbiehanson/XcodeColors

    MagicLogManager.h文件

    #import <Foundation/Foundation.h>
    #import <CocoaLumberjack/CocoaLumberjack.h>
    //开源依赖
    #if DEBUG
    static const DDLogLevel ddLogLevel = DDLogLevelVerbose;
    #else
    static const DDLogLevel ddLogLevel = DDLogLevelWarning;
    #endif
    
    @interface MagicLogManager : NSObject
    @property(nonatomic, strong)DDFileLogger *fileLogger;
    + (MagicLogManager *)shareManager;
    - (void)start;                              // 配置日志信息
    - (NSArray *)getAllLogFilePath;             // 获取日志路径
    - (NSArray *)getAllLogFileContent;          // 获取日志内容
    @end
    

    MagicLogManager.m文件

    #import "MagicLogManager.h"
    #import "MagicLogFormatter.h"
    
    @implementation MagicLogManager
    + (MagicLogManager *)shareManager{    
        static MagicLogManager *manager = nil;
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            manager = [[MagicLogManager alloc] init];
            manager.fileLogger = [[DDFileLogger alloc] init];
            manager.fileLogger.rollingFrequency = 60 * 60 * 24;                 // 刷新频率为24小时
            manager.fileLogger.logFileManager.maximumNumberOfLogFiles = 7;      // 保存一周的日志,即7天
            manager.fileLogger.maximumFileSize = 1024 * 1024 * 2;               // 最大文件大小
    
        });
        return manager;
    }
    /**
     配置日志信息
     */
    - (void)start{
        // 1.自定义Log格式
        MagicLogFormatter *logFormatter = [[MagicLogFormatter alloc] init];
        // 2.DDASLLogger,日志语句发送到苹果文件系统、日志状态发送到Console.app
        [[DDASLLogger sharedInstance] setLogFormatter:logFormatter];
        [DDLog addLogger:[DDASLLogger sharedInstance]];
        // 3.DDFileLogger,日志语句写入到文件中(默认路径:Library/Caches/Logs/目录下,文件名为bundleid+空格+日期.log)
        DDFileLogger *fileLogger = [MagicLogManager shareManager].fileLogger;
        [fileLogger setLogFormatter:logFormatter];
        [DDLog addLogger:fileLogger withLevel:DDLogLevelError];        // 错误日志,写到文件中
        // 4.DDTTYLogger,日志语句发送到Xcode
        [[DDTTYLogger sharedInstance] setLogFormatter:logFormatter];
        [[DDTTYLogger sharedInstance] setColorsEnabled:YES];           // 启用颜色区分
        [[DDTTYLogger sharedInstance] setForegroundColor:DDMakeColor(255, 0, 0) backgroundColor:nil forFlag:DDLogFlagError];
        [[DDTTYLogger sharedInstance] setForegroundColor:DDMakeColor(105, 200, 80) backgroundColor:nil forFlag:DDLogFlagInfo];
        [[DDTTYLogger sharedInstance] setForegroundColor:DDMakeColor(100, 100, 200) backgroundColor:nil forFlag:DDLogFlagDebug];
        [DDLog addLogger:[DDTTYLogger sharedInstance]];
    }
    /**
     获取日志路径(文件名bundleid+空格+日期)
     */
    - (NSArray *)getAllLogFilePath{
        NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
        NSString *cachesPath = [paths objectAtIndex:0];
        NSString *logPath = [cachesPath stringByAppendingPathComponent:@"Logs"];
        NSFileManager *fileManger = [NSFileManager defaultManager];
        NSError *error = nil;
        NSArray *fileArray = [fileManger contentsOfDirectoryAtPath:logPath error:&error];
        NSMutableArray *result = [NSMutableArray array];
        [fileArray enumerateObjectsUsingBlock:^(NSString *filePath, NSUInteger idx, BOOL * _Nonnull stop) {
            if([filePath hasPrefix:[NSBundle mainBundle].bundleIdentifier]){
                NSString *logFilePath = [logPath stringByAppendingPathComponent:filePath];
                [result addObject:logFilePath];
            }
        }];
        return result;
    }
    /**
     获取日志内容
     */
    - (NSArray *)getAllLogFileContent{
        NSMutableArray *result = [NSMutableArray array];
        NSArray *logfilePaths = [self getAllLogFilePath];
        [logfilePaths enumerateObjectsUsingBlock:^(NSString *filePath, NSUInteger idx, BOOL * _Nonnull stop) {
            NSData *data = [NSData dataWithContentsOfFile:filePath];
            NSString *content = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
            [result addObject:content];
        }];
        return result;
    }
    

    2、创建MagicLogFormatter类,自定义Log日志格式

    通过MagicLogFormatter自定义日志格式,继承DDAbstractLogger,遵循DDLogFormatter协议,重写formatLogMessage方法,返回最终Log消息字符串。

    MagicLogFormatter.h文件

    #import <Foundation/Foundation.h>
    #import "DDLog.h"
    @interface MagicLogFormatter : DDAbstractLogger<DDLogFormatter>
    @end
    

    MagicLogFormatter.m文件

    #import "MagicLogFormatter.h"
    @implementation MagicLogFormatter
    - (NSString *)formatLogMessage:(DDLogMessage *)logMessage{
        NSString *loglevel = nil;
        switch (logMessage.flag){
            case LOG_FLAG_ERROR:
                loglevel = @"[ERROR]--->";
                break;
            case LOG_FLAG_WARN:
                loglevel = @"[WARN]--->";
                break;
            case LOG_FLAG_INFO:
                loglevel = @"[INFO]--->";
                break;
            case LOG_FLAG_DEBUG:
                loglevel = @"[DEBUG]--->";
                break;
            case LOG_FLAG_VERBOSE:
                loglevel = @"[VBOSE]--->";
                break;
            default:
                break;
        }
        NSString *resultString = [NSString stringWithFormat:@"%@ %@___line[%ld]__%@", loglevel, logMessage->_function, logMessage->_line, logMessage->_message];
        return resultString;
    }
    @end
      /*
        DDLogMessage中返回信息
        
        NSString *_message;             // 具体logger内容
        DDLogLevel _level;              // 全局lever等级
        DDLogFlag _flag;                // log的flag等级
        NSInteger _context;             // 
        NSString *_file;                // 文件
        NSString *_fileName;            // 文件名称
        NSString *_function;            // 函数名
        NSUInteger _line;               // 行号
        id _tag;                        // 
        DDLogMessageOptions _options;   //
        NSDate *_timestamp;             // 时间
        NSString *_threadID;            // 线程id 
        NSString *_threadName;          // 线程名称
        NSString *_queueLabel;          // gcd线程名称
        */
    

    3、使用

    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
        [[MagicLogManager shareManager] start];
        DDLogVerbose(@"Verbose");    // 详细日志
        DDLogDebug(@"Debug");        // 调试日志
        DDLogInfo(@"Info");          // 信息日志
        DDLogWarn(@"Warn");          // 警告日志
        DDLogError(@"Error");        // 错误日志
        NSLog(@"日志路径%@", [[MagicLogManager shareManager] getAllLogFilePath]);
        NSLog(@"日志内容%@", [[MagicLogManager shareManager] getAllLogFileContent]);
    }
    

     

    展开全文
  • 浅谈iOS日志收集系统

    万次阅读 2017-07-28 16:02:52
    在浅谈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文件即是符号化解析后的文件。

    展开全文
  • iOS APP日志写入文件(日志收集)

    千次阅读 2018-03-14 16:18:24
    iOS APP日志写入文件(日志收集)- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOption //日志收集(日志会被拦截,开发的时候注掉) [self ...
  • iOS:crash崩溃日志分析

    千次阅读 2018-04-12 15:22:50
    一、前言:作为一个合格的iOS开发者,除了具有规范强悍的编码能力外,还应该具有过硬的查错纠错能力。在项目运行时,程序崩溃是不可避免的,遇到这个问题,有时会出现一大堆的crash日志,艹,貌似看不懂呀。其实没有...
  • IOS日志记录

    万次阅读 2014-08-19 11:30:42
    在开发过程中
  • iOS 崩溃日志收集及分析

    万次阅读 2017-02-10 11:11:44
    最近几天,项目中在增加推送功能,选用的极光推送...话说,由于iOS10之后,苹果对推送进行了重大更新,主要是新增了 User Notifications Framework 框架, 具体信息可以查看苹果官方文档,这里就不多解释了。于是我就突
  • iOS-四种查看线上崩溃日志的办法

    万次阅读 2017-08-23 15:24:15
    方法一:使用iTunes工具查看崩溃文件 方法二:使用Xcode工具直接查看崩溃内容(推荐) 方法三:使用iTunes Connect查看崩溃日志(新版已不支持) 方法四:使用第三方软件itools
  • ios开发中打印日志消息控制

    千次阅读 2014-10-18 16:47:13
    ios项目开发中,项目发布时需要去掉NSLog消息,不然会非常影响性能,但是去掉
  • ios 获取真机的crash日志(一)

    千次阅读 2016-05-03 14:55:28
    在项目真机运行中, 可能会出现我们意想不到的crash,在我们找不到原因的时候,可以通过Xcode将crash文件导出来,...3.获取所有的crash日志文件 4. 右键可以Export,就可以查看相关的crash的原因 下节:如何分析crash文件
  • windows下的ios log工具

    千次阅读 2013-07-01 14:44:44
    1.http://www.apple.com/support/iphone/enterprise/ http://support.apple.com/kb/DL1466 在windows系统中安装此软件,可以查看ios操作日志
  • iOS-加载webview,打印网页的console.log

    千次阅读 2017-10-30 15:50:12
    iOS开发的时候,用webview加载一个网页。问题就是我们想看输入的日志调试东西的时候,但是网页端的日志只能打开safari开发中心调试了。 于是乎有这样一个方法就可以看见h5端输出的日志 - (void)catchJsLog{ if...
  • iOS app设备日志查看

    千次阅读 2017-07-20 08:41:56
    http://www.cocoachina.com/ios/20170719/19933.html
  • IOS端APP测试日志查看方法

    千次阅读 2017-04-17 22:36:05
    Android端移动测试一般都会使用adb 命令行工具来查看应用的一些指标,特别是日志,对于IOS端的同学,如果没有mac系统,测试起来比较无力。 本文介绍如何在IOS端如何实现日志轻松查看的方法。 前提条件: 1.越狱的...
  • m 我们常常会遇到iPhone手机或者iPad平板上运行APP崩溃的问题,有时候打开某个APP,却一下子“闪退”了。有的再次进入就正常了,有些可能就再也进不去了。 ...我们常常会遇到iPhone手机或者iPad平板上...
  • 查看iOS真机日志

    千次阅读 2015-08-06 13:58:07
    打包测试之后不连接电脑如果有闪退不能查看log日志??? 将手机连接x-code 选中Devices
  • iOS设备日志查看工具syslog和socat安装syslog在cydia搜索syslogd to /var/log/syslog安装即可syslog用法syslog是把系统日志写入到/var/log/syslog文件里,用法很简单,执行tail -f /var/log/syslog就能看到了 ...
  • iOS开发76-使用Xcode查看实时日志

    千次阅读 2017-03-22 09:55:47
    iOS开发76-使用Xcode查看实时日志 在Xcode上运行App代码的时候,可以实时查看到日志。 在没有APP源代码的情况下,直接使用Xcode查看Debug版本的App来查看实时日志。 步骤如下: 1、打开Xcode->Window->Devices 2...
  • iOS如何获取测试 iPhone 上的崩溃日志

    千次阅读 2019-07-22 15:54:22
    原理:APP崩溃后的日志是保存在手机上的,需要与电脑上的 iTunes 同步,即将崩溃日志保存在电脑上的文件夹中。 具体文件地址: Mac OS X:~/Library/Logs/CrashReporter/MobileDevice Windows XP:C:\...
1 2 3 4 5 ... 20
收藏数 46,900
精华内容 18,760
关键字:

ios日志